const CLUB_ID_KEY = 'admin_club_id'; const CLUB_SCOPE_KEY = 'admin_club_scope'; const CLUB_CONTEXT_KEY = 'admin_club_context'; const MENU_ACCESS_KEY = 'kefu_menu_access'; const MENU_SESSION_KEY = 'kefu_menu_session_v'; const MENU_SESSION_VERSION = '27'; const DEFAULT_CLUB_ID = 'xq'; let menuAccessInflight = null; export function getAdminClubId() { return localStorage.getItem(CLUB_ID_KEY) || DEFAULT_CLUB_ID; } export function getAdminClubScope() { return localStorage.getItem(CLUB_SCOPE_KEY) || 'single'; } export function setAdminClubContext(ctx) { if (!ctx) return; localStorage.setItem(CLUB_CONTEXT_KEY, JSON.stringify(ctx)); localStorage.setItem(CLUB_ID_KEY, ctx.club_id || DEFAULT_CLUB_ID); localStorage.setItem(CLUB_SCOPE_KEY, ctx.scope === 'ALL_CLUBS' ? 'all' : 'single'); } export function mergeServerClubContext(serverCtx) { if (!serverCtx) return; const allowedIds = new Set((serverCtx.clubs || []).map((c) => c.club_id)); const prevScope = localStorage.getItem(CLUB_SCOPE_KEY); const prevClubId = localStorage.getItem(CLUB_ID_KEY); let clubId = serverCtx.club_id || DEFAULT_CLUB_ID; let scopeKey = serverCtx.scope === 'ALL_CLUBS' ? 'all' : 'single'; if (prevClubId && allowedIds.has(prevClubId)) { clubId = prevClubId; if (prevScope === 'all' && serverCtx.is_group_admin) { scopeKey = 'all'; } else if (prevScope === 'single') { scopeKey = 'single'; } } else if (prevScope === 'all' && serverCtx.is_group_admin) { scopeKey = 'all'; } else if (!allowedIds.has(clubId) && allowedIds.size > 0) { clubId = serverCtx.club_id || [...allowedIds][0] || DEFAULT_CLUB_ID; } const merged = { ...serverCtx, club_id: clubId, scope: scopeKey === 'all' ? 'ALL_CLUBS' : 'SINGLE_CLUB', }; if (scopeKey === 'single' && merged.assignments?.length) { const hit = merged.assignments.find((a) => a.club_id === clubId); if (hit) { merged.role_code = hit.role_code; merged.role_name = hit.role_name; } } if (prevClubId && prevClubId !== clubId) { clearMenuAccess(); } setAdminClubContext(merged); return merged; } export function resetAdminClubContextOnLogin() { localStorage.removeItem(CLUB_ID_KEY); localStorage.removeItem(CLUB_SCOPE_KEY); localStorage.removeItem(CLUB_CONTEXT_KEY); clearMenuAccess(); } export function getAdminClubContext() { try { const raw = localStorage.getItem(CLUB_CONTEXT_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } export function setMenuAccess(access) { if (!access) return; localStorage.setItem(MENU_ACCESS_KEY, JSON.stringify(access)); sessionStorage.setItem(MENU_SESSION_KEY, MENU_SESSION_VERSION); } export function clearMenuAccess() { localStorage.removeItem(MENU_ACCESS_KEY); sessionStorage.removeItem(MENU_SESSION_KEY); } export function getMenuAccess() { try { const raw = localStorage.getItem(MENU_ACCESS_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } export function isMenuSessionFresh() { return sessionStorage.getItem(MENU_SESSION_KEY) === MENU_SESSION_VERSION; } export function shouldShowAllMenus(access) { if (!access) return false; if (Array.isArray(access.visible_page_ids) && access.visible_page_ids.length > 0) { return false; } if (Array.isArray(access.visible_paths) && access.visible_paths.length > 0) { return false; } if (Array.isArray(access.permission_codes) && access.permission_codes.length > 0) { return false; } return false; } /** ??? kefu_menu_definitions ???visible_paths ???? permission_codes ?? */ const KEFU_MENU_ROWS = [ { page_id: 'finance.bankuai', parent_id: 'finance', path: '/finance/bankuai', sort_order: 11, perm_codes: ['bankuai'] }, { page_id: 'finance.data', parent_id: 'finance', path: '/finance/data', sort_order: 12, perm_codes: ['caiwu'] }, { page_id: 'assessment.examiner', parent_id: 'assessment', path: '/assessment/examiner', sort_order: 21, perm_codes: ['kaohepeizhi'] }, { page_id: 'assessment.config', parent_id: 'assessment', path: '/assessment/config', sort_order: 22, perm_codes: ['kaohepeizhi'] }, { page_id: 'assessment.record', parent_id: 'assessment', path: '/assessment/record', sort_order: 23, perm_codes: ['kaohepeizhi'] }, { page_id: 'order.platform', parent_id: 'order', path: '/order/platform', sort_order: 31, perm_codes: ['002ab'] }, { page_id: 'order.merchant', parent_id: 'order', path: '/order/merchant', sort_order: 32, perm_codes: ['002ac', '002ab', '003aa'] }, { page_id: 'cross-platform.order', parent_id: 'cross-platform', path: '/cross-order', sort_order: 41, perm_codes: ['003aa'] }, { page_id: 'cross-platform.settings', parent_id: 'cross-platform', path: '/cross-settings', sort_order: 42, perm_codes: ['003aa'] }, { page_id: 'cross-platform.data', parent_id: 'cross-platform', path: '/cross-data', sort_order: 43, perm_codes: ['003aa'] }, { page_id: 'cross-platform.pre', parent_id: 'cross-platform', path: '/cross-pre', sort_order: 44, perm_codes: ['003aa'] }, { page_id: 'user.player', parent_id: 'user', path: '/user/player', sort_order: 51, perm_codes: ['001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'] }, { page_id: 'user.guanli', parent_id: 'user', path: '/user/guanli', sort_order: 52, perm_codes: ['4400a', '4400b', '4400c', '4400d', '4400e', '4400f'] }, { page_id: 'user.shangjia', parent_id: 'user', path: '/user/shangjia', sort_order: 53, perm_codes: ['1100a', '1100b'] }, { page_id: 'user.zuzhang', parent_id: 'user', path: '/user/zuzhang', sort_order: 54, perm_codes: ['6600a', '6600b', '6600c', '6600d', '6600e'] }, { page_id: 'user.identity-tag', parent_id: 'user', path: '/user/identity-tag', sort_order: 55, perm_codes: ['8080a', 'sfbq666'] }, { page_id: 'withdraw.audit', parent_id: 'withdraw', path: '/withdraw/audit', sort_order: 61, perm_codes: ['005bb'] }, { page_id: 'withdraw.data', parent_id: 'withdraw', path: '/withdraw/data', sort_order: 63, perm_codes: ['005bb'] }, { page_id: 'withdraw.settings', parent_id: 'withdraw', path: '/withdraw/settings', sort_order: 64, perm_codes: ['5500a', '5500b', '5500c'] }, // 管理员:超级管理 000001 可见(不靠 perm_codes) { page_id: 'admin.role', parent_id: 'admin', path: '/admin/role', sort_order: 71, require_super: true }, { page_id: 'admin.user', parent_id: 'admin', path: '/admin/user', sort_order: 74, require_super: true }, { page_id: 'admin.operation-log', parent_id: 'admin', path: '/admin/operation-log', sort_order: 75, perm_codes: ['czrz666'] }, { page_id: 'punishment', parent_id: '', path: '/punishment', sort_order: 80, perm_codes: ['005aa', '66693a', '66693b', '66693c', '66694c', '99933abs'] }, { page_id: 'gongdan', parent_id: '', path: '/gongdan', sort_order: 81, perm_codes: ['gdck666', 'gdcl666'] }, { page_id: 'product.list', parent_id: 'product', path: '/product/list', sort_order: 91, perm_codes: ['2200a'] }, { page_id: 'product.type-zone', parent_id: 'product', path: '/product/type-zone', sort_order: 92, perm_codes: ['7007a'] }, { page_id: 'product.turntable', parent_id: 'product', path: '/product/turntable', sort_order: 93, perm_codes: ['2200a'] }, { page_id: 'product.data', parent_id: 'product', path: '/product/data', sort_order: 94, perm_codes: ['2200a'] }, { page_id: 'product.fake-orders', parent_id: 'product', path: '/product/fake-orders', sort_order: 95, perm_codes: ['2200a'] }, { page_id: 'member.list', parent_id: 'member', path: '/member/list', sort_order: 101, perm_codes: ['3300a'] }, { page_id: 'member.data', parent_id: 'member', path: '/member/data', sort_order: 102, perm_codes: ['3300a'] }, { page_id: 'miniapp.carousel', parent_id: 'miniapp', path: '/miniapp/carousel', sort_order: 111, perm_codes: ['8080a'] }, { page_id: 'miniapp.assets', parent_id: 'miniapp', path: '/miniapp/assets', sort_order: 112, perm_codes: ['8080a'] }, { page_id: 'miniapp.popup', parent_id: 'miniapp', path: '/miniapp/popup-notice', sort_order: 113, perm_codes: ['8080a'] }, { page_id: 'miniapp.leixing', parent_id: 'miniapp', path: '/miniapp/leixing', sort_order: 115, perm_codes: ['8080a'] }, { page_id: 'miniapp.kaohe-tags', parent_id: 'miniapp', path: '/miniapp/kaohe-tags', sort_order: 116, perm_codes: ['8080a'] }, { page_id: 'miniapp.dashou-exam', parent_id: 'miniapp', path: '/miniapp/dashou-exam', sort_order: 117, perm_codes: ['dsks666', '8080a'] }, { page_id: 'miniapp.rank-reward', parent_id: 'miniapp', path: '/miniapp/rank-reward', sort_order: 118, perm_codes: ['phbj666', '8080a'] }, { page_id: 'miniapp.recharge-copy', parent_id: 'miniapp', path: '/miniapp/recharge-copy', sort_order: 119, perm_codes: ['8080a'] }, { page_id: 'miniapp.withdraw-settings', parent_id: 'miniapp', path: '/withdraw/settings', sort_order: 114, perm_codes: ['5500a', '5500b', '5500c'] }, { page_id: 'shop.list', parent_id: 'shop', path: '/shop/list', sort_order: 121, perm_codes: ['999a', '999b', '999c', '999d', '999e'] }, { page_id: 'shop.withdraw', parent_id: 'shop', path: '/shop/withdraw-apply', sort_order: 122, perm_codes: ['999a', '999b', '999c', '999d', '999e'] }, { page_id: 'shop.products', parent_id: 'shop', path: '/shop/products', sort_order: 123, perm_codes: ['999a', '999b', '999c', '999d', '999e'] }, { page_id: 'chat.agent', parent_id: '', path: '/chat/agent', sort_order: 130, perm_codes: ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] }, ]; const KEFU_MENU_PARENT_IDS = { admin: ['admin.role', 'admin.data-scope', 'admin.club-config', 'admin.order-grab-share', 'admin.user', 'admin.operation-log'], finance: ['finance.bankuai', 'finance.data'], assessment: ['assessment.examiner', 'assessment.config', 'assessment.record'], order: ['order.platform', 'order.merchant'], 'cross-platform': ['cross-platform.order', 'cross-platform.settings', 'cross-platform.data', 'cross-platform.pre'], user: ['user.player', 'user.guanli', 'user.shangjia', 'user.zuzhang', 'user.identity-tag'], withdraw: ['withdraw.audit', 'withdraw.data', 'withdraw.settings'], product: ['product.list', 'product.type-zone', 'product.turntable', 'product.data', 'product.fake-orders'], member: ['member.list', 'member.data'], miniapp: ['miniapp.carousel', 'miniapp.assets', 'miniapp.popup', 'miniapp.leixing', 'miniapp.kaohe-tags', 'miniapp.dashou-exam', 'miniapp.rank-reward', 'miniapp.recharge-copy', 'miniapp.withdraw-settings'], shop: ['shop.list', 'shop.withdraw', 'shop.products'], }; function pathsFromPermissionCodes(codes) { if (!codes?.length) return []; const seen = new Set(); const paths = []; for (const row of KEFU_MENU_ROWS) { if (!row.path) continue; const perms = row.perm_codes || []; if (!perms.some((p) => codes.includes(p))) continue; if (seen.has(row.path)) continue; seen.add(row.path); paths.push(row.path); } return paths; } function pageAllowedByCodes(pageId, codes) { const row = KEFU_MENU_ROWS.find((r) => r.page_id === pageId); if (row?.perm_codes?.some((p) => codes.includes(p))) return true; const children = KEFU_MENU_PARENT_IDS[pageId]; if (!children) return false; return children.some((childId) => pageAllowedByCodes(childId, codes)); } export function getEffectiveVisiblePaths(access = null) { const a = access || getMenuAccess(); const paths = a?.visible_paths; return Array.isArray(paths) ? paths : []; } /** * 兼容尚未 seed 到服务端的新菜单:本地已定义且 canShowMenuPage 通过即可进路由。 * 避免「侧栏看得见、点进去却被踢到默认页(如板块配置)」。 */ export function isLocallyDefinedMenuPathAllowed(path) { if (!path) return false; const row = KEFU_MENU_ROWS.find( (r) => r.path && (path === r.path || path.startsWith(`${r.path}/`)), ); if (!row) return false; return canShowMenuPage(row.page_id); } export function hasPermCode(access, ...codes) { if (!access || !codes.length) return false; const list = access.permission_codes || []; return codes.some((c) => list.includes(c)); } export function hasMerchantOrderPerm(access) { return hasPermCode(access, '002ac', '002ab', '003aa'); } export function canShowMenuPage(pageId) { const access = getMenuAccess(); if (!access) return false; const codes = access.permission_codes || []; const hasSuper = !!access.is_super || codes.includes('000001'); // 本店超级管理:管理员分组 / 角色管理 / 后台用户(不依赖服务端是否漏 seed) if ( hasSuper && (pageId === 'admin' || pageId === 'admin.role' || pageId === 'admin.user') ) { return true; } // 集团权限:订单互通配置(菜单 seed 未跑时本地兜底) if (pageId === 'admin.order-grab-share' && access.is_group_admin) { return true; } const ids = access?.visible_page_ids; if (Array.isArray(ids) && ids.length > 0) { if (ids.includes(pageId)) return true; const children = KEFU_MENU_PARENT_IDS[pageId]; if (children && children.some((id) => ids.includes(id))) return true; // 兼容尚未 seed 的新菜单:本地已定义 + 用户有对应权限(或超管) const row = KEFU_MENU_ROWS.find((r) => r.page_id === pageId); if (!row) return false; if (access.is_super || hasSuper) { if (row.require_super) return true; } return pageAllowedByCodes(pageId, codes); } if (access.is_super || access.is_group_admin || hasSuper) { const row = KEFU_MENU_ROWS.find((r) => r.page_id === pageId); if (row?.require_super && hasSuper) return true; return !!KEFU_MENU_ROWS.find((r) => r.page_id === pageId); } return pageAllowedByCodes(pageId, codes); } /** 写入菜单缓存,并同步服务端校正后的 effective_club_id */ export function applyMenuAccessPayload(data) { if (!data) return null; const scope = getAdminClubScope(); const expectedClub = getAdminClubId(); if ( data.effective_club_id && scope !== 'all' && data.effective_club_id !== expectedClub ) { const ctx = getAdminClubContext() || {}; setAdminClubContext({ ...ctx, club_id: data.effective_club_id, scope: 'SINGLE_CLUB', }); } setMenuAccess(data); return data; } export function getFirstVisiblePath() { const paths = getEffectiveVisiblePaths(); return paths.length > 0 ? paths[0] : null; } export function getDefaultLandingPath(access = null) { const paths = getEffectiveVisiblePaths(access); return paths.length > 0 ? paths[0] : null; } /** menu_ready=false: server seed missing; empty codes: role not bound */ export function formatNoMenuAccessMessage(access) { const codes = access?.permission_codes || []; const roles = access?.role_names || []; const uid = access?.yonghuid || ''; const idHint = uid ? ` UserID=${uid}.` : ''; if (access?.menu_ready === false && access?.menu_source === 'db') { return `Menu table empty.${idHint} Server: python manage.py migrate jituan && python manage.py seed_kefu_menu`; } if (!codes.length) { if (roles.length) { return `No permissions.${idHint} Roles: [${roles.join(', ')}]. Add perm kaohepeizhi to these roles in Role Mgmt.`; } return `No role on account.${idHint} Admin Users -> open THIS user (check UserID) -> Add Role -> Save -> re-login.`; } if (!getEffectiveVisiblePaths(access).length) { return `Perms: [${codes.join(', ')}].${idHint} No menu path from server.`; } return `Perms: [${codes.join(', ')}].${idHint} Contact admin.`; } export function canSwitchClubByMenu() { const ctx = getAdminClubContext(); if (ctx?.can_switch_club) return true; const access = getMenuAccess(); return !!access?.can_switch_club; } export async function syncClubContextFromServer(fetcher) { const prevClubId = getAdminClubId(); try { const data = await fetcher(); if (data) { mergeServerClubContext(data); if (getAdminClubId() !== prevClubId) { clearMenuAccess(); } } } catch (e) { console.error('syncClubContextFromServer failed:', e); } } export async function ensureMenuAccess(fetcher, { force = false } = {}) { if (!localStorage.getItem('token')) return getMenuAccess(); if (!force && isMenuSessionFresh() && getMenuAccess()) { return getMenuAccess(); } if (!menuAccessInflight) { menuAccessInflight = (async () => { try { const data = await fetcher(); if (data) return applyMenuAccessPayload(data); return getMenuAccess(); } catch (e) { console.error('ensureMenuAccess failed:', e); return getMenuAccess(); } finally { menuAccessInflight = null; } })(); } return menuAccessInflight; } export function applyClubHeaders(config = {}) { const headers = { ...(config.headers || {}) }; const clubId = getAdminClubId(); const scope = getAdminClubScope(); headers['X-Club-Id'] = clubId; if (scope === 'all') { headers['X-Club-Scope'] = 'all'; } return { ...config, headers }; } export const JITUAN_API = { kefuLogin: '/jituan/auth/kefu-login', meContext: '/jituan/auth/me-context', menuAccess: '/jituan/auth/menu-access', permDebug: '/jituan/auth/perm-debug', clubList: '/jituan/club/list', caiwu: '/jituan/houtai/caiwu', szxx: '/jituan/houtai/szxx', auditLog: '/jituan/houtai/audit-log', orderTypes: '/jituan/houtai/cwddhqlx', orderFinance: '/jituan/houtai/cwhqjtddsj', chongzhiFinance: '/jituan/houtai/cwqtczhq', huiyuanBankuai: '/jituan/houtai/cwhybkhq', huiyuanStats: '/jituan/houtai/hybkjtsj', userSummary: '/jituan/houtai/user-summary', dashouList: '/jituan/houtai/kefuhqdslb', guanshiList: '/jituan/houtai/hthqgslb', shangjiaList: '/jituan/houtai/hqsjgllb', shangjiaAdd: '/jituan/houtai/tjsj', zuzhangList: '/jituan/houtai/hthqzzlb', operationLog: '/jituan/houtai/hqczrz', withdrawGet: '/jituan/houtai/hthqtxpz', withdrawUpdate: '/jituan/houtai/htxgtxsz', lunboList: '/jituan/houtai/lunbo-list', lunboManage: '/jituan/houtai/lunbo-manage', gonggao: '/jituan/houtai/gonggao', rechargePageCopyGet: '/jituan/houtai/recharge-page-copy/get', rechargePageCopySave: '/jituan/houtai/recharge-page-copy/save', tupianList: '/jituan/houtai/tupian-list', tupianManage: '/jituan/houtai/tupian-manage', popupList: '/jituan/houtai/hthqtcxx', popupModify: '/jituan/houtai/htxgtcxx', penaltyStats: '/jituan/houtai/htglyhqcfsltj', penaltyList: '/jituan/houtai/hthqfklb', penaltyAction: '/jituan/houtai/glyclfk', penaltyPlatformAudit: '/jituan/houtai/glyptshfk', penaltyCreate: '/jituan/houtai/htfksc', kefuCfgl: '/jituan/houtai/kefu-cfgl', adminAssignments: '/jituan/houtai/admin-assignments', clubManage: '/jituan/houtai/club-manage', orderGrabLink: '/jituan/houtai/order-grab-link', paymentChannelManage: '/jituan/houtai/payment-channel-manage', clubCertUpload: '/jituan/houtai/club-cert-upload', memberList: '/jituan/houtai/hthqhylb', memberUpdate: '/jituan/houtai/htxghyxx', memberAdd: '/jituan/houtai/httjhy', memberCatalog: '/jituan/houtai/hthycatalog', memberEnable: '/jituan/houtai/hthysj', memberDisable: '/jituan/houtai/hthyxj', memberCardUpload: '/jituan/houtai/hy-card-upload', memberRechargeRecords: '/jituan/houtai/hy-recharge-records', memberBundleCreate: '/jituan/houtai/hy-bundle-create', identityTagList: '/jituan/houtai/identity-tag/list', identityTagManage: '/jituan/houtai/identity-tag/manage', identityTagBindList: '/jituan/houtai/identity-tag/bind-list', identityTagBind: '/jituan/houtai/identity-tag/bind', miniappIconList: '/jituan/houtai/miniapp-icon-list', miniappIconManage: '/jituan/houtai/miniapp-icon-manage', pindaoManage: '/jituan/houtai/pindao-manage', fakeGrabOrderList: '/jituan/houtai/fake-grab-order-list', fakeGrabOrderManage: '/jituan/houtai/fake-grab-order-manage', fakeGrabOrderAvatar: '/jituan/houtai/fake-grab-order-avatar', clubLeixingList: '/jituan/houtai/club-leixing-list', clubLeixingCatalog: '/jituan/houtai/club-leixing-catalog', clubLeixingEnable: '/jituan/houtai/club-leixing-enable', clubLeixingDisable: '/jituan/houtai/club-leixing-disable', clubLeixingIcon: '/jituan/houtai/club-leixing-icon', clubKaoheList: '/jituan/houtai/club-kaohe-list', clubKaoheCatalog: '/jituan/houtai/club-kaohe-catalog', clubKaoheEnable: '/jituan/houtai/club-kaohe-enable', clubKaoheDisable: '/jituan/houtai/club-kaohe-disable', khggl: '/jituan/houtai/khggl', khgglAdd: '/jituan/houtai/khggl-add', shgxgsj: '/jituan/houtai/shgxgsj', dashouExamConfig: '/jituan/houtai/dashou-exam-config', dashouExamQuestionList: '/jituan/houtai/dashou-exam-question-list', dashouExamQuestionManage: '/jituan/houtai/dashou-exam-question-manage', dashouExamImageManage: '/jituan/houtai/dashou-exam-image-manage', };