1026 lines
42 KiB
Java
1026 lines
42 KiB
Java
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<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) + "...";
|
||
}
|
||
}
|