Files
QiufengandClaude Opus 4.8 5e51dc3f56 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>
2026-06-15 19:19:15 +08:00

362 lines
13 KiB
JavaScript

#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const globalAdminForbiddenText = [
'Element Plus 定制 ERP 管理界面',
'Generated Element Plus preview',
'Migration Foundation',
'Page Definition',
'Storybook',
'不是页面预览',
'业务回归记录',
'业务等价',
'原应用清单',
'技术预览',
'旧 URL',
'旧入口',
'界面方案',
'组件展示页',
'组件展厅',
'组件规范',
'覆盖台账',
'迁移',
'页面标识',
'页面清单',
'待验收',
'历史地址'
]
const requiredDefaultRuntimeCaseIds = [
'login-door',
'admin-workbench',
'order-admin',
'party-admin',
'product-admin',
'accounting-admin',
'inventory-admin',
'manufacturing-admin',
'humanres-admin',
'sales-admin',
'procurement-admin',
'scrum-admin',
'operations-admin',
'content-admin',
'marketing-admin',
'commerce-admin',
'pos-admin',
'marketplace-admin',
'analytics-admin',
'extension-admin',
'global-business-center',
'route-unavailable',
'system-maintenance',
'security-admin',
'system-operations'
]
// The professional shell drives navigation from a grouped left sidebar tree
// (module = el-sub-menu, its quick pages = leaf items). These maps express the
// expected active module label (the sub-menu title) or the expected active
// top-level item for routes that are not nested under a module.
const moduleNavLabelByPath = new Map([
['#/orders', '订单'],
['#/catalog/products', '商品'],
['#/parties', '客户'],
['#/sales', '销售'],
['#/procurement', '采购'],
['#/accounting', '财务'],
['#/facility', '库存'],
['#/manufacturing', '生产'],
['#/humanres', '人事'],
['#/scrum', '敏捷'],
['#/operations', '运营'],
['#/content', '内容'],
['#/marketing', '营销'],
['#/commerce', '电商'],
['#/marketplace', '店铺'],
['#/pos', 'POS'],
['#/analytics', '报表'],
['#/extensions', '配置']
])
const moduleNavLabelById = new Map([
['order', '订单'],
['catalog', '商品'],
['party', '客户'],
['sales', '销售'],
['procurement', '采购'],
['accounting', '财务'],
['facility', '库存'],
['manufacturing', '生产'],
['humanres', '人事'],
['scrum', '敏捷'],
['operations', '运营'],
['content', '内容'],
['marketing', '营销'],
['commerce', '电商'],
['marketplace', '店铺'],
['pos', 'POS'],
['analytics', '报表'],
['system-admin', '运维'],
['extensions', '配置']
])
const topLevelNavByPath = new Map([
['#/', '运营工作台'],
['#/system', '系统管理'],
['#/system/security', '账号与角色'],
['#/system/operations', '运行监控']
])
const procurementPageIds = new Set([
'order__FindRequirements',
'order__ApproveRequirements',
'order__ApprovedProductRequirementsByVendor',
'ap__FindVendors',
'catalog__EditSupplierProduct',
'facility__ReceiveInventoryAgainstPurchaseOrder'
])
// Page id prefix -> owning module nav label, mirroring moduleCatalog prefixes.
const pagePrefixNavLabel = [
['SalesForceAutomation__', '销售'],
['accounting__', '财务'],
['ap__', '财务'],
['ar__', '财务'],
['order__', '订单'],
['catalog__', '商品'],
['party__', '客户'],
['facility__', '库存'],
['manufacturing__', '生产'],
['humanres__', '人事'],
['scrum__', '敏捷'],
['workeffort__', '运营'],
['projectmgr__', '运营'],
['content__', '内容'],
['marketing__', '营销'],
['ecommerce__', '电商'],
['ebay__', '店铺'],
['ebaystore__', '店铺'],
['webpos__', 'POS'],
['bi__', '报表'],
['birt__', '报表'],
['webtools__', '运维'],
['ofbizsetup__', '运维'],
['assetmaint__', '运维'],
['myportal__', '运维'],
['example__', '配置'],
['exampleext__', '配置'],
['pricat__', '配置'],
['pricatdemo__', '配置'],
['firstdata__', '配置'],
['msggateway__', '配置'],
['ismgr__', '配置'],
['scrumdemo__', '配置']
]
export function expectedRouteHash(route) {
const value = String(route || '#/')
const hashIndex = value.indexOf('#')
if (hashIndex >= 0) return value.slice(hashIndex)
return value.startsWith('/') ? `#${value}` : `#/${value.replace(/^\/+/, '')}`
}
function pathOnly(route) {
return expectedRouteHash(route).replace(/\?.*$/, '')
}
function pageIdForRoute(route) {
const path = pathOnly(route)
if (!path.startsWith('#/pages/')) return ''
return decodeURIComponent(path.slice('#/pages/'.length))
}
/**
* The expected active navigation target for a route:
* - { topLevel } when a top-level sidebar item should be active (workbench, system)
* - { moduleLabel } when a module sub-menu should be the active/open group
* - null when the route has no dedicated sidebar entry (business center, unknown)
*/
export function expectedSideNavForRoute(route) {
const path = pathOnly(route)
if (topLevelNavByPath.has(path)) return { topLevel: topLevelNavByPath.get(path) }
if (moduleNavLabelByPath.has(path)) return { moduleLabel: moduleNavLabelByPath.get(path) }
if (path.startsWith('#/module/')) {
const moduleId = decodeURIComponent(path.slice('#/module/'.length))
const label = moduleNavLabelById.get(moduleId)
return label ? { moduleLabel: label } : null
}
const pageId = pageIdForRoute(route)
if (pageId) {
if (procurementPageIds.has(pageId)) return { moduleLabel: '采购' }
const match = pagePrefixNavLabel.find(([prefix]) => pageId.startsWith(prefix))
if (match) return { moduleLabel: match[1] }
}
return null
}
// Back-compat export: returns the single expected nav label for a route, or ''.
export function expectedTopMenuTextForRoute(route) {
const expected = expectedSideNavForRoute(route)
if (!expected) return ''
return expected.topLevel || expected.moduleLabel || ''
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, '').trim()
}
function hasNormalizedText(items, expected) {
const normalizedExpected = normalizeText(expected)
return items.some((item) => normalizeText(item.text).includes(normalizedExpected))
}
export function runtimeRouteHashWaitExpression(route) {
return `location.hash === ${JSON.stringify(expectedRouteHash(route))}`
}
export function adminRuntimePolicySnapshotExpression(route) {
return `(() => {
const forbiddenTerms = ${JSON.stringify(globalAdminForbiddenText)}
const linkSnapshot = (selector) => Array.from(document.querySelectorAll(selector)).map((node) => ({
text: String(node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim(),
href: node.getAttribute('href') || '',
className: String(node.className || '')
}))
const text = document.body ? document.body.innerText : ''
const commandInput = document.querySelector('.erp-command input')
const activeSubMenu = document.querySelector('.erp-nav .el-sub-menu.is-active > .el-sub-menu__title')
|| document.querySelector('.erp-nav .el-sub-menu.is-opened > .el-sub-menu__title')
return {
expectedHash: ${JSON.stringify(expectedRouteHash(route))},
hash: location.hash,
href: location.href,
textLength: text.trim().length,
shellText: text.slice(0, 2000),
forbiddenVisible: forbiddenTerms.filter((term) => text.includes(term)),
hasApp: Boolean(document.querySelector('#app')),
hasAdminShell: Boolean(document.querySelector('.erp-shell')),
hasAside: Boolean(document.querySelector('.erp-shell__aside')),
hasTopbar: Boolean(document.querySelector('.erp-shell__topbar')),
hasMain: Boolean(document.querySelector('.erp-shell__main')),
hasCommand: Boolean(document.querySelector('.erp-command')),
commandPlaceholder: commandInput ? commandInput.getAttribute('placeholder') || '' : '',
hasQuickActions: Boolean(document.querySelector('.erp-quick-actions')),
hasSession: Boolean(document.querySelector('.erp-session')),
sessionText: document.querySelector('.erp-session')?.innerText || '',
hasSidebarNav: Boolean(document.querySelector('.erp-nav')),
sideActive: linkSnapshot('.erp-nav .el-menu-item.is-active'),
sideActiveModule: activeSubMenu ? String(activeSubMenu.innerText || '').replace(/\\s+/g, ' ').trim() : '',
loginDoorVisible: text.includes('登录 ERP 后台') || text.includes('使用 OFBiz 账号继续')
}
})()`
}
export function assertAdminRuntimeCaseCoverage(cases, options = {}) {
const selectedIds = new Set((cases || []).map((testCase) => testCase.id))
const missing = requiredDefaultRuntimeCaseIds.filter((id) => !selectedIds.has(id))
if (options.enforceDefaultCoverage && missing.length) {
throw new Error(`Default admin runtime coverage is missing required case(s): ${missing.join(', ')}`)
}
return {
id: 'admin-runtime-default-coverage',
status: missing.length ? 'skipped' : 'passed',
required: requiredDefaultRuntimeCaseIds,
missing
}
}
export function assertAdminRuntimePolicy(snapshot, testCase = {}) {
const route = testCase.route || snapshot?.expectedHash || '#/'
const session = testCase.session || 'admin'
const failures = []
const expectedHash = expectedRouteHash(route)
if (!snapshot || typeof snapshot !== 'object') {
failures.push('runtime policy snapshot is missing')
} else {
if (!snapshot.hasApp) failures.push('Vue app root is missing')
if (snapshot.hash !== expectedHash) {
failures.push(`route hash mismatch: expected ${expectedHash}, got ${snapshot.hash || '-'}`)
}
if (!snapshot.textLength) failures.push('document body has no visible text')
}
if (snapshot && session === 'admin') {
const requiredFlags = [
['hasAdminShell', 'administrator shell'],
['hasAside', 'administrator sidebar'],
['hasSidebarNav', 'administrator sidebar navigation tree'],
['hasTopbar', 'administrator topbar'],
['hasMain', 'administrator main content'],
['hasCommand', 'administrator command search'],
['hasQuickActions', 'administrator quick actions'],
['hasSession', 'administrator session area']
]
for (const [flag, label] of requiredFlags) {
if (!snapshot[flag]) failures.push(`${label} is missing`)
}
for (const text of ['OFBiz ERP', '统一运营管理平台', '运营工作台', '核心业务', '渠道与扩展', '系统治理', '新建', '待办', '在线']) {
if (!String(snapshot.shellText || '').includes(text)) failures.push(`administrator shell text is missing: ${text}`)
}
if (!String(snapshot.commandPlaceholder || '').includes('搜索订单、商品、客户、发票')) {
failures.push('administrator command search placeholder is missing business-object wording')
}
if (snapshot.loginDoorVisible) failures.push('administrator route rendered the login door')
if (snapshot.forbiddenVisible?.length) {
failures.push(`forbidden preview/migration copy is visible: ${snapshot.forbiddenVisible.join(', ')}`)
}
const expectedNav = expectedSideNavForRoute(route)
const sideActive = Array.isArray(snapshot.sideActive) ? snapshot.sideActive : []
if (expectedNav?.topLevel) {
if (sideActive.length !== 1) {
failures.push(`sidebar should have exactly one active item for ${expectedHash}; active=${sideActive.map((item) => item.text).join(', ') || '-'}`)
}
if (!hasNormalizedText(sideActive, expectedNav.topLevel)) {
failures.push(`sidebar active item should be ${expectedNav.topLevel}; active=${sideActive.map((item) => item.text).join(', ') || '-'}`)
}
} else if (expectedNav?.moduleLabel) {
if (sideActive.length !== 1) {
failures.push(`sidebar should have exactly one active leaf for ${expectedHash}; active=${sideActive.map((item) => item.text).join(', ') || '-'}`)
}
if (!normalizeText(snapshot.sideActiveModule).includes(normalizeText(expectedNav.moduleLabel))) {
failures.push(`sidebar active module should be ${expectedNav.moduleLabel}; activeModule=${snapshot.sideActiveModule || '-'}`)
}
}
}
if (snapshot && session !== 'admin' && snapshot.hasAdminShell) {
failures.push('guest route rendered the administrator shell')
}
return {
id: 'administrator-runtime-policy',
status: failures.length ? 'failed' : 'passed',
route: expectedHash,
session,
expectedNav: session === 'admin' ? expectedSideNavForRoute(route) : null,
sideActive: Array.isArray(snapshot?.sideActive) ? snapshot.sideActive : [],
sideActiveModule: snapshot?.sideActiveModule || '',
forbiddenVisible: Array.isArray(snapshot?.forbiddenVisible) ? snapshot.forbiddenVisible : [],
failures
}
}