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': '记录不存在'})