2026-08-11 00:49:41 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 功能:校验平台总后台资源定义、页面路由与后端资源契约保持一致。
|
|
|
|
|
|
* 版本:v1.1.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { readFileSync } from 'node:fs';
|
2026-07-28 11:08:47 +08:00
|
|
|
|
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');
|
2026-08-11 00:49:41 +08:00
|
|
|
|
const routeFiles = ['platform.ts', 'finance-route.ts'];
|
|
|
|
|
|
const routes = routeFiles
|
|
|
|
|
|
.map((name) =>
|
|
|
|
|
|
readFileSync(resolve(root, 'src/router/routes/modules', name), 'utf8'),
|
|
|
|
|
|
)
|
|
|
|
|
|
.join('\n');
|
2026-07-28 11:08:47 +08:00
|
|
|
|
const contract = JSON.parse(
|
|
|
|
|
|
readFileSync(resolve(root, 'src/contracts/platform-resources.json'), 'utf8'),
|
|
|
|
|
|
);
|
|
|
|
|
|
const backend = new Map(contract.resources.map((item) => [item.name, item]));
|
2026-07-28 14:26:09 +08:00
|
|
|
|
const frontendDefinitions = [
|
2026-08-11 00:49:41 +08:00
|
|
|
|
...source.matchAll(
|
|
|
|
|
|
/define\(\s*'([^']+)'\s*,\s*'[^']+'\s*,\s*'([^']+)'/g,
|
|
|
|
|
|
),
|
2026-07-28 14:26:09 +08:00
|
|
|
|
].map((match) => ({ name: match[1], mode: match[2] }));
|
|
|
|
|
|
const frontendNames = frontendDefinitions.map((item) => item.name);
|
2026-07-29 15:29:49 +08:00
|
|
|
|
const embeddedResources = new Set([
|
2026-08-11 00:49:41 +08:00
|
|
|
|
'wallet_basic',
|
|
|
|
|
|
'wallet_bank',
|
|
|
|
|
|
'payment_order',
|
|
|
|
|
|
'wallet_record',
|
|
|
|
|
|
'payment_refund',
|
|
|
|
|
|
'wallet_apply_cash',
|
2026-07-29 15:29:49 +08:00
|
|
|
|
]);
|
2026-08-11 22:22:36 +08:00
|
|
|
|
const fieldLabelKeys = new Set(
|
|
|
|
|
|
[...source.matchAll(/^\s{2}([a-z0-9_]+):\s*'/gm)].map(
|
|
|
|
|
|
(match) => match[1],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-07-28 11:08:47 +08:00
|
|
|
|
|
|
|
|
|
|
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}`);
|
2026-07-28 14:26:09 +08:00
|
|
|
|
const frontend = frontendDefinitions.find(
|
|
|
|
|
|
(definition) => definition.name === item.name,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (frontend?.mode !== item.mode)
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`资源模式不一致:${item.name},后端=${item.mode},前端=${frontend?.mode}`,
|
|
|
|
|
|
);
|
2026-07-29 15:29:49 +08:00
|
|
|
|
if (!embeddedResources.has(item.name) && !routes.includes(`'/${item.name}'`))
|
2026-07-28 14:26:09 +08:00
|
|
|
|
throw new Error(`缺少后端资源路由:${item.name}`);
|
2026-08-11 22:22:36 +08:00
|
|
|
|
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}`);
|
2026-07-28 11:08:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 22:22:36 +08:00
|
|
|
|
if (backend.get('user_address')?.searchFields?.length)
|
|
|
|
|
|
throw new Error('用户地址包含动态用户关系,本轮必须隐藏搜索框');
|
|
|
|
|
|
|
2026-07-28 11:08:47 +08:00
|
|
|
|
const forbidden = [
|
2026-08-11 00:49:41 +08:00
|
|
|
|
'/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',
|
2026-07-28 11:08:47 +08:00
|
|
|
|
];
|
|
|
|
|
|
for (const value of forbidden) {
|
|
|
|
|
|
if (source.includes(value) || routes.includes(value))
|
|
|
|
|
|
throw new Error(`仍存在旧资源或旧路径:${value}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(`契约检查通过:${frontendNames.length} 个资源`);
|