diff --git a/config/views/merchant_links.py b/config/views/merchant_links.py index 9cd3260..830e3ab 100644 --- a/config/views/merchant_links.py +++ b/config/views/merchant_links.py @@ -541,8 +541,11 @@ class KehuGetDingdanLianjieView(APIView): # ========== 新增:确定俱乐部配置 ========== # 默认头像(相对路径) default_avatar = 'a_long/morentouxiang.jpg' - # 从 settings 获取配置 - kefu_link = getattr(settings, 'KEFU_LINK', '') + # 商家端企微客服(库配置优先,空则 settings) + from jituan.services.club_context import resolve_club_id_from_request + from jituan.services.wecom_kefu import resolve_side_link + club_id = resolve_club_id_from_request(request) + kefu_link = resolve_side_link(club_id, 'merchant') or getattr(settings, 'KEFU_LINK', '') oss_url = getattr(settings, 'COS_DOMAIN', '') goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') diff --git a/config/views/miniapp_config.py b/config/views/miniapp_config.py index 5e92785..69aa335 100644 --- a/config/views/miniapp_config.py +++ b/config/views/miniapp_config.py @@ -77,13 +77,16 @@ class GetDynamicConfigView(APIView): cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai') goeasy_host = 'hangzhou.goeasy.io' goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') - kefu_link = getattr(settings, 'KEFU_LINK', '') - kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '') rule_img = get_tupianpeizhi_url(request, 1) avatar_img = get_tupianpeizhi_url(request, 2) morentouxiang = avatar_img or 'beijing/morentouxiang.jpg' dashouguize = rule_img or 'a_long/dashouguize.jpg' + from jituan.services.club_context import resolve_club_id_from_request + from jituan.services.wecom_kefu import miniapp_kefu_payload + club_id = resolve_club_id_from_request(request) + kefu_payload = miniapp_kefu_payload(club_id) + # 构造返回数据 data = { "code": 0, @@ -102,10 +105,7 @@ class GetDynamicConfigView(APIView): "morentouxiang": morentouxiang, "dashouguize": dashouguize }, - "kefu": { - "link": kefu_link, - "enterpriseId": kefu_enterprise_id - }, + "kefu": kefu_payload, "miniappAssets": build_miniapp_assets_payload(request), } } diff --git a/jituan/migrations/0029_club_wecom_kefu_config.py b/jituan/migrations/0029_club_wecom_kefu_config.py new file mode 100644 index 0000000..8328db2 --- /dev/null +++ b/jituan/migrations/0029_club_wecom_kefu_config.py @@ -0,0 +1,29 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('jituan', '0028_deal_config_timeout_minutes'), + ] + + operations = [ + migrations.CreateModel( + name='ClubWecomKefuConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16, unique=True, verbose_name='俱乐部')), + ('dashou_link', models.CharField(blank=True, default='', max_length=512, verbose_name='打手端客服链接')), + ('dashou_corp_id', models.CharField(blank=True, default='', max_length=64, verbose_name='打手端企业ID')), + ('merchant_link', models.CharField(blank=True, default='', max_length=512, verbose_name='商家端客服链接')), + ('merchant_corp_id', models.CharField(blank=True, default='', max_length=64, verbose_name='商家端企业ID')), + ('updated_by', models.CharField(blank=True, default='', max_length=32)), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ('UpdateTime', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': '企业微信客服配置', + 'db_table': 'club_wecom_kefu_config', + }, + ), + ] diff --git a/jituan/models.py b/jituan/models.py index fe9c9a2..1860b2b 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -688,6 +688,25 @@ class ClubRoleAgreementSign(QModel): ] +class ClubWecomKefuConfig(QModel): + """ + 俱乐部企业微信客服链接(小程序 wx.openCustomerServiceChat)。 + 打手端 / 商家端各一套,互不影响;未配置时小程序可读 settings 兜底。 + """ + club_id = models.CharField(max_length=16, unique=True, db_index=True, verbose_name='俱乐部') + dashou_link = models.CharField(max_length=512, blank=True, default='', verbose_name='打手端客服链接') + dashou_corp_id = models.CharField(max_length=64, blank=True, default='', verbose_name='打手端企业ID') + merchant_link = models.CharField(max_length=512, blank=True, default='', verbose_name='商家端客服链接') + merchant_corp_id = models.CharField(max_length=64, blank=True, default='', verbose_name='商家端企业ID') + updated_by = models.CharField(max_length=32, blank=True, default='') + CreateTime = models.DateTimeField(auto_now_add=True) + UpdateTime = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'club_wecom_kefu_config' + verbose_name = '企业微信客服配置' + + class ClubOrderDealConfig(QModel): """俱乐部:成交指标统计开关 + 可控自动结算开关(默认全关)。""" club_id = models.CharField(max_length=16, unique=True, db_index=True, verbose_name='俱乐部') diff --git a/jituan/services/kefu_menu_definitions.py b/jituan/services/kefu_menu_definitions.py index 51c28ea..9d37eed 100644 --- a/jituan/services/kefu_menu_definitions.py +++ b/jituan/services/kefu_menu_definitions.py @@ -111,6 +111,8 @@ KEFU_MENU_ROW_DEFS = [ 'perm_codes': ['phbj666', '8080a']}, {'page_id': 'miniapp.recharge-copy', 'name': '充值页文案', 'path': '/miniapp/recharge-copy', 'parent_id': 'miniapp', 'sort_order': 119, 'perm_codes': ['8080a']}, + {'page_id': 'miniapp.wecom-kefu', 'name': '企微客服链接', 'path': '/miniapp/wecom-kefu', 'parent_id': 'miniapp', 'sort_order': 1195, + 'perm_codes': ['8080a']}, {'page_id': 'miniapp.role-agreement', 'name': '角色协议', 'path': '/miniapp/role-agreement', 'parent_id': 'miniapp', 'sort_order': 120, 'perm_codes': ['xycfg001', '8080a']}, {'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120}, diff --git a/jituan/services/wecom_kefu.py b/jituan/services/wecom_kefu.py new file mode 100644 index 0000000..13461a0 --- /dev/null +++ b/jituan/services/wecom_kefu.py @@ -0,0 +1,123 @@ +"""俱乐部企业微信客服链接:打手端 / 商家端分端配置。""" +from django.conf import settings + +from jituan.models import ClubWecomKefuConfig + +LINK_MAX = 512 +CORP_MAX = 64 + + +def _settings_fallback(): + return { + 'link': (getattr(settings, 'KEFU_LINK', '') or '').strip(), + 'enterpriseId': (getattr(settings, 'KEFU_ENTERPRISE_ID', '') or '').strip(), + } + + +def get_or_create_wecom_kefu(club_id: str): + cid = (club_id or '').strip() + if not cid: + return None + row = ClubWecomKefuConfig.query.filter(club_id=cid).first() + if not row: + row = ClubWecomKefuConfig(club_id=cid) + row.save() + return row + + +def config_to_dict(row) -> dict: + if not row: + return { + 'club_id': '', + 'dashou': {'link': '', 'enterpriseId': ''}, + 'merchant': {'link': '', 'enterpriseId': ''}, + 'updated_by': '', + } + return { + 'club_id': row.club_id, + 'dashou': { + 'link': (row.dashou_link or '').strip(), + 'enterpriseId': (row.dashou_corp_id or '').strip(), + }, + 'merchant': { + 'link': (row.merchant_link or '').strip(), + 'enterpriseId': (row.merchant_corp_id or '').strip(), + }, + 'updated_by': (row.updated_by or '').strip(), + } + + +def _pick_side(payload: dict, key: str) -> dict: + raw = payload.get(key) + if isinstance(raw, dict): + return raw + return {} + + +def save_wecom_kefu(club_id: str, data: dict, updated_by: str = '') -> dict: + row = get_or_create_wecom_kefu(club_id) + if not row: + raise ValueError('缺少俱乐部') + + dashou = _pick_side(data, 'dashou') + merchant = _pick_side(data, 'merchant') + + # 兼容扁平字段 + dashou_link = dashou.get('link', data.get('dashou_link')) + dashou_corp = dashou.get('enterpriseId', data.get('dashou_corp_id', data.get('dashou_enterpriseId'))) + merchant_link = merchant.get('link', data.get('merchant_link')) + merchant_corp = merchant.get( + 'enterpriseId', + data.get('merchant_corp_id', data.get('merchant_enterpriseId')), + ) + + if dashou_link is not None: + row.dashou_link = str(dashou_link or '').strip()[:LINK_MAX] + if dashou_corp is not None: + row.dashou_corp_id = str(dashou_corp or '').strip()[:CORP_MAX] + if merchant_link is not None: + row.merchant_link = str(merchant_link or '').strip()[:LINK_MAX] + if merchant_corp is not None: + row.merchant_corp_id = str(merchant_corp or '').strip()[:CORP_MAX] + if updated_by: + row.updated_by = str(updated_by)[:32] + row.save() + return config_to_dict(row) + + +def miniapp_kefu_payload(club_id: str) -> dict: + """ + 小程序动态配置用。 + 分端优先读库;空则回落 settings(兼容旧单链接)。 + 顶层 link / enterpriseId 对齐打手端,兼容旧前端。 + """ + fb = _settings_fallback() + row = None + cid = (club_id or '').strip() + if cid: + row = ClubWecomKefuConfig.query.filter(club_id=cid).first() + + dashou_link = ((row.dashou_link if row else '') or '').strip() or fb['link'] + dashou_corp = ((row.dashou_corp_id if row else '') or '').strip() or fb['enterpriseId'] + merchant_link = ((row.merchant_link if row else '') or '').strip() or fb['link'] + merchant_corp = ((row.merchant_corp_id if row else '') or '').strip() or fb['enterpriseId'] + + return { + 'link': dashou_link, + 'enterpriseId': dashou_corp, + 'dashou': { + 'link': dashou_link, + 'enterpriseId': dashou_corp, + }, + 'merchant': { + 'link': merchant_link, + 'enterpriseId': merchant_corp, + }, + } + + +def resolve_side_link(club_id: str, side: str = 'merchant') -> str: + """商家链接页等只需 link 字符串时用。""" + payload = miniapp_kefu_payload(club_id) + key = 'merchant' if side in ('merchant', 'shangjia') else 'dashou' + return (payload.get(key) or {}).get('link') or payload.get('link') or '' diff --git a/jituan/urls.py b/jituan/urls.py index 3a94736..636796b 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -127,6 +127,7 @@ from jituan.views_order_deal import ( MerchantDealStatView, DashouDealStatView, ) +from jituan.views_wecom_kefu import WecomKefuConfigView urlpatterns = [ path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'), @@ -156,6 +157,7 @@ urlpatterns = [ path('houtai/order-deal-config', OrderDealConfigView.as_view(), name='jituan_order_deal_config'), path('houtai/merchant-deal-stat', MerchantDealStatView.as_view(), name='jituan_merchant_deal_stat'), path('houtai/dashou-deal-stat', DashouDealStatView.as_view(), name='jituan_dashou_deal_stat'), + path('houtai/wecom-kefu-config', WecomKefuConfigView.as_view(), name='jituan_wecom_kefu_config'), path('houtai/payment-channel-manage', ClubPaymentChannelManageView.as_view(), name='jituan_payment_channel_manage'), path('houtai/club-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'), path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'), diff --git a/jituan/views_wecom_kefu.py b/jituan/views_wecom_kefu.py new file mode 100644 index 0000000..3524ad6 --- /dev/null +++ b/jituan/views_wecom_kefu.py @@ -0,0 +1,56 @@ +"""客服后台:企业微信客服链接(打手端 / 商家端)。""" +from rest_framework.parsers import JSONParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from backend.utils import verify_kefu_permission, has_club_super_perm, has_perm_code +from jituan.services.club_context import resolve_club_id_from_request +from jituan.services import wecom_kefu as svc + +PERM_CODE = '8080a' + + +def _auth(request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return None, Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return None, permissions + user = getattr(request, 'user', None) + if getattr(user, 'IsSuperuser', False) or has_club_super_perm(permissions): + return kefu, None + if has_perm_code(permissions, PERM_CODE): + return kefu, None + return None, Response({'code': 403, 'msg': '无权限配置企业微信客服'}) + + +class WecomKefuConfigView(APIView): + """ + POST /jituan/houtai/wecom-kefu-config + action: get | save + body.save: dashou: {link, enterpriseId}, merchant: {link, enterpriseId} + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + kefu, err = _auth(request) + if err: + return err + club_id = (resolve_club_id_from_request(request) or '').strip() + if not club_id: + return Response({'code': 400, 'msg': '请先选择俱乐部'}) + action = (request.data.get('action') or 'get').strip().lower() + if action == 'get': + row = svc.get_or_create_wecom_kefu(club_id) + return Response({'code': 0, 'msg': 'ok', 'data': svc.config_to_dict(row)}) + if action == 'save': + try: + operator = getattr(request.user, 'UserUID', '') or '' + data = svc.save_wecom_kefu(club_id, request.data, updated_by=operator) + return Response({'code': 0, 'msg': '已保存', 'data': data}) + except ValueError as e: + return Response({'code': 400, 'msg': str(e)}) + return Response({'code': 400, 'msg': '未知 action'})