"""角色/后台客服按俱乐部隔离(权限码全局共用,角色实体按 club 分开)。""" from django.db.models import Q from gvsdsdk.models import Role, UserRole 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: return (explicit or '').strip() scope = resolve_club_scope(request) if scope == DATA_SCOPE_ALL: return '' return resolve_club_id_from_request(request) 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()) 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 过滤(角色统计等场景)。""" scope = resolve_club_scope(request) if scope == DATA_SCOPE_ALL: return qs club_id = resolve_club_id_from_request(request) if club_id == CLUB_ID_DEFAULT: return qs.filter(Q(ClubID=club_id) | Q(ClubID__isnull=True) | Q(ClubID='')) return qs.filter(ClubID=club_id) def count_role_users_in_scope(role, request, kefu_user_uuids): """统计角色下客服数(仅计当前数据范围内的客服)。""" from users.business_models import User uuids = list( UserRole.objects.filter(RoleUUID=role.RoleUUID) .values_list('UserUUID', flat=True) ) if not uuids: return 0 qs = User.objects.filter(UserUUID__in=uuids, KefuProfile__isnull=False) qs = filter_kefu_users_qs(qs, request) if kefu_user_uuids is not None: qs = qs.filter(UserUUID__in=kefu_user_uuids) return qs.count() def role_name_exists(role_name, club_id): return Role.objects.filter(RoleName=role_name, ClubID=club_id or '').exists() def resolve_role_by_name(role_name, club_id=None): """按角色名解析 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: hit = qs.filter(ClubID=cid).first() if hit: return hit return qs.filter(_global_role_q()).first() def role_uuid_bytes(role_uuid): """RoleUUID 统一转 bytes(兼容 UUIDObj / bytes / str)。""" from gvsdsdk.model_utils import UUIDObj if isinstance(role_uuid, bytes): return role_uuid return UUIDObj(role_uuid).bytes def resolve_roles_for_binding(role_names, club_id=None): """ 按角色名解析并去重;任一角色在当前俱乐部下找不到则整体失败。 返回 (roles, missing_names)。 """ resolved = [] missing = [] seen_bytes = set() for raw in role_names or []: name = (raw or '').strip() if not name: continue role = resolve_role_by_name(name, club_id) if not role: missing.append(name) continue key = role_uuid_bytes(role.RoleUUID) if key in seen_bytes: continue seen_bytes.add(key) resolved.append(role) return resolved, missing def filter_role_uuids_for_club_scope(role_uuids, club_id, scope, allowed_club_ids=None): """ 按当前俱乐部上下文过滤 UserRole 绑定的 RoleUUID。 SINGLE_CLUB:本俱乐部角色 + 全局角色(ClubID 空/NULL)。 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] 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): """当前俱乐部上下文下的 gvsdsdk 权限码(与 verify_kefu_permission / 菜单共用)。""" from jituan.constants import DATA_SCOPE_SINGLE from gvsdsdk.models import Permission as SdkPermission, RolePermission as SdkRolePermission, UserRole as SdkUserRole eff_scope = scope or DATA_SCOPE_SINGLE role_uuids = list(SdkUserRole.objects.filter( UserUUID=user.UserUUID, ).values_list('RoleUUID', flat=True)) role_uuids = filter_role_uuids_for_club_scope( role_uuids, club_id, eff_scope, allowed_club_ids, ) if not role_uuids: return [] perm_uuids = SdkRolePermission.objects.filter( RoleUUID__in=role_uuids, ).values_list('PermUUID', flat=True) return list( SdkPermission.objects.filter( PermUUID__in=perm_uuids, PermStatus=1, ).values_list('PermCode', flat=True).distinct() ) def resolve_effective_permission_codes(user, club_id, scope, allowed_club_ids=None): """ 菜单与各业务接口统一权限码:当前俱乐部 gvsdsdk 角色 + 过渡期 legacy 兜底。 """ from backend.utils import _legacy_backend_role_permissions from gvsdsdk.models import UserRole as SdkUserRole codes = resolve_permission_codes_for_club( user, club_id, allowed_club_ids, scope=scope, ) if codes: return codes legacy_codes, _ = _legacy_backend_role_permissions(getattr(user, 'Phone', '')) 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 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) ) 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): """定位后台客服 User;优先 yonghuid(同手机号多账号时必须用 ID)。""" from users.business_models import User uid = (yonghuid or '').strip() if uid: user = User.objects.filter(UserUID=uid, KefuProfile__isnull=False).first() if user: return user ph = (phone or '').strip() if not ph: return None return User.objects.filter(Phone=ph, KefuProfile__isnull=False).order_by('-UserCreateTime').first()