From 470546ae82737b389076204308e8f3a6816b61ed Mon Sep 17 00:00:00 2001 From: XingQue Date: Sat, 20 Jun 2026 03:54:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=95=86=E5=AE=B6?= =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E6=8E=A5=E5=8F=A3=E6=B7=BB=E5=8A=A0=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E6=93=8D=E4=BD=9C=E4=B8=93=E5=B1=9E=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E5=85=B6=E6=AC=A1=E5=95=86=E5=AE=B6=E7=9C=8B=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- merchant_ops/constants.py | 37 ++++ merchant_ops/services/audit.py | 39 +++- merchant_ops/services/link.py | 330 ++++++++++++++++++++++++++++++++ merchant_ops/views_manage.py | 29 +-- merchant_ops/views_orders.py | 2 - merchant_ops/views_templates.py | 46 ++++- 6 files changed, 459 insertions(+), 24 deletions(-) create mode 100644 merchant_ops/services/link.py diff --git a/merchant_ops/constants.py b/merchant_ops/constants.py index 3a7ba7d..e2b8328 100644 --- a/merchant_ops/constants.py +++ b/merchant_ops/constants.py @@ -86,3 +86,40 @@ SYSTEM_ROLE_TEMPLATES = [ ] OWNER_ALL_PERMISSIONS = [code for code, _ in PERMISSION_DEFINITIONS] + +# 操作审计:仅展示「重要行为」,不含查看类 +IMPORTANT_AUDIT_ACTIONS = frozenset([ + 'DISPATCH', 'LINK_GENERATE', 'SETTLE', 'CANCEL', 'REFUND_APPLY', + 'CHANGE_PLAYER', 'PENALTY_APPLY', 'PENALTY_MANAGE', + 'TEMPLATE_CREATE', 'TEMPLATE_UPDATE', 'TEMPLATE_DELETE', + 'WALLET_ALLOCATE', 'INVITE_CREATE', 'INVITE_REVOKE', + 'MEMBER_UPDATE_ROLE', 'MEMBER_REMOVE', 'MEMBER_DISABLE', 'MEMBER_ENABLE', + 'ROLE_CREATE', 'ROLE_SET_PERMS', 'MEMBER_SET_PERMS', + 'STAFF_BIND', +]) + +AUDIT_ACTION_LABELS = { + 'DISPATCH': '派单发单', + 'LINK_GENERATE': '生成链接', + 'SETTLE': '结单', + 'CANCEL': '撤销订单', + 'REFUND_APPLY': '申请退款', + 'CHANGE_PLAYER': '更换打手', + 'PENALTY_APPLY': '申请罚单', + 'PENALTY_MANAGE': '改撤罚单', + 'TEMPLATE_CREATE': '新增模板', + 'TEMPLATE_UPDATE': '修改模板', + 'TEMPLATE_DELETE': '删除模板', + 'WALLET_ALLOCATE': '划额度', + 'INVITE_CREATE': '生成邀请码', + 'INVITE_REVOKE': '作废邀请码', + 'MEMBER_UPDATE_ROLE': '更换客服角色', + 'MEMBER_REMOVE': '移除客服', + 'MEMBER_DISABLE': '禁用客服', + 'MEMBER_ENABLE': '启用客服', + 'ROLE_CREATE': '创建角色', + 'ROLE_SET_PERMS': '设置角色权限', + 'MEMBER_SET_PERMS': '调整成员权限', + 'STAFF_BIND': '客服绑定入驻', + 'ORDER_DETAIL': '查看订单详情', +} diff --git a/merchant_ops/services/audit.py b/merchant_ops/services/audit.py index 1b14eac..1aa955b 100644 --- a/merchant_ops/services/audit.py +++ b/merchant_ops/services/audit.py @@ -1,8 +1,8 @@ """操作审计。""" from decimal import Decimal -from merchant_ops.constants import ACTOR_MERCHANT, ACTOR_STAFF -from merchant_ops.models import MerchantStaffAuditLog +from merchant_ops.constants import ACTOR_MERCHANT, ACTOR_STAFF, AUDIT_ACTION_LABELS +from merchant_ops.models import MerchantStaffAuditLog, MerchantStaffMember def write_audit( @@ -64,3 +64,38 @@ def _client_ip(request): if xff: return xff.split(',')[0].strip() return request.META.get('REMOTE_ADDR', '') or '' + + +def audit_action_label(action): + return AUDIT_ACTION_LABELS.get(action, action) + + +def serialize_audit_row(row, member_map=None): + """格式化审计记录供前端展示。""" + member_map = member_map or {} + label = audit_action_label(row.action) + operator_name = '商家老板' + role_name = '' + if row.operator_type == ACTOR_STAFF and row.staff_member_id: + m = member_map.get(row.staff_member_id) + if m: + operator_name = m.display_name or m.staff_user_id + role_name = m.role.role_name if m.role else '' + else: + operator_name = row.operator_user_id + elif row.operator_type == ACTOR_MERCHANT: + operator_name = '商家老板' + return { + 'id': row.id, + 'operator_user_id': row.operator_user_id, + 'operator_type': row.operator_type, + 'operator_name': operator_name, + 'role_name': role_name, + 'action': row.action, + 'action_label': label, + 'resource_type': row.resource_type, + 'resource_id': row.resource_id, + 'amount': str(row.amount) if row.amount is not None else '', + 'remark': row.remark, + 'time': row.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if row.CreateTime else '', + } diff --git a/merchant_ops/services/link.py b/merchant_ops/services/link.py new file mode 100644 index 0000000..a14f2aa --- /dev/null +++ b/merchant_ops/services/link.py @@ -0,0 +1,330 @@ +"""子客服专用:极速派单链接生成 / 列表(独立实现,不修改商家原接口)。""" +import hashlib +import logging +import os +import random +import secrets +import time +from decimal import Decimal + +from django.conf import settings +from django.core.paginator import Paginator +from django.db import transaction +from django.db.models import F +from django.utils import timezone + +from backend.utils import update_shangjia_daily +from config.models import ShangjiaLianjie, ShangjiaMoban +from merchant_ops.constants import ACTOR_STAFF, STAT_ACTION_DISPATCH +from merchant_ops.models import MerchantOrderDispatch, MerchantStaffMember +from merchant_ops.services.stats import update_staff_daily +from merchant_ops.services.wallet import check_wallet_available, confirm_dispatch_deduct +from orders.models import MerchantOrderExt, Order +from products.models import ShangpinLeixing +from rank.models import Chenghao, DingdanBiaoqian +from users.models import CommissionRate, UserShangjia +from users.business_models import User + +logger = logging.getLogger(__name__) + + +def _generate_dingdan_id(): + ts = int(time.time() * 1000) + pid = os.getpid() % 1000 + rnd = random.randint(1000, 9999) + return f"SJ{ts}{pid:03d}{rnd}" + + +def _generate_secure_token(dd_id, uid): + timestamp = int(time.time() * 1_000_000) + salt = secrets.token_hex(8) + raw = f"{timestamp}:{dd_id}:{uid}:{salt}" + token = hashlib.sha256(raw.encode()).hexdigest()[:24] + if ShangjiaLianjie.query.filter(LinkToken=token).exists(): + return _generate_secure_token(dd_id, uid) + return token + + +def _generate_link_url(token): + base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/') + return f"{base}/order/{token}" + + +def _staff_operator_payload(member): + role = member.role + return { + 'operatorType': ACTOR_STAFF, + 'staffMemberId': member.id, + 'staffUserId': member.staff_user_id, + 'roleCode': role.role_code if role else '', + 'roleName': role.role_name if role else '', + 'displayName': member.display_name or '', + } + + +def staff_generate_link(member, staff_user, data): + """ + 子客服生成发单链接。 + 扣商家余额 + 扣客服额度,并写入 merchant_order_dispatch 标记操作人。 + """ + shangpin_type_id = data.get('shangpinTypeId') + moban_id = data.get('mobanId') + if not all([shangpin_type_id, moban_id]): + return False, {'code': 400, 'msg': '缺少参数', 'data': {}} + + try: + shangpin_type_id = int(shangpin_type_id) + moban_id = int(moban_id) + except (TypeError, ValueError): + return False, {'code': 400, 'msg': '参数格式错误', 'data': {}} + + merchant_id = member.merchant_id + try: + shangjia = UserShangjia.query.get(user__UserUID=merchant_id) + except UserShangjia.DoesNotExist: + return False, {'code': 403, 'msg': '所属商家不存在', 'data': {}} + if shangjia.zhuangtai != 1: + return False, {'code': 403, 'msg': '商家状态异常', 'data': {}} + + try: + shangpin_leixing = ShangpinLeixing.query.get(id=shangpin_type_id, shenhezhuangtai=1) + except ShangpinLeixing.DoesNotExist: + return False, {'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}} + + try: + moban = ShangjiaMoban.query.get( + id=moban_id, UserID=merchant_id, ProductTypeID=shangpin_type_id, + ) + except ShangjiaMoban.DoesNotExist: + return False, {'code': 400, 'msg': '模板不存在', 'data': {}} + + jiage = moban.Price + if jiage > shangjia.yue: + return False, {'code': 400, 'msg': f'商家余额不足,当前{shangjia.yue}元', 'data': {}} + + try: + lilu = float(CommissionRate.query.get(Platform='3').Rate) or 1.0 + except CommissionRate.DoesNotExist: + lilu = 1.0 + dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) + + chenghao_id = moban.TitleID + if moban.CommissionGrabEnabled and moban.CommissionPrice is not None: + final_yaoqiuleixing = 2 + final_yongjin = moban.CommissionPrice + final_huiyuan_id = '' + else: + final_yaoqiuleixing = shangpin_leixing.yaoqiuleixing or 1 + final_yongjin = None + final_huiyuan_id = shangpin_leixing.huiyuan_id or '' + + label_name = '' + if chenghao_id: + ch = Chenghao.query.filter(id=chenghao_id).first() + if ch: + label_name = ch.mingcheng + + merchant_user = User.query.filter(UserUID=merchant_id).first() + operator = _staff_operator_payload(member) + + try: + with transaction.atomic(): + check_wallet_available(member, jiage) + + dingdan_id = _generate_dingdan_id() + secure_token = _generate_secure_token(dingdan_id, merchant_id) + + dingdan = Order.query.create( + OrderID=dingdan_id, + Status=13, + Platform=2, + Amount=jiage, + PlayerCommission=dashou_fencheng, + ProductTypeID=shangpin_type_id, + GrabRequirement=final_yaoqiuleixing, + MembershipID=final_huiyuan_id, + CommissionReq=final_yongjin, + Description=moban.TemplateDesc[:2000], + User1ID=f"Sj{merchant_id}", + ImageURL=merchant_user.Avatar if merchant_user else '', + ) + + MerchantOrderExt.query.create( + Order=dingdan, + MerchantID=merchant_id, + MerchantNickname=shangjia.nicheng or f"商家{merchant_id}", + ) + + if chenghao_id: + ch = Chenghao.query.filter(id=chenghao_id).first() + if ch: + DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch) + + UserShangjia.query.filter(id=shangjia.id).update( + fabu=F('fabu') + 1, + yue=F('yue') - jiage, + jinridingdan=F('jinridingdan') + 1, + jinriliushui=F('jinriliushui') + jiage, + jinyuedingdan=F('jinyuedingdan') + 1, + jinyueliushui=F('jinyueliushui') + jiage, + ) + + link_url = _generate_link_url(secure_token) + lianjie_obj = ShangjiaLianjie.query.create( + UserID=merchant_id, + ProductTypeID=shangpin_type_id, + TemplateID=moban_id, + LinkURL=link_url, + LinkToken=secure_token, + OrderID=dingdan_id, + is_used=False, + ExpireTime=timezone.now() + timezone.timedelta(days=1), + ) + + ShangjiaMoban.query.filter(id=moban_id).update( + PublishedCount=F('PublishedCount') + 1, + ) + + MerchantOrderDispatch.objects.create( + order_id=dingdan_id, + merchant_id=merchant_id, + dispatch_actor_type=ACTOR_STAFF, + dispatch_actor_id=member.staff_user_id, + staff_member_id=member.id, + ) + + confirm_dispatch_deduct(member, jiage, dingdan_id, member.staff_user_id) + update_shangjia_daily(merchant_id, Decimal(str(jiage)), STAT_ACTION_DISPATCH) + update_staff_daily( + merchant_id, member.staff_user_id, member.id, jiage, STAT_ACTION_DISPATCH, + ) + MerchantStaffMember.query.filter(id=member.id).update( + today_dispatch_count=F('today_dispatch_count') + 1, + today_dispatch_amount=F('today_dispatch_amount') + Decimal(str(jiage)), + ) + + shangjia.refresh_from_db() + from merchant_ops.services.wallet import get_or_create_wallet + wallet = get_or_create_wallet(member) + quota_available = str(wallet.quota_available) + + except ValueError as e: + return False, {'code': 400, 'msg': str(e), 'data': {}} + except Exception as e: + logger.error(f'子客服生成链接失败: {e}', exc_info=True) + return False, {'code': 500, 'msg': '系统错误,生成链接失败', 'data': {}} + + logger.info( + f"子客服链接生成 - 订单:{dingdan_id} 客服:{member.staff_user_id} " + f"角色:{operator.get('roleName')}" + ) + + return True, { + 'code': 200, + 'msg': '链接生成成功', + 'data': { + 'linkUrl': link_url, + 'newBalance': quota_available, + 'quotaAvailable': quota_available, + 'dingdanId': dingdan_id, + 'dingdanJieshao': moban.TemplateDesc, + 'jiage': str(jiage), + 'labelId': chenghao_id, + 'labelName': label_name, + 'youxiaoShijian': lianjie_obj.ExpireTime.strftime('%Y-%m-%d %H:%M:%S'), + **operator, + }, + } + + +def staff_list_links(member, data): + """子客服链接列表;总管看全部,派单员只看自己生成的。""" + from merchant_ops.services.authz import member_has_perm + + moban_id = data.get('mobanId') + used = int(data.get('used', 0)) + page = max(1, int(data.get('page', 1))) + page_size = min(50, max(1, int(data.get('pageSize', 5)))) + + if not moban_id: + return False, {'code': 400, 'msg': '模板ID不能为空', 'data': {}} + + merchant_id = member.merchant_id + try: + ShangjiaMoban.query.get(id=moban_id, UserID=merchant_id) + except ShangjiaMoban.DoesNotExist: + return False, {'code': 404, 'msg': '模板不存在或无权查看', 'data': {}} + + links = ShangjiaLianjie.query.filter( + UserID=merchant_id, + TemplateID=moban_id, + is_used=(used == 1), + ).order_by('-CreateTime') + + if not member_has_perm(member, 'order_view_all'): + own_order_ids = MerchantOrderDispatch.query.filter( + merchant_id=merchant_id, + staff_member_id=member.id, + ).values_list('order_id', flat=True) + links = links.filter(OrderID__in=own_order_ids) + + paginator = Paginator(links, page_size) + if page > paginator.num_pages and paginator.count > 0: + return False, {'code': 400, 'msg': '页码超出范围', 'data': {}} + + current_page = paginator.page(page) if paginator.count else paginator.page(1) + order_ids = [lk.OrderID for lk in current_page.object_list if lk.OrderID] + dispatch_map = {} + if order_ids: + for d in MerchantOrderDispatch.query.filter(order_id__in=order_ids): + dispatch_map[d.order_id] = d + + member_ids = {d.staff_member_id for d in dispatch_map.values() if d.staff_member_id} + members = {} + if member_ids: + for m in MerchantStaffMember.query.filter(id__in=member_ids).select_related('role'): + members[m.id] = m + + data_list = [] + for link in current_page.object_list: + item = { + 'id': link.id, + 'lianjie': link.LinkURL, + 'LinkToken': link.LinkToken, + 'is_used': link.is_used, + 'dingdan_id': link.OrderID, + 'ExpireTime': link.ExpireTime.strftime('%Y-%m-%d %H:%M:%S') if link.ExpireTime else '', + 'CreateTime': link.CreateTime.strftime('%Y-%m-%d %H:%M:%S'), + 'operatorType': 'MERCHANT', + 'staffMemberId': None, + 'staffUserId': '', + 'roleCode': '', + 'roleName': '', + 'displayName': '商家老板', + } + disp = dispatch_map.get(link.OrderID) + if disp and disp.dispatch_actor_type == ACTOR_STAFF and disp.staff_member_id: + sm = members.get(disp.staff_member_id) + if sm: + item.update({ + 'operatorType': ACTOR_STAFF, + 'staffMemberId': sm.id, + 'staffUserId': sm.staff_user_id, + 'roleCode': sm.role.role_code if sm.role else '', + 'roleName': sm.role.role_name if sm.role else '', + 'displayName': sm.display_name or sm.role.role_name if sm.role else '', + }) + data_list.append(item) + + return True, { + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': data_list, + 'total': paginator.count, + 'hasMore': current_page.has_next(), + 'currentPage': page, + 'pageSize': page_size, + 'viewScope': 'all' if member_has_perm(member, 'order_view_all') else 'self', + }, + } diff --git a/merchant_ops/views_manage.py b/merchant_ops/views_manage.py index 700ad4b..127035b 100644 --- a/merchant_ops/views_manage.py +++ b/merchant_ops/views_manage.py @@ -369,10 +369,15 @@ class StaffWalletLedgerView(APIView): class StaffAuditListView(APIView): + """POST /yonghu/merchant-staff/audit/list — 商家老板或有 audit_view 权限的客服可查看重要操作日志。""" + permission_classes = [IsAuthenticated] def post(self, request): try: + from merchant_ops.constants import IMPORTANT_AUDIT_ACTIONS + from merchant_ops.services.audit import serialize_audit_row + mid = request.user.UserUID member = None if is_merchant_owner(request.user): @@ -384,23 +389,21 @@ class StaffAuditListView(APIView): mid = member.merchant_id page = max(1, int(request.data.get('page', 1))) page_size = min(50, int(request.data.get('page_size', 20))) - qs = MerchantStaffAuditLog.query.filter(merchant_id=mid) + qs = MerchantStaffAuditLog.query.filter( + merchant_id=mid, + action__in=IMPORTANT_AUDIT_ACTIONS, + ) staff_filter = request.data.get('staff_user_id') if staff_filter: qs = qs.filter(operator_user_id=staff_filter) total = qs.count() - rows = qs.order_by('-CreateTime')[(page - 1) * page_size: page * page_size] - items = [{ - 'id': r.id, - 'operator_user_id': r.operator_user_id, - 'operator_type': r.operator_type, - 'action': r.action, - 'resource_type': r.resource_type, - 'resource_id': r.resource_id, - 'amount': str(r.amount) if r.amount is not None else '', - 'remark': r.remark, - 'time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', - } for r in rows] + rows = list(qs.order_by('-CreateTime')[(page - 1) * page_size: page * page_size]) + member_ids = {r.staff_member_id for r in rows if r.staff_member_id} + member_map = {} + if member_ids: + for m in MerchantStaffMember.query.filter(id__in=member_ids).select_related('role'): + member_map[m.id] = m + items = [serialize_audit_row(r, member_map) for r in rows] return Response({'code': 0, 'msg': '成功', 'data': { 'list': items, 'total': total, 'has_more': page * page_size < total, }}) diff --git a/merchant_ops/views_orders.py b/merchant_ops/views_orders.py index 55222ac..f806103 100644 --- a/merchant_ops/views_orders.py +++ b/merchant_ops/views_orders.py @@ -105,8 +105,6 @@ class StaffOrderDetailView(APIView): resp = proxy_merchant_order_view( request, member, ShangjiaDingdanXiangqingView, 'order_detail', ) - if resp.status_code == 200 and resp.data.get('code') in (0, 200): - audit_staff(member, 'ORDER_DETAIL', request, resource_id=pick_order_id(request.data)) return resp except Exception as e: return _err(e) diff --git a/merchant_ops/views_templates.py b/merchant_ops/views_templates.py index 46138b3..bb104ed 100644 --- a/merchant_ops/views_templates.py +++ b/merchant_ops/views_templates.py @@ -1,4 +1,5 @@ """子客服专用:派单模板 / 链接(独立路径;读模板用 merchant_id,写模板需 template_manage)。""" +from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -7,12 +8,12 @@ from config.views import ( ShangjiaTianjiaMobanView, ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, - ShangjiaGenerateLinkView, - ShangjiaLianjieListView, ) from merchant_ops.helpers import proxy_as_merchant_user from merchant_ops.services.authz import StaffAuthError, get_active_staff_member, member_has_perm from merchant_ops.services.template_query import list_merchant_templates +from merchant_ops.services.link import staff_generate_link, staff_list_links +from merchant_ops.services.audit import audit_staff from merchant_ops.views_manage import _err @@ -52,7 +53,11 @@ class StaffTemplateCreateView(APIView): member = _require_staff(request) if not member_has_perm(member, 'template_manage'): raise StaffAuthError(403, '无权限添加模板') - return proxy_as_merchant_user(request, member, ShangjiaTianjiaMobanView, 'template_manage') + resp = proxy_as_merchant_user(request, member, ShangjiaTianjiaMobanView, 'template_manage') + if resp.status_code == 200 and resp.data.get('code') in (0, 200): + audit_staff(member, 'TEMPLATE_CREATE', request, + resource_id=(resp.data.get('data') or {}).get('id', '')) + return resp except Exception as e: return _err(e) @@ -65,7 +70,11 @@ class StaffTemplateUpdateView(APIView): member = _require_staff(request) if not member_has_perm(member, 'template_manage'): raise StaffAuthError(403, '无权限修改模板') - return proxy_as_merchant_user(request, member, ShangjiaGengxinMobanView, 'template_manage') + moban_id = request.data.get('mobanId') or request.data.get('id') or '' + resp = proxy_as_merchant_user(request, member, ShangjiaGengxinMobanView, 'template_manage') + if resp.status_code == 200 and resp.data.get('code') in (0, 200): + audit_staff(member, 'TEMPLATE_UPDATE', request, resource_id=str(moban_id)) + return resp except Exception as e: return _err(e) @@ -78,28 +87,51 @@ class StaffTemplateDeleteView(APIView): member = _require_staff(request) if not member_has_perm(member, 'template_manage'): raise StaffAuthError(403, '无权限删除模板') - return proxy_as_merchant_user(request, member, ShangjiaShanchuMobanView, 'template_manage') + moban_id = request.data.get('mobanId') or request.data.get('id') or '' + resp = proxy_as_merchant_user(request, member, ShangjiaShanchuMobanView, 'template_manage') + if resp.status_code == 200 and resp.data.get('code') in (0, 200): + audit_staff(member, 'TEMPLATE_DELETE', request, resource_id=str(moban_id)) + return resp except Exception as e: return _err(e) class StaffLinkGenerateView(APIView): + """POST /yonghu/merchant-staff/link/generate — 子客服独立实现,标记操作人身份。""" + permission_classes = [IsAuthenticated] def post(self, request): try: member = _require_staff(request) - return proxy_as_merchant_user(request, member, ShangjiaGenerateLinkView, 'order_dispatch') + if not member_has_perm(member, 'order_dispatch'): + raise StaffAuthError(403, '无派单/生成链接权限') + ok, payload = staff_generate_link(member, request.user, request.data) + if ok: + audit_staff( + member, 'LINK_GENERATE', request, + resource_id=payload.get('data', {}).get('dingdanId'), + amount=payload.get('data', {}).get('jiage'), + remark=f"{member.role.role_name}:{member.display_name or member.staff_user_id}", + ) + code = payload.get('code', 500) + return Response(payload, status=status.HTTP_200_OK if code in (0, 200) else status.HTTP_400_BAD_REQUEST) except Exception as e: return _err(e) class StaffLinkListView(APIView): + """POST /yonghu/merchant-staff/link/list — 子客服链接列表,含操作人身份。""" + permission_classes = [IsAuthenticated] def post(self, request): try: member = _require_staff(request) - return proxy_as_merchant_user(request, member, ShangjiaLianjieListView, 'order_dispatch') + if not member_has_perm(member, 'order_dispatch'): + raise StaffAuthError(403, '无权限') + ok, payload = staff_list_links(member, request.data) + code = payload.get('code', 500) + return Response(payload, status=status.HTTP_200_OK if code in (0, 200) else status.HTTP_400_BAD_REQUEST) except Exception as e: return _err(e)