diff --git a/jituan/services/club_rbac.py b/jituan/services/club_rbac.py index 2026e25..b47aadd 100644 --- a/jituan/services/club_rbac.py +++ b/jituan/services/club_rbac.py @@ -7,6 +7,11 @@ from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope +def _global_role_q(): + """集团全局角色:club_id 为空或 NULL。""" + return Q(ClubID='') | Q(ClubID__isnull=True) + + def resolve_role_club_id(request, explicit=None): """创建/筛选角色时使用的俱乐部;集团视图可显式指定 target_club_id。""" if explicit: @@ -27,7 +32,7 @@ def filter_roles_for_request(request): if scope == DATA_SCOPE_ALL: return qs club_id = resolve_club_id_from_request(request) - return qs.filter(ClubID=club_id) + return qs.filter(Q(ClubID=club_id) | _global_role_q()) def filter_kefu_users_by_admin_scope(qs, request): @@ -87,15 +92,17 @@ def role_name_exists(role_name, club_id): def resolve_role_by_name(role_name, club_id=None): - """按角色名解析 Role;优先匹配用户俱乐部,其次集团全局角色(club_id 为空)。""" + """按角色名解析 Role;优先匹配指定俱乐部,其次全局角色(club_id 为空)。""" name = (role_name or '').strip() if not name: return None qs = Role.objects.filter(RoleName=name, RoleStatus=1) cid = (club_id or '').strip() if cid: - return qs.filter(ClubID=cid).first() - return qs.filter(ClubID='').first() + hit = qs.filter(ClubID=cid).first() + if hit: + return hit + return qs.filter(_global_role_q()).first() def role_uuid_bytes(role_uuid): @@ -133,36 +140,22 @@ def resolve_roles_for_binding(role_names, club_id=None): def filter_role_uuids_for_club_scope(role_uuids, club_id, scope, allowed_club_ids=None): """ 按当前俱乐部上下文过滤 UserRole 绑定的 RoleUUID。 - SINGLE_CLUB:仅本俱乐部角色(Role.ClubID=当前俱乐部),不含其他俱乐部/全局。 - ALL_CLUBS:已授权各俱乐部角色 + 全局角色(club_id 为空)。 + SINGLE_CLUB:本俱乐部角色 + 全局角色(ClubID 空/NULL)。 + ALL_CLUBS:已授权各俱乐部角色 + 全局角色。 """ if not role_uuids: return [] - wanted = {role_uuid_bytes(u) for u in role_uuids} - + qs = Role.objects.filter(RoleUUID__in=role_uuids, RoleStatus=1) if scope == DATA_SCOPE_ALL: - club_ids = {c for c in (allowed_club_ids or []) if c} - matched = [] - for role in Role.objects.filter(RoleStatus=1): - key = role_uuid_bytes(role.RoleUUID) - if key not in wanted: - continue - rcid = (getattr(role, 'ClubID', None) or '').strip() - if rcid == '' or rcid in club_ids: - matched.append(role.RoleUUID) - return matched - - cid = (club_id or CLUB_ID_DEFAULT).strip() - matched = [] - for role in Role.objects.filter(RoleStatus=1): - key = role_uuid_bytes(role.RoleUUID) - if key not in wanted: - continue - rcid = (getattr(role, 'ClubID', None) or '').strip() - # 本俱乐部角色 + 全局角色;不含其他俱乐部角色 - if rcid == cid or rcid == '': - matched.append(role.RoleUUID) - return matched + club_ids = [c for c in (allowed_club_ids or []) if c] + q = _global_role_q() + if club_ids: + q |= Q(ClubID__in=club_ids) + qs = qs.filter(q) + else: + cid = (club_id or CLUB_ID_DEFAULT).strip() + qs = qs.filter(Q(ClubID=cid) | _global_role_q()) + return list(qs.values_list('RoleUUID', flat=True)) def resolve_permission_codes_for_club(user, club_id, allowed_club_ids=None, scope=None): @@ -191,8 +184,7 @@ def resolve_permission_codes_for_club(user, club_id, allowed_club_ids=None, scop def resolve_effective_permission_codes(user, club_id, scope, allowed_club_ids=None): """ - 菜单与各业务接口统一权限码:当前俱乐部 gvsdsdk 角色。 - gvsdsdk 完全无绑定时才兜底旧 user_role 表(旧表不区分俱乐部,避免多俱乐部串权)。 + 菜单与各业务接口统一权限码:当前俱乐部 gvsdsdk 角色 + 过渡期 legacy 兜底。 """ from backend.utils import _legacy_backend_role_permissions from gvsdsdk.models import UserRole as SdkUserRole @@ -202,14 +194,54 @@ def resolve_effective_permission_codes(user, club_id, scope, allowed_club_ids=No ) if codes: return codes - if SdkUserRole.objects.filter(UserUUID=user.UserUUID).exists(): - return [] + legacy_codes, _ = _legacy_backend_role_permissions(getattr(user, 'Phone', '')) - if legacy_codes: + if not legacy_codes: + return [] + + if not SdkUserRole.objects.filter(UserUUID=user.UserUUID).exists(): + return list(legacy_codes) + + # gvsdsdk 有绑定但当前俱乐部视图下无权限码:单俱乐部账号仍走 legacy(未迁移完) + allowed = {c for c in (allowed_club_ids or []) if c} + if len(allowed) <= 1: return list(legacy_codes) return [] +def describe_roles_in_scope(user, club_id, scope, allowed_club_ids=None): + """诊断:当前俱乐部上下文中生效的角色与权限码。""" + from gvsdsdk.models import Permission as SdkPermission, RolePermission as SdkRolePermission, UserRole as SdkUserRole + + all_bound = list(SdkUserRole.objects.filter( + UserUUID=user.UserUUID, + ).values_list('RoleUUID', flat=True)) + in_scope = filter_role_uuids_for_club_scope( + all_bound, club_id, scope, allowed_club_ids, + ) + rows = [] + for role in Role.objects.filter(RoleUUID__in=in_scope, RoleStatus=1): + rcid = (getattr(role, 'ClubID', None) or '').strip() or '(全局)' + perm_uuids = SdkRolePermission.objects.filter( + RoleUUID=role.RoleUUID, + ).values_list('PermUUID', flat=True) + perm_codes = list( + SdkPermission.objects.filter( + PermUUID__in=perm_uuids, PermStatus=1, + ).values_list('PermCode', flat=True) + ) + rows.append({ + 'role_name': role.RoleName, + 'club_id': rcid, + 'perm_codes': perm_codes, + }) + return { + 'bound_role_count': len(all_bound), + 'in_scope_role_count': len(in_scope), + 'roles': rows, + } + + def user_bound_role_names(user, club_id=None, scope=None, allowed_club_ids=None): from backend.utils import _legacy_backend_role_permissions diff --git a/jituan/services/kefu_menu.py b/jituan/services/kefu_menu.py index ca7e28a..29ae044 100644 --- a/jituan/services/kefu_menu.py +++ b/jituan/services/kefu_menu.py @@ -12,7 +12,7 @@ from jituan.services.admin_context import ( is_system_super_admin, ) from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope -from jituan.services.club_rbac import user_bound_role_names +from jituan.services.club_rbac import user_bound_role_names, describe_roles_in_scope from jituan.services.kefu_menu_definitions import KEFU_MENU_ROW_DEFS logger = logging.getLogger(__name__) @@ -145,6 +145,7 @@ def build_menu_access_payload(request, username=None): allowed = list(get_allowed_club_ids(request.user)) perm_codes = _permission_codes_list(permissions) role_names = user_bound_role_names(request.user, club_id, scope, allowed) + roles_detail = describe_roles_in_scope(request.user, club_id, scope, allowed) perm_hint = '' if scope == DATA_SCOPE_SINGLE and not perm_codes: @@ -174,6 +175,7 @@ def build_menu_access_payload(request, username=None): 'club_scope': scope, 'effective_club_id': club_id, 'perm_hint': perm_hint, + 'roles_in_scope': roles_detail, 'can_switch_club': bool(club_ctx.get('can_switch_club')), 'is_group_admin': bool(club_ctx.get('is_group_admin')), 'menu_ready': True, diff --git a/jituan/services/kefu_menu_definitions.py b/jituan/services/kefu_menu_definitions.py index 7448af6..74b54cc 100644 --- a/jituan/services/kefu_menu_definitions.py +++ b/jituan/services/kefu_menu_definitions.py @@ -17,7 +17,7 @@ KEFU_MENU_ROW_DEFS = [ {'page_id': 'order.platform', 'name': '平台订单', 'path': '/order/platform', 'parent_id': 'order', 'sort_order': 31, 'perm_codes': ['002ab']}, {'page_id': 'order.merchant', 'name': '商家订单', 'path': '/order/merchant', 'parent_id': 'order', 'sort_order': 32, - 'perm_codes': ['002ac']}, + 'perm_codes': ['002ac', '002ab', '003aa']}, {'page_id': 'cross-platform', 'name': '跨平台管理', 'path': '', 'parent_id': '', 'sort_order': 40}, {'page_id': 'cross-platform.order', 'name': '跨平台订单', 'path': '/cross-order', 'parent_id': 'cross-platform', 'sort_order': 41, 'perm_codes': ['003aa']}, diff --git a/jituan/urls.py b/jituan/urls.py index c152a77..1d49541 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -48,6 +48,7 @@ from jituan.views import ( ClubHuiyuanStatsView, ClubKefuLoginView, ClubKefuMenuAccessView, + ClubPermDebugView, ClubListView, ClubMeContextView, ClubOrderFinanceView, @@ -96,6 +97,7 @@ urlpatterns = [ path('auth/kefu-login', ClubKefuLoginView.as_view(), name='jituan_kefu_login'), path('auth/me-context', ClubMeContextView.as_view(), name='jituan_me_context'), path('auth/menu-access', ClubKefuMenuAccessView.as_view(), name='jituan_menu_access'), + path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'), path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'), path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'), path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'), diff --git a/jituan/views.py b/jituan/views.py index 3d094ff..399224e 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -229,6 +229,45 @@ class ClubKefuMenuAccessView(APIView): return Response({'code': 0, 'msg': 'ok', 'data': payload}) +class ClubPermDebugView(APIView): + """GET /jituan/auth/perm-debug — 当前俱乐部权限解析明细。""" + + permission_classes = [IsAuthenticated] + + def get(self, request): + from backend.utils import verify_kefu_permission + from jituan.services.admin_context import get_allowed_club_ids, resolve_admin_effective_club + from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope + from jituan.services.club_rbac import describe_roles_in_scope + + user = request.user + if not is_kefu_backend_account(user): + return Response({'code': 403, 'msg': '非后台账号'}, status=403) + + requested_club = resolve_club_id_from_request(request) + requested_scope = resolve_club_scope(request) + eff_club, eff_scope, _ = resolve_admin_effective_club(user, request) + allowed = list(get_allowed_club_ids(user)) + _, permissions = verify_kefu_permission(request, None) + codes = list(permissions) if isinstance(permissions, list) else ['*'] + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'yonghuid': user.UserUID, + 'phone': user.Phone, + 'requested_club_id': requested_club, + 'requested_scope': requested_scope, + 'effective_club_id': eff_club, + 'effective_scope': eff_scope, + 'allowed_club_ids': allowed, + 'permission_codes': codes, + 'roles_in_scope': describe_roles_in_scope(user, eff_club, eff_scope, allowed), + }, + }) + + class ClubAdminAssignmentListView(APIView): """POST /jituan/houtai/admin-assignments — 数据范围任职(非 gvsdsdk 功能权限)。""" diff --git a/users/views/kefu_orders.py b/users/views/kefu_orders.py index 42fa2fa..6b2feb6 100644 --- a/users/views/kefu_orders.py +++ b/users/views/kefu_orders.py @@ -56,7 +56,7 @@ from orders.utils import ( from backend.utils import ( update_dashou_daily_by_action, update_guanshi_daily_by_action, update_shangjia_daily, update_zuzhang_daily_by_action, - verify_kefu_permission, has_perm_code, + verify_kefu_permission, has_perm_code, has_merchant_order_permission, ) from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan @@ -303,8 +303,19 @@ class KefuGetShangjiaOrderListView(APIView): if kefu is None: return permissions - if not has_perm_code(permissions, '002ac'): - return Response({'code': 403, 'msg': '您无权查看商家订单列表'}) + if not has_merchant_order_permission(permissions): + from backend.utils import _AllPermissions + if isinstance(permissions, _AllPermissions): + codes = ['*'] + else: + codes = list(permissions or []) + return Response({ + 'code': 403, + 'msg': ( + f'您无权查看商家订单列表(需要 002ac/002ab/003aa,' + f'当前俱乐部权限码:{",".join(codes) or "无"})' + ), + }) # 4. 获取分页和筛选参数 page = int(request.data.get('page', 1))