恢复点(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>
362 lines
13 KiB
JavaScript
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
|
|
}
|
|
}
|