/** * 功能:校验平台总后台资源定义、页面路由与后端资源契约保持一致。 * 版本:v1.1.0 */ import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const source = readFileSync(resolve(root, 'src/api/resources.ts'), 'utf8'); const routeFiles = ['platform.ts', 'finance-route.ts']; const routes = routeFiles .map((name) => readFileSync(resolve(root, 'src/router/routes/modules', name), 'utf8'), ) .join('\n'); const contract = JSON.parse( readFileSync(resolve(root, 'src/contracts/platform-resources.json'), 'utf8'), ); const backend = new Map(contract.resources.map((item) => [item.name, item])); const frontendDefinitions = [ ...source.matchAll( /define\(\s*'([^']+)'\s*,\s*'[^']+'\s*,\s*'([^']+)'/g, ), ].map((match) => ({ name: match[1], mode: match[2] })); const frontendNames = frontendDefinitions.map((item) => item.name); const embeddedResources = new Set([ 'wallet_basic', 'wallet_bank', 'payment_order', 'wallet_record', 'payment_refund', 'wallet_apply_cash', ]); const fieldLabelKeys = new Set( [...source.matchAll(/^\s{2}([a-z0-9_]+):\s*'/gm)].map( (match) => match[1], ), ); for (const name of frontendNames) { const item = backend.get(name); if (!item) throw new Error(`前端配置了后端不存在的资源:${name}`); if (item.path !== `/${name}`) throw new Error(`资源路径不一致:${name}`); } for (const item of contract.resources) { if (!frontendNames.includes(item.name)) throw new Error(`缺少后端资源配置:${item.name}`); const frontend = frontendDefinitions.find( (definition) => definition.name === item.name, ); if (frontend?.mode !== item.mode) throw new Error( `资源模式不一致:${item.name},后端=${item.mode},前端=${frontend?.mode}`, ); if (!embeddedResources.has(item.name) && !routes.includes(`'/${item.name}'`)) throw new Error(`缺少后端资源路由:${item.name}`); const searchKeys = new Set(); for (const field of item.searchFields ?? []) { if (searchKeys.has(field.key)) throw new Error(`搜索字段重复:${item.name}.${field.key}`); searchKeys.add(field.key); if (!fieldLabelKeys.has(field.key)) throw new Error(`搜索字段缺少中文列名:${item.name}.${field.key}`); if (!['text', 'enum'].includes(field.kind)) throw new Error(`搜索字段类型无效:${item.name}.${field.key}`); if (field.kind === 'enum') { if (!field.values?.length) throw new Error(`搜索枚举缺少值:${item.name}.${field.key}`); for (const value of field.values) { if (!value.value || !value.label || value.value === value.label) throw new Error(`搜索枚举中英文映射无效:${item.name}.${field.key}`); } } } if (item.pageKind === 'tree' && searchKeys.size) throw new Error(`树形资源不应声明标准列表搜索:${item.name}`); } if (backend.get('user_address')?.searchFields?.length) throw new Error('用户地址包含动态用户关系,本轮必须隐藏搜索框'); const forbidden = [ '/platform/platform_', '/gas/gas_', '/delivery/delivery_', '/staff/', '/user/', '/ec/ec_', '/finance/fin_', '/wallet/wallet', 'delivery_task', 'delivery_track', 'delivery_track_point', 'dev_device_binding', 'dev_smart_cylinder_valve', 'dev_telemetry', 'wallet_ledger', 'wallet_recharge', 'wallet_withdrawal', ]; for (const value of forbidden) { if (source.includes(value) || routes.includes(value)) throw new Error(`仍存在旧资源或旧路径:${value}`); } console.log(`契约检查通过:${frontendNames.length} 个资源`);