"""客服后台菜单可见性(侧栏展示,不影响各页面原有权限校验)。""" import logging from types import SimpleNamespace from backend.utils import _AllPermissions, verify_kefu_permission from jituan.models import KefuMenuPage from jituan.services.admin_context import ( build_admin_club_context, can_access_admin_user_management, 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, describe_roles_in_scope from jituan.services.kefu_menu_definitions import KEFU_MENU_ROW_DEFS logger = logging.getLogger(__name__) def _is_super_permissions(permissions): return isinstance(permissions, _AllPermissions) def _has_super_perm(permissions): """俱乐部超级管理权限 000001(角色/后台用户等),不含集团级配置。""" if _is_super_permissions(permissions): return True return '000001' in permissions def _page_visible(page, permissions, user): page_id = getattr(page, 'page_id', '') or '' if page.require_group_manage: return can_manage_admin_assignments(user) # 后台用户:本店超级管理 000001 / 集团 / 系统超管均可。 # 注意:历史上 DB 模型缺 require_admin_user_manage 字段,导致该页永远不可见,必须按 page_id 兜底。 if page_id == 'admin.user' or getattr(page, 'require_admin_user_manage', False): return can_access_admin_user_management(user, permissions) if page.require_super: return _has_super_perm(permissions) or is_system_super_admin(user) # 仅系统级超管可见全部菜单;俱乐部 000001 不展开业务菜单 if is_system_super_admin(user): return True codes = page.perm_codes or [] if not codes: return False return any(code in permissions for code in codes) def _permission_codes_list(permissions): if isinstance(permissions, _AllPermissions): return list(dict.fromkeys(permissions)) if len(permissions) else ['000001'] return list(permissions) if permissions else [] def _builtin_menu_pages(): """DB 无数据时的内置菜单(与 seed_kefu_menu 定义一致)。""" pages = [] for row in KEFU_MENU_ROW_DEFS: pages.append(SimpleNamespace( page_id=row['page_id'], name=row['name'], path=row.get('path', ''), parent_id=row.get('parent_id', ''), sort_order=row.get('sort_order', 0), perm_codes=row.get('perm_codes', []), require_super=row.get('require_super', False), require_group_manage=row.get('require_group_manage', False), require_admin_user_manage=row.get('require_admin_user_manage', False), )) return pages def _load_menu_pages(): """从 DB 加载菜单;失败或空表时尝试 seed,仍空则用内置定义。""" pages = [] menu_source = 'db' try: pages = KefuMenuPage.objects.all().order_by('sort_order', 'page_id') pages = list(pages) except Exception as e: logger.warning('KefuMenuPage.objects 读取失败: %s', e) pages = [] if not pages: try: pages = KefuMenuPage.query.order_by('sort_order', 'page_id').to_list() except Exception as e: logger.warning('KefuMenuPage.query 读取失败: %s', e) pages = [] need_seed = not pages or not any( p.page_id in ('miniapp.assets', 'product.fake-orders', 'miniapp.recharge-copy') for p in pages ) if need_seed: try: from django.core.management import call_command call_command('seed_kefu_menu') pages = list(KefuMenuPage.objects.all().order_by('sort_order', 'page_id')) except Exception as e: logger.warning('seed_kefu_menu 自动执行失败: %s', e) if not pages: pages = _builtin_menu_pages() menu_source = 'builtin' logger.warning('kefu_menu_page 表无数据,使用内置菜单定义(请执行 migrate jituan + seed_kefu_menu)') return pages, menu_source def build_menu_access_payload(request, username=None): """ 根据用户 gvsdsdk 权限计算可见菜单 page_id 列表。 返回 (payload, error_response);error_response 非空时表示校验失败。 """ phone = (username or getattr(request.user, 'Phone', '') or '').strip() kefu, permissions = verify_kefu_permission(request, phone) if kefu is None: return None, permissions pages, menu_source = _load_menu_pages() visible_ids = [] for page in pages: if not page.path: continue if _page_visible(page, permissions, request.user): visible_ids.append(page.page_id) for page in pages: if page.path: continue children = [p for p in pages if p.parent_id == page.page_id and p.path] if any(child.page_id in visible_ids for child in children): visible_ids.append(page.page_id) from jituan.constants import DATA_SCOPE_SINGLE from jituan.models import AdminAssignment club_ctx = build_admin_club_context(request.user) club_id = getattr(request, 'club_id', None) or resolve_club_id_from_request(request) scope = getattr(request, 'club_scope', None) or resolve_club_scope(request) allowed = list(get_allowed_club_ids(request.user)) perm_codes = _permission_codes_list(permissions) role_names = user_bound_role_names(request.user, club_id, scope, allowed) roles_detail = describe_roles_in_scope(request.user, club_id, scope, allowed) perm_hint = '' if scope == DATA_SCOPE_SINGLE and not perm_codes: has_assignment = AdminAssignment.query.filter( yonghuid=request.user.UserUID, status=1, club_id=club_id, ).exists() if has_assignment and not role_names: perm_hint = ( f'当前俱乐部({club_id})已有任职但未绑定功能角色,' '请在「后台用户」中为该俱乐部分配角色后重新登录。' ) elif not has_assignment: perm_hint = ( f'当前俱乐部({club_id})无功能角色;' '请确认已在该俱乐部绑定 gvsdsdk 角色(与数据范围任职是两套配置)。' ) 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] is_super = _has_super_perm(permissions) or is_system_super_admin(request.user) return { 'visible_page_ids': sorted(set(visible_ids)), 'visible_paths': visible_paths, 'permission_codes': perm_codes, 'role_names': role_names, 'yonghuid': getattr(request.user, 'UserUID', '') or '', 'club_id': club_id, 'club_scope': scope, 'effective_club_id': club_id, 'perm_hint': perm_hint, 'roles_in_scope': roles_detail, 'can_switch_club': bool(club_ctx.get('can_switch_club')), 'is_group_admin': bool(club_ctx.get('is_group_admin')), 'is_super': bool(is_super), 'menu_ready': True, 'menu_source': menu_source, }, None