import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' /** * Phase 19(统一运营后台)门控:后台角色导航契约。 * * 背景:本轮复验发现 MainLayout 的 navigation 数组**缺少多个业务入口**—— * `/service-appointment`(服务站运营人员的本职工作)、`/banner`、`/service-program`、 * `/appeals`、`/audit`、`/matchmaker` 都只有路由没有菜单项。结果是这些角色登录后 * **看不到自己的本职入口**(Gate「七类后台角色只看到自己的工作」被击穿为"连自己的工作也看不到")。 * * 本门控把两件事变成可自动验证的不变量: * 1. 关键业务域必须在菜单中有入口(删菜单项立即变红); * 2. 菜单不得指向不存在的路由(避免点进去 404 / 白屏)。 */ const root = resolve(import.meta.dirname, '..') const read = (p) => readFileSync(resolve(root, p), 'utf8') const layout = read('admin-web/src/layouts/MainLayout.vue') const router = read('admin-web/src/router/index.ts') // —— 解析 navigation 数组 —— const NAV_ENTRY = /\{\s*label:\s*'([^']+)',\s*path:\s*'([^']+)',\s*permissions:\s*\[([^\]]*)\][^}]*\}/g const navigation = [] for (const m of layout.matchAll(NAV_ENTRY)) { const perms = [...m[3].matchAll(/'([^']+)'/g)].map((x) => x[1]) navigation.push({ label: m[1], path: m[2], permissions: perms }) } assert.ok(navigation.length >= 18, `导航项数量异常偏少(${navigation.length}),解析可能失效`) // —— 1. 关键业务域必须有菜单入口(path, 该入口所需的其中之一权限)—— const requiredMenuEntries = [ ['/review', 'review.view'], ['/users', 'user.view'], ['/activities', 'activity.view'], ['/activities/checkin', 'activity.checkin'], ['/activities/interests', 'activity.review_registration'], ['/referral', 'referral.manage'], ['/matchmaker', 'matchmaker.service.view'], ['/counseling', 'counseling.service.view'], ['/banner', 'banner.view'], ['/content', 'content.view'], ['/service-station', 'service_location.view'], ['/service-appointment', 'service_appointment.view'], ['/service-program', 'service_program.manage'], ['/risk/reports', 'risk.report.view'], ['/appeals', 'risk.report.process'], ['/analytics', 'analytics.view'], ['/organizations', 'organization.manage'], ['/audit', 'user.view'], ['/operators', 'permission.manage'], ] const byPath = new Map(navigation.map((n) => [n.path, n])) for (const [path, perm] of requiredMenuEntries) { const entry = byPath.get(path) assert.ok(entry, `后台菜单缺少业务入口 ${path}(角色将看不到自己的本职工作)`) assert.ok( entry.permissions.includes(perm), `菜单 ${path} 的权限应为 ${perm},实际 ${JSON.stringify(entry.permissions)}`, ) } // —— 2. 菜单不得指向不存在的路由 —— const routerPaths = new Set( [...router.matchAll(/path:\s*'([^']+)'/g)].map((m) => m[1]), ) for (const entry of navigation) { if (entry.path === '/dashboard') continue assert.ok( routerPaths.has(entry.path) || routerPaths.has(entry.path.replace(/^\//, '')), `菜单 ${entry.label} 指向不存在的路由 ${entry.path}`, ) } // —— 3. 菜单项路径不得重复(防止同一入口被登记两次导致角色看到重复项)—— const seen = new Set() for (const entry of navigation) { assert.ok(!seen.has(entry.path), `菜单路径重复:${entry.path}`) seen.add(entry.path) } // —— 4. 角色名判定必须覆盖七类后台角色(缺分支会让角色被降级显示为"运营人员")—— for (const marker of [ '平台管理员', '咨询管理员', '内容编辑员', '风控运营人员', '公益咨询人员', '服务站运营人员', '资料审核人员', ]) { assert.ok(layout.includes(marker), `角色名判定缺少「${marker}」分支`) } console.log('Admin role navigation contracts passed')