From c870e7e6e918be1453354075cbae8afa5911da92 Mon Sep 17 00:00:00 2001 From: XingQue Date: Sun, 26 Jul 2026 01:48:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8D=95=E4=BF=B1=E4=B9=90=E9=83=A8?= =?UTF-8?q?=E8=A7=86=E5=9B=BE=E4=B8=8B=E8=A7=92=E8=89=B2/=E7=94=A8?= =?UTF-8?q?=E6=88=B7/=E4=BC=9A=E5=91=98=E4=B8=A5=E6=A0=BC=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=EF=BC=8C=E7=A6=81=E6=AD=A2=E6=B3=84=E9=9C=B2=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E4=BF=B1=E4=B9=90=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- backend/views/roles.py | 95 ++++++++++++++++++++++------ jituan/services/club_member_admin.py | 16 +++-- jituan/services/club_rbac.py | 34 +++++++--- 3 files changed, 110 insertions(+), 35 deletions(-) diff --git a/backend/views/roles.py b/backend/views/roles.py index c2a4a42..e083428 100644 --- a/backend/views/roles.py +++ b/backend/views/roles.py @@ -223,9 +223,15 @@ class ModifyRolePermissionView(APIView): if not role_code: return Response({'code': 400, 'msg': '缺少角色编码'}) + from jituan.services.admin_context import can_manage_admin_assignments, can_manage_roles_for_club from jituan.services.club_rbac import resolve_role_by_name, resolve_role_club_id explicit_club = (request.data.get('target_club_id') or request.data.get('club_id') or '').strip() or None bind_club_id = resolve_role_club_id(request, explicit_club) + if bind_club_id: + if not can_manage_roles_for_club(request.user, permissions, bind_club_id): + return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色'}) + elif not can_manage_admin_assignments(request.user, permissions): + return Response({'code': 403, 'msg': '您无权管理全局角色'}) role = resolve_role_by_name(role_code, bind_club_id) if not role: return Response({'code': 404, 'msg': '角色不存在或与当前俱乐部不匹配'}) @@ -354,11 +360,17 @@ class AddRoleView(APIView): if '000001' not in permissions: return Response({'code': 403, 'msg': '您无权进行此操作'}) - # 检查角色名称是否已存在(同俱乐部内唯一) + # 检查角色名称是否已存在(同俱乐部内唯一);单俱乐部视图强制写入当前顶栏俱乐部 + from jituan.services.admin_context import can_manage_admin_assignments, can_manage_roles_for_club from jituan.services.club_rbac import resolve_role_club_id, role_name_exists target_club_id = resolve_role_club_id( request, (request.data.get('target_club_id') or '').strip() or None, ) + if target_club_id: + if not can_manage_roles_for_club(request.user, permissions, target_club_id): + return Response({'code': 403, 'msg': '您无权在该俱乐部下创建角色'}) + elif not can_manage_admin_assignments(request.user, permissions): + return Response({'code': 403, 'msg': '仅集团权限可创建全局角色'}) if role_name_exists(role_name, target_club_id): return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'}) @@ -494,16 +506,45 @@ class GetAdminUserListView(APIView): 'is_primary': a.is_primary, }) + from jituan.constants import DATA_SCOPE_ALL + from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope + from jituan.services.club_rbac import filter_roles_for_request + + scope = resolve_club_scope(request) + curr_club = (resolve_club_id_from_request(request) or '').strip() + # 单俱乐部视图:角色下拉/展示只允许「本俱乐部角色」;若本店无角色则仅全局 + allowed_role_club_ids = None + if scope != DATA_SCOPE_ALL: + scoped_roles = list(filter_roles_for_request(request).values_list('ClubID', flat=True)) + allowed_role_club_ids = {(c or '').strip() for c in scoped_roles} + + def _roles_in_view(roles_data): + if scope == DATA_SCOPE_ALL or allowed_role_club_ids is None: + return roles_data + return [ + r for r in roles_data + if (r.get('club_id') or '').strip() in allowed_role_club_ids + ] + + def _assignments_in_view(assignments): + if scope == DATA_SCOPE_ALL: + return assignments + # 本俱乐部任职 + 集团级(club_id 空);绝不展示其他子公司任职 + return [ + a for a in assignments + if not a.get('club_id') or a.get('club_id') == curr_club + ] + data_list = [] for user in _page_users: - roles_data = _roles_by_user.get(user.UserUUID, []) + roles_data = _roles_in_view(_roles_by_user.get(user.UserUUID, [])) data_list.append({ 'phone': user.Phone or '', 'yonghuid': user.UserUID or '', 'nicheng': user.KefuProfile.nicheng, 'club_id': getattr(user, 'ClubID', '') or '', 'roles': roles_data, - 'assignments': _assignments_by_user.get(user.UserUID, []), + 'assignments': _assignments_in_view(_assignments_by_user.get(user.UserUID, [])), 'status': user.KefuProfile.zhuangtai, 'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', 'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '', @@ -512,6 +553,10 @@ class GetAdminUserListView(APIView): }) from jituan.services.admin_context import can_manage_admin_assignments, get_manageable_club_ids + manageable = sorted(get_manageable_club_ids(request.user, permissions)) + if scope != DATA_SCOPE_ALL: + # 顶栏在具体俱乐部时,可管理集合收敛到当前店(含超级管理 000001) + manageable = [c for c in manageable if c == curr_club] return Response({ 'code': 0, 'data': { @@ -520,7 +565,9 @@ class GetAdminUserListView(APIView): 'page': page, 'pageSize': page_size, 'can_manage_club_assignments': can_manage_admin_assignments(request.user, permissions), - 'manageable_club_ids': sorted(get_manageable_club_ids(request.user, permissions)), + 'manageable_club_ids': manageable, + 'current_club_id': curr_club, + 'scope': scope, } }) @@ -546,19 +593,23 @@ class GetAdminRolesView(APIView): if not can_access_admin_user_management(request.user, permissions): return Response({'code': 403, 'msg': '您无权访问此页面'}) - from jituan.services.admin_context import can_manage_admin_assignments, get_manageable_club_ids + from jituan.constants import DATA_SCOPE_ALL + from jituan.services.admin_context import can_manage_admin_assignments + from jituan.services.club_context import resolve_club_scope from jituan.services.club_rbac import filter_roles_for_request + + scope = resolve_club_scope(request) all_clubs = request.data.get('all_clubs') in (True, 1, '1', 'true', 'yes') - if all_clubs and can_manage_admin_assignments(request.user, permissions): + # 仅集团汇总视图 + 集团任职管理权 才可拉全量子公司角色; + # 顶栏在具体俱乐部时(含集团账号切到某店),绝不容许其他俱乐部角色泄露。 + if ( + scope == DATA_SCOPE_ALL + and all_clubs + and can_manage_admin_assignments(request.user, permissions) + ): roles = Role.objects.filter(RoleStatus=1).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID') else: - manageable = get_manageable_club_ids(request.user, permissions) - if manageable: - roles = Role.objects.filter( - RoleStatus=1, ClubID__in=manageable, - ).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID') - else: - roles = filter_roles_for_request(request).values('RoleName', 'ClubID') + roles = filter_roles_for_request(request).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID') return Response({ 'code': 0, 'data': { @@ -721,11 +772,12 @@ class ModifyAdminUserView(APIView): role_codes = request.data.get('role_codes', []) if not role_codes: return Response({'code': 400, 'msg': '请选择角色'}) - from jituan.services.club_rbac import resolve_role_by_name - from jituan.services.club_context import resolve_club_id_from_request - bind_club_id = (request.data.get('target_club_id') or '').strip() + from jituan.services.club_rbac import resolve_role_by_name, resolve_role_club_id + bind_club_id = resolve_role_club_id( + request, (request.data.get('target_club_id') or '').strip() or None, + ) if not bind_club_id: - bind_club_id = resolve_club_id_from_request(request) + return Response({'code': 400, 'msg': '请先切换到具体俱乐部再分配角色'}) if not can_manage_roles_for_club(request.user, permissions, bind_club_id): return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'}) added, missing = [], [] @@ -766,11 +818,12 @@ class ModifyAdminUserView(APIView): role_code = request.data.get('role_code') if not role_code: return Response({'code': 400, 'msg': '缺少角色编码'}) - from jituan.services.club_rbac import resolve_role_by_name - from jituan.services.club_context import resolve_club_id_from_request - bind_club_id = (request.data.get('target_club_id') or '').strip() + from jituan.services.club_rbac import resolve_role_by_name, resolve_role_club_id + bind_club_id = resolve_role_club_id( + request, (request.data.get('target_club_id') or '').strip() or None, + ) if not bind_club_id: - bind_club_id = resolve_club_id_from_request(request) + return Response({'code': 400, 'msg': '请先切换到具体俱乐部再操作角色'}) if not can_manage_roles_for_club(request.user, permissions, bind_club_id): return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'}) role = resolve_role_by_name(role_code, bind_club_id) diff --git a/jituan/services/club_member_admin.py b/jituan/services/club_member_admin.py index 32c8dbb..3c11310 100644 --- a/jituan/services/club_member_admin.py +++ b/jituan/services/club_member_admin.py @@ -128,13 +128,17 @@ def build_member_list_payload(request): for m in members: member_list.append(_format_member(m)) else: + # 有本俱乐部专属价:只展示本店上架项;尚无专属价:回落集团默认会员定义。 + # 绝不混入其他俱乐部的 ClubHuiyuanPrice。 + has_club_prices = bool(price_map) for m in members: row = price_map.get(m.huiyuan_id) - if row is None and club_id != CLUB_ID_DEFAULT: - continue - if row is not None and not row.is_enabled: - continue - item = _format_member(m, row, club_id=club_id) + if has_club_prices: + if row is None or not row.is_enabled: + continue + item = _format_member(m, row, club_id=club_id) + else: + item = _format_member(m, club_id=club_id) item['goumai_cishu'] = _sales_map.get(m.huiyuan_id, 0) member_list.append(item) @@ -143,7 +147,7 @@ def build_member_list_payload(request): 'club_id': club_id, 'scope': scope, 'price_note': ( - f'当前俱乐部 {club_id} 专属售价(club_huiyuan_price)。' + f'当前俱乐部 {club_id} 专属售价;未配置专属价时展示集团默认会员。' if scope != DATA_SCOPE_ALL else '集团汇总:展示全局默认价;切换具体俱乐部可配置售价与体验会员。' ), diff --git a/jituan/services/club_rbac.py b/jituan/services/club_rbac.py index 4e93594..7adbf43 100644 --- a/jituan/services/club_rbac.py +++ b/jituan/services/club_rbac.py @@ -13,26 +13,44 @@ def _global_role_q(): def resolve_role_club_id(request, explicit=None): - """创建/筛选角色时使用的俱乐部;集团视图可显式指定 target_club_id。""" + """ + 创建/筛选角色时使用的俱乐部。 + 单俱乐部视图:强制当前顶栏俱乐部,忽略跨店 target_club_id。 + 集团汇总视图:可显式指定;未指定则为全局(club_id 空)。 + """ + scope = resolve_club_scope(request) + if scope != DATA_SCOPE_ALL: + return resolve_club_id_from_request(request) if explicit: return (explicit or '').strip() - scope = resolve_club_scope(request) - if scope == DATA_SCOPE_ALL: - return '' - return resolve_club_id_from_request(request) + return '' def filter_roles_for_request(request): """ - 子公司视图:仅本俱乐部角色。 - 集团汇总视图:全部角色(含各俱乐部 + 全局 club_id 为空)。 + 子公司视图:优先本俱乐部角色;本俱乐部尚无角色时仅回落集团全局角色。 + 绝不混入其他俱乐部角色。 + 集团汇总视图:全部角色(含各俱乐部 + 全局)。 """ qs = Role.objects.filter(RoleStatus=1) scope = resolve_club_scope(request) if scope == DATA_SCOPE_ALL: return qs club_id = resolve_club_id_from_request(request) - return qs.filter(Q(ClubID=club_id) | _global_role_q()) + club_qs = qs.filter(ClubID=club_id) + if club_qs.exists(): + return club_qs + return qs.filter(_global_role_q()) + + +def role_visible_in_scope(role_club_id, request): + """列表展示用:单俱乐部视图只允许本俱乐部或全局(无本俱乐部角色时由调用方决定)。""" + scope = resolve_club_scope(request) + if scope == DATA_SCOPE_ALL: + return True + curr = (resolve_club_id_from_request(request) or '').strip() + cid = (role_club_id or '').strip() + return cid == '' or cid == curr def filter_kefu_users_by_admin_scope(qs, request):