Files
workspace/code/fms/fms-api/tools/migration/FieldNameSync.java
T
2026-09-27 21:59:13 +08:00

577 lines
23 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.*;
/**
* 字段配置迁移工具(面向 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') 的表单字段(行节点内的 field)
*
* 关联规则:
* 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:
* view :{"schemaVersion":1,"columns":[{"field":..,"width":..}]}
* edit :{"schemaVersion":2,"children":[{"type":"row","children":[{"field":..,"span":24}]}]}
* —— 显式行节点,每个字段生成一行;行内 sum(span) <= 24,空行不写出。
* —— v1 的平铺 + newline 写法只在加载端容错,本工具不再生成。
*
* 用法:
* 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 <moduleId> [oldTableName] [选项]
*
* <moduleId> 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<Field> fields = new ArrayList<>();
final Map<String, OldColumn> 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<String, String> 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<String> 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<String> 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 <moduleId> [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<Field> includedFields(String schemaType) {
boolean view = "view".equals(schemaType);
List<Field> 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<Field> list) {
Field bz = null;
for (Iterator<Field> 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<Field> 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":2,"children":[{"type":"row","children":[{"field":..,"span":24}]}]},
* 只含旧库 col_edit='1' 的字段;备注列固定排最后。
* 每个字段独占一行(schemaVersion 2 的显式行节点),行内 sum(span) = 24。
*/
String buildEditJson() {
StringBuilder sb = new StringBuilder("{\"schemaVersion\":2,\"children\":[");
List<Field> 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("{\"type\":\"row\",\"children\":[{\"field\":\"").append(escape(f.name)).append('"');
sb.append(",\"span\":24}]}");
}
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();
}
}