liu
2026-09-30 91491239c5d4611b5b1404a7abeb2bb23d0edad5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// 一次性脚本:把档案管理模块(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} 行)`);