- admin-role-navigation-contracts.mjs L40-41 原断言 referral.manage / matchmaker.service.view,但后端 permissions 表不存在这两个幽灵码—— v1.1.1 权限码对齐(4edb761)时漏更新守卫基线,导致该守卫自 v1.1.1 起恒红 - 修正为后端真实 authority 码:/referral→matchmaker.service、 /matchmaker→matchmaker.view(守卫内容修正,非删守卫,MIN_GUARDS 不变) - 实测 UI 契约 24/24 全绿
99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
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', 'matchmaker.service'],
|
|
['/matchmaker', 'matchmaker.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')
|