feat(gxmu): 模块下拉改由模块注册表驱动,审批链每级支持审批人规则

- 新增模块注册表 api 与进程内缓存(首次使用拉一次、并发共用同一个请求),
  启用清单驱动「可新建审批链」的模块下拉与审核列表模块筛选
- reviewFlowConfigEdit 不再维护硬编码模块清单;停用模块的历史配置按
  「标题(已停用)」回显,避免下拉空白
- module → detailKey 的映射收敛到 detailKeyOf 一处,moduleDetailConfig
  未命中注册表时回退 module 本身(停用模块的详情仍可打开)
- 审核流每级新增「审批人规则」输入(USER:<userId> / COLLEGE_ROLE:<roleCode>,
  分号拼装多段,留空沿用「指定人」),随该级提交 reviewerRule 并回显
- 新增 scripts/selfcheck-module-registry.mjs:前端无单测基建时验证下拉过滤、
  detailKey 回退与停用模块回显
This commit is contained in:
2026-11-03 22:40:43 +08:00
parent 0444846422
commit 74f801a081
11 changed files with 396 additions and 29 deletions
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* 模块注册表前端逻辑的最小自检(仓库无单测基建,用 Node + typescript.transpileModule 跑纯函数)。
*
* 覆盖 ticket #65 的可见行为:
* 1. 「可新建审批链」下拉只含 enabled=1 的模块,按 sortNumber 升序,标题取注册表 title;
* 2. module → detailKey 的映射收在 resolveDetailKey 一处,注册表里没有该模块时回退 module 本身,
* 保证停用模块的历史审批链仍能回显标题、仍能打开详情;
* 3. 编辑历史配置时把停用的模块补进下拉(带「已停用」标记),不重复添加。
*
* 运行:node scripts/selfcheck-module-registry.mjs
*/
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;
};
let failed = 0;
const assert = (name, condition, detail) => {
if (condition) {
console.log(' ok ' + name);
return;
}
failed += 1;
console.log(' FAIL ' + name + (detail ? ' -> ' + detail : ''));
};
const core = loadTs('src/utils/module-registry-core.ts');
// 与后端种子数据一致:10 个模块里只有 4 个 enabled=1
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_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: 0, sortNumber: 10 }
];
console.log('新建审批链的模块下拉(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',
JSON.stringify(options)
);
assert('label 取注册表标题', options[0].label === '十佳青年岗位能手');
assert('停用模块不出现', !options.some((o) => o.value === 'gxmu_sjtbzbsj'));
console.log('审核列表的模块筛选项(同理,按注册表顺序)');
assert(
'与新建下拉同源',
core.toModuleOptions(REGISTRY).length === options.length
);
console.log('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);
console.log('标题解析(历史配置回显)');
assert('注册表命中', core.resolveModuleTitle(REGISTRY, 'gxmu_qmgc_form') === '青马工程');
assert('注册表未命中回退 module 本身', core.resolveModuleTitle(REGISTRY, 'legacy_module') === 'legacy_module');
console.log('编辑历史配置:把停用模块补进下拉');
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(已停用)'
);
if (failed) {
console.error('\n自检失败:' + failed + ' 项');
process.exit(1);
}
console.log('\n自检通过:' + 5 + ' 组行为');