Files
tuanwei-admin/scripts/selfcheck-module-registry.mjs
T
weicw 5ec514afe4 fix(gxmu): 修复 spec #58 双轴 review 的 admin 侧问题
1. 新建审批链丢审批人规则:新建/编辑共用 toReviewFlowPayload,reviewerRule 不再漏带;
   自检新增「审批链保存体」一组断言。
2. 审核列表模块筛选取注册表全量清单(?all=true),停用模块标题带「(已停用)」;
   只有审批链配置下拉仍用启用集。
3. 审批人规则 placeholder 换成真实角色码 YouthLeagueCommitteeOfSecondaryColleges。
4. getModuleDetailConfig 改为显式传入注册表快照(纯函数),消除加载前后配置不一致;
   快照在 module-registry 里冻结,并落 localStorage 供失败兜底。
5. 自检脚本:fixture 与种子对齐(10 行、5 个启用,含 cms_manuscript),
   「5 组行为」改为按实际组数统计;新增 npm run selfcheck:module-registry。
6. 注册表取不到时:保留上次成功清单 + 顶部 a-alert 提示 + 重试按钮(不静默留空下拉)。

门禁:npm run build 通过;npm run selfcheck:module-registry 6 组行为全绿。
2026-11-03 23:16:12 +08:00

