fix: align platform admin resource paths
This commit is contained in:
@@ -1,90 +1,54 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const baseline = process.argv.includes('--baseline');
|
||||
|
||||
const expectedResources = [
|
||||
['gas', 'gas_basic', 'list'], ['gas', 'gas_account', 'list'],
|
||||
['delivery', 'delivery_basic', 'list'], ['delivery', 'delivery_account', 'list'], ['delivery', 'delivery_task', 'list'], ['delivery', 'delivery_track', 'list'], ['delivery', 'delivery_track_point', 'list'],
|
||||
['staff', 'staff_account', 'list'], ['staff', 'staff_credential', 'list'],
|
||||
['user', 'user_account', 'list'], ['user', 'user_address', 'list'], ['user', 'user_service_relation', 'list'],
|
||||
['device', 'dev_smart_cylinder_valve', 'list'], ['device', 'dev_device_binding', 'list'], ['device', 'dev_telemetry', 'list'], ['device', 'saf_rule', 'list'], ['device', 'saf_event', 'list'], ['device', 'saf_event_disposal', 'list'], ['device', 'saf_inspection', 'list'],
|
||||
['commerce', 'ec_category', 'list'], ['commerce', 'ec_product', 'list'], ['commerce', 'ec_product_attribute', 'list'], ['commerce', 'ec_product_image', 'list'], ['commerce', 'ec_cart', 'list'], ['commerce', 'ec_order', 'list'], ['commerce', 'ec_review', 'list'],
|
||||
['finance', 'fin_payment', 'list'], ['finance', 'fin_settlement', 'list'], ['finance', 'fin_reconciliation', 'list'],
|
||||
['content', 'cnt_content', 'list'], ['notification', 'ntf_template', 'list'], ['customer_service', 'cs_ticket', 'list'],
|
||||
['platform', 'platfrom_account', 'list'], ['platform', 'platform_role', 'list'], ['platform', 'platform_menu', 'tree'],
|
||||
['wallet', 'wallet', 'list'], ['wallet', 'wallet_ledger', 'list'], ['wallet', 'wallet_recharge', 'list'], ['wallet', 'wallet_withdrawal', 'list'],
|
||||
['report', 'report', 'list'], ['report', 'report_item', 'list'], ['report', 'report_metric_snapshot', 'list'],
|
||||
['audit', 'aud_operation_log', 'list'], ['audit', 'aud_export_log', 'list'], ['audit', 'aud_approval', 'list'],
|
||||
const contracts = [
|
||||
['dashboard', 'overview', '/dashboard/overview', 'readonly', 'dashboard'],
|
||||
['gas', 'gas_basic', '/gas/gas_basic', 'writable', 'list'], ['gas', 'gas_account', '/gas/gas_account', 'writable', 'list'],
|
||||
['delivery', 'delivery_basic', '/delivery/delivery_basic', 'writable', 'list'], ['delivery', 'delivery_account', '/delivery/delivery_account', 'writable', 'list'], ['delivery', 'delivery_task', '/delivery/delivery_task', 'writable', 'list'], ['delivery', 'delivery_track', '/delivery/delivery_track', 'writable', 'list'], ['delivery', 'delivery_track_point', '/delivery/delivery_track_point', 'writable', 'list'],
|
||||
['staff', 'staff_account', '/staff/account', 'writable', 'list'], ['staff', 'staff_credential', '/staff/credential', 'writable', 'list'],
|
||||
['user', 'user_account', '/user/account', 'writable', 'list'], ['user', 'user_address', '/user/address', 'writable', 'list'], ['user', 'user_service_relation', '/user/service_relation', 'writable', 'list'],
|
||||
['device', 'dev_smart_cylinder_valve', '/device/dev_smart_cylinder_valve', 'writable', 'list'], ['device', 'dev_device_binding', '/device/dev_device_binding', 'writable', 'list'], ['device', 'dev_telemetry', '/device/dev_telemetry', 'readonly', 'list'],
|
||||
['safety', 'saf_rule', '/safety/saf_rule', 'writable', 'list'], ['safety', 'saf_event', '/safety/saf_event', 'writable', 'list'], ['safety', 'saf_inspection', '/safety/saf_inspection', 'writable', 'list'],
|
||||
['ec', 'ec_category', '/ec/ec_category', 'writable', 'list'], ['ec', 'ec_product', '/ec/ec_product', 'writable', 'list'], ['ec', 'ec_product_attribute', '/ec/ec_product_attribute', 'writable', 'list'], ['ec', 'ec_product_image', '/ec/ec_product_image', 'writable', 'list'], ['ec', 'ec_cart', '/ec/ec_cart', 'writable', 'list'], ['ec', 'ec_order', '/ec/ec_order', 'writable', 'list'], ['ec', 'ec_order_item', '/ec/ec_order_item', 'writable', 'list'], ['ec', 'ec_review', '/ec/ec_review', 'writable', 'list'],
|
||||
['finance', 'fin_payment', '/finance/fin_payment', 'writable', 'list'], ['finance', 'fin_settlement', '/finance/fin_settlement', 'writable', 'list'], ['finance', 'fin_reconciliation', '/finance/fin_reconciliation', 'writable', 'list'],
|
||||
['content', 'cnt_content', '/content/cnt_content', 'writable', 'list'], ['notification', 'ntf_template', '/notification/ntf_template', 'writable', 'list'], ['customer_service', 'cs_ticket', '/customer_service/cs_ticket', 'writable', 'list'],
|
||||
['platform', 'platfrom_account', '/platform/platfrom_account', 'writable', 'list'], ['platform', 'platform_role', '/platform/platform_role', 'writable', 'list'], ['platform', 'platform_menu', '/platform/platform_menu', 'writable', 'tree'],
|
||||
['wallet', 'wallet', '/wallet/wallet', 'readonly', 'list'], ['wallet', 'wallet_ledger', '/wallet/wallet_ledger', 'readonly', 'list'], ['wallet', 'wallet_recharge', '/wallet/wallet_recharge', 'readonly', 'list'], ['wallet', 'wallet_withdrawal', '/wallet/wallet_withdrawal', 'readonly', 'list'],
|
||||
['report', 'report', '/report/report', 'readonly', 'list'], ['report', 'report_item', '/report/report_item', 'readonly', 'list'], ['report', 'report_metric_snapshot', '/report/report_metric_snapshot', 'readonly', 'list'],
|
||||
['audit', 'aud_operation_log', '/audit/aud_operation_log', 'readonly', 'list'], ['audit', 'aud_export_log', '/audit/aud_export_log', 'readonly', 'list'], ['audit', 'aud_approval', '/audit/aud_approval', 'readonly', 'list'],
|
||||
];
|
||||
|
||||
function fileIncludes(file, value) {
|
||||
return fs.existsSync(file) && fs.readFileSync(file, 'utf8').includes(value);
|
||||
}
|
||||
const resourceSource = fs.readFileSync('src/api/resources.ts', 'utf8');
|
||||
const resourceEntry = (name, resource, mode, pageKind) => new RegExp(`name: '${name}', resource: '${resource}', title: '[^']+', mode: '${mode}', pageKind: '${pageKind}'`);
|
||||
const files = (dir) => fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? files(path.join(dir, entry.name)) : [path.join(dir, entry.name)]) : [];
|
||||
|
||||
function checkResourceLayers() {
|
||||
const missing = [];
|
||||
for (const [domain, name, pageKind] of expectedResources) {
|
||||
if (!fileIncludes('src/api/resources.ts', name)) missing.push(`${domain}/${name}: missing api`);
|
||||
if (!fileIncludes(path.join('src/router/routes/modules', `${domain}.ts`), name)) missing.push(`${domain}/${name}: missing route`);
|
||||
const page = pageKind === 'tree' ? 'TreePage.vue' : 'ListPage.vue';
|
||||
if (!fs.existsSync(path.join('src/views', domain, name, page))) missing.push(`${domain}/${name}: missing view`);
|
||||
function checkContracts() {
|
||||
const failures = [];
|
||||
if (contracts.length !== 46) failures.push(`catalogue: expected 46 resources, got ${contracts.length}`);
|
||||
for (const [domain, name, resource, mode, pageKind] of contracts) {
|
||||
const label = `${domain}/${name}`;
|
||||
if (!resourceEntry(name, resource, mode, pageKind).test(resourceSource)) failures.push(`${label}: missing or incorrect api contract`);
|
||||
const route = path.join('src/router/routes/modules', domain === 'dashboard' ? 'platform.ts' : `${domain}.ts`);
|
||||
if (!fs.existsSync(route) || !fs.readFileSync(route, 'utf8').includes(name)) failures.push(`${label}: missing route`);
|
||||
const page = pageKind === 'dashboard' ? path.join('src/views/dashboard/DashboardPage.vue') : path.join('src/views', domain, name, pageKind === 'tree' ? 'TreePage.vue' : 'ListPage.vue');
|
||||
if (!fs.existsSync(page)) failures.push(`${label}: missing view`);
|
||||
if (mode === 'readonly' && pageKind !== 'dashboard' && (!fs.existsSync(page) || !fs.readFileSync(page, 'utf8').includes('ReadOnlyListPage'))) failures.push(`${label}: readonly page exposes write surface`);
|
||||
}
|
||||
return missing;
|
||||
const readonlyPage = fs.readFileSync('src/views/shared/ReadOnlyListPage.vue', 'utf8');
|
||||
if (/(CrudListPage|resourceApi\.(create|update|archive|updateStatus))/.test(readonlyPage)) failures.push('readonly shared page: contains write surface');
|
||||
const treePage = fs.readFileSync('src/views/shared/TreePage.vue', 'utf8');
|
||||
if (!treePage.includes('<a-tree') || !treePage.includes('parent_identity') || treePage.includes('CrudListPage')) failures.push('tree shared page: missing tree semantics');
|
||||
const crudSource = fs.readFileSync('src/views/shared/CrudListPage.vue', 'utf8');
|
||||
if (!crudSource.includes("key !== 'id'") || !crudSource.includes("key.endsWith('_id')")) failures.push('detail view: does not remove internal identifiers');
|
||||
const identityLeaks = files('src/api').concat(files('src/views')).filter((file) => /\.(ts|vue)$/.test(file) && !file.endsWith('CrudListPage.vue') && !file.endsWith('ReadOnlyListPage.vue')).filter((file) => /\b[A-Za-z][A-Za-z0-9]*_id\b|\bid\b/.test(fs.readFileSync(file, 'utf8')));
|
||||
if (identityLeaks.length) failures.push(`identity leak: ${identityLeaks.join(', ')}`);
|
||||
if (resourceSource.includes('saf_event_disposal') || files('src/router/routes/modules').some((file) => fs.readFileSync(file, 'utf8').includes('saf_event_disposal'))) failures.push('saf_event_disposal: must not be displayed as an independent resource');
|
||||
const obsoletePaths = /\/(?:commerce|staff\/staff_|user\/user_)/;
|
||||
const stalePathFiles = files('src/api').concat(files('src/views')).filter((file) => /\.(ts|vue)$/.test(file) && obsoletePaths.test(fs.readFileSync(file, 'utf8')));
|
||||
if (stalePathFiles.length) failures.push(`obsolete resource path: ${stalePathFiles.join(', ')}`);
|
||||
return failures;
|
||||
}
|
||||
|
||||
function walk(dir, acc = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
||||
walk(full, acc);
|
||||
} else if (/\.(ts|vue)$/.test(entry.name)) {
|
||||
acc.push(full);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const srcFiles = walk('src');
|
||||
const badPlaceholder = [];
|
||||
const zhLocales = srcFiles.filter((f) => f.includes(`${path.sep}locale${path.sep}zh-CN.ts`));
|
||||
|
||||
for (const file of srcFiles) {
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
if (text.includes("'???'") || /'(\?\?[^']*)'/.test(text)) {
|
||||
badPlaceholder.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
let zhOk = 0;
|
||||
for (const file of zhLocales) {
|
||||
if (/[\u4e00-\u9fff]/.test(fs.readFileSync(file, 'utf8'))) zhOk += 1;
|
||||
}
|
||||
|
||||
let mockInDist = false;
|
||||
if (fs.existsSync('dist/assets')) {
|
||||
for (const name of fs.readdirSync('dist/assets')) {
|
||||
if (!name.endsWith('.js')) continue;
|
||||
const chunk = fs.readFileSync(path.join('dist/assets', name), 'utf8');
|
||||
if (chunk.includes('mockjs') || chunk.includes('Mock.mock')) {
|
||||
mockInDist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
badPlaceholder: badPlaceholder.length,
|
||||
zhLocales: `${zhOk}/${zhLocales.length}`,
|
||||
mockInDist,
|
||||
hasGit: fs.existsSync('.env.development') && fs.readFileSync('.env.development', 'utf8').includes('VITE_API_BASE_URL=http'),
|
||||
settingsHttp: fs.readFileSync('src/locale/zh-CN/settings.ts', 'utf8').includes('http.logout.title'),
|
||||
rootMenu: fs.readFileSync('src/locale/zh-CN.ts', 'utf8').includes('仪表盘'),
|
||||
}, null, 2));
|
||||
|
||||
const missingLayers = checkResourceLayers();
|
||||
for (const missing of missingLayers) console.log(missing);
|
||||
|
||||
if (missingLayers.length > 0 && !baseline) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
const failures = checkContracts();
|
||||
for (const failure of failures) console.log(failure);
|
||||
if (failures.length) process.exitCode = 1;
|
||||
|
||||
Reference in New Issue
Block a user