diff --git a/backend/utils.py b/backend/utils.py index 81d94d9..4a35203 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -77,6 +77,37 @@ class _AllPermissions(list): return True +# 2 号超级管理(权限码 000002):接近本店超管,但敏感码必须显式持有 +CLUB_SECOND_SUPER_CODE = '000002' +CLUB_SUPER_CODE = '000001' + +# 2 号超管默认不拥有的敏感码(须角色显式绑定才放行;角色配置时也不展示给纯 2 号) +SECOND_SUPER_EXPLICIT_ONLY_CODES = frozenset({ + '000001', # 旧本店超管:不可碰、不可赋 + '005bb', # 提现审核通过/驳回 + 'bankuai', # 板块配置 + 'djcfg001', # 资金冻结规则 + 'djled001', # 资金冻结明细 + 'djrisk001', # 罚款投诉指标 +}) + +# 角色权限配置里对纯 2 号额外隐藏(含集团相关分类在视图层再滤) +SECOND_SUPER_ROLE_CONFIG_HIDDEN_CODES = frozenset(SECOND_SUPER_EXPLICIT_ONLY_CODES) + + +class _SecondSuperPermissions(list): + """ + 仅持有 000002(无 000001)时使用:多数权限码 in 检查视为 True, + 但 SECOND_SUPER_EXPLICIT_ONLY_CODES 必须出现在真实列表里才通过。 + 这样旧闸门里大量 `xxx in permissions` 对 2 号自动放行,又不动 000001 行为。 + """ + + def __contains__(self, item): + if item in SECOND_SUPER_EXPLICIT_ONLY_CODES: + return list.__contains__(self, item) + return True + + def has_perm_code(permissions, *codes): """权限码 in 检查;系统超管 _AllPermissions 视为全部通过。""" if isinstance(permissions, _AllPermissions): @@ -86,12 +117,68 @@ def has_perm_code(permissions, *codes): def has_club_super_perm(permissions): - """本店超级管理 000001(含系统超管 _AllPermissions)。""" + """本店超级管理 000001(含系统超管 _AllPermissions)。不含 000002。""" if isinstance(permissions, _AllPermissions): return True + # 勿用 `in permissions`:_SecondSuperPermissions 对非敏感码恒为 True + if isinstance(permissions, _SecondSuperPermissions): + return list.__contains__(permissions, '000001') return '000001' in (permissions or []) +def has_second_super_perm(permissions): + """是否持有 2 号超管 000002(系统超管视为 True)。""" + if isinstance(permissions, _AllPermissions): + return True + if isinstance(permissions, _SecondSuperPermissions): + return True + return CLUB_SECOND_SUPER_CODE in (permissions or []) + + +def is_second_super_only(permissions): + """纯 2 号:有 000002、无 000001、非系统超管展开。""" + if isinstance(permissions, _AllPermissions): + return False + if isinstance(permissions, _SecondSuperPermissions): + return True + perms = permissions or [] + return CLUB_SECOND_SUPER_CODE in perms and '000001' not in perms + + +def has_role_manage_perm(permissions): + """可维护本店角色/后台用户:000001 或 000002 或系统超管。""" + if isinstance(permissions, _AllPermissions): + return True + if isinstance(permissions, _SecondSuperPermissions): + return True + perms = permissions or [] + return '000001' in perms or CLUB_SECOND_SUPER_CODE in perms + + +def wrap_second_super_permissions(permissions): + """若仅有 000002,则包装为 _SecondSuperPermissions;其余原样返回。""" + if isinstance(permissions, (_AllPermissions, _SecondSuperPermissions)): + return permissions + perms = list(permissions or []) + if CLUB_SECOND_SUPER_CODE in perms and '000001' not in perms: + return _SecondSuperPermissions(perms) + return permissions + + +def second_super_display_perm_name(perm_code, perm_name, viewer_permissions): + """ + 展示名:查看者有旧超管时,000002 显示「2号超级管理员」; + 否则显示「超级管理员」(不标 2 号)。 + """ + code = (perm_code or '').strip() + name = (perm_name or '').strip() or code + if code != CLUB_SECOND_SUPER_CODE: + return name + if has_club_super_perm(viewer_permissions): + return '2号超级管理员' + return '超级管理员' + + # 本店超管可直接使用的核心业务码(不必再绑集团权限) CLUB_SUPER_IMPLIED_PERMS = frozenset({ '002ab', # 平台订单 @@ -100,6 +187,13 @@ CLUB_SUPER_IMPLIED_PERMS = frozenset({ '005bb', # 提现审核 }) +# 2 号超管隐含核心业务码(不含提现审核) +CLUB_SECOND_SUPER_IMPLIED_PERMS = frozenset({ + '002ab', + '002ac', + '003aa', +}) + def has_perm_code_or_club_super(permissions, *codes): """指定业务码,或本店超管 000001(对核心业务码生效)。""" @@ -109,7 +203,6 @@ def has_perm_code_or_club_super(permissions, *codes): return True return False - def has_merchant_order_permission(permissions): """商家/链接订单处理权限:002ac 商家订单、002ab 平台订单、003aa 跨平台;本店超管 000001 亦可。""" return has_perm_code_or_club_super(permissions, '002ac', '002ab', '003aa') @@ -274,6 +367,9 @@ def verify_kefu_permission(request, username_from_frontend=None): if '000001' not in permissions: permissions.insert(0, '000001') permissions = _AllPermissions(permissions) + else: + # 纯 000002:多数业务闸门放行,敏感码仍须显式持有(不影响 000001) + permissions = wrap_second_super_permissions(permissions) if not permissions: logger.warning(f"用户 {current_user.Phone} 没有任何权限,请检查角色分配") @@ -283,7 +379,6 @@ def verify_kefu_permission(request, username_from_frontend=None): - def update_dashou_daily_by_action(yonghuid, amount, action, club_id=None): """ 根据行为类型更新打手每日统计(接单、成交、退款) diff --git a/backend/views/roles.py b/backend/views/roles.py index 9422344..8f93b3d 100644 --- a/backend/views/roles.py +++ b/backend/views/roles.py @@ -63,6 +63,11 @@ from ..utils import ( XIUGAI_LEIXING_SHANGJIA, XIUGAI_LEIXING_ZUZHANG, XIUGAI_LEIXING_LABEL, + has_role_manage_perm, + has_club_super_perm, + is_second_super_only, + second_super_display_perm_name, + SECOND_SUPER_ROLE_CONFIG_HIDDEN_CODES, ) # models 集中导入 @@ -114,23 +119,79 @@ from ..serializers import PopupPageSerializer logger = logging.getLogger('houtai') SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] -def _serialize_permission_row(perm_row): +def _serialize_permission_row(perm_row, viewer_permissions=None): """gvsdsdk Permission → 后台 Vue 约定的小写字段""" if isinstance(perm_row, dict): code = perm_row.get('PermCode') or perm_row.get('perm_code') or '' name = perm_row.get('PermName') or perm_row.get('perm_name') or code desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or '' + category = perm_row.get('PermCategory') or perm_row.get('perm_category') or '' else: code = perm_row.PermCode or '' name = perm_row.PermName or code desc = perm_row.PermDesc or '' - return {'perm_code': code, 'perm_name': name, 'perm_desc': desc} + category = getattr(perm_row, 'PermCategory', '') or '' + if viewer_permissions is not None: + name = second_super_display_perm_name(code, name, viewer_permissions) + return { + 'perm_code': code, + 'perm_name': name, + 'perm_desc': desc, + 'perm_category': category, + } + + +def _perm_hidden_for_second_super(perm_row): + """纯 2 号在角色配置可选列表中不可见的权限。""" + if isinstance(perm_row, dict): + code = perm_row.get('PermCode') or perm_row.get('perm_code') or '' + category = perm_row.get('PermCategory') or perm_row.get('perm_category') or '' + else: + code = getattr(perm_row, 'PermCode', '') or '' + category = getattr(perm_row, 'PermCategory', '') or '' + if code in SECOND_SUPER_ROLE_CONFIG_HIDDEN_CODES: + return True + if '集团' in (category or ''): + return True + return False + + +def _role_has_perm_code(role, perm_code): + try: + return RolePermission.objects.filter( + RoleUUID=role.RoleUUID, + PermUUID__in=Permission.objects.filter( + PermCode=perm_code, PermStatus=1, + ).values_list('PermUUID', flat=True), + ).exists() + except Exception: + return False + + +def _second_super_forbid_touch_club_super(permissions, perm_code, action_label='操作'): + """纯 2 号不可碰旧超管码 000001。""" + if not is_second_super_only(permissions): + return None + if (perm_code or '').strip() == '000001': + return Response({'code': 403, 'msg': f'无权{action_label}旧超级管理员权限(000001)'}) + if (perm_code or '').strip() in SECOND_SUPER_ROLE_CONFIG_HIDDEN_CODES: + return Response({'code': 403, 'msg': f'无权{action_label}该权限'}) + return None + + +def _second_super_forbid_bind_club_super_role(permissions, role): + """纯 2 号不可把带 000001 的角色绑给别人。""" + if not is_second_super_only(permissions): + return None + if _role_has_perm_code(role, '000001') or (role.RoleName or '') in ('管理员', '超级管理员'): + return Response({'code': 403, 'msg': '无权分配旧超级管理员角色'}) + return None class GetRolePermissionView(APIView): """ 获取所有角色、权限以及角色关联的用户数 - 权限要求:用户必须拥有 000001 权限 + 权限要求:000001 或 000002(或集团/系统超管) """ permission_classes = [IsAuthenticated] @@ -148,9 +209,17 @@ class GetRolePermissionView(APIView): if not can_access_admin_user_management(request.user, permissions): return Response({'code': 403, 'msg': '您无权访问此页面'}) - # 3. 获取所有权限 - all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc') - permissions_list = [_serialize_permission_row(p) for p in all_perms] + # 3. 获取所有权限(纯 2 号不展示旧超管/提现/冻结/板块/集团类) + all_perms = list( + Permission.objects.filter(PermStatus=1).values( + 'PermCode', 'PermName', 'PermDesc', 'PermCategory', + ) + ) + if is_second_super_only(permissions): + all_perms = [p for p in all_perms if not _perm_hidden_for_second_super(p)] + permissions_list = [ + _serialize_permission_row(p, viewer_permissions=permissions) for p in all_perms + ] from jituan.services.club_rbac import count_role_users_in_scope, filter_roles_for_request @@ -168,13 +237,16 @@ class GetRolePermissionView(APIView): _perm_rows_by_uuid = { p['PermUUID']: p for p in Permission.objects.filter(PermUUID__in=_perm_uuids) - .values('PermUUID', 'PermCode', 'PermName', 'PermDesc') + .values('PermUUID', 'PermCode', 'PermName', 'PermDesc', 'PermCategory') } if _perm_uuids else {} _perms_by_role = {} for role_uuid, perm_uuid in _rp_pairs: perm_row = _perm_rows_by_uuid.get(perm_uuid) if perm_row is not None: - _perms_by_role.setdefault(role_uuid, []).append(_serialize_permission_row(perm_row)) + # 角色已绑权限仍展示(避免保存时误删);仅可选列表做隐藏 + _perms_by_role.setdefault(role_uuid, []).append( + _serialize_permission_row(perm_row, viewer_permissions=permissions) + ) roles_data = [] for role in roles: @@ -197,7 +269,6 @@ class GetRolePermissionView(APIView): } }) - class ModifyRolePermissionView(APIView): """ 通用接口:支持删除角色、为角色添加权限、移除角色权限 @@ -215,7 +286,7 @@ class ModifyRolePermissionView(APIView): kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions - if '000001' not in permissions: + if not has_role_manage_perm(permissions): return Response({'code': 403, 'msg': '您无权进行此操作'}) # 获取角色(按当前俱乐部解析,避免多俱乐部同名角色冲突) @@ -243,6 +314,8 @@ class ModifyRolePermissionView(APIView): # 根据 action 处理 if action == 'delete_role': + if is_second_super_only(permissions) and _role_has_perm_code(role, '000001'): + return Response({'code': 403, 'msg': '无权删除带旧超级管理员权限的角色'}) # 删除角色:同时删除角色权限关联、用户角色关联 with transaction.atomic(): RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete() @@ -254,6 +327,9 @@ class ModifyRolePermissionView(APIView): perm_code = request.data.get('perm_code') if not perm_code: return Response({'code': 400, 'msg': '缺少权限编码'}) + deny = _second_super_forbid_touch_club_super(permissions, perm_code, '添加') + if deny: + return deny if role.RoleName == '管理员' and perm_code == '000001': return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'}) try: @@ -275,6 +351,9 @@ class ModifyRolePermissionView(APIView): perm_code = request.data.get('perm_code') if not perm_code: return Response({'code': 400, 'msg': '缺少权限编码'}) + deny = _second_super_forbid_touch_club_super(permissions, perm_code, '移除') + if deny: + return deny if role.RoleName == '管理员' and perm_code == '000001': return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'}) try: @@ -298,6 +377,22 @@ class ModifyRolePermissionView(APIView): RoleName=role_name, ClubID=role_club_id, ).exclude(RoleUUID=role.RoleUUID).exists(): return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'}) + # 纯 2 号:保存时保留角色上已有的敏感码,避免因前端未展示而误删 + preserve_codes = set() + if is_second_super_only(permissions): + existing_codes = set( + Permission.objects.filter( + PermUUID__in=RolePermission.objects.filter( + RoleUUID=role.RoleUUID, + ).values_list('PermUUID', flat=True), + PermStatus=1, + ).values_list('PermCode', flat=True) + ) + preserve_codes = existing_codes & SECOND_SUPER_ROLE_CONFIG_HIDDEN_CODES + perm_codes = [ + pc for pc in (perm_codes or []) + if pc not in SECOND_SUPER_ROLE_CONFIG_HIDDEN_CODES + ] with transaction.atomic(): if role.RoleName != '管理员': if role_name: @@ -308,6 +403,8 @@ class ModifyRolePermissionView(APIView): for pc in perm_codes: if pc == '000001' and role.RoleName != '管理员': continue + if is_second_super_only(permissions) and pc == '000001': + continue try: perm = Permission.objects.get(PermCode=pc, PermStatus=1) except Permission.DoesNotExist: @@ -318,6 +415,20 @@ class ModifyRolePermissionView(APIView): PermUUID=perm.PermUUID, CreateTime=timezone.now(), ) + for pc in preserve_codes: + try: + perm = Permission.objects.get(PermCode=pc, PermStatus=1) + except Permission.DoesNotExist: + continue + if not RolePermission.objects.filter( + RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID, + ).exists(): + RolePermission.objects.create( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=role.RoleUUID, + PermUUID=perm.PermUUID, + CreateTime=timezone.now(), + ) if role.RoleName == '管理员': try: super_perm = Permission.objects.get(PermCode='000001', PermStatus=1) @@ -333,7 +444,6 @@ class ModifyRolePermissionView(APIView): except Permission.DoesNotExist: pass return Response({'code': 0, 'msg': '角色保存成功'}) - else: return Response({'code': 400, 'msg': '无效的 action'}) @@ -357,7 +467,7 @@ class AddRoleView(APIView): kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions - if '000001' not in permissions: + if not has_role_manage_perm(permissions): return Response({'code': 403, 'msg': '您无权进行此操作'}) # 检查角色名称是否已存在(同俱乐部内唯一);单俱乐部视图强制写入当前顶栏俱乐部 @@ -395,8 +505,13 @@ class AddRoleView(APIView): # 不允许添加 000001 权限(此权限仅供超级管理员) if pc == '000001': continue + # 纯 2 号不可写入敏感/集团类权限 + if is_second_super_only(permissions) and pc in SECOND_SUPER_ROLE_CONFIG_HIDDEN_CODES: + continue try: perm = Permission.objects.get(PermCode=pc) + if is_second_super_only(permissions) and _perm_hidden_for_second_super(perm): + continue RolePermission.objects.create( RolePermUUID=uuid.uuid4().bytes, RoleUUID=role.RoleUUID, @@ -409,7 +524,6 @@ class AddRoleView(APIView): return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}}) - class GetAdminUserListView(APIView): permission_classes = [IsAuthenticated] @@ -557,10 +671,10 @@ class GetAdminUserListView(APIView): request.user, permissions, current_club_id=curr_club, )) if scope != DATA_SCOPE_ALL: - # 顶栏在具体俱乐部时,可管理集合收敛到当前店(含超级管理 000001) + # 顶栏在具体俱乐部时,可管理集合收敛到当前店(含超级管理 000001/000002) manageable = [c for c in manageable if c == curr_club] # 兜底:本店超级管理一定能管本店用户(不依赖集团任职) - if not manageable and curr_club and permissions and '000001' in permissions: + if not manageable and curr_club and permissions and has_role_manage_perm(permissions): manageable = [curr_club] return Response({ 'code': 0, @@ -571,14 +685,13 @@ class GetAdminUserListView(APIView): 'pageSize': page_size, 'can_manage_club_assignments': can_manage_admin_assignments(request.user, permissions), 'can_manage_club_users': bool(manageable) or ( - bool(permissions) and '000001' in permissions and scope != DATA_SCOPE_ALL + bool(permissions) and has_role_manage_perm(permissions) and scope != DATA_SCOPE_ALL ), 'manageable_club_ids': manageable, 'current_club_id': curr_club, 'scope': scope, } }) - except Exception as e: logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}") return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}) @@ -734,10 +847,10 @@ class ModifyAdminUserView(APIView): if not can_manage_admin_assignments(request.user, permissions): return Response({'code': 403, 'msg': '无权分配俱乐部任职(需集团权限)'}) elif action in role_manage_actions: - # 本店超级管理 000001 可给本俱乐部用户增删功能角色 + # 本店超级管理 000001/000002 可给本俱乐部用户增删功能角色 if not can_access_admin_user_management(request.user, permissions): return Response({'code': 403, 'msg': '您无权管理角色分配'}) - elif not can_manage_admin_assignments(request.user, permissions) and '000001' not in permissions: + elif not can_manage_admin_assignments(request.user, permissions) and not has_role_manage_perm(permissions): return Response({'code': 403, 'msg': '您无权进行此操作'}) target_yonghuid = request.data.get('yonghuid', '').strip() @@ -800,7 +913,7 @@ class ModifyAdminUserView(APIView): return Response({'code': 400, 'msg': '请先切换到具体俱乐部再分配角色'}) if not ( can_manage_roles_for_club(request.user, permissions, bind_club_id) - or ('000001' in permissions) + or has_role_manage_perm(permissions) ): return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'}) added, missing = [], [] @@ -813,6 +926,9 @@ class ModifyAdminUserView(APIView): if role_club and role_club != bind_club_id: missing.append(rc) continue + deny_bind = _second_super_forbid_bind_club_super_role(permissions, role) + if deny_bind: + return deny_bind _, created = UserRole.objects.get_or_create( UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID, @@ -849,12 +965,15 @@ class ModifyAdminUserView(APIView): return Response({'code': 400, 'msg': '请先切换到具体俱乐部再操作角色'}) if not ( can_manage_roles_for_club(request.user, permissions, bind_club_id) - or ('000001' in permissions) + or has_role_manage_perm(permissions) ): return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'}) role = resolve_role_by_name(role_code, bind_club_id) if not role: return Response({'code': 404, 'msg': '角色不存在或与所选俱乐部不匹配'}) + deny_bind = _second_super_forbid_bind_club_super_role(permissions, role) + if deny_bind: + return deny_bind UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete() return Response({'code': 0, 'msg': '角色移除成功'}) @@ -924,11 +1043,11 @@ class AddAdminUserView(APIView): target_club_id = resolve_club_id_from_request(request) if resolve_club_scope(request) == DATA_SCOPE_ALL or not target_club_id: return Response({'code': 400, 'msg': '请先切换到具体俱乐部再添加用户'}) - # 俱乐部超级管理 000001 即可添加本店后台用户,无需集团权限 + # 俱乐部超级管理 000001/000002 即可添加本店后台用户,无需集团权限 if not ( can_manage_admin_assignments(request.user, permissions) or can_manage_roles_for_club(request.user, permissions, target_club_id) - or ('000001' in permissions) + or has_role_manage_perm(permissions) ): return Response({'code': 403, 'msg': '您无权在该俱乐部添加用户'}) @@ -974,6 +1093,10 @@ class AddAdminUserView(APIView): }) if not roles_to_bind: return Response({'code': 400, 'msg': '请至少选择一个有效角色'}) + for role in roles_to_bind: + deny_bind = _second_super_forbid_bind_club_super_role(permissions, role) + if deny_bind: + return deny_bind with transaction.atomic(): # 事务内二次校验,防止并发重复创建 diff --git a/jituan/management/commands/seed_gvsdsdk_permissions.py b/jituan/management/commands/seed_gvsdsdk_permissions.py index 378d1db..4d17c30 100644 --- a/jituan/management/commands/seed_gvsdsdk_permissions.py +++ b/jituan/management/commands/seed_gvsdsdk_permissions.py @@ -80,6 +80,13 @@ EXTRA_PERMISSIONS = [ '修改平台订单/本店商品的抢单要求类型(会员/押金门槛)。须显式绑定;本店超管 000001 不会自动拥有。', '订单管理', ), + ( + '000002', + '超级管理员', + '2号超级管理员:接近本店超管;不可碰000001/提现审核/资金冻结/成交自动结算/板块/集团配置。', + '系统', + ), + # ---------- 店铺管理(999 系列:列表/详情/复制/提现等) ---------- ('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'), ('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'), diff --git a/jituan/management/commands/seed_second_super_perm.py b/jituan/management/commands/seed_second_super_perm.py new file mode 100644 index 0000000..f1569fa --- /dev/null +++ b/jituan/management/commands/seed_second_super_perm.py @@ -0,0 +1,137 @@ +"""写入 2 号超级管理员权限码 000002(可重复执行)。 + +权限码: + 000002 超级管理员(展示名:有旧超管 000001 的查看者看到「2号超级管理员」) + +说明: + - 不修改 000001 行为 + - 默认不自动挂到任何角色;挂角色请显式传 --club-id / --role-name + +用法: + python manage.py seed_second_super_perm + python manage.py seed_second_super_perm --club-id eh --role-name 运营管理 + python manage.py seed_second_super_perm --dry-run +""" +import uuid + +from django.core.management.base import BaseCommand +from django.db import transaction +from django.utils import timezone + +from gvsdsdk.models import Permission, Role, RolePermission + +from jituan.models import Club + +PERM_CODE = '000002' +PERM_NAME = '超级管理员' +PERM_DESC = ( + '2号超级管理员:可维护本店角色与后台用户,业务能力接近本店超管;' + '不可碰旧超管000001、不可提现审核、不可资金冻结/成交自动结算/板块配置/集团配置。' +) +PERM_CATEGORY = '系统' + + +class Command(BaseCommand): + help = '写入 2 号超级管理员权限码 000002' + + def add_arguments(self, parser): + parser.add_argument('--club-id', default='', help='仅处理该俱乐部(配合 --role-name)') + parser.add_argument('--all-clubs', action='store_true') + parser.add_argument('--role-name', default='', help='角色名;空则只确保权限码') + parser.add_argument('--dry-run', action='store_true') + + def handle(self, *args, **options): + dry_run = options['dry_run'] + club_id = (options.get('club_id') or '').strip() + all_clubs = options.get('all_clubs') + role_name = (options.get('role_name') or '').strip() + + sample = Permission.objects.filter(PermStatus=1).first() + tenant_uuid = sample.TenantUUID if sample else uuid.UUID( + '00000000-0000-0000-0000-000000000001' + ).bytes + + perm = Permission.objects.filter(PermCode=PERM_CODE).first() + if perm: + self.stdout.write(f' 权限码已存在: {PERM_CODE}') + if not dry_run: + changed = False + if perm.PermName != PERM_NAME: + perm.PermName = PERM_NAME + changed = True + if (perm.PermDesc or '') != PERM_DESC: + perm.PermDesc = PERM_DESC + changed = True + if (perm.PermCategory or '') != PERM_CATEGORY: + perm.PermCategory = PERM_CATEGORY + changed = True + if perm.PermStatus != 1: + perm.PermStatus = 1 + changed = True + if changed: + perm.UpdateTime = timezone.now() + perm.save(update_fields=[ + 'PermName', 'PermDesc', 'PermCategory', 'PermStatus', 'UpdateTime', + ]) + self.stdout.write(self.style.SUCCESS(' 已更新权限元数据')) + else: + if dry_run: + self.stdout.write(f'would create perm {PERM_CODE}') + else: + perm = Permission.objects.create( + PermUUID=uuid.uuid4().bytes, + TenantUUID=tenant_uuid, + PermCode=PERM_CODE, + PermName=PERM_NAME, + PermDesc=PERM_DESC, + PermCategory=PERM_CATEGORY, + PermStatus=1, + CreateTime=timezone.now(), + UpdateTime=timezone.now(), + ) + self.stdout.write(self.style.SUCCESS(f' 已创建权限码: {PERM_CODE}')) + + if not role_name: + self.stdout.write('未指定 --role-name,跳过挂角色') + self.stdout.write(self.style.SUCCESS( + '完成。请在「角色与权限」里给需要的角色勾选 000002(超级管理员)。' + )) + return + + if not perm: + self.stdout.write(self.style.ERROR('权限码不存在,无法挂角色')) + return + + clubs = [] + if all_clubs: + clubs = list(Club.query.filter(status=1).values_list('club_id', flat=True)) + elif club_id: + clubs = [club_id] + else: + self.stdout.write(self.style.WARNING('请指定 --club-id 或 --all-clubs')) + return + + with transaction.atomic(): + for cid in clubs: + roles = Role.objects.filter(RoleName=role_name, ClubID=cid, RoleStatus=1) + if not roles.exists(): + roles = Role.objects.filter(RoleName=role_name, RoleStatus=1) + roles = [r for r in roles if (getattr(r, 'ClubID', None) or '') == cid] + for role in roles: + exists = RolePermission.objects.filter( + RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID, + ).exists() + if exists: + self.stdout.write(f' 已挂: {cid} {role_name} <- {PERM_CODE}') + continue + if dry_run: + self.stdout.write(f'would link {cid} {role_name} <- {PERM_CODE}') + else: + RolePermission.objects.create( + RoleUUID=role.RoleUUID, + PermUUID=perm.PermUUID, + CreateTime=timezone.now(), + ) + self.stdout.write(self.style.SUCCESS( + f' 已挂: {cid} {role_name} <- {PERM_CODE}' + )) diff --git a/jituan/services/admin_context.py b/jituan/services/admin_context.py index cb9a7a0..b17d091 100644 --- a/jituan/services/admin_context.py +++ b/jituan/services/admin_context.py @@ -215,25 +215,28 @@ def get_switchable_club_ids(user): def can_manage_roles_for_club(user, permissions, club_id): """ 是否可为指定俱乐部分配/移除功能角色、添加本店后台用户。 - 集团高层:全部俱乐部;否则须在该俱乐部拥有 000001 超级管理(不必有集团权限)。 + 集团高层:全部俱乐部;否则须在该俱乐部拥有 000001/000002(不必有集团权限)。 """ if can_manage_admin_assignments(user, permissions): return True cid = (club_id or '').strip() if not cid: return False + from backend.utils import has_role_manage_perm from jituan.constants import DATA_SCOPE_SINGLE from jituan.services.club_rbac import resolve_effective_permission_codes # 与菜单/verify_kefu 同一套:含 legacy,避免「页面能进但添加按钮出不来」 codes = resolve_effective_permission_codes(user, cid, DATA_SCOPE_SINGLE) - return '000001' in (codes or []) + return has_role_manage_perm(codes) def get_manageable_club_ids(user, permissions=None, current_club_id=None): """ 当前操作者可管理角色/后台用户的俱乐部集合。 - current_club_id:若请求上下文已带 000001,务必把当前顶栏俱乐部算进去。 + current_club_id:若请求上下文已带 000001/000002,务必把当前顶栏俱乐部算进去。 """ + from backend.utils import has_role_manage_perm + if can_manage_admin_assignments(user, permissions): return {c.club_id for c in Club.query.filter(status=1)} ids = { @@ -241,7 +244,7 @@ def get_manageable_club_ids(user, permissions=None, current_club_id=None): if can_manage_roles_for_club(user, permissions, c.club_id) } cid = (current_club_id or '').strip() - if cid and permissions and '000001' in permissions: + if cid and permissions and has_role_manage_perm(permissions): ids.add(cid) return ids @@ -249,17 +252,18 @@ def get_manageable_club_ids(user, permissions=None, current_club_id=None): def can_access_admin_user_management(user, permissions=None): """ 是否可访问「后台用户」页(列表、按俱乐部分配功能角色)。 - 系统超管 / 000001 / 集团高管 / 任一俱乐部超管均可。 + 系统超管 / 000001 / 000002 / 集团高管 / 任一俱乐部超管均可。 """ + from backend.utils import has_role_manage_perm + if is_system_super_admin(user): return True - if permissions and '000001' in permissions: + if permissions and has_role_manage_perm(permissions): return True if can_manage_admin_assignments(user, permissions): return True return bool(get_manageable_club_ids(user, permissions)) - def _pack(scope, club_id, is_group_admin, assignments, clubs, can_switch_club, role_code, role_name): allowed_club_ids = sorted({ diff --git a/jituan/services/kefu_menu.py b/jituan/services/kefu_menu.py index 961aac2..964de0f 100644 --- a/jituan/services/kefu_menu.py +++ b/jituan/services/kefu_menu.py @@ -2,7 +2,15 @@ import logging from types import SimpleNamespace -from backend.utils import _AllPermissions, verify_kefu_permission, CLUB_SUPER_IMPLIED_PERMS +from backend.utils import ( + _AllPermissions, + verify_kefu_permission, + CLUB_SUPER_IMPLIED_PERMS, + CLUB_SECOND_SUPER_IMPLIED_PERMS, + has_club_super_perm, + has_role_manage_perm, + is_second_super_only, +) from jituan.models import KefuMenuPage from jituan.services.admin_context import ( build_admin_club_context, @@ -17,16 +25,26 @@ from jituan.services.kefu_menu_definitions import KEFU_MENU_ROW_DEFS logger = logging.getLogger(__name__) +# 纯 2 号超管不可见的菜单(即便隐含订单权限也不展示成交配置页) +_SECOND_SUPER_HIDDEN_PAGE_IDS = frozenset({ + 'order.deal-config', + 'finance.bankuai', + 'finance.fund-freeze-config', + 'finance.fund-freeze-ledger', + 'finance.fund-risk-stats', + 'withdraw.audit', + 'withdraw.detail', + 'withdraw.data', +}) + def _is_super_permissions(permissions): return isinstance(permissions, _AllPermissions) def _has_super_perm(permissions): - """俱乐部超级管理权限 000001(角色/后台用户等),不含集团级配置。""" - if _is_super_permissions(permissions): - return True - return '000001' in permissions + """俱乐部超级管理权限 000001(角色/后台用户等),不含集团级配置、不含纯 000002。""" + return has_club_super_perm(permissions) def _page_visible(page, permissions, user): @@ -35,13 +53,17 @@ def _page_visible(page, permissions, user): if page.require_group_manage: return can_manage_admin_assignments(user) - # 后台用户:本店超级管理 000001 / 集团 / 系统超管均可。 + # 纯 2 号:敏感页一律隐藏 + if is_second_super_only(permissions) and page_id in _SECOND_SUPER_HIDDEN_PAGE_IDS: + return False + + # 后台用户:本店超级管理 000001/000002 / 集团 / 系统超管均可。 # 注意:历史上 DB 模型缺 require_admin_user_manage 字段,导致该页永远不可见,必须按 page_id 兜底。 if page_id == 'admin.user' or getattr(page, 'require_admin_user_manage', False): return can_access_admin_user_management(user, permissions) if page.require_super: - return _has_super_perm(permissions) or is_system_super_admin(user) + return has_role_manage_perm(permissions) or is_system_super_admin(user) # 仅系统级超管可见全部菜单;俱乐部 000001 不展开全部业务菜单 if is_system_super_admin(user): @@ -55,9 +77,13 @@ def _page_visible(page, permissions, user): # 本店超管:解锁订单/提现审核等核心页(与加余额闸门一致,不要求集团权限) if _has_super_perm(permissions) and any(code in CLUB_SUPER_IMPLIED_PERMS for code in codes): return True + # 2 号超管:解锁订单等核心页,但不含提现审核 + if is_second_super_only(permissions) and any( + code in CLUB_SECOND_SUPER_IMPLIED_PERMS for code in codes + ): + return True return False - def _permission_codes_list(permissions): if isinstance(permissions, _AllPermissions): # 系统超管前端也要拿到全量码(含新 seed 的 002ad),否则按钮被 v-if 藏掉 @@ -191,6 +217,8 @@ def build_menu_access_payload(request, username=None): path_by_id = {p.page_id: p.path for p in pages if p.path} visible_paths = [path_by_id[pid] for pid in visible_ids if pid in path_by_id] is_super = _has_super_perm(permissions) or is_system_super_admin(request.user) + # 仅「纯 2 号」打标;同时拥有旧超管时走 is_super,展示层再标「2号」权限名 + is_second_super = (not is_super) and is_second_super_only(permissions) return { 'visible_page_ids': sorted(set(visible_ids)), 'visible_paths': visible_paths, @@ -205,6 +233,7 @@ def build_menu_access_payload(request, username=None): 'can_switch_club': bool(club_ctx.get('can_switch_club')), 'is_group_admin': bool(club_ctx.get('is_group_admin')), 'is_super': bool(is_super), + 'is_second_super': bool(is_second_super), 'menu_ready': True, 'menu_source': menu_source, - }, None + }, None \ No newline at end of file diff --git a/jituan/views_fund_freeze.py b/jituan/views_fund_freeze.py index 8bc1bd6..bcfe3d8 100644 --- a/jituan/views_fund_freeze.py +++ b/jituan/views_fund_freeze.py @@ -4,7 +4,7 @@ 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 +from backend.utils import verify_kefu_permission, has_club_super_perm, has_perm_code from jituan.services import fund_freeze_admin as admin_svc PERM_CFG = 'djcfg001' @@ -19,11 +19,11 @@ def _auth(request, perm_code: str): kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return None, None, permissions - codes = set(permissions or []) user = getattr(request, 'user', None) - if getattr(user, 'IsSuperuser', False) or '000001' in codes: + # 仅旧本店超管 000001 / 系统超管;纯 000002 不放行 + if getattr(user, 'IsSuperuser', False) or has_club_super_perm(permissions): return kefu, permissions, None - if perm_code not in codes: + if not has_perm_code(permissions, perm_code): return None, None, Response({'code': 403, 'msg': f'无权限(需要 {perm_code})'}) return kefu, permissions, None diff --git a/jituan/views_order_deal.py b/jituan/views_order_deal.py index c4aa396..1a91711 100644 --- a/jituan/views_order_deal.py +++ b/jituan/views_order_deal.py @@ -4,7 +4,13 @@ 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_merchant_order_permission +from backend.utils import ( + verify_kefu_permission, + has_merchant_order_permission, + has_club_super_perm, + has_perm_code, + is_second_super_only, +) from jituan.services.club_context import resolve_club_id_from_request from jituan.services import order_deal as deal_cfg from jituan.services import deal_stats as stats_svc @@ -13,29 +19,39 @@ from jituan.services import deal_stats as stats_svc def _auth_order(request): username = (request.data.get('username') or request.data.get('phone') or '').strip() if not username: - return None, Response({'code': 400, 'msg': '缺少username'}) + return None, None, Response({'code': 400, 'msg': '缺少username'}) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: - return None, permissions - codes = set(permissions or []) + return None, None, permissions user = getattr(request, 'user', None) - if getattr(user, 'IsSuperuser', False) or '000001' in codes: - return kefu, None - if has_merchant_order_permission(permissions) or '002ab' in codes: - return kefu, None - return None, Response({'code': 403, 'msg': '无订单管理权限'}) + if getattr(user, 'IsSuperuser', False) or has_club_super_perm(permissions): + return kefu, permissions, None + if has_merchant_order_permission(permissions) or has_perm_code(permissions, '002ab'): + return kefu, permissions, None + return None, None, Response({'code': 403, 'msg': '无订单管理权限'}) + + +def _auth_deal_config(request): + """成交与自动结算配置页:纯 000002 不可;其余同订单权限。""" + kefu, permissions, err = _auth_order(request) + if err: + return None, err + if is_second_super_only(permissions): + return None, Response({'code': 403, 'msg': '无权管理成交与自动结算配置'}) + return kefu, None class OrderDealConfigView(APIView): """ POST /jituan/houtai/order-deal-config action: get | save | whitelist_list | whitelist_save | whitelist_delete + 纯 000002 不可;旧超管 000001 / 系统超管 / 持订单权限者可。 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): - kefu, err = _auth_order(request) + kefu, err = _auth_deal_config(request) if err: return err club_id = (resolve_club_id_from_request(request) or '').strip() @@ -92,7 +108,7 @@ class MerchantDealStatView(APIView): parser_classes = [JSONParser] def post(self, request): - kefu, err = _auth_order(request) + kefu, permissions, err = _auth_order(request) if err: return err club_id = (resolve_club_id_from_request(request) or '').strip() @@ -123,7 +139,7 @@ class DashouDealStatView(APIView): parser_classes = [JSONParser] def post(self, request): - kefu, err = _auth_order(request) + kefu, permissions, err = _auth_order(request) if err: return err club_id = (resolve_club_id_from_request(request) or '').strip()