import java.sql.*; import java.util.*; /** * 字段配置迁移工具(面向 V2 表结构)。 * * 从旧库 G3HY2025.dbo.s_columnLib 同步到新库 FMS: * 1. col_Caption → s_field.b_name (字段名称) * 2. col_Visible → s_module_schema(b_schema_type='view') 的 columns (列表列) * 3. col_edit → s_module_schema(b_schema_type='edit') 的 children (表单字段) * * 关联规则: * s_columnLib.col_TableName = s_module.b_view_table * s_columnLib.col_FieldName = s_field.b_field * 两侧用 Chinese_PRC_CI_AS 比较,兼容 v_b_OtherCompany 这类大小写差异。 * 重命名字段走别名映射(b_vessel_name → b_name、b_name_c → b_name,见 FIELD_ALIAS)。 * * 列表/表单的成员资格沿用旧库语义:旧库标志为 '1' 的字段才进入对应配置, * 其余字段(标志为 '0'、空、无记录)**直接不写入 JSON**,不写 visible:false。 * view 的列宽取 col_Width、顺序取 col_Position;edit 的顺序取 col_edit_position。 * * 写入方式为「整份重建」:view / edit 两份 b_schema_json 完整重新生成, * 不保留模块管理界面里手工调整的布局,已有非空行需 --force 才覆盖。 * * JSON 结构与消费侧一致,见 fms-vue/src/components/fms-module-common/schemaRender.js。 * * 用法: * javac -encoding UTF-8 -cp "mssql-jdbc-13.4.0.jre11.jar" FieldNameSync.java * java -Dstdout.encoding=UTF-8 -cp ".;mssql-jdbc-13.4.0.jre11.jar" FieldNameSync [oldTableName] [选项] * * s_module.b_id,例如 v_b_othercompany * [oldTableName] 可选,覆盖旧库表/视图名;缺省用 s_module.b_view_table * --dry-run 只报告不写库 * --skip-schema 只同步 s_field.b_name,不重建 s_module_schema * --force 允许覆盖已存在且非空的 s_module_schema 行 * --all-otherdata 批量模式:同步 base_otherdata 下所有数据模块(不需要位置参数,失败跳过继续) */ public class FieldNameSync { 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 skipSchema = false; boolean force = false; boolean allOtherData = false; boolean batchMode = false; // 批量子任务:出错抛异常交给批量汇总,而不是 System.exit String moduleId; String oldTableOverride; // ---- 运行期数据 ---- Connection conn; String oldTable; String moduleName; String moduleType; final List fields = new ArrayList<>(); final Map oldColumns = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); /** s_field 行 */ static class Field { String name; String label; } /** s_columnLib 行(只取本工具用得到的列) */ static class OldColumn { String caption; String visible; String edit; Integer width; // col_Width,未配置时为 null Integer position; // col_Position Integer editPosition; // col_edit_position } /** * 旧库字段名 → 新系统字段名。 * 表设计里做过重命名的字段:b_vessel_name → b_name(船名)、b_name_c → b_name(中文名)。 */ private static final Map FIELD_ALIAS = Map.of( "b_vessel_name", "b_name", "b_name_c", "b_name"); // ------------------------------------------------------------------ SQL static final String QUERY_MODULE = """ SELECT b_id, b_view_table, b_name, b_module_type FROM dbo.s_module WHERE b_id = ? """; static final String QUERY_FIELDS = """ SELECT b_field, b_name FROM dbo.s_field WHERE b_module_id = ? AND b_canuse = 1 ORDER BY b_xh, b_field """; static final String QUERY_OLD_COLUMNS = """ SELECT col_FieldName, col_Caption, col_Visible, col_edit, col_Width, col_Position, col_edit_position FROM G3HY2025.dbo.s_columnLib WHERE col_TableName COLLATE Chinese_PRC_CI_AS = CONVERT(varchar(128), ?) COLLATE Chinese_PRC_CI_AS ORDER BY col_FieldName """; static final String UPDATE_FIELD_NAME = """ UPDATE dbo.s_field SET b_name = ? WHERE b_module_id = ? AND b_field = ? """; static final String QUERY_SCHEMA_ROW = """ SELECT b_schema_json FROM dbo.s_module_schema WHERE b_module_id = ? AND b_schema_type = ? """; static final String UPDATE_SCHEMA = """ UPDATE dbo.s_module_schema SET b_schema_json = ?, b_canuse = 1, b_updated_at = SYSDATETIME() WHERE b_module_id = ? AND b_schema_type = ? """; static final String INSERT_SCHEMA = """ INSERT INTO dbo.s_module_schema (b_module_id, b_schema_type, b_schema_json, b_canuse, b_updated_at) VALUES (?, ?, ?, 1, SYSDATETIME()) """; // --------------------------------------------------------------- 入口 public static void main(String[] args) throws Exception { FieldNameSync tool = new FieldNameSync(); tool.parseArgs(args); if (tool.allOtherData) { tool.runBatch(); } else { tool.run(); } } /** 批量:同步 base_otherdata 下所有数据模块;每个模块独立事务,失败跳过并继续 */ void runBatch() throws SQLException { List ids = new ArrayList<>(); try (Connection c = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps = c.prepareStatement( "SELECT b_id FROM dbo.s_module WHERE b_path LIKE '/base/base_otherdata/%' " + "AND b_module_type = 'data' ORDER BY b_xh, b_id"); ResultSet rs = ps.executeQuery()) { while (rs.next()) { ids.add(rs.getString(1)); } } System.out.println("批量同步 " + ids.size() + " 个模块(base_otherdata)" + (dryRun ? "[dry-run]" : "") + "\n"); int ok = 0; int failed = 0; for (String id : ids) { FieldNameSync one = new FieldNameSync(); one.dryRun = dryRun; one.skipSchema = skipSchema; one.force = force; one.batchMode = true; one.moduleId = id; try { one.run(); ok++; } catch (Exception e) { failed++; System.err.println("[失败] " + id + ": " + e.getMessage()); } } System.out.println("\n批量完成:成功 " + ok + " 个,失败 " + failed + " 个"); } void parseArgs(String[] args) { List positional = new ArrayList<>(); for (String raw : args) { String arg = raw.trim(); switch (arg.toLowerCase(Locale.ROOT)) { case "--dry-run" -> dryRun = true; case "--skip-schema" -> skipSchema = true; case "--force" -> force = true; case "--all-otherdata" -> allOtherData = true; default -> { if (arg.startsWith("--")) { System.err.println("未知选项: " + arg); usage(); System.exit(1); } positional.add(arg); } } } if (positional.isEmpty()) { if (!allOtherData) { usage(); System.exit(1); } return; } moduleId = positional.get(0); if (positional.size() > 1) oldTableOverride = positional.get(1); if (positional.size() > 2) { System.err.println("多余参数: " + positional.get(2)); usage(); System.exit(1); } } static void usage() { System.err.println("用法: java FieldNameSync [oldTableName] [--dry-run] [--skip-schema] [--force]"); System.err.println(" java FieldNameSync --all-otherdata [--dry-run] [--force] 批量同步 base_otherdata 下所有数据模块"); } void run() throws SQLException { try (Connection c = DriverManager.getConnection(URL, USER, PASSWORD)) { conn = c; conn.setAutoCommit(false); try { loadModule(); loadFields(); loadOldColumns(); if (fields.isEmpty()) { System.out.println("\n模块 " + moduleId + " 在 s_field 中没有启用字段,无可同步内容。"); conn.rollback(); return; } printSnapshot("同步前"); if (!skipSchema) { // 写入前先校验:已有非空行且未加 --force 时整体拒绝,避免半途写入 checkSchemaGuard(); } int renamed = syncFieldNames(); int[] viewStat = null; int[] editStat = null; if (!skipSchema) { viewStat = writeSchema("view", buildViewJson()); editStat = writeSchema("edit", buildEditJson()); } if (dryRun) { conn.rollback(); System.out.println("\n[Dry-run] 已回滚,未写入任何数据。"); } else { conn.commit(); System.out.println("\n=== 事务已提交 ==="); } printSummary(renamed, viewStat, editStat); if (!dryRun) { // 重新读一次 s_field,同步后的快照才反映实际写入的名称 fields.clear(); loadFields(); printSnapshot("同步后"); } } catch (Exception e) { conn.rollback(); if (batchMode) { throw new SQLException(e.getMessage(), e); } System.err.println("\n[错误] " + e.getMessage()); System.exit(1); } } } // ------------------------------------------------------------ 数据加载 void loadModule() throws SQLException { String bViewTable; try (PreparedStatement ps = conn.prepareStatement(QUERY_MODULE)) { ps.setString(1, moduleId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { throw new IllegalStateException("模块不存在: " + moduleId + "(s_module.b_id)"); } bViewTable = trim(rs.getString("b_view_table")); moduleName = trim(rs.getString("b_name")); moduleType = trim(rs.getString("b_module_type")); } } System.out.println("模块: " + moduleId + (moduleName != null ? "(" + moduleName + ")" : "") + " 类型: " + (moduleType != null ? moduleType : "(null)")); if (oldTableOverride != null) { oldTable = oldTableOverride; System.out.println("关联旧库表: " + oldTable + "(命令行指定,覆盖 b_view_table=" + bViewTable + ")"); } else { oldTable = bViewTable; if (oldTable == null || oldTable.isEmpty()) { throw new IllegalStateException( "模块 " + moduleId + " 未配置 b_view_table,请用第二个参数显式指定旧库表/视图名"); } System.out.println("关联旧库表: " + oldTable + "(取自 s_module.b_view_table)"); } } void loadFields() throws SQLException { try (PreparedStatement ps = conn.prepareStatement(QUERY_FIELDS)) { ps.setString(1, moduleId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Field f = new Field(); f.name = trim(rs.getString("b_field")); f.label = trim(rs.getString("b_name")); fields.add(f); } } } } void loadOldColumns() throws SQLException { int rows = 0; try (PreparedStatement ps = conn.prepareStatement(QUERY_OLD_COLUMNS)) { ps.setString(1, oldTable); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { rows++; OldColumn col = new OldColumn(); col.caption = trim(rs.getString("col_Caption")); col.visible = trim(rs.getString("col_Visible")); col.edit = trim(rs.getString("col_edit")); col.width = intOrNull(rs, "col_Width"); col.position = intOrNull(rs, "col_Position"); col.editPosition = intOrNull(rs, "col_edit_position"); String fieldName = trim(rs.getString("col_FieldName")); oldColumns.put(FIELD_ALIAS.getOrDefault(fieldName.toLowerCase(Locale.ROOT), fieldName), col); } } } long inList = countIncluded("view"); long inForm = countIncluded("edit"); System.out.println("旧库 s_columnLib: " + rows + " 行,去重字段 " + oldColumns.size() + " 个;" + "当前模块 " + fields.size() + " 个字段中:进列表 " + inList + " 个、进表单 " + inForm + " 个"); if (inList == 0 && inForm == 0) { System.out.println(" [警告] 没有任何字段匹配上,请确认旧库表名是否正确(可用第二个参数显式指定)"); } } static Integer intOrNull(ResultSet rs, String column) throws SQLException { int v = rs.getInt(column); return rs.wasNull() ? null : v; } // ------------------------------------------------------------ 成员与顺序 /** 旧库标志为 '1' 才进入对应配置 */ static boolean flagOn(String flag) { return "1".equals(flag); } /** 取模块字段在旧库里的对应行;旧库没有该字段时返回 null */ OldColumn oldOf(Field f) { return oldColumns.get(f.name); } /** 按 schemaType 取出旧库要求进入配置的字段,顺序取旧库位置列 */ List includedFields(String schemaType) { boolean view = "view".equals(schemaType); List result = new ArrayList<>(); for (Field f : fields) { OldColumn old = oldOf(f); if (old == null) continue; if (flagOn(view ? old.visible : old.edit)) result.add(f); } result.sort(Comparator .comparingInt((Field f) -> positionOf(oldOf(f), view)) .thenComparing(f -> f.name)); return result; } static int positionOf(OldColumn old, boolean view) { Integer p = view ? old.position : old.editPosition; // 位置缺失的排在最后 return p == null ? Integer.MAX_VALUE : p; } long countIncluded(String schemaType) { return includedFields(schemaType).size(); } // ---------------------------------------------------------------- 步骤 int syncFieldNames() throws SQLException { System.out.println("\n=== 1) 字段名称 (col_Caption → s_field.b_name) ==="); int updated = 0; int missing = 0; try (PreparedStatement ps = conn.prepareStatement(UPDATE_FIELD_NAME)) { for (Field f : fields) { OldColumn old = oldOf(f); String caption = old != null ? old.caption : null; if (caption == null || caption.isEmpty()) { missing++; continue; } if (caption.equals(f.label)) { continue; } ps.setString(1, caption); ps.setString(2, moduleId); ps.setString(3, f.name); updated += ps.executeUpdate(); } } System.out.println(" 更新: " + updated + " 条;旧库无名称、保留原值: " + missing + " 条"); return updated; } /** 备注列统一排到最后(其他数据字典表的展示习惯) */ private static void moveBzLast(List list) { Field bz = null; for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { Field field = iterator.next(); if ("b_bz".equalsIgnoreCase(field.name)) { bz = field; iterator.remove(); break; } } if (bz != null) { list.add(bz); } } /** view 配置:{"schemaVersion":1,"columns":[{"field":..,"width":..}]},只含旧库 col_Visible='1' 的字段;备注列固定排最后 */ String buildViewJson() { StringBuilder sb = new StringBuilder("{\"schemaVersion\":1,\"columns\":["); List list = new ArrayList<>(includedFields("view")); moveBzLast(list); for (int i = 0; i < list.size(); i++) { Field f = list.get(i); if (i > 0) sb.append(','); sb.append("{\"field\":\"").append(escape(f.name)).append('"'); Integer width = oldOf(f).width; if (width != null && width > 0) sb.append(",\"width\":").append(width); sb.append('}'); } return sb.append("]}").toString(); } /** edit 配置:{"schemaVersion":1,"children":[{"field":..,"span":24}]},只含旧库 col_edit='1' 的字段;备注列固定排最后 */ String buildEditJson() { StringBuilder sb = new StringBuilder("{\"schemaVersion\":1,\"children\":["); List list = new ArrayList<>(includedFields("edit")); moveBzLast(list); for (int i = 0; i < list.size(); i++) { Field f = list.get(i); if (i > 0) sb.append(','); sb.append("{\"field\":\"").append(escape(f.name)).append('"'); sb.append(",\"span\":24"); sb.append('}'); } return sb.append("]}").toString(); } /** 字段名是 [A-Za-z0-9_],仍保留最小转义以防意外字符 */ static String escape(String s) { return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\""); } void checkSchemaGuard() throws SQLException { for (String type : List.of("view", "edit")) { String existing = readSchemaRow(type); if (existing != null && !existing.isEmpty() && !force) { throw new IllegalStateException( "s_module_schema 已有非空 " + type + " 配置(" + existing.length() + " 字符),整份重建会覆盖模块管理中手工调整的布局。确认后加 --force 重跑。"); } } } String readSchemaRow(String schemaType) throws SQLException { try (PreparedStatement ps = conn.prepareStatement(QUERY_SCHEMA_ROW)) { ps.setString(1, moduleId); ps.setString(2, schemaType); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) return rs.getString(1); } } return null; } /** @return {节点数, 未进入配置的字段数, 写入方式(1=更新, 2=新增)} */ int[] writeSchema(String schemaType, String json) throws SQLException { boolean exists = readSchemaRow(schemaType) != null; if (exists) { try (PreparedStatement ps = conn.prepareStatement(UPDATE_SCHEMA)) { ps.setString(1, json); ps.setString(2, moduleId); ps.setString(3, schemaType); ps.executeUpdate(); } } else { try (PreparedStatement ps = conn.prepareStatement(INSERT_SCHEMA)) { ps.setString(1, moduleId); ps.setString(2, schemaType); ps.setString(3, json); ps.executeUpdate(); } } int included = includedFields(schemaType).size(); return new int[]{included, fields.size() - included, exists ? 1 : 2}; } // -------------------------------------------------------------- 输出 void printSummary(int renamed, int[] viewStat, int[] editStat) { System.out.println("\n=== 摘要 ==="); System.out.println(" s_field.b_name 更新: " + renamed + " 条"); if (skipSchema) { System.out.println(" s_module_schema: 已跳过(--skip-schema)"); return; } printStat("view", viewStat); printStat("edit", editStat); if (readSchemaRowSafe("query") == null) { System.out.println(" 提示: 该模块尚无 query 配置行,工具不处理 query,需要时在模块管理中生成。"); } } static void printStat(String schemaType, int[] stat) { if (stat == null) return; System.out.println(" s_module_schema(" + schemaType + "): " + stat[0] + " 个节点,未进入配置 " + stat[1] + " 个字段," + (stat[2] == 1 ? "更新已有行" : "新增行")); } String readSchemaRowSafe(String schemaType) { try { return readSchemaRow(schemaType); } catch (SQLException e) { return null; } } void printSnapshot(String title) throws SQLException { System.out.println("\n=== " + title + " ==="); System.out.printf(" %-24s %-22s %-22s %-10s %-10s%n", "b_field", "s_field.b_name", "col_Caption", "进列表", "进表单"); System.out.println(" " + "-".repeat(94)); for (Field f : fields) { OldColumn old = oldOf(f); System.out.printf(" %-24s %-22s %-22s %-10s %-10s%n", truncate(f.name, 23), truncate(orNull(f.label), 21), truncate(orNull(old != null ? old.caption : null), 21), yesNo(old != null && flagOn(old.visible)), yesNo(old != null && flagOn(old.edit))); } } static String yesNo(boolean b) { return b ? "是" : "否"; } static String orNull(String s) { return s == null ? "(null)" : s; } static String truncate(String s, int max) { if (s == null) return ""; return s.length() <= max ? s : s.substring(0, max - 1) + "…"; } static String trim(String s) { return s == null ? null : s.trim(); } }