// 一次性脚本:把档案管理模块(yudao-module-archive)并入 config_export_all_20260929.sql 的阶段一 // 用法:node _merge_archive.js const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const DIR = __dirname; const TARGET = path.join(DIR, 'config_export_all_20260929.sql'); const MODULE = path.join(DIR, 'archive_module.sql'); const BPM = path.join(DIR, '_archive_bpm_export.sql'); const TMP = path.join(os.homedir(), 'AppData', 'Local', 'Temp'); const raw = fs.readFileSync(TARGET, 'utf8'); if (!raw.includes('\r\n')) throw new Error('目标文件不是 CRLF'); let doc = raw.replace(/\r\n/g, '\n'); const norm = (s) => s.replace(/\r\n/g, '\n'); const moduleLines = norm(fs.readFileSync(MODULE, 'utf8')).split('\n'); const bpmLines = norm(fs.readFileSync(BPM, 'utf8')).split('\n'); /** 断言锚点唯一 */ function anchor(text) { const n = doc.split(text).length - 1; if (n !== 1) throw new Error(`锚点出现 ${n} 次,期望 1 次:${text.slice(0, 90)}`); } /** 在锚点前插入(插入内容与锚点之间留一个空行) */ function insertBefore(text, before) { anchor(text); doc = doc.replace(text, before + '\n\n' + text); } /** 替换一次 */ function replaceOnce(from, to) { anchor(from); doc = doc.replace(from, to); } // ---------------------------------------------------------------- 数据行风格归一 /** 按顶层逗号切分 VALUES 元组(尊重单引号与反斜杠转义) */ function splitTuple(s) { const out = []; let cur = ''; let inS = false; for (let i = 0; i < s.length; i++) { const c = s[i]; if (inS) { cur += c; if (c === '\\' && i + 1 < s.length) { cur += s[++i]; } else if (c === "'") { if (s[i + 1] === "'") cur += s[++i]; else inS = false; } } else if (c === "'") { inS = true; cur += c; } else if (c === ',') { out.push(cur.trim()); cur = ''; } else { cur += c; } } if (cur.trim()) out.push(cur.trim()); return out; } /** * 把从库 JDBC 导出的 INSERT 行改写成本文件的既有风格: * 字符串/数字/日期一律加单引号,bit 列 deleted 出 X'00',NULL 保持 NULL。 */ function restyle(line) { const m = line.match(/^INSERT INTO `([^`]+)` \(([^)]+)\) VALUES \((.*)\);$/); if (!m) throw new Error(`无法解析数据行:${line.slice(0, 90)}`); const [, table, colText, valsText] = m; const cols = colText.split(',').map((c) => c.trim().replace(/`/g, '')); const vals = splitTuple(valsText); if (cols.length !== vals.length) { throw new Error(`${table} 列数 ${cols.length} 与值数 ${vals.length} 不符`); } const out = vals.map((v, i) => { if (v === 'NULL') return 'NULL'; if (cols[i] === 'deleted') { if (v !== '0') throw new Error(`${table}.deleted 期望 0,实际 ${v}`); return "X'00'"; } return v.startsWith("'") ? v : `'${v}'`; }); return `INSERT INTO \`${table}\` (${cols.map((c) => '`' + c + '`').join(', ')}) VALUES (${out.join(', ')});`; } function readRows(file) { const text = norm(fs.readFileSync(path.join(TMP, file), 'utf8')); const rows = text .split('\n') .map((l) => l.trim()) .filter((l) => l.startsWith('INSERT INTO ')) .map(restyle); return rows; } const CATEGORY_ROWS = readRows('archive_category_rows.sql'); const DICT_TYPE_ROWS = readRows('archive_dict_type_rows.sql'); const DICT_DATA_ROWS = readRows('archive_dict_data_rows.sql'); const BPM_CATEGORY_ROWS = readRows('archive_bpm_category_row.sql'); if (CATEGORY_ROWS.length !== 6) throw new Error(`分类数据 ${CATEGORY_ROWS.length} 行,期望 6`); if (DICT_TYPE_ROWS.length !== 6) throw new Error(`字典类型 ${DICT_TYPE_ROWS.length} 行,期望 6`); if (DICT_DATA_ROWS.length !== 19) throw new Error(`字典数据 ${DICT_DATA_ROWS.length} 行,期望 19`); if (BPM_CATEGORY_ROWS.length !== 1) throw new Error(`BPM 分类 ${BPM_CATEGORY_ROWS.length} 行,期望 1`); // ================================================================ 1. 表数量 427 → 430 replaceOnce( '-- 范围:全库 BASE TABLE 共 427 张(0918 的 419 张 + 2 张 finance_* + 2 张 ums_* + 4 张 research_*),全部输出 CREATE TABLE;', '-- 范围:全库 BASE TABLE 共 430 张(0918 的 419 张 + 2 张 finance_* + 2 张 ums_* + 4 张 research_* + 3 张 archive_*),全部输出 CREATE TABLE;', ); // ============================================= 2. 带数据表清单补 ⑦(档案管理模块) insertBefore( '-- ⑥ AI 配置(仅模型+key)+ 基础设施配置管理:ai_model、ai_api_key、infra_config、infra_job、infra_file_config、infra_data_source_config、infra_codegen_table、infra_codegen_column', '-- ⑦ 档案管理模块(1 张带初始数据 + 2 张仅结构):archive_category(初始 6 条分类)、archive_document、archive_borrow', ); // ============================================================== 3. 补丁登记块 const PREAMBLE = [ '-- 补丁内容(2026-09-29,档案管理模块 yudao-module-archive):', '-- ① 补 3 张 archive_* 表结构(档案分类 / 档案主表 / 借阅单),其中 archive_category 带初始 6 条分类数据:', '-- archive_category、archive_document、archive_borrow', '-- ② 补 6 个字典类型 + 19 条字典数据(并入阶段一的 system_dict_type / system_dict_data):', '-- archive_category_status(启用/停用)', '-- archive_document_status(在库/借出中)', '-- archive_secret_level(公开/内部/保密)', '-- archive_carrier_type(电子/实体/电子+实体)', '-- archive_source_type(手工录入/课题归档/会员归档/财务归档)', '-- archive_borrow_status(草稿/审批中/借出中/已归还/已驳回)', '-- ③ 补 1 条 BPM 分类(并入阶段一的 bpm_category):', '-- id=8003 \'档案借阅审批\' / code=\'archive_borrow_approve\'', '-- ④ 补 1 条 BPM 流程定义(档案借阅两级审批:档案管理员审核→分管领导审批),', '-- 分别落 act_re_deployment / act_re_procdef / act_ge_bytearray(3 条) / act_re_model /', '-- bpm_process_definition_info 五张表,流程 key=\'archive_borrow_approve\'。', '-- bpm_process_definition_info.form_type=20(自定义表单),', '-- form_custom_create_path=/archive/borrow/index、', '-- form_custom_view_path=/archive/borrow/detail/approve/index.vue。', '-- ⚠ 该流程的 BPMN 为手写两节点,**不是** bpm_deploy_flow_from_template 生成的单节点流程。', '-- ⑤ 补 3 张来源表各 1 列 archive_document_id(bigint NULL,归档联动回写档案 ID):', '-- research_application(课题申报单)、ums_member(会员)、finance_reimbursement(报销单)。', '-- 列可空、不影响既有数据;「是否已归档」以该列为硬标记(另有 archive_document 侧', '-- source_type+source_id 幂等兜底)。', '-- ⑥ 补 24 条档案管理菜单:1075600「档案管理」(**顶级目录**,parent_id=0,path=/archive)', '-- + 1075601「档案分类」/ 1075602「档案管理」/ 1075603「借阅登记」/ 1075604「档案统计」', '-- + 1075610~1075613(分类查询/创建/更新/删除)、', '-- 1075620~1075625(档案查询/创建/更新/删除/导出/保密档案查看)、', '-- 1075630~1075635(借阅查询/登记/修改/删除/提交审批/归还登记)、', '-- 1075640(档案统计查询)、', '-- 1075536(课题申报归档,挂 1075512 课题申报下)、', '-- 1075504(会员归档,挂 1075492 会员列表下)。', '-- 财务归档复用既有 finance:reimbursement:archive(1075424),不新增权限点。', '-- **菜单段本文件无需为此改动**:阶段② 的写法是 `INSERT INTO system_menu SELECT *', '-- FROM \\`mom-jssm\\`.system_menu WHERE deleted=b\'0\'`,是执行期跨库全量拷贝,', '-- 只要来源库 mom-jssm 有这几行就会被带过去 —— 与阶段一的字面量 INSERT 不同。', '-- 在此登记只为说明「这版文件确实覆盖了该变更」,避免后来者以为漏了。', '-- ⚠ 档案模块的 BPMN 部署产物(④)为**先部署后导出**:新库初始化时依赖本文件的字面量 INSERT,', '-- 若重跑导出请勿遗漏五张表(含 act_ge_bytearray 的 3 条:bpmn / png / source)。', ].join('\n'); insertBefore('-- 历史补丁内容(2026-09-18):', PREAMBLE); // ======================================== 4. 3 张 archive_* 表 DDL + 初始分类数据 function ddlBlock(fromLine, toLine, tableName, rows) { const body = moduleLines.slice(fromLine - 1, toLine).join('\n'); const titles = rows ? `导出 ${rows.length} 行` : '仅 DDL,无数据'; return [`-- ============ 表:${tableName}(${titles}) ============`, body, ...(rows || []), ''].join('\n'); } const ARCHIVE_DDL = [ '-- ***********************************************************************', '-- ⑦ 档案管理模块(yudao-module-archive:分类树 + 档案主表 + 借阅单)', '-- ***********************************************************************', '', ddlBlock(9, 28, 'archive_category', CATEGORY_ROWS), ddlBlock(31, 67, 'archive_document', null), ddlBlock(70, 104, 'archive_borrow', null), ].join('\n'); insertBefore( ['-- ***********************************************************************', '-- 其余 382 张表(仅表结构,无数据)', '-- ***********************************************************************'].join('\n'), ARCHIVE_DDL, ); // ===================================== 5. 三张来源表各补 1 列 archive_document_id const ARCHIVE_COL = " `archive_document_id` bigint DEFAULT NULL COMMENT '归档生成的档案 ID(archive_document.id,归档联动回写)',"; // 5.1 research_application(放在 notice_id 之后) replaceOnce( " `notice_id` bigint DEFAULT NULL COMMENT '已发布的立项通知公告 ID',", " `notice_id` bigint DEFAULT NULL COMMENT '已发布的立项通知公告 ID',\n" + ARCHIVE_COL, ); // 5.2 ums_member(放在 status 之后;两行锚点以消歧) replaceOnce( " `expire_date` date DEFAULT NULL COMMENT '有效期至',\n" + " `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-正常 0-暂停',", " `expire_date` date DEFAULT NULL COMMENT '有效期至',\n" + " `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-正常 0-暂停',\n" + ARCHIVE_COL, ); // 5.3 finance_reimbursement(放在 archive_time 之后;两行锚点以消歧) replaceOnce( " `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM流程实例ID',\n" + " `archive_time` datetime DEFAULT NULL COMMENT '归档时间',", " `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'BPM流程实例ID',\n" + " `archive_time` datetime DEFAULT NULL COMMENT '归档时间',\n" + ARCHIVE_COL, ); // "其余 382 张表" → 384(⑦ 已插入 3 张,其中 2 张无数据故不进带数据清单) replaceOnce( ['-- ***********************************************************************', '-- 其余 382 张表(仅表结构,无数据)', '-- ***********************************************************************'].join('\n'), ['-- ***********************************************************************', '-- 其余 384 张表(仅表结构,无数据)', '-- ***********************************************************************'].join('\n'), ); // ===================================================== 6. BPM 部署产物(7 条 INSERT) const bpmInserts = bpmLines.filter((l) => l.startsWith('INSERT INTO ')); if (bpmInserts.length !== 7) throw new Error(`BPM INSERT 条数 ${bpmInserts.length},期望 7`); const pick = (table) => bpmInserts.filter((l) => l.startsWith(`INSERT INTO \`${table}\``)); const insProcdef = pick('act_re_procdef'); const insDeployment = pick('act_re_deployment'); const insBytearray = pick('act_ge_bytearray'); const insModel = pick('act_re_model'); const insProcInfo = pick('bpm_process_definition_info'); if ( insProcdef.length !== 1 || insDeployment.length !== 1 || insBytearray.length !== 3 || insModel.length !== 1 || insProcInfo.length !== 1 ) { throw new Error('BPM INSERT 分类数量不符'); } // 6.1 act_re_procdef(23 → 24 行) insertBefore('-- ============ 表:act_re_deployment(导出 23 行) ============', insProcdef.join('\n')); replaceOnce( '-- ============ 表:act_re_procdef(导出 23 行) ============', '-- ============ 表:act_re_procdef(导出 24 行) ============', ); // 6.2 act_re_deployment(23 → 24 行) insertBefore('-- ============ 表:act_ge_bytearray(导出 28 行) ============', insDeployment.join('\n')); replaceOnce( '-- ============ 表:act_re_deployment(导出 23 行) ============', '-- ============ 表:act_re_deployment(导出 24 行) ============', ); // 6.3 act_ge_bytearray(28 → 31 行,bpmn / png / source 三条) insertBefore('-- ============ 表:act_re_model(导出 23 行) ============', insBytearray.join('\n')); replaceOnce( '-- ============ 表:act_ge_bytearray(导出 28 行) ============', '-- ============ 表:act_ge_bytearray(导出 31 行) ============', ); // 6.4 act_re_model(23 → 24 行) insertBefore('-- ============ 表:bpm_process_definition_info(导出 23 行) ============', insModel.join('\n')); replaceOnce( '-- ============ 表:act_re_model(导出 23 行) ============', '-- ============ 表:act_re_model(导出 24 行) ============', ); // 6.5 bpm_process_definition_info(23 → 24 行,插在 ② 分组横幅之前) insertBefore( ['-- ***********************************************************************', '-- ② 协同(业务附件配置 + 分类 + 表单 + 扩展配置)(DDL + 数据)', '-- ***********************************************************************'].join( '\n', ), insProcInfo.join('\n'), ); replaceOnce( '-- ============ 表:bpm_process_definition_info(导出 23 行) ============', '-- ============ 表:bpm_process_definition_info(导出 24 行) ============', ); // ===================================================== 7. bpm_category 补 8003(25 → 26 行) insertBefore('-- ============ 表:bpm_form(导出 4 行) ============', BPM_CATEGORY_ROWS.join('\n')); replaceOnce( '-- ============ 表:bpm_category(导出 25 行) ============', '-- ============ 表:bpm_category(导出 26 行) ============', ); // ================================================ 8. 字典类型 6 条(254 → 260 行) insertBefore('-- ============ 表:system_dict_data(导出 1205 行) ============', DICT_TYPE_ROWS.join('\n')); replaceOnce( '-- ============ 表:system_dict_type(导出 254 行) ============', '-- ============ 表:system_dict_type(导出 260 行) ============', ); // ================================================ 9. 字典数据 19 条(1205 → 1224 行) insertBefore('-- ============ 表:system_tenant(导出 3 行) ============', DICT_DATA_ROWS.join('\n')); replaceOnce( '-- ============ 表:system_dict_data(导出 1205 行) ============', '-- ============ 表:system_dict_data(导出 1224 行) ============', ); // ---------------------------------------------------------------- 写出 doc = doc.replace(/\n/g, '\r\n'); fs.writeFileSync(TARGET, doc, 'utf8'); console.log(`合并完成 → ${path.basename(TARGET)}(${doc.split('\r\n').length} 行)`);