feat: 小程序客服话术表与自动回复接口
- 新增 MiniappScriptScene / MiniappScriptAutoReply 模型与迁移 - 新增 /peizhi/huashuhq、/peizhi/huashu_match 接口及 Admin 配置 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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')
|
||||
|
||||
52
config/migrations/0012_miniapp_script.py
Normal file
52
config/migrations/0012_miniapp_script.py
Normal file
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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}'
|
||||
|
||||
@@ -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='客服关键词自动回复'),
|
||||
]
|
||||
|
||||
109
config/views.py
109
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', '')):
|
||||
@@ -3378,3 +3379,101 @@ class CheckPhoneAuthView(APIView):
|
||||
except UserBoss.DoesNotExist:
|
||||
return False
|
||||
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': ''}})
|
||||
Reference in New Issue
Block a user