import java.sql.*; import java.util.*; /** * 其他数据子系统全量迁移工具 * * 从旧库 G3HY2025 迁移 b_other_bmfl 分类树 + 各基础字典表数据到新库 FMS。 * 替代 FieldNameSync.java 的单表单字段同步,一次性完成整个"其他数据"子系统的迁移。 * * 注意:本工具面向 V1 表结构(s_module_field / s_module_field_list / s_module_field_edit), * 与《FMS新系统核心表结构设计V2》的 s_field / s_module_schema 不兼容,仅作历史参考。 * * 4 个阶段: * 1. 发现:读取旧 b_other_bmfl,列出分类和关联表 * 2. 树迁移:创建 category/module 节点树 * 3. 数据迁移:逐表复制数据(类型转换 + Snowflake ID) * 4. 元数据同步:col_Caption/col_Visible/col_edit → s_module_field* * * 用法:java -cp ".;mssql-jdbc.jar" OtherDataMigration [options] * * 选项: * --dry-run 仅发现并报告,不执行迁移 * --clean-tree 清理新库中已有的 b_other_bmfl 树节点 * --skip-data 跳过阶段 3(数据迁移),仅做树结构迁移 * --skip-metadata 跳过阶段 4(字段元数据同步) */ public class OtherDataMigration { static final String URL = "jdbc:sqlserver://118.89.70.199:1433;databaseName=FMS;encrypt=true;trustServerCertificate=true"; static final String USER = "sa"; static final String PASSWORD = "zhang520.."; // ---- 选项 ---- boolean dryRun = false; boolean cleanTree = false; boolean skipData = false; boolean skipMetadata = false; Connection conn; // ---- 阶段 1 数据 ---- static class OldBmflEntry { String oldId; String name; // mc_b String tableName; // b_tablename String groupBy; // b_groupby int canuse; int xh; } List oldEntries = new ArrayList<>(); Map groupNameMap = new LinkedHashMap<>(); // groupBy -> 第一个 mc_b 作为分类名 // ---- 阶段 2 数据 ---- static class TableMapping { long moduleId; String moduleCode; String viewTable; String moduleName; boolean hasModule; } Map categoryIds = new LinkedHashMap<>(); // groupBy -> 新 category b_id Map moduleNodeIds = new LinkedHashMap<>(); // moduleCode -> 新 module node b_id Map tableMappings = new LinkedHashMap<>(); // tableName -> mapping info // ---- ID 缓存(避免反复查询 MAX)---- Map nextIdCache = new HashMap<>(); // ====================================================================== // main // ====================================================================== public static void main(String[] args) throws Exception { OtherDataMigration tool = new OtherDataMigration(); tool.parseArgs(args); tool.printHeader(); tool.conn = DriverManager.getConnection(URL, USER, PASSWORD); try { // ===== 阶段 1:发现 ===== tool.phaseDiscovery(); // ===== 阶段 2:树迁移 ===== tool.phaseTreeMigration(); // ===== 阶段 3:数据迁移 ===== tool.phaseDataMigration(); // ===== 阶段 4:元数据同步 ===== tool.phaseMetadataSync(); System.out.println("\n" + "=".repeat(60)); System.out.println("所有阶段完成。"); System.out.println("=".repeat(60)); } catch (Exception e) { System.err.println("\n[错误] " + e.getMessage()); e.printStackTrace(); if (tool.conn != null) { try { tool.conn.rollback(); } catch (SQLException ignored) {} } } finally { if (tool.conn != null) tool.conn.close(); } } // ====================================================================== // 命令行参数解析 // ====================================================================== void parseArgs(String[] args) { for (String arg : args) { switch (arg.trim().toLowerCase(Locale.ROOT)) { case "--dry-run" -> dryRun = true; case "--clean-tree" -> cleanTree = true; case "--skip-data" -> skipData = true; case "--skip-metadata" -> skipMetadata = true; default -> { System.err.println("未知选项: " + arg); System.err.println("用法: java OtherDataMigration [--dry-run] [--clean-tree] [--skip-data] [--skip-metadata]"); System.exit(1); } } } } void printHeader() { System.out.println("=".repeat(60)); System.out.println(" 其他数据子系统全量迁移工具"); System.out.println(" G3HY2025 → FMS"); System.out.println("=".repeat(60)); System.out.println(" dry-run: " + dryRun); System.out.println(" clean-tree: " + cleanTree); System.out.println(" skip-data: " + skipData); System.out.println(" skip-metadata: " + skipMetadata); System.out.println(); } // ====================================================================== // 阶段 1:发现 // ====================================================================== static final String DISCOVERY_SQL = """ SELECT b_id, mc_b, b_tablename, CASE WHEN b_canuse = '1' THEN 1 ELSE 0 END AS canuse, CONVERT(int, ISNULL(NULLIF(b_xh,''), '0')) AS xh, ISNULL(NULLIF(b_groupby,''), '(未分组)') AS groupby FROM G3HY2025.dbo.b_other_bmfl WHERE b_tablename IS NOT NULL AND LTRIM(RTRIM(b_tablename)) != '' ORDER BY groupby, xh, mc_b """; void phaseDiscovery() throws SQLException { System.out.println("=".repeat(60)); System.out.println("阶段 1:发现旧系统数据结构"); System.out.println("=".repeat(60)); try (PreparedStatement ps = conn.prepareStatement(DISCOVERY_SQL); ResultSet rs = ps.executeQuery()) { Set seenGroups = new LinkedHashSet<>(); while (rs.next()) { OldBmflEntry entry = new OldBmflEntry(); entry.oldId = rs.getString("b_id"); entry.name = trim(rs.getString("mc_b")); entry.tableName = trim(rs.getString("b_tablename")); entry.canuse = rs.getInt("canuse"); entry.xh = rs.getInt("xh"); entry.groupBy = trim(rs.getString("groupby")); oldEntries.add(entry); if (seenGroups.add(entry.groupBy)) { groupNameMap.put(entry.groupBy, entry.name); } } } // 打印分类 System.out.println("\n发现 " + groupNameMap.size() + " 个分类:"); for (Map.Entry e : groupNameMap.entrySet()) { long count = oldEntries.stream().filter(oe -> oe.groupBy.equals(e.getKey())).count(); System.out.println(" 【" + e.getKey() + "】" + (e.getValue() != null ? " (示例: " + e.getValue() + ")" : "") + " — " + count + " 个子项"); } // 打印所有条目 System.out.println("\n共 " + oldEntries.size() + " 条分类记录:"); System.out.printf(" %-20s %-25s %-18s %-6s %s%n", "b_groupby", "mc_b", "b_tablename", "启用", "排序"); System.out.println(" " + "-".repeat(80)); for (OldBmflEntry e : oldEntries) { System.out.printf(" %-20s %-25s %-18s %-6d %d%n", e.groupBy, truncate(e.name, 24), e.tableName, e.canuse, e.xh); } // 检查各表在新库中是否存在,以及是否有 s_module 条目 System.out.println("\n检查表在新库中的状态:"); System.out.printf(" %-20s %-10s %-20s %s%n", "表名", "表存在", "s_module_code", "模块名"); System.out.println(" " + "-".repeat(75)); Set processed = new LinkedHashSet<>(); for (OldBmflEntry e : oldEntries) { if (!processed.add(e.tableName)) continue; boolean tableExists = tableExistsInNew(e.tableName); TableMapping mapping = findModuleByViewTable(e.tableName); String moduleCode = mapping != null && mapping.hasModule ? mapping.moduleCode : "(无)"; String moduleName = mapping != null && mapping.hasModule ? mapping.moduleName : "(无)"; if (mapping != null && mapping.hasModule) { tableMappings.put(e.tableName, mapping); } System.out.printf(" %-20s %-10s %-20s %s%n", e.tableName, tableExists ? "存在" : "不存在", moduleCode, truncate(moduleName, 19)); } if (!tableMappings.isEmpty()) { System.out.println("\n以下 " + tableMappings.size() + " 个表已有 s_module 配置,可以直接迁移:"); for (Map.Entry e : tableMappings.entrySet()) { System.out.println(" " + e.getKey() + " → " + e.getValue().moduleCode + " (" + e.getValue().moduleName + ")"); } } Set missing = new LinkedHashSet<>(processed); missing.removeAll(tableMappings.keySet()); if (!missing.isEmpty()) { System.out.println("\n以下 " + missing.size() + " 个表缺少 s_module 配置,将跳过:"); for (String s : missing) { System.out.println(" " + s); } } } // ====================================================================== // 阶段 2:树结构迁移 // ====================================================================== static final String CLEAN_TREE_SQL = """ DELETE FROM dbo.b_other_bmfl WHERE b_node_type IS NOT NULL """; static final String INSERT_TREE_NODE = """ INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_node_type, b_module_code, b_xh, b_canuse, b_inputuser_id, b_inputdatetime, b_path, b_level) VALUES (?, ?, ?, ?, ?, ?, 1, NULL, SYSDATETIME(), '', 1) """; void phaseTreeMigration() throws SQLException { System.out.println("\n" + "=".repeat(60)); System.out.println("阶段 2:树结构迁移"); System.out.println("=".repeat(60)); // 检查 b_other_bmfl 是否有 b_node_type 列,没有则自动添加 if (!ensureColumn("b_other_bmfl", "b_node_type", "varchar(20) NULL")) { System.out.println("无法添加 b_node_type 列,跳过树迁移。"); return; } if (!ensureColumn("b_other_bmfl", "b_module_code", "varchar(100) NULL")) { System.out.println("无法添加 b_module_code 列,跳过树迁移。"); return; } if (groupsWithModules().isEmpty()) { System.out.println("没有可迁移的分类(所有表都缺少 s_module 配置),跳过树迁移。"); return; } conn.setAutoCommit(false); try { // 可选:清理旧数据 if (cleanTree) { System.out.println("\n清理已有的 b_other_bmfl 树节点..."); int deleted = 0; try (PreparedStatement ps = conn.prepareStatement(CLEAN_TREE_SQL)) { deleted = ps.executeUpdate(); } System.out.println(" 删除 " + deleted + " 条记录"); } else { System.out.println("\n保留已有树节点(使用 --clean-tree 可清理后重建)"); } // Step 1: 创建顶层"其他数据"虚拟根节点(可选,用于组织) long rootId = getNextId("b_other_bmfl"); try (PreparedStatement ps = conn.prepareStatement(INSERT_TREE_NODE)) { ps.setLong(1, rootId); ps.setNull(2, Types.BIGINT); ps.setString(3, "其他数据"); ps.setString(4, "category"); ps.setNull(5, Types.VARCHAR); ps.setInt(6, 10); ps.executeUpdate(); } System.out.println(" 根分类 " + rootId + ": 其他数据"); // Step 2: 创建各分类节点 int catIndex = 10; for (String groupBy : groupNameMap.keySet()) { if (!groupsWithModules().contains(groupBy)) continue; long catId = getNextId("b_other_bmfl"); categoryIds.put(groupBy, catId); try (PreparedStatement ps = conn.prepareStatement(INSERT_TREE_NODE)) { ps.setLong(1, catId); ps.setLong(2, rootId); ps.setString(3, groupBy); ps.setString(4, "category"); ps.setNull(5, Types.VARCHAR); ps.setInt(6, catIndex); ps.executeUpdate(); } System.out.println(" 分类 " + catId + ": " + groupBy + " (parent=" + rootId + ")"); catIndex += 10; } // Step 3: 为每个旧条目创建模块节点 int nodeXh = 10; String currentGroup = null; for (OldBmflEntry entry : oldEntries) { // 跳过没有模块映射的表 TableMapping mapping = tableMappings.get(entry.tableName); if (mapping == null) continue; // 跳过没有分类 ID 的组 Long parentId = categoryIds.get(entry.groupBy); if (parentId == null) continue; // 同一模块只创建一个节点 if (moduleNodeIds.containsKey(mapping.moduleCode)) continue; long nodeId = getNextId("b_other_bmfl"); moduleNodeIds.put(mapping.moduleCode, nodeId); if (!entry.groupBy.equals(currentGroup)) { nodeXh = 10; currentGroup = entry.groupBy; } try (PreparedStatement ps = conn.prepareStatement(INSERT_TREE_NODE)) { ps.setLong(1, nodeId); ps.setLong(2, parentId); ps.setString(3, mapping.moduleName); ps.setString(4, "module"); ps.setString(5, mapping.moduleCode); ps.setInt(6, entry.xh > 0 ? entry.xh : nodeXh); ps.executeUpdate(); } System.out.println(" 模块 " + nodeId + ": " + mapping.moduleCode + " (" + mapping.moduleName + ") → parent=" + parentId); nodeXh += 10; } if (!dryRun) { conn.commit(); System.out.println("\n树结构迁移已提交。"); } else { System.out.println("\n[Dry-run] 树结构迁移已模拟完成,未实际提交。"); conn.rollback(); } } catch (Exception e) { conn.rollback(); throw e; } } // ====================================================================== // 阶段 3:数据迁移 // ====================================================================== static final String TABLE_EXISTS_SQL = """ SELECT COUNT(*) FROM fms.INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ? """; static final String NEW_TABLE_COLUMNS = """ SELECT COLUMN_NAME, ORDINAL_POSITION FROM fms.INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION """; static final String OLD_TABLE_COLUMNS = """ SELECT COLUMN_NAME, DATA_TYPE FROM G3HY2025.INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION """; static final Set SKIP_COLUMNS = Set.of( "b_inputuser_id", "b_updateuser_id", "b_inputdatetime", "b_updatedatetime", "b_inputuserid", "b_updateuserid", "b_inputtime", "b_updatetime", "mx_inputuser_id", "mx_updateuser_id", "mx_inputdatetime", "mx_updatedatetime" ); static final Set BOOLEAN_COLUMNS = Set.of( "b_canuse", "b_visible", "b_used", "b_default", "b_isdefault", "b_black", "b_finish", "b_sh", "b_bill", "b_zx", "b_toall", "b_visible_customer", "b_visible_supply", "b_levelvisible", "b_fromcustomer", "b_othermachinelogin", "b_salesman", "b_docman", "b_dealman", "b_logintime", "b_checkxy", "b_loginfee", "b_reminder", "mx_finish", "mx_sh", "b_isconvert" ); static final Set KEY_COLUMNS = Set.of("b_code"); void phaseDataMigration() throws SQLException { System.out.println("\n" + "=".repeat(60)); System.out.println("阶段 3:数据迁移"); System.out.println("=".repeat(60)); if (skipData) { System.out.println("已跳过(--skip-data)"); return; } // 获取所有需要迁移的表(去重) Set allTables = new LinkedHashSet<>(); for (OldBmflEntry e : oldEntries) { allTables.add(e.tableName); } if (allTables.isEmpty()) { System.out.println("没有需要迁移的表。"); return; } System.out.println("\n准备迁移 " + allTables.size() + " 个表的数据..."); conn.setAutoCommit(false); try { Set existingTables = new LinkedHashSet<>(); Set missingTables = new LinkedHashSet<>(); Set failedTables = new LinkedHashSet<>(); int totalMigrated = 0; for (String tableName : allTables) { // 检查新库中表是否存在 if (!tableExistsInNew(tableName)) { System.out.println(" 跳过 " + tableName + "(新库中不存在)"); missingTables.add(tableName); continue; } existingTables.add(tableName); TableMapping mapping = tableMappings.get(tableName); String label = mapping != null ? mapping.moduleCode + " (" + mapping.moduleName + ")" : tableName; System.out.println("\n--- 迁移表 " + tableName + " → " + label + " ---"); try { // 获取列信息 List newColumns = getNewColumnNames(tableName); Map oldColumnTypes = getOldColumnTypes(tableName); if (newColumns.isEmpty()) { System.out.println(" 新表无列,跳过。"); continue; } // 找出新旧共有的列(仅用于显示) List commonColumns = new ArrayList<>(); for (String col : newColumns) { if (oldColumnTypes.containsKey(col) || col.equals("b_id")) { commonColumns.add(col); } } System.out.println(" 共有列: " + String.join(", ", commonColumns)); // 生成并执行迁移 SQL(传全部新表列,迁移方法内部处理旧表缺失的列) int count = migrateTableData(tableName, newColumns, oldColumnTypes); if (dryRun) { System.out.println(" [Dry-run] 共 " + count + " 条记录将被迁移"); } else { conn.commit(); System.out.println(" 迁移 " + count + " 条记录"); totalMigrated += count; } } catch (SQLException e) { if (!dryRun) { conn.rollback(); } System.err.println(" [失败] " + tableName + ": " + e.getMessage()); failedTables.add(tableName); } } if (dryRun) { System.out.println("\n[Dry-run] 数据迁移已模拟完成。"); System.out.println("可迁移表: " + existingTables.size() + " 个"); conn.rollback(); } else { System.out.println("\n数据迁移完成。共迁移 " + totalMigrated + " 条记录到 " + (existingTables.size() - failedTables.size()) + " 个表。"); } if (!missingTables.isEmpty()) { System.out.println("跳过 " + missingTables.size() + " 个不存在的表: " + String.join(", ", missingTables)); } if (!failedTables.isEmpty()) { System.out.println("失败 " + failedTables.size() + " 个表(已回滚,可重试): " + String.join(", ", failedTables)); } } catch (Exception e) { conn.rollback(); throw e; } } int migrateTableData(String tableName, List newColumns, Map oldColumnTypes) throws SQLException { // 直接查询 MAX(b_id) 作为 ID 基准(不使用 getNextId 缓存,因为批量插入会生成大量 ID) long maxId = 0; String maxSql = "SELECT ISNULL(MAX(b_id), 0) FROM dbo.[" + tableName.replace("]", "]]") + "]"; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(maxSql)) { if (rs.next()) maxId = rs.getLong(1); } long baseId = maxId + 1; // 构建 INSERT 列列表(包含 b_inputdatetime) List insertCols = new ArrayList<>(); List selectExprs = new ArrayList<>(); for (String col : newColumns) { if (SKIP_COLUMNS.contains(col.toLowerCase(Locale.ROOT))) { // 审计用户字段设为 NULL insertCols.add("[" + col + "]"); selectExprs.add("NULL"); continue; } if (col.equalsIgnoreCase("b_id")) { // b_id:使用 ROW_NUMBER 生成 insertCols.add("[b_id]"); selectExprs.add("(" + baseId + " + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))"); continue; } if (col.equalsIgnoreCase("b_inputdatetime")) { insertCols.add("[b_inputdatetime]"); selectExprs.add("SYSDATETIME()"); continue; } if (!oldColumnTypes.containsKey(col)) { // 旧表没有此列,用特殊值填充 insertCols.add("[" + col + "]"); if (col.equalsIgnoreCase("b_code")) { // b_code:使用旧的 b_id(保留旧 varchar ID 作为 code) selectExprs.add("LTRIM(RTRIM(old.b_id))"); } else if (col.equalsIgnoreCase("b_parent_id")) { selectExprs.add("NULL"); } else if (col.equalsIgnoreCase("b_canuse") || col.equalsIgnoreCase("b_visible") || col.equalsIgnoreCase("b_used")) { selectExprs.add("1"); } else { // 设为 NULL(nullable 列应该 OK) selectExprs.add("NULL"); } continue; } String oldType = oldColumnTypes.get(col).toLowerCase(Locale.ROOT); insertCols.add("[" + col + "]"); if (BOOLEAN_COLUMNS.contains(col.toLowerCase(Locale.ROOT)) && isStringType(oldType)) { // varchar(1) 布尔 → int selectExprs.add("CASE WHEN LTRIM(RTRIM(old.[" + col + "])) = '1' THEN 1 ELSE 0 END"); } else if (col.equalsIgnoreCase("b_code") && isStringType(oldType)) { // b_code 可能为 NULL,回退到旧 b_id selectExprs.add("CASE WHEN LTRIM(RTRIM(old.b_code)) != '' THEN LTRIM(RTRIM(old.b_code)) ELSE LTRIM(RTRIM(old.b_id)) END"); } else if (col.toLowerCase(Locale.ROOT).endsWith("_id") && isStringType(oldType)) { // varchar ID → bigint(尝试转换,失败则为 NULL) selectExprs.add("TRY_CAST(LTRIM(RTRIM(old.[" + col + "])) AS bigint)"); } else { selectExprs.add("old.[" + col + "]"); } } // 确定唯一键用于去重 String uniqueCheck = ""; for (String keyCol : KEY_COLUMNS) { if (oldColumnTypes.containsKey(keyCol) && newColumns.stream().anyMatch(c -> c.equalsIgnoreCase(keyCol))) { uniqueCheck = keyCol; break; } } // 构建 SQL StringBuilder sql = new StringBuilder(); sql.append("INSERT INTO fms.dbo.[").append(tableName).append("] ("); sql.append(String.join(", ", insertCols)); sql.append(")\nSELECT "); sql.append(String.join(",\n ", selectExprs)); // 源表去重 + NOT EXISTS boolean hasBcodeInOld = oldColumnTypes.containsKey("b_code"); boolean hasBcodeInNew = newColumns.stream().anyMatch(c -> c.equalsIgnoreCase("b_code")); String bcodeExpr = null; // 实际存入新表 b_code 的表达式(用于 SELECT 列表中生成 b_code 值) String bcodeValueExpr = null; if (hasBcodeInOld && hasBcodeInNew) { bcodeExpr = "COALESCE(NULLIF(LTRIM(RTRIM(old.b_code)),''), LTRIM(RTRIM(old.b_id)))"; bcodeValueExpr = "COALESCE(NULLIF(LTRIM(RTRIM(b_code)),''), LTRIM(RTRIM(b_id)))"; } else if (!hasBcodeInOld && hasBcodeInNew) { bcodeExpr = "LTRIM(RTRIM(old.b_id))"; bcodeValueExpr = "LTRIM(RTRIM(b_id))"; } List whereClauses = new ArrayList<>(); if (bcodeExpr != null) { // 源表去重(按实际 b_code 表达式分区,取第一条) sql.append("\nFROM ("); sql.append("\n SELECT *, ROW_NUMBER() OVER (PARTITION BY "); sql.append(bcodeValueExpr); sql.append(" ORDER BY b_id) AS _rn"); sql.append("\n FROM G3HY2025.dbo.[").append(tableName).append("]"); sql.append("\n) old"); whereClauses.add("old._rn = 1"); // NOT EXISTS 去重新表中已有的数据(加 COLLATE 解决排序规则冲突) whereClauses.add("NOT EXISTS (SELECT 1 FROM fms.dbo.[" + tableName + "] new WHERE new.b_code COLLATE Chinese_PRC_CI_AS = " + bcodeExpr + ")"); } else { sql.append("\nFROM G3HY2025.dbo.[").append(tableName).append("] old"); } if (!whereClauses.isEmpty()) { sql.append("\nWHERE ").append(String.join(" AND ", whereClauses)); } String sqlStr = sql.toString(); if (dryRun) { System.out.println(" SQL: " + sqlStr.substring(0, Math.min(200, sqlStr.length())) + "..."); // Count rows that would be migrated String countSql = "SELECT COUNT(*) FROM G3HY2025.dbo.[" + tableName + "]"; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(countSql)) { rs.next(); return rs.getInt(1); } } else { int count; try (Statement st = conn.createStatement()) { count = st.executeUpdate(sqlStr); } return count; } } // ====================================================================== // 阶段 4:字段元数据同步 // ====================================================================== void phaseMetadataSync() throws SQLException { System.out.println("\n" + "=".repeat(60)); System.out.println("阶段 4:字段元数据同步(col_Caption/col_Visible/col_edit)"); System.out.println("=".repeat(60)); if (skipMetadata) { System.out.println("已跳过(--skip-metadata)"); return; } List mappings = tableMappings.values().stream() .filter(m -> m.hasModule) .distinct() .sorted(Comparator.comparing(m -> m.viewTable)) .toList(); if (mappings.isEmpty()) { System.out.println("没有可同步的表。"); return; } conn.setAutoCommit(false); try { // 初始化 ID 生成器 long listNextId = getNextId("s_module_field_list"); long editNextId = getNextId("s_module_field_edit"); for (TableMapping mapping : mappings) { String viewTable = mapping.viewTable; System.out.println("\n--- 同步 " + viewTable + " (" + mapping.moduleCode + ") ---"); // 1. 字段名称 int nameUpdated = syncFieldNames(conn, viewTable); System.out.println(" 字段名称: " + nameUpdated + " 条更新"); // 2. 列表显示 int listUpdated = syncListVisibility(conn, viewTable); int listInserted = insertMissingListRecords(conn, viewTable, listNextId); listNextId += listInserted; System.out.println(" 列表显示: " + listUpdated + " 条更新, " + listInserted + " 条新增"); // 3. 编辑显示 int editUpdated = syncEditVisibility(conn, viewTable); int editInserted = insertMissingEditRecords(conn, viewTable, editNextId); editNextId += editInserted; System.out.println(" 编辑显示: " + editUpdated + " 条更新, " + editInserted + " 条新增"); } if (!dryRun) { conn.commit(); System.out.println("\n元数据同步已提交。"); } else { System.out.println("\n[Dry-run] 元数据同步已模拟完成,未实际提交。"); conn.rollback(); } } catch (Exception e) { conn.rollback(); throw e; } } // ---- FieldNameSync 复用的 SQL ---- static final String SYNC_NAME = """ UPDATE sf SET sf.b_name = LTRIM(RTRIM(sc.col_Caption)) FROM dbo.s_module_field sf JOIN dbo.s_module m ON m.b_id = sf.b_module_id JOIN G3HY2025.dbo.s_columnLib sc ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field WHERE m.b_viewtable = ? AND sc.col_Caption IS NOT NULL AND LTRIM(RTRIM(sc.col_Caption)) != '' """; static final String SYNC_LIST_UPDATE = """ UPDATE sfl SET sfl.b_visible = CASE WHEN sc.col_Visible = '1' THEN 1 ELSE 0 END FROM dbo.s_module_field_list sfl JOIN dbo.s_module_field sf ON sf.b_id = sfl.b_field_id JOIN dbo.s_module m ON m.b_id = sf.b_module_id JOIN G3HY2025.dbo.s_columnLib sc ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field WHERE m.b_viewtable = ? AND sc.col_Visible IS NOT NULL AND LTRIM(RTRIM(sc.col_Visible)) != '' """; static final String LIST_INSERT_CANDIDATES = """ SELECT sf.b_id AS field_id, m.b_id AS module_id, CASE WHEN sc.col_Visible = '1' THEN 1 ELSE 0 END AS visible FROM dbo.s_module_field sf JOIN dbo.s_module m ON m.b_id = sf.b_module_id JOIN G3HY2025.dbo.s_columnLib sc ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field WHERE m.b_viewtable = ? AND sc.col_Visible IS NOT NULL AND LTRIM(RTRIM(sc.col_Visible)) != '' AND NOT EXISTS ( SELECT 1 FROM dbo.s_module_field_list sfl WHERE sfl.b_field_id = sf.b_id ) """; static final String INSERT_LIST = """ INSERT INTO dbo.s_module_field_list (b_id, b_module_id, b_field_id, b_visible, b_xh) VALUES (?, ?, ?, ?, 0) """; static final String SYNC_EDIT_UPDATE = """ UPDATE sfe SET sfe.b_visible = CASE WHEN sc.col_edit = '1' THEN 1 ELSE 0 END FROM dbo.s_module_field_edit sfe JOIN dbo.s_module_field sf ON sf.b_id = sfe.b_field_id JOIN dbo.s_module m ON m.b_id = sf.b_module_id JOIN G3HY2025.dbo.s_columnLib sc ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field WHERE m.b_viewtable = ? AND sc.col_edit IS NOT NULL AND LTRIM(RTRIM(sc.col_edit)) != '' """; static final String EDIT_INSERT_CANDIDATES = """ SELECT sf.b_id AS field_id, m.b_id AS module_id, CASE WHEN sc.col_edit = '1' THEN 1 ELSE 0 END AS visible FROM dbo.s_module_field sf JOIN dbo.s_module m ON m.b_id = sf.b_module_id JOIN G3HY2025.dbo.s_columnLib sc ON sc.col_TableName COLLATE Chinese_PRC_CI_AS = m.b_viewtable AND sc.col_FieldName COLLATE Chinese_PRC_CI_AS = sf.b_field WHERE m.b_viewtable = ? AND sc.col_edit IS NOT NULL AND LTRIM(RTRIM(sc.col_edit)) != '' AND NOT EXISTS ( SELECT 1 FROM dbo.s_module_field_edit sfe WHERE sfe.b_field_id = sf.b_id ) """; static final String INSERT_EDIT = """ INSERT INTO dbo.s_module_field_edit (b_id, b_module_id, b_field_id, b_visible, b_required, b_readonly, b_disabled, b_xh) VALUES (?, ?, ?, ?, 0, 0, 0, 0) """; int syncFieldNames(Connection conn, String viewTable) throws SQLException { try (PreparedStatement ps = conn.prepareStatement(SYNC_NAME)) { ps.setString(1, viewTable); return ps.executeUpdate(); } } int syncListVisibility(Connection conn, String viewTable) throws SQLException { try (PreparedStatement ps = conn.prepareStatement(SYNC_LIST_UPDATE)) { ps.setString(1, viewTable); return ps.executeUpdate(); } } int insertMissingListRecords(Connection conn, String viewTable, long startId) throws SQLException { List candidates = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(LIST_INSERT_CANDIDATES)) { ps.setString(1, viewTable); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { candidates.add(new long[]{rs.getLong("field_id"), rs.getLong("module_id"), rs.getLong("visible")}); } } } if (!candidates.isEmpty()) { try (PreparedStatement ps = conn.prepareStatement(INSERT_LIST)) { for (long[] row : candidates) { ps.setLong(1, startId++); ps.setLong(2, row[1]); ps.setLong(3, row[0]); ps.setInt(4, (int) row[2]); ps.addBatch(); } ps.executeBatch(); } } return candidates.size(); } int syncEditVisibility(Connection conn, String viewTable) throws SQLException { try (PreparedStatement ps = conn.prepareStatement(SYNC_EDIT_UPDATE)) { ps.setString(1, viewTable); return ps.executeUpdate(); } } int insertMissingEditRecords(Connection conn, String viewTable, long startId) throws SQLException { List candidates = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(EDIT_INSERT_CANDIDATES)) { ps.setString(1, viewTable); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { candidates.add(new long[]{rs.getLong("field_id"), rs.getLong("module_id"), rs.getLong("visible")}); } } } if (!candidates.isEmpty()) { try (PreparedStatement ps = conn.prepareStatement(INSERT_EDIT)) { for (long[] row : candidates) { ps.setLong(1, startId++); ps.setLong(2, row[1]); ps.setLong(3, row[0]); ps.setInt(4, (int) row[2]); ps.addBatch(); } ps.executeBatch(); } } return candidates.size(); } // ====================================================================== // 辅助方法 // ====================================================================== List groupsWithModules() { Set groups = new LinkedHashSet<>(); for (OldBmflEntry e : oldEntries) { if (tableMappings.containsKey(e.tableName)) { groups.add(e.groupBy); } } return new ArrayList<>(groups); } boolean tableExistsInNew(String tableName) throws SQLException { try (PreparedStatement ps = conn.prepareStatement(TABLE_EXISTS_SQL)) { ps.setString(1, tableName); try (ResultSet rs = ps.executeQuery()) { return rs.next() && rs.getInt(1) > 0; } } } TableMapping findModuleByViewTable(String tableName) throws SQLException { String sql = """ SELECT m.b_id, m.b_code, m.b_name, m.b_viewtable FROM dbo.s_module m WHERE m.b_viewtable = ? AND m.b_module_type = 'data' AND m.b_canuse = 1 """; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, tableName); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { TableMapping tm = new TableMapping(); tm.moduleId = rs.getLong("b_id"); tm.moduleCode = rs.getString("b_code"); tm.moduleName = rs.getString("b_name"); tm.viewTable = rs.getString("b_viewtable"); tm.hasModule = true; return tm; } } } // 也尝试匹配视图(v_ 前缀) String viewTable = "v_" + tableName; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, viewTable); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { TableMapping tm = new TableMapping(); tm.moduleId = rs.getLong("b_id"); tm.moduleCode = rs.getString("b_code"); tm.moduleName = rs.getString("b_name"); tm.viewTable = rs.getString("b_viewtable"); tm.hasModule = true; return tm; } } } return null; } List getNewColumnNames(String tableName) throws SQLException { List columns = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(NEW_TABLE_COLUMNS)) { ps.setString(1, tableName); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { columns.add(rs.getString("COLUMN_NAME")); } } } return columns; } Map getOldColumnTypes(String tableName) throws SQLException { Map types = new LinkedHashMap<>(); try (PreparedStatement ps = conn.prepareStatement(OLD_TABLE_COLUMNS)) { ps.setString(1, tableName); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { types.put(rs.getString("COLUMN_NAME"), rs.getString("DATA_TYPE")); } } } return types; } boolean ensureColumn(String tableName, String columnName, String columnDef) throws SQLException { String checkSql = """ SELECT COUNT(*) FROM fms.INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = ? AND COLUMN_NAME = ? """; try (PreparedStatement ps = conn.prepareStatement(checkSql)) { ps.setString(1, tableName); ps.setString(2, columnName); try (ResultSet rs = ps.executeQuery()) { if (rs.next() && rs.getInt(1) > 0) { System.out.println(" b_other_bmfl 已有 " + columnName + " 列。"); return true; } } } // 列不存在 if (dryRun) { System.out.println(" [Dry-run] 缺少列 " + columnName + "(需要 " + columnDef + "),将跳过树迁移。"); System.out.println(" 请先运行: ALTER TABLE dbo." + tableName + " ADD " + columnName + " " + columnDef); return false; } String alterSql = "ALTER TABLE dbo.[" + tableName + "] ADD " + columnName + " " + columnDef; try (Statement st = conn.createStatement()) { st.executeUpdate(alterSql); System.out.println(" 自动添加列 " + columnName + " " + columnDef + " 到 " + tableName + "。"); return true; } catch (SQLException e) { System.err.println(" 添加列 " + columnName + " 失败: " + e.getMessage()); return false; } } long getNextId(String tableName) throws SQLException { if (nextIdCache.containsKey(tableName)) { long next = nextIdCache.get(tableName); nextIdCache.put(tableName, next + 1); return next; } long maxId = 0; String sql = "SELECT ISNULL(MAX(b_id), 0) FROM dbo.[" + tableName.replace("]", "]]") + "]"; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { if (rs.next()) maxId = rs.getLong(1); } long next = maxId + 1; nextIdCache.put(tableName, next + 1); return next; } static boolean isStringType(String type) { String lower = type.toLowerCase(Locale.ROOT); return lower.equals("varchar") || lower.equals("nvarchar") || lower.equals("char") || lower.equals("nchar") || lower.equals("text") || lower.equals("ntext"); } static String trim(String s) { return s == null ? null : s.trim(); } static String truncate(String s, int maxLen) { if (s == null) return "(null)"; if (s.length() <= maxLen) return s; return s.substring(0, maxLen - 3) + "..."; } }