Files
workspace/code/fms/fms-api/tools/migration/OtherDataMigration.java
T
2026-08-02 22:05:44 +08:00

1023 lines
42 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import java.sql.*;
import java.util.*;
/**
* 其他数据子系统全量迁移工具
*
* 从旧库 G3HY2025 迁移 b_other_bmfl 分类树 + 各基础字典表数据到新库 fms。
* 替代 FieldNameSync.java 的单表单字段同步,一次性完成整个"其他数据"子系统的迁移。
*
* 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<OldBmflEntry> oldEntries = new ArrayList<>();
Map<String, String> groupNameMap = new LinkedHashMap<>(); // groupBy -> 第一个 mc_b 作为分类名
// ---- 阶段 2 数据 ----
static class TableMapping {
long moduleId;
String moduleCode;
String viewTable;
String moduleName;
boolean hasModule;
}
Map<String, Long> categoryIds = new LinkedHashMap<>(); // groupBy -> 新 category b_id
Map<String, Long> moduleNodeIds = new LinkedHashMap<>(); // moduleCode -> 新 module node b_id
Map<String, TableMapping> tableMappings = new LinkedHashMap<>(); // tableName -> mapping info
// ---- ID 缓存(避免反复查询 MAX)----
Map<String, Long> 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<String> 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<String, String> 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<String> 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<String, TableMapping> e : tableMappings.entrySet()) {
System.out.println(" " + e.getKey() + " → " + e.getValue().moduleCode + " (" + e.getValue().moduleName + ")");
}
}
Set<String> 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<String> 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<String> 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<String> 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<String> 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<String> existingTables = new LinkedHashSet<>();
Set<String> missingTables = new LinkedHashSet<>();
Set<String> 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<String> newColumns = getNewColumnNames(tableName);
Map<String, String> oldColumnTypes = getOldColumnTypes(tableName);
if (newColumns.isEmpty()) {
System.out.println(" 新表无列,跳过。");
continue;
}
// 找出新旧共有的列(仅用于显示)
List<String> 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<String> newColumns, Map<String, String> 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<String> insertCols = new ArrayList<>();
List<String> 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<String> 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<TableMapping> 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<long[]> 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<long[]> 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<String> groupsWithModules() {
Set<String> 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<String> getNewColumnNames(String tableName) throws SQLException {
List<String> 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<String, String> getOldColumnTypes(String tableName) throws SQLException {
Map<String, String> 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) + "...";
}
}