feat: 多俱乐部后台权限与管理员用户俱乐部分配
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -166,12 +166,32 @@ def verify_kefu_permission(request, username_from_frontend=None):
|
||||
|
||||
# ---------- 4. 获取用户的所有权限 ----------
|
||||
# 使用 gvsdsdk RBAC 表(UserRole → RolePermission → Permission)
|
||||
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_SINGLE
|
||||
from jituan.services.admin_context import resolve_admin_effective_club, get_allowed_club_ids, is_system_super_admin
|
||||
from jituan.services.club_rbac import filter_role_uuids_for_club_scope
|
||||
|
||||
eff_club = getattr(request, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
eff_scope = getattr(request, 'club_scope', None) or DATA_SCOPE_SINGLE
|
||||
allowed_club_ids = None
|
||||
if not is_system_super_admin(current_user):
|
||||
eff_club, eff_scope, club_err = resolve_admin_effective_club(current_user, request)
|
||||
if club_err:
|
||||
return None, Response({'code': 403, 'msg': club_err}, status=403)
|
||||
request.club_id = eff_club
|
||||
request.club_scope = eff_scope
|
||||
allowed_club_ids = list(get_allowed_club_ids(current_user))
|
||||
|
||||
try:
|
||||
user_uuid = current_user.UserUUID
|
||||
role_uuids = list(SdkUserRole.objects.filter(
|
||||
UserUUID=user_uuid
|
||||
).values_list('RoleUUID', flat=True))
|
||||
|
||||
if role_uuids and not is_system_super_admin(current_user):
|
||||
role_uuids = filter_role_uuids_for_club_scope(
|
||||
role_uuids, eff_club, eff_scope, allowed_club_ids,
|
||||
)
|
||||
|
||||
if role_uuids:
|
||||
perm_uuids = list(SdkRolePermission.objects.filter(
|
||||
RoleUUID__in=role_uuids
|
||||
|
||||
@@ -420,8 +420,10 @@ class GetAdminUserListView(APIView):
|
||||
KefuProfile__isnull=False
|
||||
).select_related('KefuProfile')
|
||||
|
||||
from jituan.services.club_rbac import filter_kefu_users_qs
|
||||
users = filter_kefu_users_qs(users, request)
|
||||
from jituan.models import AdminAssignment, Club
|
||||
from jituan.constants import ADMIN_ROLE_LABELS
|
||||
from jituan.services.club_rbac import filter_kefu_users_by_admin_scope
|
||||
users = filter_kefu_users_by_admin_scope(users, request)
|
||||
|
||||
if phone:
|
||||
users = users.filter(Phone__icontains=phone)
|
||||
@@ -452,17 +454,37 @@ class GetAdminUserListView(APIView):
|
||||
.values_list('UserUUID', 'RoleUUID')
|
||||
) if _page_user_uuids else []
|
||||
_role_uuids_in_page = list({r for _, r in _ur_pairs})
|
||||
_role_name_by_uuid = {
|
||||
r.RoleUUID: r.RoleName
|
||||
_role_info_by_uuid = {
|
||||
r.RoleUUID: {'role_name': r.RoleName, 'club_id': getattr(r, 'ClubID', '') or ''}
|
||||
for r in Role.objects.filter(RoleUUID__in=_role_uuids_in_page)
|
||||
} if _role_uuids_in_page else {}
|
||||
_roles_by_user = {}
|
||||
for user_uuid, role_uuid in _ur_pairs:
|
||||
role_name = _role_name_by_uuid.get(role_uuid)
|
||||
if role_name is not None:
|
||||
_roles_by_user.setdefault(user_uuid, []).append(
|
||||
{'role_code': role_name, 'role_name': role_name}
|
||||
)
|
||||
info = _role_info_by_uuid.get(role_uuid)
|
||||
if info is not None:
|
||||
_roles_by_user.setdefault(user_uuid, []).append({
|
||||
'role_code': info['role_name'],
|
||||
'role_name': info['role_name'],
|
||||
'club_id': info['club_id'],
|
||||
})
|
||||
|
||||
_page_yonghuids = [u.UserUID for u in _page_users]
|
||||
_club_name_map = {c.club_id: c.name for c in Club.query.filter(status=1)}
|
||||
_assignments_by_user = {}
|
||||
if _page_yonghuids:
|
||||
for a in AdminAssignment.query.filter(yonghuid__in=_page_yonghuids, status=1):
|
||||
if a.club_id:
|
||||
club_label = _club_name_map.get(a.club_id, a.club_id)
|
||||
else:
|
||||
club_label = '集团(全部子公司)'
|
||||
_assignments_by_user.setdefault(a.yonghuid, []).append({
|
||||
'id': a.id,
|
||||
'club_id': a.club_id,
|
||||
'club_label': club_label,
|
||||
'role_code': a.role_code,
|
||||
'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code),
|
||||
'is_primary': a.is_primary,
|
||||
})
|
||||
|
||||
data_list = []
|
||||
for user in _page_users:
|
||||
@@ -473,6 +495,7 @@ class GetAdminUserListView(APIView):
|
||||
'nicheng': user.KefuProfile.nicheng,
|
||||
'club_id': getattr(user, 'ClubID', '') or '',
|
||||
'roles': roles_data,
|
||||
'assignments': _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,7 +535,11 @@ class GetAdminRolesView(APIView):
|
||||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||
|
||||
from jituan.services.club_rbac import filter_roles_for_request
|
||||
roles = filter_roles_for_request(request).values('RoleName', 'ClubID')
|
||||
all_clubs = request.data.get('all_clubs') in (True, 1, '1', 'true', 'yes')
|
||||
if all_clubs:
|
||||
roles = Role.objects.filter(RoleStatus=1).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID')
|
||||
else:
|
||||
roles = filter_roles_for_request(request).values('RoleName', 'ClubID')
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
@@ -660,10 +687,13 @@ class ModifyAdminUserView(APIView):
|
||||
if not role_codes:
|
||||
return Response({'code': 400, 'msg': '请选择角色'})
|
||||
from jituan.services.club_rbac import resolve_role_by_name
|
||||
club_id = getattr(target_user, 'ClubID', '') or ''
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
bind_club_id = (request.data.get('target_club_id') or '').strip()
|
||||
if not bind_club_id:
|
||||
bind_club_id = resolve_club_id_from_request(request)
|
||||
added, missing = [], []
|
||||
for rc in role_codes:
|
||||
role = resolve_role_by_name(rc, club_id)
|
||||
role = resolve_role_by_name(rc, bind_club_id)
|
||||
if not role:
|
||||
missing.append(rc)
|
||||
continue
|
||||
@@ -696,13 +726,52 @@ class ModifyAdminUserView(APIView):
|
||||
if not role_code:
|
||||
return Response({'code': 400, 'msg': '缺少角色编码'})
|
||||
from jituan.services.club_rbac import resolve_role_by_name
|
||||
club_id = getattr(target_user, 'ClubID', '') or ''
|
||||
role = resolve_role_by_name(role_code, club_id)
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
bind_club_id = (request.data.get('target_club_id') or '').strip()
|
||||
if not bind_club_id:
|
||||
bind_club_id = resolve_club_id_from_request(request)
|
||||
role = resolve_role_by_name(role_code, bind_club_id)
|
||||
if not role:
|
||||
return Response({'code': 404, 'msg': '角色不存在或与用户俱乐部不匹配'})
|
||||
return Response({'code': 404, 'msg': '角色不存在或与所选俱乐部不匹配'})
|
||||
UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete()
|
||||
return Response({'code': 0, 'msg': '角色移除成功'})
|
||||
|
||||
elif action == 'add_club_assignment':
|
||||
club_id = (request.data.get('club_id') or '').strip()
|
||||
if not club_id:
|
||||
return Response({'code': 400, 'msg': '请选择俱乐部'})
|
||||
from jituan.services.admin_assignments import create_or_reactivate_assignment
|
||||
from jituan.constants import DATA_SCOPE_SINGLE
|
||||
role_code = (request.data.get('role_code') or 'CLUB_ADMIN').strip()
|
||||
is_primary = bool(request.data.get('is_primary', False))
|
||||
_, err = create_or_reactivate_assignment(
|
||||
yonghuid=target_user.UserUID,
|
||||
club_id=club_id,
|
||||
role_code=role_code,
|
||||
data_scope=DATA_SCOPE_SINGLE,
|
||||
is_primary=is_primary,
|
||||
granted_by=request.user.UserUID,
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 400, 'msg': err})
|
||||
return Response({'code': 0, 'msg': '俱乐部任职已添加'})
|
||||
|
||||
elif action == 'remove_club_assignment':
|
||||
assignment_id = request.data.get('assignment_id')
|
||||
if not assignment_id:
|
||||
return Response({'code': 400, 'msg': '缺少任职 id'})
|
||||
from jituan.services.admin_assignments import deactivate_assignment
|
||||
from jituan.models import AdminAssignment
|
||||
row = AdminAssignment.query.filter(
|
||||
id=assignment_id, yonghuid=target_user.UserUID, status=1,
|
||||
).first()
|
||||
if not row:
|
||||
return Response({'code': 404, 'msg': '任职记录不存在'})
|
||||
ok, err = deactivate_assignment(assignment_id)
|
||||
if not ok:
|
||||
return Response({'code': 404, 'msg': err or '停用失败'})
|
||||
return Response({'code': 0, 'msg': '俱乐部任职已移除'})
|
||||
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '无效的action'})
|
||||
|
||||
@@ -816,6 +885,17 @@ class AddAdminUserView(APIView):
|
||||
},
|
||||
)
|
||||
|
||||
from jituan.services.admin_assignments import create_or_reactivate_assignment
|
||||
from jituan.constants import DATA_SCOPE_SINGLE
|
||||
create_or_reactivate_assignment(
|
||||
yonghuid=yonghuid,
|
||||
club_id=target_club_id,
|
||||
role_code='CLUB_ADMIN',
|
||||
data_scope=DATA_SCOPE_SINGLE,
|
||||
is_primary=True,
|
||||
granted_by=request.user.UserUID,
|
||||
)
|
||||
|
||||
return Response({'code': 0, 'msg': '添加成功'})
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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': [
|
||||
{
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user