SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const scriptPath = fileURLToPath(import.meta.url)
|
||||
const appRoot = path.resolve(path.dirname(scriptPath), '..')
|
||||
|
||||
async function source(relativePath) {
|
||||
return readFile(path.join(appRoot, relativePath), 'utf8')
|
||||
}
|
||||
|
||||
const [
|
||||
navigationSource,
|
||||
moduleCatalogSource,
|
||||
inventorySource,
|
||||
pageRendererSource,
|
||||
adapterBlockSource,
|
||||
searchFormSource,
|
||||
entityFormSource
|
||||
] = await Promise.all([
|
||||
source('src/utils/modernNavigation.ts'),
|
||||
source('src/data/moduleCatalog.ts'),
|
||||
source('public/generated/ui-inventory.json'),
|
||||
source('src/components/erp/ErpPageRenderer.vue'),
|
||||
source('src/components/erp/ErpAdapterBlock.vue'),
|
||||
source('src/components/erp/ErpSearchForm.vue'),
|
||||
source('src/components/erp/ErpEntityForm.vue')
|
||||
])
|
||||
|
||||
const inventory = JSON.parse(inventorySource)
|
||||
|
||||
function loadNavigationExports(sourceCode) {
|
||||
const executableSource = sourceCode
|
||||
.replace(/export const legacyControlPagePrefixes:\s*Record<string,\s*string>\s*=/, 'const legacyControlPagePrefixes =')
|
||||
.replace(/export function /g, 'function ')
|
||||
.replace(/([,(]\s*)([A-Za-z_$][\w$]*):\s*(?:string|unknown)/g, '$1$2')
|
||||
|
||||
return Function(`${executableSource}
|
||||
return {
|
||||
legacyControlPagePrefixes,
|
||||
isInternalLegacyControlTarget,
|
||||
pageHashForTarget,
|
||||
navigateTarget,
|
||||
normalizeModernNavigationTarget
|
||||
}
|
||||
`)()
|
||||
}
|
||||
|
||||
function moduleBlocks(sourceCode) {
|
||||
return [...sourceCode.matchAll(/\{\n\s+id: '([^']+)'[\s\S]*?\n\s+workflows:/g)]
|
||||
.map((match) => {
|
||||
const block = match[0]
|
||||
return {
|
||||
id: match[1],
|
||||
prefixes: stringArrayValue(block, 'prefixes'),
|
||||
legacyIncludes: stringArrayValue(block, 'legacyIncludes'),
|
||||
quickPages: [...block.matchAll(/pageId: '([^']+)'/g)].map((item) => item[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function stringArrayValue(sourceCode, propertyName) {
|
||||
const match = sourceCode.match(new RegExp(`${propertyName}: \\[([\\s\\S]*?)\\]`))
|
||||
return match?.[1].match(/'([^']+)'/g)?.map((item) => item.slice(1, -1)) || []
|
||||
}
|
||||
|
||||
function moduleOwnsRoute(module, route) {
|
||||
const haystack = [
|
||||
route.pageId,
|
||||
route.title,
|
||||
route.component,
|
||||
route.domain,
|
||||
route.legacyPath,
|
||||
route.modernPath,
|
||||
...(route.permissions || [])
|
||||
].filter(Boolean).join(' ').toLowerCase()
|
||||
|
||||
return module.quickPages.includes(route.pageId)
|
||||
|| module.prefixes.some((prefix) => String(route.pageId || '').startsWith(prefix) || haystack.includes(prefix.toLowerCase()))
|
||||
|| module.legacyIncludes.some((include) => haystack.includes(include.toLowerCase()))
|
||||
}
|
||||
|
||||
const navigationExports = loadNavigationExports(navigationSource)
|
||||
const routeManifest = inventory.routeManifest || []
|
||||
const routePageIds = new Set(routeManifest.map((route) => route.pageId))
|
||||
const modules = moduleBlocks(moduleCatalogSource)
|
||||
|
||||
for (const exportName of [
|
||||
'legacyControlPagePrefixes',
|
||||
'isInternalLegacyControlTarget',
|
||||
'pageHashForTarget',
|
||||
'navigateTarget',
|
||||
'normalizeModernNavigationTarget'
|
||||
]) {
|
||||
assert.match(
|
||||
navigationSource,
|
||||
new RegExp(`export function ${exportName}|export const ${exportName}`),
|
||||
`modern navigation utility must export ${exportName}`
|
||||
)
|
||||
}
|
||||
|
||||
for (const [legacyContext, pagePrefix] of [
|
||||
['ordermgr', 'order'],
|
||||
['partymgr', 'party'],
|
||||
['sfa', 'SalesForceAutomation'],
|
||||
['projectmgr', 'projectmgr'],
|
||||
['webtools', 'webtools']
|
||||
]) {
|
||||
assert.match(
|
||||
navigationSource,
|
||||
new RegExp(`${legacyContext}: '${pagePrefix}'`),
|
||||
`/${legacyContext}/control routes must map to ${pagePrefix} PageDefinition ids`
|
||||
)
|
||||
}
|
||||
|
||||
const generatedContexts = new Set()
|
||||
for (const route of routeManifest) {
|
||||
const context = String(route.legacyPath || '').match(/^\/([^/]+)\/control\//)?.[1]
|
||||
if (context) generatedContexts.add(context)
|
||||
}
|
||||
|
||||
const missingContextPrefixes = [...generatedContexts]
|
||||
.filter((context) => !navigationExports.legacyControlPagePrefixes[context])
|
||||
.sort()
|
||||
assert.deepEqual(
|
||||
missingContextPrefixes,
|
||||
[],
|
||||
'every generated legacy /control context must map to a PageDefinition prefix'
|
||||
)
|
||||
|
||||
const mismatchedLegacyTargets = routeManifest
|
||||
.map((route) => ({
|
||||
legacyPath: route.legacyPath,
|
||||
pageId: route.pageId,
|
||||
actual: navigationExports.pageHashForTarget(route.legacyPath, 'ofbiz'),
|
||||
expected: `#/pages/${route.pageId}`
|
||||
}))
|
||||
.filter((item) => item.actual !== item.expected)
|
||||
assert.deepEqual(
|
||||
mismatchedLegacyTargets,
|
||||
[],
|
||||
'legacy /control targets must normalize to existing #/pages PageDefinition ids'
|
||||
)
|
||||
|
||||
const missingQuickPages = modules.flatMap((module) =>
|
||||
module.quickPages
|
||||
.filter((pageId) => !routePageIds.has(pageId))
|
||||
.map((pageId) => ({ moduleId: module.id, pageId }))
|
||||
)
|
||||
assert.deepEqual(
|
||||
missingQuickPages,
|
||||
[],
|
||||
'moduleCatalog quickPages must point at generated PageDefinition ids'
|
||||
)
|
||||
|
||||
const unownedRoutes = routeManifest
|
||||
.filter((route) => !modules.some((module) => moduleOwnsRoute(module, route)))
|
||||
.map((route) => ({
|
||||
pageId: route.pageId,
|
||||
legacyPath: route.legacyPath,
|
||||
component: route.component
|
||||
}))
|
||||
assert.deepEqual(
|
||||
unownedRoutes,
|
||||
[],
|
||||
'every generated route must be owned by a Modern UI ERP module'
|
||||
)
|
||||
|
||||
assert.doesNotMatch(
|
||||
navigationSource,
|
||||
/startsWith\('http'\)[\s\S]*return value/,
|
||||
'absolute external URLs must not be treated as internal SPA page routes'
|
||||
)
|
||||
|
||||
assert.match(
|
||||
pageRendererSource,
|
||||
/import \{[^}]*navigateTarget[^}]*normalizeModernNavigationTarget[^}]*\} from '\.\.\/\.\.\/utils\/modernNavigation'/,
|
||||
'page renderer must use the shared modern navigation utility'
|
||||
)
|
||||
assert.match(
|
||||
pageRendererSource,
|
||||
/isSharedInternalLegacyControlTarget|sharedLegacyControlPagePrefixes|pageHashForTarget/,
|
||||
'page renderer compatibility shims must delegate to the shared modern navigation utility'
|
||||
)
|
||||
assert.match(
|
||||
pageRendererSource,
|
||||
/normalizeModernNavigationTarget\(target, props\.page\.pageId\.split\('__'\)\[0\]/,
|
||||
'action navigation targets must be normalized before changing location'
|
||||
)
|
||||
|
||||
for (const [label, componentSource] of [
|
||||
['adapter block', adapterBlockSource],
|
||||
['search form', searchFormSource],
|
||||
['entity form', entityFormSource]
|
||||
]) {
|
||||
assert.match(
|
||||
componentSource,
|
||||
/import \{[^}]*navigateTarget[^}]*\} from '\.\.\/\.\.\/utils\/modernNavigation'/,
|
||||
`${label} must use the shared modern navigation utility`
|
||||
)
|
||||
assert.match(
|
||||
componentSource,
|
||||
/navigateTarget\([^)]*field\.target|navigateTarget\(item\.target/,
|
||||
`${label} links must normalize generated internal targets`
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Modern navigation source policy passed.')
|
||||
Reference in New Issue
Block a user