Files
Django/config/views/scripts.py
2026-07-04 19:58:19 +08:00

366 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
# 小程序客服话术 / 自动回复默认值(原位于 miniapp_config.py按功能组归类到话术模块
MINIAPP_SCRIPT_DEFAULTS = {
'cs_welcome': {
'title': '',
'content': '你好,请问有什么可以帮到您的?',
'confirm_text': '',
'cancel_text': '',
},
'chat_image_confirm': {
'title': '温馨提示',
'content': '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
'confirm_text': '确认',
'cancel_text': '取消',
},
'merchant_dispatch_confirm': {
'title': '确认派单',
'content': '确认向该打手派发此订单?',
'confirm_text': '确认',
'cancel_text': '取消',
},
}
def _resolve_script_club_id(request):
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_user_club_id
user = getattr(request, 'user', None)
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
if not club_id:
club_id = resolve_club_id_from_request(request)
return club_id or 'xq'
def _merge_script_scene(scene_key, row):
base = dict(MINIAPP_SCRIPT_DEFAULTS.get(scene_key) or {})
if not row:
return base
if row.title:
base['title'] = row.title
if row.content:
base['content'] = row.content
if row.confirm_text:
base['confirm_text'] = row.confirm_text
if row.cancel_text:
base['cancel_text'] = row.cancel_text
return base
class HuashuHqView(APIView):
"""POST /peizhi/huashuhq 批量获取场景话术"""
permission_classes = [IsAuthenticated]
def post(self, request):
scene_keys = request.data.get('scene_keys') or []
if not isinstance(scene_keys, list) or not scene_keys:
return Response({'code': 400, 'msg': 'scene_keys 不能为空', 'data': None}, status=400)
club_id = _resolve_script_club_id(request)
rows = MiniappScriptScene.query.filter(
club_id=club_id, scene_key__in=scene_keys, enabled=True,
)
row_map = {r.scene_key: r for r in rows}
data = {}
for key in scene_keys:
if not isinstance(key, str) or not key.strip():
continue
data[key.strip()] = _merge_script_scene(key.strip(), row_map.get(key.strip()))
return Response({'code': 200, 'msg': 'success', 'data': data})
class HuashuMatchView(APIView):
"""POST /peizhi/huashu_match 客服关键词自动回复匹配"""
permission_classes = [IsAuthenticated]
def post(self, request):
scene_key = (request.data.get('scene_key') or '').strip()
user_text = (request.data.get('user_text') or '').strip()
if scene_key != 'cs_auto_reply':
return Response({'code': 400, 'msg': '不支持的 scene_key', 'data': {'matched': False, 'content': ''}})
if not user_text:
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
club_id = _resolve_script_club_id(request)
rules = MiniappScriptAutoReply.query.filter(
club_id=club_id, enabled=True,
).order_by('-sort_order', 'id')
text_lower = user_text.lower()
for rule in rules:
kw_raw = (rule.keyword or '').strip()
if not kw_raw:
continue
keywords = [k.strip() for k in kw_raw.split(',') if k.strip()]
for kw in keywords:
if kw.lower() in text_lower or kw in user_text:
reply_type = rule.reply_type or MiniappScriptAutoReply.REPLY_TEXT
return Response({
'code': 200,
'msg': 'success',
'data': {
'matched': True,
'content': rule.reply_content or '',
'reply_type': reply_type,
},
})
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
# ---------- 后台:客服话术配置(/houtai/hthqhs、/houtai/htxghs ----------
SCRIPT_CONFIG_PERMS = ('kfhs666', '8080a')
SCRIPT_SCENE_KEYS = ('chat_image_confirm', 'cs_welcome', 'merchant_dispatch_confirm')
def _has_script_config_perm(permissions):
return any(code in permissions for code in SCRIPT_CONFIG_PERMS)
def _script_scene_item_type(scene_key):
return 'text_display' if scene_key == 'cs_welcome' else 'confirm_modal'
def _serialize_script_scene(row):
return {
'id': row.id,
'scene_key': row.scene_key,
'item_type': _script_scene_item_type(row.scene_key),
'title': row.title or '',
'content': row.content or '',
'confirm_text': row.confirm_text or '',
'cancel_text': row.cancel_text or '',
'is_active': bool(row.enabled),
}
def _serialize_script_auto_reply(row):
keywords = [k.strip() for k in (row.keyword or '').split(',') if k.strip()]
return {
'id': row.id,
'scene_key': 'cs_auto_reply',
'item_type': 'auto_reply',
'title': '',
'keywords': keywords,
'match_mode': 'contains',
'priority': row.sort_order or 0,
'content': row.reply_content or '',
'is_active': bool(row.enabled),
}
class MiniappScriptHoutaiListView(APIView):
"""POST /houtai/hthqhs 后台获取话术与自动回复配置"""
permission_classes = [IsAuthenticated]
def post(self, request):
from backend.utils import verify_kefu_permission
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
if not _has_script_config_perm(permissions):
return Response({'code': 403, 'msg': '您没有权限访问客服聊天配置'})
club_id = _resolve_script_club_id(request)
scenes = MiniappScriptScene.query.filter(
club_id=club_id, scene_key__in=SCRIPT_SCENE_KEYS,
).order_by('scene_key')
auto_rules = MiniappScriptAutoReply.query.filter(club_id=club_id).order_by('-sort_order', 'id')
items = [_serialize_script_scene(r) for r in scenes]
items.extend(_serialize_script_auto_reply(r) for r in auto_rules)
return Response({'code': 0, 'data': {'items': items, 'club_id': club_id}})
class MiniappScriptHoutaiModifyView(APIView):
"""POST /houtai/htxghs 后台增删改话术与自动回复"""
permission_classes = [IsAuthenticated]
def post(self, request):
from backend.utils import verify_kefu_permission
from jituan.services.display_config import forbid_display_write_in_all_scope
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
if not _has_script_config_perm(permissions):
return Response({'code': 403, 'msg': '您没有权限操作客服聊天配置'})
action = (request.data.get('action') or '').strip()
if not action:
return Response({'code': 400, 'msg': '缺少 action 参数'})
club_id = _resolve_script_club_id(request)
if action == 'delete':
return self._delete_item(request, club_id)
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
if action in ('create', 'update'):
item_type = (request.data.get('item_type') or '').strip()
if item_type == 'auto_reply':
return self._save_auto_reply(request, club_id, action)
return self._save_scene(request, club_id, action)
return Response({'code': 400, 'msg': f'未知的 action: {action}'})
def _save_scene(self, request, club_id, action):
scene_key = (request.data.get('scene_key') or '').strip()
if scene_key not in SCRIPT_SCENE_KEYS:
return Response({'code': 400, 'msg': '不支持的 scene_key'})
content = (request.data.get('content') or '').strip()
if not content:
return Response({'code': 400, 'msg': '请填写正文内容'})
fields = {
'title': (request.data.get('title') or '').strip(),
'content': content,
'confirm_text': (request.data.get('confirm_text') or '').strip(),
'cancel_text': (request.data.get('cancel_text') or '').strip(),
'enabled': bool(request.data.get('is_active', True)),
}
if action == 'update':
row_id = request.data.get('id')
if not row_id:
return Response({'code': 400, 'msg': '缺少 id'})
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
if not row:
return Response({'code': 404, 'msg': '配置不存在'})
for k, v in fields.items():
setattr(row, k, v)
row.save()
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(row)})
existing = MiniappScriptScene.query.filter(club_id=club_id, scene_key=scene_key).first()
if existing:
for k, v in fields.items():
setattr(existing, k, v)
existing.save()
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(existing)})
row = MiniappScriptScene.query.create(club_id=club_id, scene_key=scene_key, **fields)
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_scene(row)})
def _save_auto_reply(self, request, club_id, action):
keywords = request.data.get('keywords') or []
if not isinstance(keywords, list) or not keywords:
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
kw_list = [str(k).strip() for k in keywords if str(k).strip()]
if not kw_list:
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
content = (request.data.get('content') or '').strip()
if not content:
return Response({'code': 400, 'msg': '请填写回复内容'})
fields = {
'keyword': ','.join(kw_list),
'reply_content': content,
'sort_order': int(request.data.get('priority') or 0),
'enabled': bool(request.data.get('is_active', True)),
'reply_type': MiniappScriptAutoReply.REPLY_TEXT,
}
if action == 'update':
row_id = request.data.get('id')
if not row_id:
return Response({'code': 400, 'msg': '缺少 id'})
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
if not row:
return Response({'code': 404, 'msg': '规则不存在'})
for k, v in fields.items():
setattr(row, k, v)
row.save()
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_auto_reply(row)})
row = MiniappScriptAutoReply.query.create(club_id=club_id, **fields)
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_auto_reply(row)})
def _delete_item(self, request, club_id):
from jituan.services.display_config import forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
row_id = request.data.get('id')
if not row_id:
return Response({'code': 400, 'msg': '缺少 id'})
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
if row:
row.delete()
return Response({'code': 0, 'msg': '删除成功'})
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
if row:
return Response({'code': 400, 'msg': '场景话术不支持删除,请改为禁用'})
return Response({'code': 404, 'msg': '记录不存在'})