feat: 多俱乐部后台权限与管理员用户俱乐部分配

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-06 17:59:59 +08:00
parent f1c8633345
commit 1dfb46c38c
7 changed files with 253 additions and 23 deletions

View File

@@ -22,6 +22,22 @@ def find_active_assignment(yonghuid, club_id, role_code):
return _club_filter(qs, club_id).first()
def clear_other_primary_assignments(yonghuid, except_id=None):
qs = AdminAssignment.query.filter(yonghuid=yonghuid, status=1, is_primary=True)
if except_id:
qs = qs.exclude(id=except_id)
ids = list(qs.values_list('id', flat=True))
if ids:
AdminAssignment.objects.filter(pk__in=ids).update(
is_primary=False,
UpdateTime=timezone.now(),
)
def _clear_other_primary(yonghuid, except_id=None):
clear_other_primary_assignments(yonghuid, except_id=except_id)
def create_or_reactivate_assignment(
yonghuid,
club_id,
@@ -34,8 +50,13 @@ def create_or_reactivate_assignment(
if active:
return None, '该任职记录已存在且有效'
if is_primary:
_clear_other_primary(yonghuid)
inactive = find_assignment(yonghuid, club_id, role_code)
if inactive and inactive.status != 1:
if is_primary:
_clear_other_primary(yonghuid)
AdminAssignment.objects.filter(pk=inactive.id).update(
status=1,
data_scope=data_scope,

View File

@@ -14,6 +14,50 @@ GROUP_MANAGE_ROLES = frozenset({
})
def get_allowed_club_ids(user):
"""用户可切换/访问的子公司 club_id 集合(不含集团汇总)。"""
if is_system_super_admin(user):
return {c.club_id for c in Club.query.filter(status=1)}
assignments = AdminAssignment.query.filter(yonghuid=user.UserUID, status=1)
has_group = assignments.filter(
club_id__isnull=True
).exists() or assignments.filter(data_scope=DATA_SCOPE_ALL).exists()
if has_group:
return {c.club_id for c in Club.query.filter(status=1)}
return {
a.club_id for a in assignments if a.club_id
}
def resolve_admin_effective_club(user, request):
"""
校验后台用户请求的俱乐部/范围,返回 (club_id, scope, error_msg)。
无权限访问请求的俱乐部时,回退到主任职俱乐部(避免 localStorage 脏数据串俱乐部)。
"""
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
requested_club = resolve_club_id_from_request(request)
requested_scope = resolve_club_scope(request)
if is_system_super_admin(user):
return requested_club, requested_scope, None
ctx = build_admin_club_context(user)
allowed = get_allowed_club_ids(user)
if requested_scope == DATA_SCOPE_ALL:
if not ctx.get('is_group_admin'):
primary = ctx.get('club_id') or CLUB_ID_DEFAULT
return primary, DATA_SCOPE_SINGLE, None
return ctx.get('club_id') or CLUB_ID_DEFAULT, DATA_SCOPE_ALL, None
if requested_club not in allowed:
primary = ctx.get('club_id') or CLUB_ID_DEFAULT
return primary, DATA_SCOPE_SINGLE, None
return requested_club, DATA_SCOPE_SINGLE, None
def build_admin_club_context(user):
"""
根据 admin_assignment + 超管推断后台登录后的俱乐部上下文。
@@ -129,16 +173,21 @@ def can_manage_admin_assignments(user, permissions=None):
def _pack(scope, club_id, is_group_admin, assignments, clubs, can_switch_club,
role_code, role_name):
allowed_club_ids = sorted({
c['club_id'] for c in clubs if c.get('club_id')
})
return {
'scope': scope,
'club_id': club_id,
'is_group_admin': is_group_admin,
'role_code': role_code,
'role_name': role_name,
'allowed_club_ids': allowed_club_ids,
'perm_source': 'gvsdsdk',
'perm_note': (
'【功能权限】在「角色管理」绑 gvsdsdk 角色(如 caiwu、dingdan、000001'
'【数据范围】在「数据范围配置」维护任职(能看/管哪个俱乐部)。两套独立'
'【功能权限】在「角色管理」为各俱乐部创建角色,在「管理员用户」绑定'
'切换俱乐部后菜单按该俱乐部角色生效'
'【数据范围】在本页添加任职,可分配多个俱乐部,无需集团权限即可切换。'
),
'assignments': [
{

View File

@@ -30,8 +30,32 @@ def filter_roles_for_request(request):
return qs.filter(ClubID=club_id)
def filter_kefu_users_by_admin_scope(qs, request):
"""
后台用户列表:按任职俱乐部过滤,而非仅 User.ClubID。
同一账号有多俱乐部任职时,在任一已分配俱乐部视图下均可见。
"""
from jituan.models import AdminAssignment
scope = resolve_club_scope(request)
if scope == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
assigned_uids = list(
AdminAssignment.query.filter(status=1, club_id=club_id).values_list('yonghuid', flat=True)
)
if club_id == CLUB_ID_DEFAULT:
return qs.filter(
Q(UserUID__in=assigned_uids)
| Q(ClubID=club_id)
| Q(ClubID__isnull=True)
| Q(ClubID='')
)
return qs.filter(Q(UserUID__in=assigned_uids) | Q(ClubID=club_id))
def filter_kefu_users_qs(qs, request):
"""后台客服账号列表按俱乐部过滤(User.ClubID"""
"""后台客服账号列表按 User.ClubID 过滤(角色统计等场景)。"""
scope = resolve_club_scope(request)
if scope == DATA_SCOPE_ALL:
return qs
@@ -108,19 +132,43 @@ def resolve_roles_for_binding(role_names, club_id=None):
return resolved, missing
def user_bound_role_names(user):
def filter_role_uuids_for_club_scope(role_uuids, club_id, scope, allowed_club_ids=None):
"""
按当前俱乐部上下文过滤 UserRole 绑定的 RoleUUID。
SINGLE_CLUB本俱乐部角色 + 全局角色ClubID 为空)。
ALL_CLUBS全局角色 + 各已授权俱乐部的角色(并集)。
"""
if not role_uuids:
return []
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]
if club_ids:
return list(
qs.filter(Q(ClubID='') | Q(ClubID__in=club_ids)).values_list('RoleUUID', flat=True)
)
return list(qs.filter(ClubID='').values_list('RoleUUID', flat=True))
cid = (club_id or CLUB_ID_DEFAULT).strip()
return list(qs.filter(Q(ClubID=cid) | Q(ClubID='')).values_list('RoleUUID', flat=True))
def user_bound_role_names(user, club_id=None, scope=None, allowed_club_ids=None):
from backend.utils import _legacy_backend_role_permissions
uuids = list(
UserRole.objects.filter(UserUUID=user.UserUUID).values_list('RoleUUID', flat=True)
)
if club_id is not None and scope is not None:
uuids = filter_role_uuids_for_club_scope(uuids, club_id, scope, allowed_club_ids)
names = []
if uuids:
names = list(
Role.objects.filter(RoleUUID__in=uuids, RoleStatus=1).values_list('RoleName', flat=True)
)
_, legacy_names = _legacy_backend_role_permissions(getattr(user, 'Phone', ''))
return sorted(set(names + legacy_names))
if club_id is None:
_, legacy_names = _legacy_backend_role_permissions(getattr(user, 'Phone', ''))
return sorted(set(names + legacy_names))
return sorted(set(names))
def resolve_kefu_admin_user(yonghuid=None, phone=None):

View File

@@ -7,8 +7,10 @@ from jituan.models import KefuMenuPage
from jituan.services.admin_context import (
build_admin_club_context,
can_manage_admin_assignments,
get_allowed_club_ids,
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.kefu_menu_definitions import KEFU_MENU_ROW_DEFS
@@ -129,14 +131,19 @@ def build_menu_access_payload(request, username=None):
visible_ids.append(page.page_id)
club_ctx = build_admin_club_context(request.user)
club_id = resolve_club_id_from_request(request)
scope = resolve_club_scope(request)
allowed = list(get_allowed_club_ids(request.user))
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]
return {
'visible_page_ids': sorted(set(visible_ids)),
'visible_paths': visible_paths,
'permission_codes': _permission_codes_list(permissions),
'role_names': user_bound_role_names(request.user),
'role_names': user_bound_role_names(request.user, club_id, scope, allowed),
'yonghuid': getattr(request.user, 'UserUID', '') or '',
'club_id': club_id,
'club_scope': scope,
'can_switch_club': bool(club_ctx.get('can_switch_club')),
'is_group_admin': bool(club_ctx.get('is_group_admin')),
'menu_ready': True,

View File

@@ -154,8 +154,10 @@ class ClubKefuLoginView(APIView):
token = str(refresh.access_token)
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
club_context = build_admin_club_context(target_user)
# 登录请求尚未带 JWT需用本次登录用户计算菜单
# 登录请求尚未带 JWT需用本次登录用户 + 主任职俱乐部计算菜单
request.user = target_user
request.club_id = club_context.get('club_id') or CLUB_ID_DEFAULT
request.club_scope = club_context.get('scope') or DATA_SCOPE_SINGLE
menu_access, _menu_err = build_menu_access_payload(request, phone)
return Response({
@@ -337,6 +339,9 @@ class ClubAdminAssignmentListView(APIView):
row.data_scope = request.data.get('data_scope') or row.data_scope
if 'is_primary' in request.data:
row.is_primary = bool(request.data.get('is_primary'))
if row.is_primary:
from jituan.services.admin_assignments import clear_other_primary_assignments
clear_other_primary_assignments(row.yonghuid, except_id=row.id)
if 'club_id' in request.data:
cid = request.data.get('club_id')
row.club_id = None if cid is None or str(cid).strip() == '' else str(cid).strip()