diff --git a/config/views/scripts.py b/config/views/scripts.py index 64cae1a..8f84d5b 100644 --- a/config/views/scripts.py +++ b/config/views/scripts.py @@ -1,365 +1,365 @@ -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': '记录不存在'}) +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): + """ + 话术/自动回复按俱乐部隔离。 + 优先顶栏/小程序 Header X-Club-Id,再用户归属店,最后默认 xq。 + (旧逻辑先取 User.ClubID,后台切俱乐部后仍读错店,界面空得像「未加载」。) + """ + from jituan.services.club_context import resolve_effective_club_id + return resolve_effective_club_id(request, getattr(request, 'user', None)) 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': '记录不存在'}) diff --git a/jituan/management/commands/copy_lxs_shenhe_catalog_to_xx.py b/jituan/management/commands/copy_lxs_shenhe_catalog_to_xx.py index 8386ea0..e9ae5e5 100644 --- a/jituan/management/commands/copy_lxs_shenhe_catalog_to_xx.py +++ b/jituan/management/commands/copy_lxs_shenhe_catalog_to_xx.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- """ 把龙先生(lxs)审核版商品目录复制到小溪(xx)。 diff --git a/jituan/services/default_invite.py b/jituan/services/default_invite.py index ba7d017..43936ac 100644 --- a/jituan/services/default_invite.py +++ b/jituan/services/default_invite.py @@ -12,21 +12,24 @@ _FALLBACK_DEFAULT_INVITE = { } -def get_club_default_dashou_invite_raw(club) -> str: - """只读 config_json 里真正保存的值(后台表单用,不含回落)。""" - if not club: - return '' - cfg = getattr(club, 'config_json', None) or {} - if not isinstance(cfg, dict): - return '' - return (cfg.get(CONFIG_KEY) or '').strip() - - -def get_club_fallback_dashou_invite(club_id: str) -> str: - """系统内置回落(仅小程序运行时;后台不应把它当已保存值展示)。""" +def get_club_default_dashou_invite(club_id: str) -> str: + """ + 优先读 Club.config_json.default_dashou_invite, + 否则回落内置各店默认管事邀请码。 + """ cid = (club_id or '').strip() if not cid: return '' + try: + from jituan.models import Club + club = Club.query.filter(club_id=cid).first() + if club: + cfg = club.config_json or {} + code = (cfg.get(CONFIG_KEY) or '').strip() + if code: + return code + except Exception: + logger.exception('read default_dashou_invite failed club=%s', cid) try: from jituan.services.club_migrate import CLUB_MIGRATE_DEFAULT_GUANSHI_INVITE code = (CLUB_MIGRATE_DEFAULT_GUANSHI_INVITE.get(cid) or '').strip() @@ -37,33 +40,8 @@ def get_club_fallback_dashou_invite(club_id: str) -> str: return (_FALLBACK_DEFAULT_INVITE.get(cid) or '').strip() -def get_club_default_dashou_invite(club_id: str) -> str: - """ - 小程序用:优先 config_json.default_dashou_invite,否则回落内置码。 - """ - cid = (club_id or '').strip() - if not cid: - return '' - try: - from jituan.models import Club - club = Club.query.filter(club_id=cid).first() - if club: - code = get_club_default_dashou_invite_raw(club) - if code: - return code - except Exception: - logger.exception('read default_dashou_invite failed club=%s', cid) - return get_club_fallback_dashou_invite(cid) - - def set_club_default_dashou_invite(club, invite_code: str) -> None: - """写入 club.config_json,调用方负责 save。空字符串表示清空已保存值。""" + """写入 club.config_json,调用方负责 save。""" cfg = dict(getattr(club, 'config_json', None) or {}) - code = (invite_code or '').strip() - if code: - cfg[CONFIG_KEY] = code - else: - # 清空:删掉键,避免再次读到旧固定值 - cfg.pop(CONFIG_KEY, None) - # 强制新 dict,避免 JSONField 变更检测失败 - club.config_json = dict(cfg) + cfg[CONFIG_KEY] = (invite_code or '').strip() + club.config_json = cfg diff --git a/products/views/zhuanpan_payment.py b/products/views/zhuanpan_payment.py index e307fc4..a6d0854 100644 --- a/products/views/zhuanpan_payment.py +++ b/products/views/zhuanpan_payment.py @@ -360,6 +360,7 @@ class ZhuanpanGoumai(APIView): pkg = f'prepay_id={prepay_id}' pay_sign_src = f'appId={APPID}&nonceStr={pay_nonce}&package={pkg}&signType=MD5&timeStamp={ts}&key={KEY}' return { + 'appId': APPID, 'timeStamp': ts, 'nonceStr': pay_nonce, 'package': pkg,