/** * 功能描述:校验平台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 = \{([\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('商城订单空支付时间必须明确显示为未支付'); } for (const displayContract of [ "f('request_no', { emptyText: '未记录' })", "f('contact_name', { emptyText: '未记录' })", "f('contact_phone', { emptyText: '未记录' })", "f('logistics_no', { emptyText: '未发货' })", "f('logistics_company', { emptyText: '未发货' })", "f('shipped_at', { emptyText: '未发货' })", "f('received_at', { emptyText: '未收货' })", "f('remark', { emptyText: '无备注' })", ]) { if (!orderDefinition.includes(displayContract)) { throw new Error(`商城订单详情缺少空值业务文案:${displayContract}`); } } if (/title="ID"|data-index="id"/.test(listPage)) { throw new Error('标准资源列表不得把数据库ID作为业务列展示'); } assertIncludes(listPage, 'title="系统唯一标识"', '列表必须中文标注系统唯一标识'); assertIncludes( treePage, '', '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');