From 549c8bd2ebdadbf2f83a506208612c1183b93883 Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Mon, 14 Sep 2026 17:28:20 +0800 Subject: [PATCH] 20260914172820 --- code/fms/FMS业务表设计.md | 154 ++++ .../tools/migration/FieldNameSync.java | 673 ++++++++++++------ code/fms/fms-api/tools/migration/README.md | 190 +++-- .../tools/migration/SchemaDumpMarkdown.java | 134 ++-- .../src/components/fms-field/FmsField.vue | 5 +- .../components/fms-file/FileCategoryPanel.vue | 12 +- .../src/components/fms-file/FmsFile.vue | 13 +- .../fms-file/composables/useFileCategory.js | 33 +- .../fms-module-list/FmsModuleListPage.vue | 2 +- .../fms-module-list/FmsQueryControl.vue | 46 +- .../fms-module-list/FmsQueryToolbar.vue | 2 +- .../src/components/fms-tree/FmsTree.vue | 146 ++++ .../fms-vue/src/components/ui/demo/select.vue | 17 +- .../src/components/ui/select/index.scss | 5 +- .../src/components/ui/select/select.vue | 56 +- .../fms/fms-vue/src/layouts/DefaultLayout.vue | 5 +- .../src/layouts/components/AppTopbar.vue | 1 - code/fms/fms-vue/src/router/routes.js | 23 + code/fms/fms-vue/src/theme/tokens.css | 15 +- .../fms-vue/src/views/base/contact/index.vue | 18 + .../base/othercompany/CategoryEditModal.vue | 147 ++++ .../base/othercompany/CategoryTreePanel.vue | 247 +++++++ .../src/views/base/othercompany/detail.vue | 21 + .../src/views/base/othercompany/index.vue | 529 ++++++++++++++ .../src/views/base/othercompany/utils.js | 131 ++++ .../src/views/base/otherdata/index.vue | 18 + .../module/menu-management/MenuBasicPanel.vue | 16 +- .../module/menu-management/MenuTreePanel.vue | 12 +- .../views/module/menu-management/index.vue | 99 ++- .../src/views/module/menu-management/utils.js | 92 ++- .../module-management/ModuleTreePanel.vue | 12 +- .../fms-vue/tests/components/fms-tree.spec.js | 219 ++++++ code/fms/fms-vue/tests/unit/fms-field.spec.js | 27 + .../tests/unit/othercompany-utils.spec.js | 164 +++++ .../tests/views/menu-save-tree-path.spec.js | 137 ++++ .../views/menu-unsaved-node-switch.spec.js | 20 +- .../fms-vue/tests/views/othercompany.spec.js | 256 +++++++ code/fms/sql/fms_business_othercompany.sql | 235 ++++++ 38 files changed, 3430 insertions(+), 502 deletions(-) create mode 100644 code/fms/FMS业务表设计.md create mode 100644 code/fms/fms-vue/src/components/fms-tree/FmsTree.vue create mode 100644 code/fms/fms-vue/src/views/base/contact/index.vue create mode 100644 code/fms/fms-vue/src/views/base/othercompany/CategoryEditModal.vue create mode 100644 code/fms/fms-vue/src/views/base/othercompany/CategoryTreePanel.vue create mode 100644 code/fms/fms-vue/src/views/base/othercompany/detail.vue create mode 100644 code/fms/fms-vue/src/views/base/othercompany/index.vue create mode 100644 code/fms/fms-vue/src/views/base/othercompany/utils.js create mode 100644 code/fms/fms-vue/src/views/base/otherdata/index.vue create mode 100644 code/fms/fms-vue/tests/components/fms-tree.spec.js create mode 100644 code/fms/fms-vue/tests/unit/othercompany-utils.spec.js create mode 100644 code/fms/fms-vue/tests/views/menu-save-tree-path.spec.js create mode 100644 code/fms/fms-vue/tests/views/othercompany.spec.js create mode 100644 code/fms/sql/fms_business_othercompany.sql diff --git a/code/fms/FMS业务表设计.md b/code/fms/FMS业务表设计.md new file mode 100644 index 00000000..70915b1d --- /dev/null +++ b/code/fms/FMS业务表设计.md @@ -0,0 +1,154 @@ +# FMS 业务表设计 + +## 往来单位(合作伙伴) + +```sql +create table dbo.b_othercompany ( + b_id bigint not null primary key, + b_ywid varchar(50) not null, + b_pycode nvarchar(50) null, + b_type varchar(200) null, + b_name nvarchar(500) null, + b_shortname nvarchar(50) null, + b_linkman nvarchar(200) null, + b_phone nvarchar(500) null, + b_addr nvarchar(500) null, + b_firstlinkdate datetime null, + b_htno varchar(50) null, + b_htdate date null, + b_htexpiredate date null, + b_black tinyint not null default 0, + b_canuse tinyint not null default 0, + b_zl int null, + b_tel nvarchar(50) null, + b_fax nvarchar(500) null, + b_www varchar(50) null, + b_email varchar(50) null, + b_bankacount nvarchar(500) null, + b_invaccount nvarchar(500) null, + b_cate_id varchar(150) null, + b_bz1 nvarchar(500) null, + b_bz2 nvarchar(500) null, + b_bz3 nvarchar(500) null, + b_bz4 nvarchar(500) null, + b_province nvarchar(50) null, + b_city nvarchar(50) null, + b_area nvarchar(50) null, + b_country nvarchar(50) null, + b_edicode varchar(50) null, + b_showcolor int null, + b_type_id nvarchar(200) null, + b_from_id nvarchar(50) null, + b_type1_id nvarchar(50) null, + b_type2_id nvarchar(50) null, + b_type3_id nvarchar(50) null, + b_type4_id nvarchar(50) null, + b_type5_id nvarchar(50) null, + b_owner_id varchar(50) null, + b_created_by varchar(50) null, + b_created_at datetime2 null, + b_updated_by varchar(50) null, + b_updated_at datetime2 null, + b_password varchar(50) null, + b_state nvarchar(50) null, + b_bankacount_e varchar(500) null, + b_addr_e varchar(500) null, + b_name_e nvarchar(500) null +); + +create unique index ux_b_othercompany_ywid on dbo.b_othercompany (b_ywid); +create index ix_b_othercompany_cate on dbo.b_othercompany (b_cate_id, b_canuse, b_id); +create index ix_b_othercompany_name on dbo.b_othercompany (b_name, b_canuse, b_id); + +create table dbo.b_othercompany_partner_type ( + b_id varchar(50) not null primary key, + b_name nvarchar(50) null, + b_i18n varchar(150) null, + b_canuse tinyint not null default 1, + b_xh int not null default 0, + b_bz nvarchar(50) null, + b_created_by varchar(50) null, + b_created_at datetime2 null, + b_updated_by varchar(50) null, + b_updated_at datetime2 null +); + +create table dbo.b_othercompany_bmfl ( + b_id varchar(150) not null primary key, + b_parent_id varchar(150) null, + b_depth int not null default 0, + b_path varchar(1000) not null, + b_name nvarchar(200) not null, + b_i18n varchar(150) null, + b_canuse tinyint not null default 1, + b_xh int not null default 0, + b_created_by varchar(50) null, + b_created_at datetime2 null, + b_updated_by varchar(50) null, + b_updated_at datetime2 null +); + +create index ix_b_othercompany_bmfl_parent + on dbo.b_othercompany_bmfl (b_parent_id, b_canuse, b_depth, b_xh, b_id); +create index ix_b_othercompany_bmfl_path + on dbo.b_othercompany_bmfl (b_path, b_canuse, b_id); + +create table dbo.b_grade ( + b_id varchar(50) not null primary key, + b_name nvarchar(50) null, + b_i18n varchar(150) null, + b_canuse tinyint not null default 1, + b_xh int not null default 0, + b_bz nvarchar(50) null, + b_created_by varchar(50) null, + b_created_at datetime2 null, + b_updated_by varchar(50) null, + b_updated_at datetime2 null +); + +create table dbo.b_from ( + b_id varchar(50) not null primary key, + b_name nvarchar(50) null, + b_i18n varchar(150) null, + b_canuse tinyint not null default 1, + b_xh int not null default 0, + b_bz nvarchar(50) null, + b_created_by varchar(50) null, + b_created_at datetime2 null, + b_updated_by varchar(50) null, + b_updated_at datetime2 null +); + +create table dbo.b_othercompany_accountbank ( + subid bigint not null primary key, + mx_xh int null, + father bigint null, + mx_bank nvarchar(800) null, + mx_bz1 nvarchar(200) null, + mx_created_by varchar(50) null, + mx_created_at datetime2 null, + mx_updated_by varchar(50) null, + mx_updated_at datetime2 null +); + +create index ix_b_othercompany_accountbank_father + on dbo.b_othercompany_accountbank (father, mx_xh, subid); + +create or alter view dbo.v_b_othercompany as +select o.b_id, o.b_ywid, o.b_pycode, o.b_type, o.b_name, o.b_shortname, + o.b_linkman, o.b_phone, o.b_addr, o.b_firstlinkdate, + o.b_htno, o.b_htdate, o.b_htexpiredate, o.b_black, o.b_canuse, + o.b_zl, o.b_tel, o.b_fax, o.b_www, o.b_email, + o.b_bankacount, o.b_invaccount, o.b_cate_id, + o.b_bz1, o.b_bz2, o.b_bz3, o.b_bz4, + o.b_province, o.b_city, o.b_area, o.b_country, + o.b_edicode, o.b_showcolor, o.b_type_id, o.b_from_id, + o.b_type1_id, o.b_type2_id, o.b_type3_id, o.b_type4_id, o.b_type5_id, + o.b_owner_id, o.b_created_by, o.b_created_at, + o.b_updated_by, o.b_updated_at, + o.b_password, o.b_state, + o.b_bankacount_e, o.b_addr_e, o.b_name_e, + u.b_name as v_ownername +from dbo.b_othercompany o + left join dbo.b_user u on u.b_id = o.b_owner_id; +``` diff --git a/code/fms/fms-api/tools/migration/FieldNameSync.java b/code/fms/fms-api/tools/migration/FieldNameSync.java index 18a681b6..3adf8a0f 100644 --- a/code/fms/fms-api/tools/migration/FieldNameSync.java +++ b/code/fms/fms-api/tools/migration/FieldNameSync.java @@ -1,15 +1,37 @@ import java.sql.*; +import java.util.*; /** + * 字段配置迁移工具(面向 V2 表结构)。 + * * 从旧库 G3HY2025.dbo.s_columnLib 同步到新库 FMS: - * 1. col_Caption → s_module_field.b_name(字段名称) - * 2. col_Visible → s_module_field_list.b_visible(列表显示,不存在则插入记录) - * 3. col_edit → s_module_field_edit.b_visible(编辑显示,不存在则插入记录) + * 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 (表单字段) * - * 注意:本工具面向 V1 表结构(s_module_field / s_module_field_list / s_module_field_edit), - * 与《FMS新系统核心表结构设计V2》的 s_field / s_module_schema 不兼容,仅作历史参考。 + * 关联规则: + * s_columnLib.col_TableName = s_module.b_view_table + * s_columnLib.col_FieldName = s_field.b_field + * 两侧用 Chinese_PRC_CI_AS 比较,兼容 v_b_OtherCompany 这类大小写差异。 * - * 用法: java -cp ".;mssql-jdbc.jar" FieldNameSync + * 列表/表单的成员资格沿用旧库语义:旧库标志为 '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 行 */ public class FieldNameSync { @@ -17,251 +39,454 @@ public class FieldNameSync { static final String USER = "sa"; static final String PASSWORD = "zhang520.."; - // 同步字段名称 - 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)) != '' + // ---- 选项 ---- + boolean dryRun = false; + boolean skipSchema = false; + boolean force = false; + 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 + } + + // ------------------------------------------------------------------ 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 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 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 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 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 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 UPDATE_FIELD_NAME = """ + UPDATE dbo.s_field SET b_name = ? + WHERE b_module_id = ? AND b_field = ? """; - // 同步编辑显示:更新已有记录 - 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 QUERY_SCHEMA_ROW = """ + SELECT b_schema_json FROM dbo.s_module_schema + WHERE b_module_id = ? AND b_schema_type = ? """; - 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 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_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) + 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()) """; - // 查询模块 ID - static final String QUERY_MODULE_ID = "SELECT b_id FROM dbo.s_module WHERE b_viewtable = ?"; - - // 查询列表/编辑配置现状 - static final String QUERY_CONFIG = """ - SELECT sf.b_field, sf.b_name, - sfl.b_visible AS list_visible, - sfe.b_visible AS edit_visible - FROM dbo.s_module_field sf - JOIN dbo.s_module m ON m.b_id = sf.b_module_id - LEFT JOIN dbo.s_module_field_list sfl ON sfl.b_field_id = sf.b_id - LEFT JOIN dbo.s_module_field_edit sfe ON sfe.b_field_id = sf.b_id - WHERE m.b_viewtable = ? - ORDER BY sf.b_field - """; - - static long nextListId; - static long nextEditId; - + // --------------------------------------------------------------- 入口 public static void main(String[] args) throws Exception { - if (args.length == 0) { - System.err.println("用法: java FieldNameSync "); - System.exit(1); - } - String viewName = args[0].trim(); - if (!viewName.matches("^[A-Za-z_][A-Za-z0-9_]{0,127}$")) { - System.err.println("非法视图名: " + viewName); - System.exit(1); - } + FieldNameSync tool = new FieldNameSync(); + tool.parseArgs(args); + tool.run(); + } - System.out.println("连接到 fms 数据库..."); - try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) { - conn.setAutoCommit(false); - - // 查询模块是否存在 - Long moduleId = null; - try (PreparedStatement ps = conn.prepareStatement(QUERY_MODULE_ID)) { - ps.setString(1, viewName); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) moduleId = rs.getLong(1); + 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; + default -> { + if (arg.startsWith("--")) { + System.err.println("未知选项: " + arg); + usage(); + System.exit(1); + } + positional.add(arg); } } - if (moduleId == null) { - System.err.println("模块 " + viewName + " 不存在!"); + } + if (positional.isEmpty()) { + usage(); + System.exit(1); + } + 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]"); + } + + 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(); + System.err.println("\n[错误] " + e.getMessage()); System.exit(1); } - - // 初始化 ID 生成器 - try (Statement st = conn.createStatement()) { - try (ResultSet rs = st.executeQuery("SELECT ISNULL(MAX(b_id),0) FROM dbo.s_module_field_list")) { - rs.next(); nextListId = rs.getLong(1) + 1; - } - try (ResultSet rs = st.executeQuery("SELECT ISNULL(MAX(b_id),0) FROM dbo.s_module_field_edit")) { - rs.next(); nextEditId = rs.getLong(1) + 1; - } - } - - // === 1. 同步前 === - System.out.println("\n=== 同步前 ==="); - printConfig(conn, viewName); - - // === 2. 字段名称 === - System.out.println("\n=== 1) 字段名称 (col_Caption → b_name) ==="); - try (PreparedStatement ps = conn.prepareStatement(SYNC_NAME)) { - ps.setString(1, viewName); - System.out.println(" 更新: " + ps.executeUpdate() + " 条"); - } - - // === 3. 列表显示 === - System.out.println("\n=== 2) 列表显示 (col_Visible → s_module_field_list.b_visible) ==="); - try (PreparedStatement ps = conn.prepareStatement(SYNC_LIST_UPDATE)) { - ps.setString(1, viewName); - System.out.println(" 更新已有: " + ps.executeUpdate() + " 条"); - } - java.util.ArrayList listCandidates = new java.util.ArrayList<>(); - try (PreparedStatement ps = conn.prepareStatement(LIST_INSERT_CANDIDATES)) { - ps.setString(1, viewName); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - listCandidates.add(new long[]{rs.getLong("field_id"), rs.getLong("module_id"), rs.getLong("visible")}); - } - } - } - try (PreparedStatement ps = conn.prepareStatement(INSERT_LIST)) { - for (long[] row : listCandidates) { - ps.setLong(1, nextListId++); - ps.setLong(2, row[1]); - ps.setLong(3, row[0]); - ps.setInt(4, (int) row[2]); - ps.addBatch(); - } - if (!listCandidates.isEmpty()) { - ps.executeBatch(); - } - } - System.out.println(" 新增: " + listCandidates.size() + " 条"); - - // === 4. 编辑显示 === - System.out.println("\n=== 3) 编辑显示 (col_edit → s_module_field_edit.b_visible) ==="); - try (PreparedStatement ps = conn.prepareStatement(SYNC_EDIT_UPDATE)) { - ps.setString(1, viewName); - System.out.println(" 更新已有: " + ps.executeUpdate() + " 条"); - } - java.util.ArrayList editCandidates = new java.util.ArrayList<>(); - try (PreparedStatement ps = conn.prepareStatement(EDIT_INSERT_CANDIDATES)) { - ps.setString(1, viewName); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - editCandidates.add(new long[]{rs.getLong("field_id"), rs.getLong("module_id"), rs.getLong("visible")}); - } - } - } - try (PreparedStatement ps = conn.prepareStatement(INSERT_EDIT)) { - for (long[] row : editCandidates) { - ps.setLong(1, nextEditId++); - ps.setLong(2, row[1]); - ps.setLong(3, row[0]); - ps.setInt(4, (int) row[2]); - ps.addBatch(); - } - if (!editCandidates.isEmpty()) { - ps.executeBatch(); - } - } - System.out.println(" 新增: " + editCandidates.size() + " 条"); - - conn.commit(); - System.out.println("\n=== 事务已提交 ==="); - - // === 5. 验证 === - System.out.println("\n=== 同步后 ==="); - printConfig(conn, viewName); } - - System.out.println("\n完成."); } - static void printConfig(Connection conn, String viewName) throws SQLException { - System.out.printf(" %-30s %-16s %-8s %-8s%n", "b_field", "b_name", "list", "edit"); - System.out.println(" " + "-".repeat(66)); - try (PreparedStatement ps = conn.prepareStatement(QUERY_CONFIG)) { - ps.setString(1, viewName); + // ------------------------------------------------------------ 数据加载 + 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()) { - String listVis = rs.getString("list_visible"); - String editVis = rs.getString("edit_visible"); - String listStr = listVis == null ? "--" : ("1".equals(listVis) ? "显示" : "隐藏"); - String editStr = editVis == null ? "--" : ("1".equals(editVis) ? "显示" : "隐藏"); - String name = rs.getString("b_name"); - System.out.printf(" %-30s %-16s %-8s %-8s%n", - rs.getString("b_field"), - name != null ? name : "(null)", - listStr, - editStr); + 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"); + oldColumns.put(trim(rs.getString("col_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; + } + + /** view 配置:{"schemaVersion":1,"columns":[{"field":..,"width":..}]},只含旧库 col_Visible='1' 的字段 */ + String buildViewJson() { + StringBuilder sb = new StringBuilder("{\"schemaVersion\":1,\"columns\":["); + List list = includedFields("view"); + 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 = includedFields("edit"); + 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(); + } } diff --git a/code/fms/fms-api/tools/migration/README.md b/code/fms/fms-api/tools/migration/README.md index 28274f92..48a07b58 100644 --- a/code/fms/fms-api/tools/migration/README.md +++ b/code/fms/fms-api/tools/migration/README.md @@ -1,65 +1,85 @@ -# FieldNameSync — 模块字段配置迁移工具 +# 迁移工具 -> **状态:面向 V1 表结构的历史工具。** 本工具操作 `s_module_field` / `s_module_field_list` / -> `s_module_field_edit`,与《FMS新系统核心表结构设计V2》的 `s_field` / `s_module_schema` -> 不兼容,仅作历史参考;V2 的字段迁移需按新结构重写。 +`fms-api/tools/migration/` 下的独立 Java 工具,用于在旧库 `G3HY2025` 与新库 `FMS` 之间 +迁移数据、同步元数据、导出表结构。每个工具都是单文件程序,用 `javac` 手动编译运行,不走 Maven。 -从旧库 `G3HY2025.dbo.s_columnLib` 同步字段配置到新库 `FMS`。 +## 工具清单 -## 同步的三项内容 +| 工具 | 状态 | 用途 | +| --- | --- | --- | +| `FieldNameSync.java` | **V2 当前版本** | 从旧库 `s_columnLib` 同步字段名称与显示配置到 `s_field` / `s_module_schema` | +| `SchemaDumpMarkdown.java` | 通用 | 导出该实例上任意库的表与视图结构为 Markdown | +| `OtherDataMigration.java` | **V1 历史工具,未改造,勿用于 V2** | 面向 V1 表结构(`s_module_field` / `s_module_field_list` / `s_module_field_edit`)与 `b_other_bmfl` 旧树结构,V2 下这些表已不存在,仅作历史参考 | + +--- + +## FieldNameSync — 字段配置同步(V2) + +从旧库 `G3HY2025.dbo.s_columnLib` 同步到新库 `FMS`: | 序号 | 旧库来源 | 新库目标 | 说明 | | --- | --- | --- | --- | -| 1 | `col_Caption` | `s_module_field.b_name` | 字段中文名称 | -| 2 | `col_Visible` | `s_module_field_list.b_visible` | 列表是否显示(1=显示, 0=隐藏) | -| 3 | `col_edit` | `s_module_field_edit.b_visible` | 编辑表单是否显示(1=显示, 0=隐藏) | +| 1 | `col_Caption` | `s_field.b_name` | 字段中文名称 | +| 2 | `col_Visible='1'` 的行 | `s_module_schema`(`b_schema_type='view'`)JSON 的 `columns` | 列表列成员 | +| 3 | `col_edit='1'` 的行 | `s_module_schema`(`b_schema_type='edit'`)JSON 的 `children` | 表单成员 | +| 4 | `col_Width` / `col_Position` | view 节点的 `width` 与顺序 | 列表列宽与排列 | +| 5 | `col_edit_position` | edit 节点的顺序 | 表单字段排列 | -## 匹配规则 - -通过视图名和字段名关联新旧库: +### 关联规则 ``` -旧库 s_columnLib.col_TableName = 新库 s_module.b_viewtable -旧库 s_columnLib.col_FieldName = 新库 s_module_field.b_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` 这类大小写差异。 +`s_module.b_view_table` 对不上时,用第二个命令行参数显式指定旧库表/视图名。 -## 前置条件 +### 字段名称 -### 1. JDK 21 +- 旧库有 `col_Caption` 时写入 `s_field.b_name`;为空或该字段在旧库没有记录时**保留原值**。 +- 只处理 `s_field.b_canuse = 1` 的字段。 -工具使用 Java 文本块语法(`"""`),需要 JDK 21+ 编译。 +### 显示配置:整份重建 -### 2. mssql-jdbc 驱动 +view / edit 两份 `b_schema_json` 由旧库名单**完整重新生成**, +**不保留**模块管理界面里手工调整的布局。已有非空行时必须加 `--force` 才覆盖, +否则工具在写入前整体拒绝退出。 -确保本地 Maven 仓库存在驱动 jar: -``` -%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar +成员资格沿用旧库语义:**旧库标志为 `'1'` 的字段才进入对应配置**, +其余字段(标志为 `'0'`、空值、旧库无记录)**直接不写入 JSON**,不写 `visible:false`。 +`s_field` 里有、旧库名单里没有的字段同样不进入配置(节点不存在 = 不显示、不参与表单)。 + +顺序取旧库位置列:view 按 `col_Position`、edit 按 `col_edit_position`,位置缺失的排在最后。 +列表列宽取 `col_Width`,未配置时省略 `width` 键(前端按自身默认值渲染)。 + +生成的 JSON 结构与前端消费侧一致 +(见 `fms-vue/src/components/fms-module-common/schemaRender.js`): + +```json +view: {"schemaVersion":1,"columns":[{"field":"b_id","width":123},{"field":"b_name","width":122}]} +edit: {"schemaVersion":1,"children":[{"field":"b_id","span":24},{"field":"b_name","span":24}]} ``` -如果不存在,从项目根目录 `fms-api/` 执行 `mvnw dependency:resolve` 下载。 +`col_ShowSummary` / `col_DisplayFormat` / `col_CanSearch` / `col_ReadOnly` / `col_DefaultValue` / +`col_edit_type` 等旧库列在 V2 的 JSON 契约里没有对应位置(`schemaRender.js` 只读 +`field` / `width` / `visible` / `options`),本工具不带迁移。 -### 3. 数据库连接 +`query` 配置行不由本工具处理(旧库没有对应来源)。 -工具硬编码连接 `118.89.70.199:1433`,数据库 `FMS`,直连新旧两库(同一实例)。 +### 前置条件 -### 4. 新库已有 `s_module_field_edit.b_visible` 列 +1. **JDK 21**(源码使用文本块语法)。 -如果报 `Invalid column name 'b_visible'`,执行以下 SQL 添加列: -```sql -ALTER TABLE dbo.s_module_field_edit ADD b_visible int NOT NULL DEFAULT 1; -``` +2. **mssql-jdbc 驱动**,本目录已放了一份 `mssql-jdbc-13.4.0.jre11.jar`。 -### 5. 模块已在 s_module 中注册 +3. **数据库连接**:工具硬编码连接 `118.89.70.199:1433`,直连 `FMS` 并用三段名访问 `G3HY2025`。 -目标模块的 `b_viewtable` 必须已在 `s_module` 表中配置,且已有 `s_module_field` 记录。 +4. **模块已注册**:目标模块的 `b_id` 必须在 `s_module` 中存在,且 `s_field` 里已有字段。 -## 编译和运行 +### 编译和运行 ```bash -# 进入迁移工具目录 cd fms-api/tools/migration # 设置 JDK 21 @@ -67,51 +87,93 @@ export JAVA_HOME=/path/to/jdk-21 # Linux/Mac set JAVA_HOME=D:\devtool\jdk\jdk-21 # Windows # 编译(源文件含中文注释,需指定 UTF-8 编码) -javac -encoding UTF-8 -cp "%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar" FieldNameSync.java +javac -encoding UTF-8 -cp "mssql-jdbc-13.4.0.jre11.jar" FieldNameSync.java -# 运行(参数为 s_module.b_viewtable 的值;本例模块注册的是表名 b_port,非视图名 v_b_port) -java -cp ".;%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar" FieldNameSync b_port +# 先 Dry-run 看清楚会发生什么 +java -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -cp ".;mssql-jdbc-13.4.0.jre11.jar" FieldNameSync v_b_othercompany --dry-run + +# 确认后执行 +java -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -cp ".;mssql-jdbc-13.4.0.jre11.jar" FieldNameSync v_b_othercompany + +# 运行完毕后删除 class 文件 +del FieldNameSync*.class ``` -## 输出示例 +> `-Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8` 用于让 Windows 控制台正确显示中文输出,可省略。 + +选项: + +| 选项 | 说明 | +| --- | --- | +| `` | 必填,`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` 行 | + +### 输出示例 ``` +模块: v_b_othercompany(合作伙伴) 类型: data +关联旧库表: v_b_othercompany(取自 s_module.b_view_table) +旧库 s_columnLib: 50 行,去重字段 50 个;当前模块 51 个字段中:进列表 15 个、进表单 41 个 + === 同步前 === - b_field b_name list edit - ------------------------------------------------------------------ - b_id 系统编号 显示 显示 - b_name 公司全称 显示 显示 + b_field s_field.b_name col_Caption 进列表 进表单 + ---------------------------------------------------------------------------------------------- + b_id 系统编号 系统编号 是 是 + b_ywid b_ywid (null) 否 否 ... -=== 1) 字段名称 (col_Caption → b_name) === - 更新: 47 条 - -=== 2) 列表显示 (col_Visible → s_module_field_list.b_visible) === - 更新已有: 50 条 - 新增: 0 条 - -=== 3) 编辑显示 (col_edit → s_module_field_edit.b_visible) === - 更新已有: 44 条 - 新增: 0 条 +=== 1) 字段名称 (col_Caption → s_field.b_name) === + 更新: 43 条;旧库无名称、保留原值: 8 条 === 事务已提交 === + +=== 摘要 === + s_field.b_name 更新: 43 条 + s_module_schema(view): 15 个节点,未进入配置 36 个字段,新增行 + s_module_schema(edit): 41 个节点,未进入配置 10 个字段,新增行 + 提示: 该模块尚无 query 配置行,工具不处理 query,需要时在模块管理中生成。 ``` -## 用 AI 助手调用 +### 用 AI 助手调用 -如果通过 AI 助手(如 CodeBuddy)执行,告诉 AI 以下步骤即可: - -> 帮我用 FieldNameSync 工具同步视图 `` 的字段配置。 +> 帮我用 FieldNameSync 同步模块 `` 的字段配置。 > > 1. 进入 `fms-api/tools/migration/` 目录 -> 2. 用 JDK 21 编译:`javac -cp "mssql-jdbc 驱动 jar 路径" FieldNameSync.java` -> 3. 运行:`java -cp ".;驱动 jar 路径" FieldNameSync ` -> 4. 运行完毕后删除 `FieldNameSync.class` 文件 +> 2. 用 JDK 21 编译:`javac -encoding UTF-8 -cp "mssql-jdbc-13.4.0.jre11.jar" FieldNameSync.java` +> 3. 先跑 `java -cp ".;mssql-jdbc-13.4.0.jre11.jar" FieldNameSync --dry-run` 核对 +> 4. 确认后去掉 `--dry-run` 执行;目标模块已有非空 schema 时需 `--force` +> 5. 运行完毕后删除 `FieldNameSync*.class` + +--- + +## SchemaDumpMarkdown — 表结构导出 + +导出该实例上任意数据库的用户表与视图结构为 Markdown 文档,用于对照旧库 `G3HY2025` 或 +新库 `FMS`。新库 `s_module.b_view_table` 大多指向视图,因此视图与表一并导出。 + +```bash +javac -encoding UTF-8 -cp "mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown.java + +# 导出新库 FMS 到指定文件 +java -Dstdout.encoding=UTF-8 -cp ".;mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown "D:\path\FMS表结构.md" FMS + +# 只导出表,不含视图 +java -Dstdout.encoding=UTF-8 -cp ".;mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown "D:\path\FMS表结构.md" FMS --tables-only +``` + +参数:`<输出文件绝对路径>` `[数据库名,默认 G3HY2025]` `[--tables-only]` + +输出包含:对象清单(名称/类型/字段数/主键)、各表字段结构(含主键与外键)、视图字段结构。 +字段「备注」列留空,供后续补充含义。 ## 已有同步记录 -| 视图名 | 同步日期 | 字段数 | 备注 | +| 模块 / 视图 | 同步日期 | 字段数 | 备注 | | --- | --- | --- | --- | -| `v_b_othercompany` | 2026-07-30 | 50 | 已同步 | -| `v_b_contact` | 2026-07-30 | 36 | 已同步 | -| `b_port` | 2026-08-01 | 13 | 已同步(`s_module.b_viewtable='b_port'` 表名,非 `v_b_port`;更新字段名 8 条,列表/编辑显示各 8 条) | +| `v_b_othercompany` | 2026-07-30 | 50 | V1 时代(`s_module_field*`,已废弃) | +| `v_b_contact` | 2026-07-30 | 36 | V1 时代(`s_module_field*`,已废弃) | +| `b_port` | 2026-08-01 | 13 | V1 时代(`s_module_field*`,已废弃) | +| `v_b_othercompany` | 2026-09-14 | 51 | V2:改名 43 条、保留原值 8 条;重建 view(15 列,含旧库列宽/顺序)与 edit(41 个字段),未进入配置的字段不写入 JSON | diff --git a/code/fms/fms-api/tools/migration/SchemaDumpMarkdown.java b/code/fms/fms-api/tools/migration/SchemaDumpMarkdown.java index 42153fca..17242b75 100644 --- a/code/fms/fms-api/tools/migration/SchemaDumpMarkdown.java +++ b/code/fms/fms-api/tools/migration/SchemaDumpMarkdown.java @@ -4,11 +4,19 @@ import java.sql.*; import java.util.*; /** - * 导出旧系统库(FMS)的用户表结构为 Markdown 文档。 + * 导出该 SQL Server 实例上任意数据库的用户表与视图结构为 Markdown 文档。 + * + * 典型用途: + * - 导出旧库 G3HY2025 的表结构(历史对照) + * - 导出新库 FMS 的表结构(V2 核心表 / 业务表 / 视图) + * 新库的 s_module.b_view_table 大多指向视图,因此视图与表一并导出, + * 视图标注为「视图」且无主键信息。 * * 用法: * javac -encoding UTF-8 -cp "mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown.java - * java -cp ".;mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown <输出文件绝对路径> [数据库名,默认 G3HY2025] + * java -Dstdout.encoding=UTF-8 -cp ".;mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown <输出文件绝对路径> [数据库名,默认 G3HY2025] [--tables-only] + * + * --tables-only 只导出表,不含视图(旧行为) */ public class SchemaDumpMarkdown { @@ -21,24 +29,39 @@ public class SchemaDumpMarkdown { } public static void main(String[] args) throws Exception { - String out = args.length > 0 ? args[0] : "Fms旧系统表结构.md"; - String db = args.length > 1 ? args[1] : "G3HY2025"; + boolean tablesOnly = false; + List positional = new ArrayList<>(); + for (String raw : args) { + String arg = raw.trim(); + if (arg.equalsIgnoreCase("--tables-only")) { + tablesOnly = true; + } else { + positional.add(arg); + } + } + + String out = positional.size() > 0 ? positional.get(0) : "Fms旧系统表结构.md"; + String db = positional.size() > 1 ? positional.get(1) : "G3HY2025"; String url = BASE_URL + ";databaseName=" + db; Map> cols = new LinkedHashMap<>(); Map pk = new LinkedHashMap<>(); Map> fk = new LinkedHashMap<>(); + List tables = new ArrayList<>(); + List views = new ArrayList<>(); try (Connection conn = DriverManager.getConnection(url, USER, PASSWORD)) { - // 表清单 - List tables = new ArrayList<>(); + // 对象清单(表在前,视图在后) try (Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES " - + "WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_NAME")) { - while (rs.next()) tables.add(rs.getString(1)); + ResultSet rs = st.executeQuery("SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES " + + "WHERE TABLE_TYPE IN ('BASE TABLE','VIEW') ORDER BY TABLE_TYPE, TABLE_NAME")) { + while (rs.next()) { + if ("VIEW".equalsIgnoreCase(rs.getString(2))) views.add(rs.getString(1)); + else tables.add(rs.getString(1)); + } } - // 字段 + // 字段(表与视图共用) try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, " + "CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, IS_NULLABLE, COLUMN_DEFAULT " @@ -58,7 +81,7 @@ public class SchemaDumpMarkdown { } } - // 主键 + // 主键(仅表有) try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT OBJECT_NAME(k.parent_object_id), c.name " + "FROM sys.key_constraints k " @@ -72,7 +95,7 @@ public class SchemaDumpMarkdown { } } - // 外键 + // 外键(仅表有) try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT OBJECT_NAME(f.parent_object_id), " + "COL_NAME(fc.parent_object_id, fc.parent_column_id), " @@ -86,49 +109,76 @@ public class SchemaDumpMarkdown { } } - StringBuilder sb = new StringBuilder(); - sb.append("# Fms 旧系统表结构(G3HY2025 库)\n\n"); - sb.append("> 来源库:`").append(db).append("`(jdbc:sqlserver://118.89.70.199:1433;databaseName=").append(db).append(")\n"); - sb.append("> 仅包含用户表(BASE TABLE),不含视图。字段「备注」列留空,供后续补充含义。\n"); - sb.append("> 拉取时间:").append(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date())).append("\n"); - sb.append("> 约束情况:该库未定义外键约束(`sys.foreign_keys` = 0);") - .append(pk.size()).append(" 张表定义了主键,").append(tables.size() - pk.size()) - .append(" 张表无主键。\n\n"); - sb.append("共 **").append(tables.size()).append("** 张表。\n\n"); + List listedViews = tablesOnly ? Collections.emptyList() : views; - sb.append("## 表清单(索引)\n\n"); - sb.append("| # | 表名 | 字段数 | 主键 |\n| --- | --- | --- | --- |\n"); + StringBuilder sb = new StringBuilder(); + sb.append("# 表结构导出 — ").append(db).append(" 库\n\n"); + sb.append("> 来源库:`").append(db).append("`(").append(url).append(")\n"); + sb.append("> 包含用户表").append(tablesOnly ? "" : "和视图") + .append(";字段「备注」列留空,供后续补充含义。").append("\n"); + sb.append("> 拉取时间:").append(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date())).append("\n"); + sb.append("> 约束情况:该库定义了 ").append(fk.size()).append(" 张表的外键约束;") + .append(pk.size()).append(" 张表定义了主键,").append(tables.size() - pk.size()) + .append(" 张表无主键。\n"); + if (!tablesOnly) { + sb.append("> 视图无主键和默认值,字段来源见视图定义。\n"); + } + sb.append("\n共 **").append(tables.size()).append("** 张表"); + if (!tablesOnly) sb.append("、**").append(listedViews.size()).append("** 个视图"); + sb.append("。\n\n"); + + sb.append("## 对象清单(索引)\n\n"); + sb.append("| # | 名称 | 类型 | 字段数 | 主键 |\n| --- | --- | --- | --- | --- |\n"); int i = 1; for (String t : tables) { - List cs = cols.getOrDefault(t, Collections.emptyList()); - sb.append("| ").append(i++).append(" | `").append(t).append("` | ").append(cs.size()) + sb.append("| ").append(i++).append(" | `").append(t).append("` | 表 | ") + .append(cols.getOrDefault(t, Collections.emptyList()).size()) .append(" | ").append(esc(pk.getOrDefault(t, ""))).append(" |\n"); } + for (String v : listedViews) { + sb.append("| ").append(i++).append(" | `").append(v).append("` | 视图 | ") + .append(cols.getOrDefault(v, Collections.emptyList()).size()) + .append(" | |\n"); + } - sb.append("\n## 各表字段结构\n"); + sb.append("\n## 表字段结构\n"); for (String t : tables) { - List cs = cols.getOrDefault(t, Collections.emptyList()); - sb.append("\n### ").append(t).append("\n\n"); - if (pk.containsKey(t)) sb.append("**主键:** ").append(esc(pk.get(t))).append("\n\n"); - List fs = fk.get(t); - if (fs != null && !fs.isEmpty()) { - sb.append("**外键:**\n"); - for (String f : fs) sb.append("- ").append(esc(f)).append("\n"); - sb.append("\n"); - } - sb.append("| 字段名 | 类型 | 长度 | 精度,小数位 | 可空 | 默认值 | 备注 |\n"); - sb.append("| --- | --- | --- | --- | --- | --- | --- |\n"); - for (Col c : cs) { - sb.append("| `").append(c.name).append("` | ").append(c.type).append(" | ").append(c.len) - .append(" | ").append(c.precScale).append(" | ").append(c.nullable) - .append(" | ").append(c.def == null ? "" : esc(c.def)).append(" | |\n"); + appendObject(sb, t, pk, fk, cols); + } + + if (!listedViews.isEmpty()) { + sb.append("\n## 视图字段结构\n"); + for (String v : listedViews) { + appendObject(sb, v, pk, fk, cols); } } try (Writer w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(out), StandardCharsets.UTF_8))) { w.write(sb.toString()); } - System.out.println("已写出 " + tables.size() + " 张表 -> " + out); + System.out.println("已写出 " + tables.size() + " 张表" + + (listedViews.isEmpty() ? "" : "、" + listedViews.size() + " 个视图") + " -> " + out); + } + } + + static void appendObject(StringBuilder sb, String name, + Map pk, Map> fk, + Map> cols) { + List cs = cols.getOrDefault(name, Collections.emptyList()); + sb.append("\n### ").append(name).append("\n\n"); + if (pk.containsKey(name)) sb.append("**主键:** ").append(esc(pk.get(name))).append("\n\n"); + List fs = fk.get(name); + if (fs != null && !fs.isEmpty()) { + sb.append("**外键:**\n"); + for (String f : fs) sb.append("- ").append(esc(f)).append("\n"); + sb.append("\n"); + } + sb.append("| 字段名 | 类型 | 长度 | 精度,小数位 | 可空 | 默认值 | 备注 |\n"); + sb.append("| --- | --- | --- | --- | --- | --- | --- |\n"); + for (Col c : cs) { + sb.append("| `").append(c.name).append("` | ").append(c.type).append(" | ").append(c.len) + .append(" | ").append(c.precScale).append(" | ").append(c.nullable) + .append(" | ").append(c.def == null ? "" : esc(c.def)).append(" | |\n"); } } diff --git a/code/fms/fms-vue/src/components/fms-field/FmsField.vue b/code/fms/fms-vue/src/components/fms-field/FmsField.vue index 909cbff5..dfa1278e 100644 --- a/code/fms/fms-vue/src/components/fms-field/FmsField.vue +++ b/code/fms/fms-vue/src/components/fms-field/FmsField.vue @@ -14,11 +14,11 @@ * 一律转发到内部控件; * - v-model:默认转发;selectId 额外支持 v-model:label(id + 显示名 双绑)。 * - * 内置 type:input | textarea | number | money | date | datetime | checkbox | select | selectId + * 内置 type:input | textarea | number | money | date | datetime | range | checkbox | select | selectId * 未识别的 type 兜底为 Input。 */ import { computed, useAttrs } from 'vue' -import { Checkbox, DatePicker, Input, Select } from '@/components/ui' +import { Checkbox, DatePicker, Input, RangePicker, Select } from '@/components/ui' import FmsSelect from '@/components/fms-select/FmsSelect.vue' defineOptions({ inheritAttrs: false }) @@ -53,6 +53,7 @@ const BUILTIN_COMPONENTS = { checkbox: Checkbox, date: DatePicker, datetime: DatePicker, + range: RangePicker, } const resolvedComponent = computed( diff --git a/code/fms/fms-vue/src/components/fms-file/FileCategoryPanel.vue b/code/fms/fms-vue/src/components/fms-file/FileCategoryPanel.vue index 339242e8..18ed6e16 100644 --- a/code/fms/fms-vue/src/components/fms-file/FileCategoryPanel.vue +++ b/code/fms/fms-vue/src/components/fms-file/FileCategoryPanel.vue @@ -1,6 +1,7 @@