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 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):
|
def __str__(self):
|
||||||
return f'{self.club_id} 频道图{self.sort_order}'
|
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, \
|
ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \
|
||||||
KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
|
KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
|
||||||
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
|
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
|
||||||
ShangjiaLianjieListView
|
ShangjiaLianjieListView, HuashuHqView, HuashuMatchView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -39,4 +39,6 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'),
|
path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'),
|
||||||
path('hqsjlslj', ShangjiaLianjieListView.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 (
|
from .models import (
|
||||||
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||||
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig
|
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||||
|
MiniappScriptScene, MiniappScriptAutoReply,
|
||||||
)
|
)
|
||||||
|
|
||||||
from users.models import (
|
from users.models import (
|
||||||
@@ -2842,8 +2843,8 @@ class GuanshiQRCodeView(APIView):
|
|||||||
logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}")
|
logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}")
|
||||||
|
|
||||||
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
||||||
# 打手端已合并至 fighter(TabBar);与旧版 dashouduan 同为扫码入口,fighter.js 已处理 inviteCode 自动注册
|
# 兼容页 dashouduan 跳转打手中心;fighter.js 处理 inviteCode 自动注册
|
||||||
page_path = f'pages/fighter/fighter?inviteCode={encoded_invite}'
|
page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}'
|
||||||
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
|
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
|
||||||
post_data = {
|
post_data = {
|
||||||
'path': page_path,
|
'path': page_path,
|
||||||
@@ -3078,8 +3079,8 @@ class ZuzhangHaibaoView(APIView):
|
|||||||
|
|
||||||
# ===== 修改点:使用 A 接口(getwxacode) =====
|
# ===== 修改点:使用 A 接口(getwxacode) =====
|
||||||
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
||||||
# 管事注册:打手中心 fighter.js 已支持 registerType=guanshi + inviteCode 自动注册
|
# 兼容页 guanshiduan 跳转打手中心(管事 registerType)
|
||||||
page_path = f'pages/fighter/fighter?registerType=guanshi&inviteCode={encoded_invite}'
|
page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}'
|
||||||
|
|
||||||
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
|
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', '')):
|
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:
|
except UserBoss.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
return (boss.alldingdan or 0) > 0
|
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