Files
platforms/frontend/platform_admin/scripts/check-backend-contract.mjs
2026-08-11 22:23:02 +08:00

107 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 功能:校验平台总后台资源定义、页面路由与后端资源契约保持一致。
* 版本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} 个资源`);