/** * 受限空间作业票审批流校验 * * 用法:node scripts/check-dangerous-work-flow-nodes.js * * 校验内容: * 1) 动火作业的既有实现文件未被改动(与 HEAD 完全一致) * 2) 受限空间 7 个后续环节的 code / name / 字段与源系统一致 * 3) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中 * 4) 默认安全措施清单与源系统 formData 逐条一致 * 5) 受限空间运行时辅助(回显 / payload / 流程变量)的取值口径正确 * * 源系统依据: * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3769 */ const fs = require('fs'); const os = require('os'); const path = require('path'); const { execFileSync } = require('child_process'); const babel = require('@babel/core'); const Module = require('module'); const ROOT = path.resolve(__dirname, '..'); const SRC = path.join( ROOT, 'src/views/bpm/handleTask/components/dangerousWorkTicket' ); const OUT = path.join(os.tmpdir(), 'kd-aiot-check-confined-space'); // 动火作业的既有实现文件:受限空间实现不得改动它们 const FIRE_FILES = [ 'fireFlowNodes.js', 'fireFlowTask.js', 'components/fireTicketDetail.vue', 'components/flowNodeForm.vue' ]; let failed = 0; function check(name, cond, extra) { if (cond) { console.log(' PASS ' + name); } else { failed++; console.log( ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '') ); } } // 编译 ESM -> CJS // confinedSpaceFlowTask.js 引用了 fireFlowTask.js(只读复用), // fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来 fs.mkdirSync(OUT, { recursive: true }); [ 'confinedSpaceFlowNodes.js', 'confinedSpaceFlowTask.js', 'fireFlowNodes.js', 'fireFlowTask.js' ].forEach((file) => { const code = fs.readFileSync(path.join(SRC, file), 'utf8'); const res = babel.transformSync(code, { filename: file, babelrc: false, configFile: false, plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')] }); fs.writeFileSync(path.join(OUT, file), res.code); }); // 受限空间运行时辅助会引用 @/api/*,这里用桩模块代替,避免校验脚本依赖后端接口 const STUB = path.join(OUT, 'stub.js'); fs.writeFileSync( STUB, 'module.exports = {\n' + ' getTaskListByProcessInstanceId: function () { return Promise.resolve([]); },\n' + ' getProcessInstance: function () { return Promise.resolve({}); }\n' + '};\n' ); const originalResolve = Module._resolveFilename; Module._resolveFilename = function (request, ...args) { if (request.startsWith('@/')) { return STUB; } return originalResolve.call(this, request, ...args); }; const cs = require(path.join(OUT, 'confinedSpaceFlowNodes.js')); const cst = require(path.join(OUT, 'confinedSpaceFlowTask.js')); console.log('\n[1] 动火作业既有实现未被改动'); FIRE_FILES.forEach((file) => { let changed = true; try { execFileSync('git', ['diff', '--quiet', 'HEAD', '--', file], { cwd: path.join(SRC, '..'), stdio: 'ignore' }); changed = false; } catch (e) { changed = true; } check(file + ' 与 HEAD 一致', !changed); }); console.log('\n[2] 受限空间环节(源系统 HO_PERMIT_CONFINED_SPACE 共 8 节点,1 级为作业申请模板)'); const expected = [ ['HO_PERMIT_CONFINED_SPACE_TWOTH', '风险辨识'], ['HO_PERMIT_CONFINED_SPACE_THREETH', '气体分析'], ['HO_PERMIT_CONFINED_SPACE_FOURTH', '现场安全措施确认'], ['HO_PERMIT_CONFINED_SPACE_FIVETH', '安全交底'], ['HO_PERMIT_CONFINED_SPACE_SIXTH', '所在单位意见'], ['HO_PERMIT_CONFINED_SPACE_SEVENTH', '作业实施'], ['HO_PERMIT_CONFINED_SPACE_EIGHTTH', '完工验收'] ]; check('环节数量为 7', cs.CONFINED_SPACE_FLOW_NODES.length === 7, { len: cs.CONFINED_SPACE_FLOW_NODES.length }); expected.forEach(([code, name], i) => { const node = cs.CONFINED_SPACE_FLOW_NODES[i]; check( '第' + (i + 2) + '级 ' + code + ' / ' + name, node.code === code && node.name === name, { code: node.code, name: node.name } ); }); console.log('\n[3] 与源系统 formJson 一致性'); const nodeOf = (k) => cs.CONFINED_SPACE_FLOW_NODES.find((n) => n.key === k); const colsOf = (k, fk) => { const node = nodeOf(k); const field = node.fields.find((f) => f.key === fk); return (field.columns || []).map((c) => c.label); }; check( '气体分析:表名「气体分析记录表」', nodeOf('gas_analysis').fields[0].label === '气体分析记录表' ); check( '气体分析:6 列且顺序与源系统一致(分析项目/气体名称/合格标准/取样分析时间/分析部位/分析人)', JSON.stringify(colsOf('gas_analysis', 'gas_analysis_list')) === JSON.stringify([ '分析项目', '气体名称', '合格标准', '取样分析时间', '分析部位', '分析人' ]), colsOf('gas_analysis', 'gas_analysis_list') ); check( '气体分析:分析项目 3 个选项(有毒有害气体/可燃气体/氧气含量)', JSON.stringify( nodeOf('gas_analysis').fields[0].columns[0].options.map((o) => o.label) ) === JSON.stringify(['有毒有害气体', '可燃气体', '氧气含量']) ); check( '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)', nodeOf('measure').fields[0].label === '安全措施确认表' && JSON.stringify(colsOf('measure', 'measure_list')) === JSON.stringify(['安全措施', '是否确认', '确认人']) ); check( '现场安全措施确认:编制人签字 + 确认人签字', JSON.stringify(nodeOf('measure').fields.map((f) => f.label)) === JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字']) ); check( '安全交底:只有 2 个签字(源系统无监护人签字)', JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) === JSON.stringify(['安全交底人签字', '接受交底人签字']) ); check( '所在单位意见:意见 + 签字', nodeOf('unit_opinion').fields.length === 2 && nodeOf('unit_opinion').fields[0].type === 'textarea' && nodeOf('unit_opinion').fields[1].type === 'sign' ); check( '作业实施:实施时间 + 结束时间 + 事中记录表 + 气体分析记录表', JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) === JSON.stringify([ '作业实施时间', '作业结束时间', '事中记录表', '气体分析记录表' ]) ); check( '作业实施:气体分析记录表 6 列与源系统一致(时间标签为「气体取样分析时间」)', JSON.stringify(colsOf('execute', 'gas_record_list')) === JSON.stringify([ '分析项目', '气体名称', '合格标准', '气体取样分析时间', '分析部位', '分析人' ]), colsOf('execute', 'gas_record_list') ); check( '事中记录表 3 列(时间/记录内容/记录人)', JSON.stringify(colsOf('execute', 'during_record_list')) === JSON.stringify(['时间', '记录内容', '记录人']) ); check( '完工验收:意见 + 签字', nodeOf('acceptance').fields.length === 2 && nodeOf('acceptance').fields[0].label === '意见' && nodeOf('acceptance').fields[1].type === 'sign' ); check( '风险辨识:多选 20 项与源系统完全一致', nodeOf('risk').fields[0].options.length === 20 && nodeOf('risk').fields[0].options[0].label === '物体打击' && nodeOf('risk').fields[0].options[19].label === '其它伤害' ); check( '风险辨识:说明字段别名不与多选框标签冲突', !nodeOf('risk').fields[1].alias.includes('风险辨识结果') ); check( '安全措施默认清单 14 条与源系统 formData 逐条一致', cs.DEFAULT_CONFINED_SPACE_MEASURES.length === 14 && cs.DEFAULT_CONFINED_SPACE_MEASURES[0] === '盛装过有毒、可燃物料的受限空间,所有与受限空间有联系的阀门、管线已加盲板隔离,并落实盲板责任人,未采用水封或关闭阀门代替盲板' && cs.DEFAULT_CONFINED_SPACE_MEASURES[3] === '转动设备已切断电源,电源开关处已加锁并悬挂“禁止合闸”标志牌' && cs.DEFAULT_CONFINED_SPACE_MEASURES[13] === '其他相关特殊作业已办理相应安全作业票' ); check( '默认行挂在 measure_list 字段上(defaultRows)', Array.isArray(nodeOf('measure').fields[0].defaultRows) && nodeOf('measure').fields[0].defaultRows.length === 14 && nodeOf('measure').fields[0].defaultRows[0].measure_content === cs.DEFAULT_CONFINED_SPACE_MEASURES[0] ); console.log('\n[4] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)'); const m = cs.matchConfinedSpaceFlowNode('HO_PERMIT_CONFINED_SPACE_THREETH'); check('按任务定义 key 精确命中', m && m.key === 'gas_analysis', { key: m && m.key }); check( '按节点名称命中', (cs.matchConfinedSpaceFlowNode('现场安全措施确认') || {}).key === 'measure' ); check( '按别名命中', (cs.matchConfinedSpaceFlowNode('气体检测') || {}).key === 'gas_analysis' ); check( '按平台自定义 key 反向包含命中', (cs.matchConfinedSpaceFlowNode('cs_gas_analysis') || {}).key === 'gas_analysis' ); check( '流程定义 key / 一级作业申请 key 不误命中后续环节', cs.matchConfinedSpaceFlowNode('HO_PERMIT_CONFINED_SPACE') === null && cs.matchConfinedSpaceFlowNode('HO_PERMIT_CONFINED_SPACE_ONETH') === null ); check('空标识返回 null', cs.matchConfinedSpaceFlowNode('', null) === null); console.log('\n[5] 运行时辅助(回显 / payload / 流程变量)'); const measureNode = nodeOf('measure'); const source = { measure_list: [ { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' } ], measure_preparer_sign: '/tmp/a.png' }; const loaded = cst.loadNodeValues(measureNode, [source]); check( '回显:按自身 key 取到已保存内容', loaded.measure_list.length === 1 && loaded.measure_preparer_sign === '/tmp/a.png' ); check('回显:未保存的字段为 undefined', loaded.measure_confirmer_sign === undefined); const payload = cst.buildNodePayload(measureNode, { measure_list: [{ measure_content: 'x' }], measure_preparer_sign: '/tmp/a.png' }); check( 'payload:保留原始类型(数组不转字符串)', Array.isArray(payload.measure_list) && payload.measure_list.length === 1 ); check( 'payload:空值不写入', !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload) ); const vars = cst.buildNodeVariables(measureNode, { measure_list: [{ measure_content: 'x' }], measure_preparer_sign: '/tmp/a.png' }); check( '流程变量:子表转 JSON 字符串、其余转字符串', typeof vars.measure_list === 'string' && vars.measure_preparer_sign === '/tmp/a.png' ); console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED')); process.exit(failed ? 1 : 0);