Files
platforms/frontend/platform_admin/scripts/check-resource-display-contracts.mjs
2026-08-16 00:47:35 +08:00

217 lines
7.4 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.
/**
* 功能描述校验平台48类资源均具备中文展示字段并防止列表、详情退回数据库ID或动态英文列。
* 版本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');
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('、')}`);
}
// 运行轨迹必须复用统一关联名称组件,避免再次把截断唯一标识当成业务名称。
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}必须以业务名称展示并保留唯一标识复制入口`);
}
}
// 购物车关联列必须展示用户和商品名称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="系统唯一标识"', '列表必须中文标注系统唯一标识');
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',
'tracks', 'confirmations', 'payments', 'points',
]) {
assertIncludes(
detailContract,
`${collection}: {`,
`缺少关联记录固定列契约:${collection}`,
);
}
assertIncludes(
display,
"if (detail.track && typeof detail.track === 'object')",
'运行轨迹详情必须解包 track 聚合主记录',
);
assertIncludes(
detailContract,
"rawColumns: ['direction']",
'轨迹点行进方向必须保留原始采集值,不得套用收支方向枚举',
);
process.stdout.write('平台48类资源中文展示契约检查通过\n');