diff --git a/config/admin.py b/config/admin.py index 694323f..2ad2332 100644 --- a/config/admin.py +++ b/config/admin.py @@ -1 +1,16 @@ from django.contrib import admin +from .models import MiniappScriptScene, MiniappScriptAutoReply + + +@admin.register(MiniappScriptScene) +class MiniappScriptSceneAdmin(admin.ModelAdmin): + list_display = ('club_id', 'scene_key', 'title', 'enabled', 'UpdateTime') + list_filter = ('club_id', 'enabled') + search_fields = ('scene_key', 'title', 'content') + + +@admin.register(MiniappScriptAutoReply) +class MiniappScriptAutoReplyAdmin(admin.ModelAdmin): + list_display = ('club_id', 'keyword', 'reply_type', 'sort_order', 'enabled', 'UpdateTime') + list_filter = ('club_id', 'reply_type', 'enabled') + search_fields = ('keyword', 'reply_content') diff --git a/config/migrations/0012_miniapp_script.py b/config/migrations/0012_miniapp_script.py new file mode 100644 index 0000000..6544c7b --- /dev/null +++ b/config/migrations/0012_miniapp_script.py @@ -0,0 +1,52 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0011_clubminiappicon_is_uploaded'), + ] + + operations = [ + migrations.CreateModel( + name='MiniappScriptScene', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')), + ('scene_key', models.CharField(db_index=True, max_length=64, verbose_name='场景键')), + ('title', models.CharField(blank=True, default='', max_length=128, verbose_name='标题')), + ('content', models.TextField(blank=True, default='', verbose_name='正文')), + ('confirm_text', models.CharField(blank=True, default='', max_length=16, verbose_name='确认按钮')), + ('cancel_text', models.CharField(blank=True, default='', max_length=16, verbose_name='取消按钮')), + ('enabled', models.BooleanField(default=True, verbose_name='启用')), + ('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '小程序场景话术', + 'verbose_name_plural': '小程序场景话术', + 'db_table': 'miniapp_script_scene', + 'unique_together': {('club_id', 'scene_key')}, + }, + ), + migrations.CreateModel( + name='MiniappScriptAutoReply', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')), + ('keyword', models.CharField(max_length=200, verbose_name='关键词')), + ('reply_type', models.CharField(choices=[('text', '文本'), ('image', '图片')], default='text', max_length=16, verbose_name='回复类型')), + ('reply_content', models.TextField(verbose_name='回复内容(文本或图片URL)')), + ('sort_order', models.IntegerField(default=0, verbose_name='排序(大优先)')), + ('enabled', models.BooleanField(default=True, verbose_name='启用')), + ('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '客服自动回复规则', + 'verbose_name_plural': '客服自动回复规则', + 'db_table': 'miniapp_script_auto_reply', + 'ordering': ['-sort_order', 'id'], + }, + ), + ] diff --git a/config/models.py b/config/models.py index ee9afd4..5179fa1 100644 --- a/config/models.py +++ b/config/models.py @@ -520,3 +520,50 @@ class ClubPindaoImage(QModel): def __str__(self): return f'{self.club_id} 频道图{self.sort_order}' + + +class MiniappScriptScene(QModel): + """小程序固定场景话术(欢迎语、发图确认等)""" + club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID') + scene_key = models.CharField(max_length=64, db_index=True, verbose_name='场景键') + title = models.CharField(max_length=128, blank=True, default='', verbose_name='标题') + content = models.TextField(blank=True, default='', verbose_name='正文') + confirm_text = models.CharField(max_length=16, blank=True, default='', verbose_name='确认按钮') + cancel_text = models.CharField(max_length=16, blank=True, default='', verbose_name='取消按钮') + enabled = models.BooleanField(default=True, verbose_name='启用') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'miniapp_script_scene' + verbose_name = '小程序场景话术' + verbose_name_plural = verbose_name + unique_together = [['club_id', 'scene_key']] + + def __str__(self): + return f'{self.club_id}:{self.scene_key}' + + +class MiniappScriptAutoReply(QModel): + """客服关键词自动回复(支持文本/图片)""" + REPLY_TEXT = 'text' + REPLY_IMAGE = 'image' + REPLY_TYPE_CHOICES = [(REPLY_TEXT, '文本'), (REPLY_IMAGE, '图片')] + + club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID') + keyword = models.CharField(max_length=200, verbose_name='关键词') + reply_type = models.CharField(max_length=16, default=REPLY_TEXT, choices=REPLY_TYPE_CHOICES, verbose_name='回复类型') + reply_content = models.TextField(verbose_name='回复内容(文本或图片URL)') + sort_order = models.IntegerField(default=0, verbose_name='排序(大优先)') + enabled = models.BooleanField(default=True, verbose_name='启用') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'miniapp_script_auto_reply' + verbose_name = '客服自动回复规则' + verbose_name_plural = verbose_name + ordering = ['-sort_order', 'id'] + + def __str__(self): + return f'{self.club_id}:{self.keyword}' diff --git a/config/urls.py b/config/urls.py index dc4a914..3b6d7fe 100644 --- a/config/urls.py +++ b/config/urls.py @@ -5,7 +5,7 @@ from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageV ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \ KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\ GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\ - ShangjiaLianjieListView + ShangjiaLianjieListView, HuashuHqView, HuashuMatchView urlpatterns = [ @@ -39,4 +39,6 @@ urlpatterns = [ path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'), path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'), + path('huashuhq', HuashuHqView.as_view(), name='小程序场景话术获取'), + path('huashu_match', HuashuMatchView.as_view(), name='客服关键词自动回复'), ] diff --git a/config/views.py b/config/views.py index c73f1d1..00234b0 100644 --- a/config/views.py +++ b/config/views.py @@ -35,7 +35,8 @@ from utils.chat_utils import subscribe_merchant_link_chat from .models import ( Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi, - ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig + ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig, + MiniappScriptScene, MiniappScriptAutoReply, ) from users.models import ( @@ -2842,8 +2843,8 @@ class GuanshiQRCodeView(APIView): logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}") encoded_invite = urllib.parse.quote(invite_code, safe='') - # 打手端已合并至 fighter(TabBar);与旧版 dashouduan 同为扫码入口,fighter.js 已处理 inviteCode 自动注册 - page_path = f'pages/fighter/fighter?inviteCode={encoded_invite}' + # 兼容页 dashouduan 跳转打手中心;fighter.js 处理 inviteCode 自动注册 + page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}' wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' post_data = { 'path': page_path, @@ -3078,8 +3079,8 @@ class ZuzhangHaibaoView(APIView): # ===== 修改点:使用 A 接口(getwxacode) ===== encoded_invite = urllib.parse.quote(invite_code, safe='') - # 管事注册:打手中心 fighter.js 已支持 registerType=guanshi + inviteCode 自动注册 - page_path = f'pages/fighter/fighter?registerType=guanshi&inviteCode={encoded_invite}' + # 兼容页 guanshiduan 跳转打手中心(管事 registerType) + page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}' wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token) if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')): @@ -3377,4 +3378,102 @@ class CheckPhoneAuthView(APIView): boss = UserBoss.query.get(user=user) except UserBoss.DoesNotExist: return False - return (boss.alldingdan or 0) > 0 \ No newline at end of file + return (boss.alldingdan or 0) > 0 + + +# ---------- 小程序客服话术 / 自动回复 ---------- + +MINIAPP_SCRIPT_DEFAULTS = { + 'cs_welcome': { + 'title': '', + 'content': '你好,请问有什么可以帮到您的?', + 'confirm_text': '', + 'cancel_text': '', + }, + 'chat_image_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 = (rule.keyword or '').strip() + if not kw: + continue + 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': ''}}) \ No newline at end of file