check-dangerous-work-flow-nodes.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * 受限空间作业票审批流校验
  3. *
  4. * 用法:node scripts/check-dangerous-work-flow-nodes.js
  5. *
  6. * 校验内容:
  7. * 1) 动火作业的既有实现文件未被改动(与 HEAD 完全一致)
  8. * 2) 受限空间 7 个后续环节的 code / name / 字段与源系统一致
  9. * 3) 环节匹配:按 code / 别名 / 名称 / 双向包含都能命中
  10. * 4) 默认安全措施清单与源系统 formData 逐条一致
  11. * 5) 受限空间运行时辅助(回显 / payload / 流程变量)的取值口径正确
  12. *
  13. * 源系统依据:
  14. * https://www.acwlkj.com/IntellSecurity-web/flow/getFlowNodeDefineByFlowid.do?flow_id=3769
  15. */
  16. const fs = require('fs');
  17. const os = require('os');
  18. const path = require('path');
  19. const { execFileSync } = require('child_process');
  20. const babel = require('@babel/core');
  21. const Module = require('module');
  22. const ROOT = path.resolve(__dirname, '..');
  23. const SRC = path.join(
  24. ROOT,
  25. 'src/views/bpm/handleTask/components/dangerousWorkTicket'
  26. );
  27. const OUT = path.join(os.tmpdir(), 'kd-aiot-check-confined-space');
  28. // 动火作业的既有实现文件:受限空间实现不得改动它们
  29. const FIRE_FILES = [
  30. 'fireFlowNodes.js',
  31. 'fireFlowTask.js',
  32. 'components/fireTicketDetail.vue',
  33. 'components/flowNodeForm.vue'
  34. ];
  35. let failed = 0;
  36. function check(name, cond, extra) {
  37. if (cond) {
  38. console.log(' PASS ' + name);
  39. } else {
  40. failed++;
  41. console.log(
  42. ' FAIL ' + name + (extra ? ' -> ' + JSON.stringify(extra) : '')
  43. );
  44. }
  45. }
  46. // 编译 ESM -> CJS
  47. // confinedSpaceFlowTask.js 引用了 fireFlowTask.js(只读复用),
  48. // fireFlowTask.js 又引用了 fireFlowNodes.js,一并编译进来
  49. fs.mkdirSync(OUT, { recursive: true });
  50. [
  51. 'confinedSpaceFlowNodes.js',
  52. 'confinedSpaceFlowTask.js',
  53. 'fireFlowNodes.js',
  54. 'fireFlowTask.js'
  55. ].forEach((file) => {
  56. const code = fs.readFileSync(path.join(SRC, file), 'utf8');
  57. const res = babel.transformSync(code, {
  58. filename: file,
  59. babelrc: false,
  60. configFile: false,
  61. plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
  62. });
  63. fs.writeFileSync(path.join(OUT, file), res.code);
  64. });
  65. // 受限空间运行时辅助会引用 @/api/*,这里用桩模块代替,避免校验脚本依赖后端接口
  66. const STUB = path.join(OUT, 'stub.js');
  67. fs.writeFileSync(
  68. STUB,
  69. 'module.exports = {\n' +
  70. ' getTaskListByProcessInstanceId: function () { return Promise.resolve([]); },\n' +
  71. ' getProcessInstance: function () { return Promise.resolve({}); }\n' +
  72. '};\n'
  73. );
  74. const originalResolve = Module._resolveFilename;
  75. Module._resolveFilename = function (request, ...args) {
  76. if (request.startsWith('@/')) {
  77. return STUB;
  78. }
  79. return originalResolve.call(this, request, ...args);
  80. };
  81. const cs = require(path.join(OUT, 'confinedSpaceFlowNodes.js'));
  82. const cst = require(path.join(OUT, 'confinedSpaceFlowTask.js'));
  83. console.log('\n[1] 动火作业既有实现未被改动');
  84. FIRE_FILES.forEach((file) => {
  85. let changed = true;
  86. try {
  87. execFileSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
  88. cwd: path.join(SRC, '..'),
  89. stdio: 'ignore'
  90. });
  91. changed = false;
  92. } catch (e) {
  93. changed = true;
  94. }
  95. check(file + ' 与 HEAD 一致', !changed);
  96. });
  97. console.log('\n[2] 受限空间环节(源系统 HO_PERMIT_CONFINED_SPACE 共 8 节点,1 级为作业申请模板)');
  98. const expected = [
  99. ['HO_PERMIT_CONFINED_SPACE_TWOTH', '风险辨识'],
  100. ['HO_PERMIT_CONFINED_SPACE_THREETH', '气体分析'],
  101. ['HO_PERMIT_CONFINED_SPACE_FOURTH', '现场安全措施确认'],
  102. ['HO_PERMIT_CONFINED_SPACE_FIVETH', '安全交底'],
  103. ['HO_PERMIT_CONFINED_SPACE_SIXTH', '所在单位意见'],
  104. ['HO_PERMIT_CONFINED_SPACE_SEVENTH', '作业实施'],
  105. ['HO_PERMIT_CONFINED_SPACE_EIGHTTH', '完工验收']
  106. ];
  107. check('环节数量为 7', cs.CONFINED_SPACE_FLOW_NODES.length === 7, {
  108. len: cs.CONFINED_SPACE_FLOW_NODES.length
  109. });
  110. expected.forEach(([code, name], i) => {
  111. const node = cs.CONFINED_SPACE_FLOW_NODES[i];
  112. check(
  113. '第' + (i + 2) + '级 ' + code + ' / ' + name,
  114. node.code === code && node.name === name,
  115. { code: node.code, name: node.name }
  116. );
  117. });
  118. console.log('\n[3] 与源系统 formJson 一致性');
  119. const nodeOf = (k) => cs.CONFINED_SPACE_FLOW_NODES.find((n) => n.key === k);
  120. const colsOf = (k, fk) => {
  121. const node = nodeOf(k);
  122. const field = node.fields.find((f) => f.key === fk);
  123. return (field.columns || []).map((c) => c.label);
  124. };
  125. check(
  126. '气体分析:表名「气体分析记录表」',
  127. nodeOf('gas_analysis').fields[0].label === '气体分析记录表'
  128. );
  129. check(
  130. '气体分析:6 列且顺序与源系统一致(分析项目/气体名称/合格标准/取样分析时间/分析部位/分析人)',
  131. JSON.stringify(colsOf('gas_analysis', 'gas_analysis_list')) ===
  132. JSON.stringify([
  133. '分析项目',
  134. '气体名称',
  135. '合格标准',
  136. '取样分析时间',
  137. '分析部位',
  138. '分析人'
  139. ]),
  140. colsOf('gas_analysis', 'gas_analysis_list')
  141. );
  142. check(
  143. '气体分析:分析项目 3 个选项(有毒有害气体/可燃气体/氧气含量)',
  144. JSON.stringify(
  145. nodeOf('gas_analysis').fields[0].columns[0].options.map((o) => o.label)
  146. ) === JSON.stringify(['有毒有害气体', '可燃气体', '氧气含量'])
  147. );
  148. check(
  149. '现场安全措施确认:表名「安全措施确认表」,3 列(安全措施/是否确认/确认人)',
  150. nodeOf('measure').fields[0].label === '安全措施确认表' &&
  151. JSON.stringify(colsOf('measure', 'measure_list')) ===
  152. JSON.stringify(['安全措施', '是否确认', '确认人'])
  153. );
  154. check(
  155. '现场安全措施确认:编制人签字 + 确认人签字',
  156. JSON.stringify(nodeOf('measure').fields.map((f) => f.label)) ===
  157. JSON.stringify(['安全措施确认表', '编制人签字', '确认人签字'])
  158. );
  159. check(
  160. '安全交底:只有 2 个签字(源系统无监护人签字)',
  161. JSON.stringify(nodeOf('brief').fields.map((f) => f.label)) ===
  162. JSON.stringify(['安全交底人签字', '接受交底人签字'])
  163. );
  164. check(
  165. '所在单位意见:意见 + 签字',
  166. nodeOf('unit_opinion').fields.length === 2 &&
  167. nodeOf('unit_opinion').fields[0].type === 'textarea' &&
  168. nodeOf('unit_opinion').fields[1].type === 'sign'
  169. );
  170. check(
  171. '作业实施:实施时间 + 结束时间 + 事中记录表 + 气体分析记录表',
  172. JSON.stringify(nodeOf('execute').fields.map((f) => f.label)) ===
  173. JSON.stringify([
  174. '作业实施时间',
  175. '作业结束时间',
  176. '事中记录表',
  177. '气体分析记录表'
  178. ])
  179. );
  180. check(
  181. '作业实施:气体分析记录表 6 列与源系统一致(时间标签为「气体取样分析时间」)',
  182. JSON.stringify(colsOf('execute', 'gas_record_list')) ===
  183. JSON.stringify([
  184. '分析项目',
  185. '气体名称',
  186. '合格标准',
  187. '气体取样分析时间',
  188. '分析部位',
  189. '分析人'
  190. ]),
  191. colsOf('execute', 'gas_record_list')
  192. );
  193. check(
  194. '事中记录表 3 列(时间/记录内容/记录人)',
  195. JSON.stringify(colsOf('execute', 'during_record_list')) ===
  196. JSON.stringify(['时间', '记录内容', '记录人'])
  197. );
  198. check(
  199. '完工验收:意见 + 签字',
  200. nodeOf('acceptance').fields.length === 2 &&
  201. nodeOf('acceptance').fields[0].label === '意见' &&
  202. nodeOf('acceptance').fields[1].type === 'sign'
  203. );
  204. check(
  205. '风险辨识:多选 20 项与源系统完全一致',
  206. nodeOf('risk').fields[0].options.length === 20 &&
  207. nodeOf('risk').fields[0].options[0].label === '物体打击' &&
  208. nodeOf('risk').fields[0].options[19].label === '其它伤害'
  209. );
  210. check(
  211. '风险辨识:说明字段别名不与多选框标签冲突',
  212. !nodeOf('risk').fields[1].alias.includes('风险辨识结果')
  213. );
  214. check(
  215. '安全措施默认清单 14 条与源系统 formData 逐条一致',
  216. cs.DEFAULT_CONFINED_SPACE_MEASURES.length === 14 &&
  217. cs.DEFAULT_CONFINED_SPACE_MEASURES[0] ===
  218. '盛装过有毒、可燃物料的受限空间,所有与受限空间有联系的阀门、管线已加盲板隔离,并落实盲板责任人,未采用水封或关闭阀门代替盲板' &&
  219. cs.DEFAULT_CONFINED_SPACE_MEASURES[3] ===
  220. '转动设备已切断电源,电源开关处已加锁并悬挂“禁止合闸”标志牌' &&
  221. cs.DEFAULT_CONFINED_SPACE_MEASURES[13] ===
  222. '其他相关特殊作业已办理相应安全作业票'
  223. );
  224. check(
  225. '默认行挂在 measure_list 字段上(defaultRows)',
  226. Array.isArray(nodeOf('measure').fields[0].defaultRows) &&
  227. nodeOf('measure').fields[0].defaultRows.length === 14 &&
  228. nodeOf('measure').fields[0].defaultRows[0].measure_content ===
  229. cs.DEFAULT_CONFINED_SPACE_MEASURES[0]
  230. );
  231. console.log('\n[4] 环节匹配(多级匹配:code / 别名 / 名称 / 双向包含)');
  232. const m = cs.matchConfinedSpaceFlowNode('HO_PERMIT_CONFINED_SPACE_THREETH');
  233. check('按任务定义 key 精确命中', m && m.key === 'gas_analysis', {
  234. key: m && m.key
  235. });
  236. check(
  237. '按节点名称命中',
  238. (cs.matchConfinedSpaceFlowNode('现场安全措施确认') || {}).key === 'measure'
  239. );
  240. check(
  241. '按别名命中',
  242. (cs.matchConfinedSpaceFlowNode('气体检测') || {}).key === 'gas_analysis'
  243. );
  244. check(
  245. '按平台自定义 key 反向包含命中',
  246. (cs.matchConfinedSpaceFlowNode('cs_gas_analysis') || {}).key === 'gas_analysis'
  247. );
  248. check(
  249. '流程定义 key / 一级作业申请 key 不误命中后续环节',
  250. cs.matchConfinedSpaceFlowNode('HO_PERMIT_CONFINED_SPACE') === null &&
  251. cs.matchConfinedSpaceFlowNode('HO_PERMIT_CONFINED_SPACE_ONETH') === null
  252. );
  253. check('空标识返回 null', cs.matchConfinedSpaceFlowNode('', null) === null);
  254. console.log('\n[5] 运行时辅助(回显 / payload / 流程变量)');
  255. const measureNode = nodeOf('measure');
  256. const source = {
  257. measure_list: [
  258. { measure_content: 'x', measure_confirm: '2', measure_confirmer: '张三' }
  259. ],
  260. measure_preparer_sign: '/tmp/a.png'
  261. };
  262. const loaded = cst.loadNodeValues(measureNode, [source]);
  263. check(
  264. '回显:按自身 key 取到已保存内容',
  265. loaded.measure_list.length === 1 && loaded.measure_preparer_sign === '/tmp/a.png'
  266. );
  267. check('回显:未保存的字段为 undefined', loaded.measure_confirmer_sign === undefined);
  268. const payload = cst.buildNodePayload(measureNode, {
  269. measure_list: [{ measure_content: 'x' }],
  270. measure_preparer_sign: '/tmp/a.png'
  271. });
  272. check(
  273. 'payload:保留原始类型(数组不转字符串)',
  274. Array.isArray(payload.measure_list) && payload.measure_list.length === 1
  275. );
  276. check(
  277. 'payload:空值不写入',
  278. !('measure_confirmer_sign' in payload) && !('measure_confirm' in payload)
  279. );
  280. const vars = cst.buildNodeVariables(measureNode, {
  281. measure_list: [{ measure_content: 'x' }],
  282. measure_preparer_sign: '/tmp/a.png'
  283. });
  284. check(
  285. '流程变量:子表转 JSON 字符串、其余转字符串',
  286. typeof vars.measure_list === 'string' &&
  287. vars.measure_preparer_sign === '/tmp/a.png'
  288. );
  289. console.log('\n' + (failed ? 'FAILED: ' + failed + ' 项' : 'ALL PASSED'));
  290. process.exit(failed ? 1 : 0);