Files
platforms/frontend/platform_admin/scripts/check-resource-display-contracts.mjs

217 lines
7.4 KiB
JavaScript
Raw Normal View History

/**
* 功能描述校验平台48类资源均具备中文展示字段并防止列表详情退回数据库ID或动态英文列
2026-08-15 23:47:33 +08:00
* 版本v1.2.1
*/
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(currentDirectory, '..');
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8');
}
function assertIncludes(source, expected, message) {
if (!source.includes(expected)) throw new Error(message);
}
const resources = read('src/api/resources.ts');
const display = read('src/api/resource-display.ts');
const detailContract = read('src/api/resource-detail-contract.ts');
const detailPage = read('src/views/resource/ResourceDetailContent.vue');
const listPage = read('src/views/shared/CrudListPage.vue');
2026-08-15 23:47:33 +08:00
const treePage = read('src/views/shared/TreePage.vue');
const identityText = read('src/components/IdentityText.vue');
const resourceNames = [...resources.matchAll(/\bdefine\('([^']+)'/g)].map(
(match) => match[1],
);
if (resourceNames.length !== 48 || new Set(resourceNames).size !== 48) {
throw new Error(`平台资源定义应为48类当前读取到${resourceNames.length}`);
}
if (/define\([^;]*?'readonly'\s*,\s*\[\s*\]/s.test(resources)) {
throw new Error('只读资源不得继续使用空字段契约');
}
const labelBlock = resources.match(
/const fieldLabels: Record<string, string> = \{([\s\S]*?)\n\};/,
)?.[1];
if (!labelBlock) throw new Error('无法读取全局中文字段字典');
const labels = new Set(
[...labelBlock.matchAll(/^\s{2}([A-Za-z_][A-Za-z0-9_]*):\s*'[^']+'/gm)].map(
(match) => match[1],
),
);
const usedKeys = new Set(
[...resources.matchAll(/\b(?:f|relation)\('([^']+)'/g)].map(
(match) => match[1],
),
);
const missingLabels = [...usedKeys].filter((key) => !labels.has(key));
if (missingLabels.length) {
throw new Error(`资源字段缺少中文名称:${missingLabels.join('、')}`);
}
2026-08-15 22:40:13 +08:00
// 运行轨迹必须复用统一关联名称组件,避免再次把截断唯一标识当成业务名称。
const trackDefinition = resources.match(
/define\('gasorder_track',[\s\S]*?\n\s*\]\),/,
)?.[0];
if (!trackDefinition) throw new Error('无法读取运行轨迹资源定义');
for (const [key, label] of [
['gasorder_basic_identity', '配送订单'],
['staff_account_identity', '配送人员'],
]) {
const field = trackDefinition.match(
new RegExp(`relation\\('${key}'[\\s\\S]*?\\}\\)`),
)?.[0];
if (
!field ||
!field.includes(`listLabel: '${label}'`) ||
!field.includes('listRelationNameOnly: true') ||
!field.includes('showIdentityCopy: true') ||
(key === 'staff_account_identity' &&
!field.includes("staffRelation: { roles: ['delivery'] }"))
) {
throw new Error(`运行轨迹的${label}必须以业务名称展示并保留唯一标识复制入口`);
}
}
2026-08-16 00:06:07 +08:00
// 购物车关联列必须展示用户和商品名称UUID仅作为复制与排障信息保留。
const cartDefinition = resources.match(
/define\('ec_cart',[\s\S]*?\n\s*\]\),/,
)?.[0];
if (!cartDefinition) throw new Error('无法读取购物车资源定义');
for (const [key, label] of [
['user_account_identity', '用户'],
['ec_product_identity', '商品'],
]) {
const field = cartDefinition.match(
new RegExp(`relation\\('${key}'[\\s\\S]*?\\}\\)`),
)?.[0];
if (
!field ||
!field.includes(`listLabel: '${label}'`) ||
!field.includes('listRelationNameOnly: true') ||
!field.includes('displayRelationLabel: true') ||
!field.includes('showIdentityCopy: true')
) {
throw new Error(`购物车的${label}列必须以业务名称展示并保留唯一标识复制入口`);
}
}
// 商城订单必须遵循独立交易状态,并以名称展示用户关系。
const orderDefinition = resources.match(
/define\('ec_order',[\s\S]*?\n\s*\]\),/,
)?.[0];
if (!orderDefinition) throw new Error('无法读取商城订单资源定义');
for (const [value, label] of [
[16, '待支付'],
[18, '已支付'],
[22, '已取消'],
]) {
if (!orderDefinition.includes(`{ label: '${label}', value: ${value} }`)) {
throw new Error(`商城订单状态 ${value} 缺少中文展示:${label}`);
}
}
const orderUserField = orderDefinition.match(
/relation\('user_account_identity'[\s\S]*?\}\)/,
)?.[0];
if (
!orderUserField ||
!orderUserField.includes("listLabel: '用户'") ||
!orderUserField.includes('listRelationNameOnly: true') ||
!orderUserField.includes('displayRelationLabel: true') ||
!orderUserField.includes('showIdentityCopy: true')
) {
throw new Error('商城订单用户列必须以用户名称展示并保留唯一标识复制入口');
}
for (const invalidStatus of [10, 35]) {
if (orderDefinition.includes(`value: ${invalidStatus}`)) {
throw new Error(`商城订单不得把跨领域通用状态 ${invalidStatus} 当作有效交易状态`);
}
}
if (!orderDefinition.includes("f('paid_at', { emptyText: '未支付' })")) {
throw new Error('商城订单空支付时间必须明确显示为未支付');
}
if (/title="ID"|data-index="id"/.test(listPage)) {
throw new Error('标准资源列表不得把数据库ID作为业务列展示');
}
assertIncludes(listPage, 'title="系统唯一标识"', '列表必须中文标注系统唯一标识');
2026-08-15 23:47:33 +08:00
assertIncludes(
treePage,
'<a-table',
'树形资源必须使用带明确列标题的树形表格',
);
assertIncludes(
treePage,
'<template #columns>',
'Arco 树形表格列必须放入 columns 插槽,避免运行时渲染为空白',
);
for (const columnTitle of ['分类名称', '排序', '状态', '系统标识', '操作']) {
assertIncludes(treePage, columnTitle, `树形表格缺少必要列:${columnTitle}`);
}
assertIncludes(
treePage,
'{{ record.name }}',
'树形表格必须以接口返回的中文业务名称作为主标题',
);
assertIncludes(
treePage,
'display-text="复制标识"',
'树形表格不得把截断唯一标识作为高优先级正文',
);
assertIncludes(
identityText,
'displayText?: string',
'唯一标识组件必须保留可选的低优先级复制文案能力',
);
if (treePage.includes('node.title')) {
throw new Error('树节点不得读取标题插槽中不存在的 title 字段');
}
assertIncludes(
listPage,
'class="resource-table-shell"',
'列表必须使用内部横向滚动容器',
);
assertIncludes(
display,
'hasResourceFieldLabel',
'详情必须提供未知字段中文契约检查',
);
assertIncludes(
detailPage,
'!hasResourceFieldLabel(props.definition, actualKey)',
'详情不得直接回显未配置字段',
);
assertIncludes(
detailPage,
'resourceDetailContract(props.definition.name)',
'详情关联表必须使用资源专属固定列契约',
);
for (const collection of [
'products', 'revisions', 'items', 'assignments', 'statuses',
2026-08-15 22:40:13 +08:00
'tracks', 'confirmations', 'payments', 'points',
]) {
assertIncludes(
detailContract,
`${collection}: {`,
`缺少关联记录固定列契约:${collection}`,
);
}
2026-08-15 22:40:13 +08:00
assertIncludes(
display,
"if (detail.track && typeof detail.track === 'object')",
'运行轨迹详情必须解包 track 聚合主记录',
);
assertIncludes(
detailContract,
"rawColumns: ['direction']",
'轨迹点行进方向必须保留原始采集值,不得套用收支方向枚举',
);
process.stdout.write('平台48类资源中文展示契约检查通过\n');