189 lines
8.6 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.
#!/usr/bin/env node
/**
* 模块注册表前端逻辑的最小自检(仓库无单测基建,用 Node + typescript.transpileModule 跑纯函数)。
*
* 覆盖 ticket #65 与 spec #58 修复后的可见行为:
* 1. 「可新建审批链」下拉只含 enabled=1 的模块,按 sortNumber 升序,标题取注册表 title;
* 2. 审核列表的模块筛选用全量清单(含停用模块),停用项标题带「(已停用)」,
* 且只有审批链配置下拉用启用清单;
* 3. module → detailKey 的映射收在 resolveDetailKey 一处,注册表里没有该模块时回退 module 本身;
* 4. 标题解析:注册表命中取 title,未命中回退 module 本身;
* 5. 编辑历史配置时把停用的模块补进下拉(带「已停用」标记),不重复添加;
* 6. 审批链保存体:新建与编辑两条分支都必须带上每级的 reviewerRule(spec #58 A5)。
*
* 运行:npm run selfcheck:module-registry
*/
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const ts = require('typescript');
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const loadTs = (relative) => {
const file = resolve(root, relative);
const source = readFileSync(file, 'utf8');
const { outputText } = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2019
},
fileName: file
});
const mod = { exports: {} };
new Function('exports', 'require', 'module', outputText)(
mod.exports,
require,
mod
);
return mod.exports;
};
const readSource = (relative) => readFileSync(resolve(root, relative), 'utf8');
let failed = 0;
let groupCount = 0;
const assert = (name, condition, detail) => {
if (condition) {
console.log(' ok ' + name);
return;
}
failed += 1;
console.log(' FAIL ' + name + (detail ? ' -> ' + detail : ''));
};
/** 开始一组行为断言,结尾按实际组数统计,避免硬编码组数 */
const group = (name) => {
groupCount += 1;
console.log(name);
};
const core = loadTs('src/utils/module-registry-core.ts');
const payloadBuilder = loadTs(
'src/views/gxmu/reviewFlow/components/reviewFlowPayload.ts'
);
// 与后端种子一致:sql/20261110_spec58_approval_chain.sql 里 10 个模块,
// 其中 5 个 enabled=1(含稿件 cms_manuscript,它有在跑的审核链)。
const REGISTRY = [
{ module: 'gxmu_sjqn', detailKey: 'gxmu_sjqn', title: '十佳青年岗位能手', enabled: 1, sortNumber: 1 },
{ module: 'gxmu_sjtbzbsj', detailKey: 'gxmu_sjtbzbsj', title: '十佳团支部书记', enabled: 0, sortNumber: 2 },
{ module: 'gxmu_wshqtw', detailKey: 'gxmu_wshqtw', title: '五四红旗团委', enabled: 0, sortNumber: 3 },
{ module: 'gxmu_wshqtzb', detailKey: 'gxmu_wshqtzb', title: '五四红旗团支部', enabled: 0, sortNumber: 4 },
{ module: 'gxmu_yxgqtdgb', detailKey: 'gxmu_yxgqtdgb', title: '优秀共青团干部', enabled: 0, sortNumber: 5 },
{ module: 'gxmu_yxgqty', detailKey: 'gxmu_yxgqty', title: '优秀共青团员', enabled: 0, sortNumber: 6 },
{ module: 'gxmu_tzbcy_form', detailKey: 'gxmu_tzbcy_form', title: '挑战杯', enabled: 1, sortNumber: 7 },
{ module: 'gxmu_qmgc_form', detailKey: 'gxmu_qmgc_form', title: '青马工程', enabled: 1, sortNumber: 8 },
{ module: 'gxmu_wxxzx_project', detailKey: 'gxmu_wxxzx_project', title: '未来学术之星', enabled: 1, sortNumber: 9 },
{ module: 'cms_manuscript', detailKey: 'cms_manuscript', title: '稿件', enabled: 1, sortNumber: 10 }
];
group('新建审批链的模块下拉(enabled=1,按 sortNumber)');
const options = core.toModuleOptions(REGISTRY);
assert(
'只含已启用模块',
options.map((o) => o.value).join(',') ===
'gxmu_sjqn,gxmu_tzbcy_form,gxmu_qmgc_form,gxmu_wxxzx_project,cms_manuscript',
JSON.stringify(options)
);
assert('label 取注册表标题', options[0].label === '十佳青年岗位能手');
assert('停用模块不出现', !options.some((o) => o.value === 'gxmu_sjtbzbsj'));
group('审核列表的模块筛选(全量清单,停用项带标记)');
const filterOptions = core.toAllModuleOptions(REGISTRY);
assert('含停用模块', filterOptions.length === REGISTRY.length, String(filterOptions.length));
assert(
'按 sortNumber 升序',
filterOptions.map((o) => o.value).join(',') === REGISTRY.map((r) => r.module).join(','),
JSON.stringify(filterOptions.map((o) => o.value))
);
const disabledOption = filterOptions.find((o) => o.value === 'gxmu_sjtbzbsj');
assert('停用项标题带「(已停用)」', disabledOption?.label === '十佳团支部书记(已停用)', JSON.stringify(disabledOption));
assert('启用项标题不加标记', filterOptions.find((o) => o.value === 'gxmu_sjqn')?.label === '十佳青年岗位能手');
assert(
'停用模块仍能被筛出来(ADR 0006 历史数据仍可查)',
['gxmu_sjtbzbsj', 'gxmu_wshqtw', 'gxmu_wshqtzb', 'gxmu_yxgqtdgb', 'gxmu_yxgqty'].every((m) =>
filterOptions.some((o) => o.value === m)
)
);
const searchSource = readSource('src/views/gxmu/reviewList/components/search.vue');
assert('审核列表筛选入口用 allModuleOptions', /allModuleOptions\(/.test(searchSource));
const configEditSource = readSource(
'src/views/gxmu/reviewFlowConfig/components/reviewFlowConfigEdit.vue'
);
assert(
'审批链配置下拉仍只用启用清单',
/enabledModuleOptions\(/.test(configEditSource) && !/allModuleOptions\(/.test(configEditSource)
);
group('module → detailKey 映射(收在一处)');
assert('走注册表 detailKey', core.resolveDetailKey(REGISTRY, 'gxmu_sjqn') === 'gxmu_sjqn');
assert(
'注册表缺该模块时回退 module(停用模块的详情仍可解析)',
core.resolveDetailKey(REGISTRY, 'legacy_module') === 'legacy_module'
);
assert('空模块返回空', core.resolveDetailKey(REGISTRY, undefined) === undefined);
group('标题解析(历史配置回显)');
assert('注册表命中', core.resolveModuleTitle(REGISTRY, 'gxmu_qmgc_form') === '青马工程');
assert('注册表未命中回退 module 本身', core.resolveModuleTitle(REGISTRY, 'legacy_module') === 'legacy_module');
group('编辑历史配置:把停用模块补进下拉');
const merged = core.ensureModuleOption(options, 'gxmu_sjtbzbsj', '十佳团支部书记');
assert('补进来的项标记已停用', merged[0]?.label === '十佳团支部书记(已停用)', JSON.stringify(merged[0]));
assert('补进来的项在最前面且可回显', merged[0]?.value === 'gxmu_sjtbzbsj');
assert('原有选项保留', merged.length === options.length + 1);
const duplicated = core.ensureModuleOption(options, 'gxmu_sjqn');
assert('已在选项里则不重复添加', duplicated.length === options.length);
assert(
'无 moduleName 时用 module 当标题',
core.ensureModuleOption(options, 'legacy_module')[0]?.label === 'legacy_module(已停用)'
);
group('审批链保存体:新建与编辑都带 reviewerRule(spec #58 A5)');
const newChainForm = {
title: '新链',
organizationIds: [1, 2],
flowItems: [{ organizationId: 1 }, { organizationId: 2 }],
id: undefined
};
const created = payloadBuilder.toReviewFlowPayload(
newChainForm,
{ organizationId: 1, reviewerRule: ' COLLEGE_ROLE:YouthLeagueCommitteeOfSecondaryColleges ' },
1
);
assert(
'新建分支带 reviewerRule(已 trim)',
created.reviewerRule === 'COLLEGE_ROLE:YouthLeagueCommitteeOfSecondaryColleges',
JSON.stringify(created.reviewerRule)
);
assert('新建分支带 level', created.level === 1);
assert(
'提交体不带前端专用字段',
!('organizationIds' in created) && !('flowItems' in created) && !('id' in created)
);
assert(
'未填规则时写 undefined 而不是空串',
payloadBuilder.toReviewFlowPayload(newChainForm, { organizationId: 2 }, 2).reviewerRule === undefined
);
const edited = payloadBuilder.toReviewFlowPayload(
newChainForm,
{ organizationId: 2, reviewUserId: 7, reviewerRule: 'USER:12' },
2
);
assert('编辑分支同样带 reviewerRule', edited.reviewerRule === 'USER:12');
assert('编辑分支保留指定人', edited.reviewUserId === 7);
const flowEditSource = readSource('src/views/gxmu/reviewFlow/components/reviewFlowEdit.vue');
assert(
'新建/编辑两条分支共用同一份构造逻辑(不再逐字段手拼)',
(flowEditSource.match(/toReviewFlowPayload\(/g) || []).length === 2 &&
!/const formData: ReviewFlow = \{/.test(flowEditSource)
);
if (failed) {
console.error('\n自检失败:' + failed + ' 项');
process.exit(1);
}
console.log('\n自检通过:' + groupCount + ' 组行为');