fix: 单俱乐部视图下角色/用户/会员严格隔离,禁止泄露其他俱乐部

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-26 01:48:51 +08:00
parent 30c2dff040
commit c870e7e6e9
3 changed files with 110 additions and 35 deletions

View File

@@ -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)

View File

@@ -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
'集团汇总:展示全局默认价;切换具体俱乐部可配置售价与体验会员。'
),

View File

@@ -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):