From b2520726dce3eb50a19bcafa708bf30236aa36d7 Mon Sep 17 00:00:00 2001 From: XingQue Date: Sat, 20 Jun 2026 04:34:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B4=BE=E5=8D=95=E7=BB=9F=E8=AE=A1=E6=97=B6?= =?UTF-8?q?=E6=9C=BA=EF=BC=9A=E7=94=9F=E6=88=90=E9=93=BE=E6=8E=A5=E4=B8=8D?= =?UTF-8?q?=E8=AE=A1=E6=B4=BE=E5=8D=95=EF=BC=9B=E5=AE=A2=E6=88=B7=E5=A1=AB?= =?UTF-8?q?=E5=8D=95=E6=88=90=E5=8A=9F=E6=89=8D=E8=AE=A1=EF=BC=9B=E9=80=80?= =?UTF-8?q?=E6=AC=BE/=E7=BB=93=E5=8D=95=E5=9C=A8=E7=9C=9F=E6=AD=A3?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=97=B6=E7=BB=9F=E8=AE=A1=E3=80=82=20?= =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E6=97=A5=E7=BB=9F=E8=AE=A1=E8=81=94=E5=8A=A8?= =?UTF-8?q?=EF=BC=9Aupdate=5Fshangjia=5Fdaily(...,=20order=5Fid=3D...)=20?= =?UTF-8?q?=E4=BC=9A=E5=90=8C=E6=AD=A5=E6=9B=B4=E6=96=B0=E5=AF=B9=E5=BA=94?= =?UTF-8?q?=E6=B4=BE=E5=8D=95=E5=AE=A2=E6=9C=8D=E7=9A=84=E6=97=A5=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E3=80=82=20=E5=AE=A2=E6=9C=8D=20/me=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=9A=E8=BF=94=E5=9B=9E=E4=BB=8A=E6=97=A5/?= =?UTF-8?q?=E7=B4=AF=E8=AE=A1=E6=B4=BE=E5=8D=95=E3=80=81=E7=BB=93=E5=8D=95?= =?UTF-8?q?=E3=80=81=E9=80=80=E6=AC=BE=E7=AD=89=20stats=E3=80=82=20?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=E4=BF=AE=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=E9=A2=84=E7=BD=AE=E8=A7=92=E8=89=B2=EF=BC=88=E6=80=BB=E7=AE=A1?= =?UTF-8?q?/=E8=B4=A2=E5=8A=A1/=E6=B4=BE=E5=8D=95=E5=91=98=EF=BC=89?= =?UTF-8?q?=E6=AF=8F=E6=AC=A1=E8=AE=BF=E9=97=AE=E6=97=B6=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=9D=83=E9=99=90=EF=BC=8C=E6=8D=A2=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=90=8E=E6=9D=83=E9=99=90=E6=89=8D=E7=94=9F=E6=95=88?= =?UTF-8?q?=E3=80=82=20=E5=B1=95=E7=A4=BA=E5=A2=9E=E5=BC=BA=EF=BC=9A?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=AF=A6=E6=83=85=E5=B8=A6=E6=B4=BE=E5=8D=95?= =?UTF-8?q?=E5=91=98=E5=A4=B4=E5=83=8F=EF=BC=9B=E5=AE=A2=E6=9C=8D=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=B8=A6=E9=87=91=E9=A2=9D=E5=AD=97=E6=AE=B5=EF=BC=9B?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=90=8C=E6=84=8F=E9=80=80=E6=AC=BE?= =?UTF-8?q?=E8=A1=A5=E5=95=86=E5=AE=B6+=E5=AE=A2=E6=9C=8D=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- merchant_ops/services/bootstrap.py | 26 +++++++++++++---------- merchant_ops/services/stats.py | 34 ++++++++++++++++++++++++++++++ merchant_ops/views_manage.py | 34 +++++++++++++++++++++++++++--- merchant_ops/views_staff.py | 6 ++++++ 4 files changed, 86 insertions(+), 14 deletions(-) diff --git a/merchant_ops/services/bootstrap.py b/merchant_ops/services/bootstrap.py index c35ed40..440c322 100644 --- a/merchant_ops/services/bootstrap.py +++ b/merchant_ops/services/bootstrap.py @@ -13,6 +13,16 @@ def seed_global_permissions(): ) +def _sync_role_permissions(role, perm_codes): + """同步角色权限(系统预置角色与模板保持一致)。""" + perm_codes = set(perm_codes or []) + existing = set(role.role_permissions.values_list('perm_code', flat=True)) + for code in perm_codes - existing: + MerchantStaffRolePermission.objects.get_or_create(role=role, perm_code=code) + if role.is_system: + role.role_permissions.exclude(perm_code__in=perm_codes).delete() + + def ensure_system_role_templates(): seed_global_permissions() for tpl in SYSTEM_ROLE_TEMPLATES: @@ -27,18 +37,15 @@ def ensure_system_role_templates(): 'status': 1, }, ) - for perm in tpl['permissions']: - MerchantStaffRolePermission.objects.get_or_create( - role=role, perm_code=perm, - ) + _sync_role_permissions(role, tpl['permissions']) def ensure_merchant_roles(merchant_id): - """为商家复制系统角色模板(若尚未存在)。""" + """为商家复制系统角色模板,并同步预置角色权限。""" ensure_system_role_templates() templates = MerchantStaffRole.query.filter(merchant_id=None, status=1) for tpl in templates: - role, created = MerchantStaffRole.objects.get_or_create( + role, _ = MerchantStaffRole.objects.get_or_create( merchant_id=merchant_id, role_code=tpl.role_code, defaults={ @@ -49,8 +56,5 @@ def ensure_merchant_roles(merchant_id): 'status': 1, }, ) - if created: - for rp in tpl.role_permissions.all(): - MerchantStaffRolePermission.objects.get_or_create( - role=role, perm_code=rp.perm_code, - ) + tpl_perms = list(tpl.role_permissions.values_list('perm_code', flat=True)) + _sync_role_permissions(role, tpl_perms) diff --git a/merchant_ops/services/stats.py b/merchant_ops/services/stats.py index 55f7c48..fab536e 100644 --- a/merchant_ops/services/stats.py +++ b/merchant_ops/services/stats.py @@ -98,6 +98,40 @@ def update_merchant_and_staff_stats(merchant_id, staff_member, amount, action, o ) +def get_staff_stats_payload(member): + """子客服个人统计(今日 + 累计),供 /me 与首页展示。""" + from django.db.models import Sum + + today = date.today() + today_stat = MerchantStaffDailyStats.objects.filter( + member_id=member.id, stat_date=today, + ).first() + total = MerchantStaffDailyStats.objects.filter(member_id=member.id).aggregate( + dispatch_count=Sum('dispatch_count'), + dispatch_amount=Sum('dispatch_amount'), + settle_count=Sum('settle_count'), + settle_amount=Sum('settle_amount'), + refund_count=Sum('refund_count'), + refund_amount=Sum('refund_amount'), + ) + return { + 'today_dispatch_count': ( + today_stat.dispatch_count if today_stat else int(member.today_dispatch_count or 0) + ), + 'today_dispatch_amount': str( + today_stat.dispatch_amount if today_stat else (member.today_dispatch_amount or '0.00') + ), + 'today_refund_count': today_stat.refund_count if today_stat else 0, + 'today_settle_count': today_stat.settle_count if today_stat else 0, + 'dispatch_count': total['dispatch_count'] or 0, + 'dispatch_amount': str(total['dispatch_amount'] or '0.00'), + 'settle_count': total['settle_count'] or 0, + 'settle_amount': str(total['settle_amount'] or '0.00'), + 'refund_count': total['refund_count'] or 0, + 'refund_amount': str(total['refund_amount'] or '0.00'), + } + + def bump_cancel_count(merchant_id, staff_member): if not staff_member: return diff --git a/merchant_ops/views_manage.py b/merchant_ops/views_manage.py index 6ca5ae2..73906a9 100644 --- a/merchant_ops/views_manage.py +++ b/merchant_ops/views_manage.py @@ -45,6 +45,18 @@ def _role_prefix(role_code): return m.get(role_code, 'KF') +def _role_hint(role_code, tpl_desc=None): + hints = { + 'CHIEF': '总管:除老板专属外,订单/派单/客服/额度/罚单等均可操作', + 'FINANCE': '财务:查看余额与额度、划额度、看全部订单与统计,不可派单结单', + 'DISPATCHER': '派单员:派单发单、只看自己派的单,不可结单退款', + } + if role_code in hints: + return hints[role_code] + name = (tpl_desc or {}).get(role_code) or role_code + return f'自定义角色:{name}' + + class StaffInviteCreateView(APIView): permission_classes = [IsAuthenticated] @@ -188,15 +200,24 @@ class StaffMemberUpdateRoleView(APIView): ensure_owner_merchant(request.user.UserUID, request.user) member_id = request.data.get('member_id') role_id = request.data.get('role_id') - m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID, status=MEMBER_STATUS_ACTIVE) - role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID) + m = MerchantStaffMember.query.get( + id=member_id, merchant_id=request.user.UserUID, status=MEMBER_STATUS_ACTIVE, + ) + role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID, status=1) old = m.role_id m.role = role m.save(update_fields=['role']) audit_owner(request.user.UserUID, request.user.UserUID, 'MEMBER_UPDATE_ROLE', request, resource_type='member', resource_id=member_id, value_before={'role_id': old}, value_after={'role_id': role_id}) - return Response({'code': 0, 'msg': '成功', 'data': None}) + perms = list(role.role_permissions.values_list('perm_code', flat=True)) + return Response({'code': 0, 'msg': '角色已更新', 'data': { + 'member_id': m.id, + 'role_id': role.id, + 'role_code': role.role_code, + 'role_name': role.role_name, + 'permissions': perms, + }}) except Exception as e: return _err(e) @@ -242,14 +263,21 @@ class StaffRoleListView(APIView): def post(self, request): try: + from merchant_ops.constants import PERMISSION_DEFINITIONS, SYSTEM_ROLE_TEMPLATES ensure_owner_merchant(request.user.UserUID, request.user) roles = MerchantStaffRole.query.filter(merchant_id=request.user.UserUID, status=1) + tpl_desc = {t['role_code']: t['role_name'] for t in SYSTEM_ROLE_TEMPLATES} data = [] for r in roles: perms = list(r.role_permissions.values_list('perm_code', flat=True)) + perm_labels = [ + dict(PERMISSION_DEFINITIONS).get(p, p) for p in perms + ] data.append({ 'id': r.id, 'role_code': r.role_code, 'role_name': r.role_name, 'is_system': r.is_system, 'permissions': perms, + 'permission_labels': perm_labels, + 'role_hint': _role_hint(r.role_code, tpl_desc), }) all_perms = list(MerchantStaffPermission.query.filter(status=1).order_by('sort_order').values( 'perm_code', 'perm_name')) diff --git a/merchant_ops/views_staff.py b/merchant_ops/views_staff.py index e446c57..0349376 100644 --- a/merchant_ops/views_staff.py +++ b/merchant_ops/views_staff.py @@ -13,7 +13,9 @@ from merchant_ops.services.authz import ( StaffAuthError, assert_can_bind_staff, get_active_staff_member, resolve_staff_context, ) from merchant_ops.services.audit import audit_staff +from merchant_ops.services.stats import get_staff_stats_payload from merchant_ops.services.wallet import get_or_create_wallet +from merchant_ops.services.bootstrap import ensure_merchant_roles from users.business_models import User from users.models import UserShangjia @@ -99,9 +101,13 @@ class StaffMeView(APIView): if not ctx: return Response({'code': 404, 'msg': '非子客服身份', 'data': None}) member = get_active_staff_member(request.user) + ensure_merchant_roles(member.merchant_id) + member.refresh_from_db() + ctx = resolve_staff_context(request.user) shangjia = UserShangjia.query.filter(user__UserUID=member.merchant_id).first() ctx['merchant_display'] = { 'nicheng': shangjia.nicheng if shangjia else '', 'sjyue': str(shangjia.yue) if shangjia else '0', } + ctx['stats'] = get_staff_stats_payload(member) return Response({'code': 0, 'msg': '成功', 'data': ctx})