diff --git a/code/fms/.codebuddy/plans/field-name-sync-from-old-db_f94a2b08.md b/code/fms/.codebuddy/plans/field-name-sync-from-old-db_f94a2b08.md new file mode 100644 index 00000000..55ac19cc --- /dev/null +++ b/code/fms/.codebuddy/plans/field-name-sync-from-old-db_f94a2b08.md @@ -0,0 +1,117 @@ +--- +name: field-name-sync-from-old-db +overview: 创建可复用的字段名同步服务,从旧库 s_columnLib.col_Caption 同步到新库 s_module_field.b_name,按视图名和字段名匹配,首批执行 v_b_othercompany。 +todos: + - id: fix-old-db-ssl + content: 修复 g3hd-old.properties:移除 sslProtocol=TLSv1 参数,JDK 21 驱动自动协商 TLSv1.2 即可连通 + status: completed + - id: create-sync-service + content: 新建 FieldNameSyncService.java:实现跨库 UPDATE SQL,按 viewName 匹配 s_columnLib,将 col_Caption 写入 s_module_field.b_name,不匹配字段不改动,返回更新行数 + status: completed + dependencies: + - fix-old-db-ssl + - id: create-admin-controller + content: 新建 AdminController.java,添加 POST /api/admin/sync-field-names 端点,JWT 认证,viewName 参数白名单校验,调用 FieldNameSyncService 并返回 ApiResponse + status: completed + dependencies: + - create-sync-service + - id: add-sql-migration-ref + content: 新建 023_sync_field_names.sql 参考脚本,记录跨库同步 SQL 作为纯 SQL 手动执行备选方案 + status: completed +--- + +## 用户需求 + +构建一个可复用的工具,将旧库 `G3HY2025.dbo.s_columnLib` 中存储的字段中文名称(`col_Caption`)同步到新库 `fms.dbo.s_module_field.b_name`。 + +### 匹配规则 + +- `s_module.b_viewtable` = `s_columnLib.col_TableName`(模块视图名对应旧库表名) +- `s_module_field.b_field` = `s_columnLib.col_FieldName`(字段名精确匹配) +- 匹配不上的字段,`b_name` 保持不变 + +### 首批同步目标 + +视图 `v_b_othercompany` + +### 可复用要求 + +以后还有更多视图需要同步,工具须参数化视图名,方便批量复用。 + +## 技术方案 + +### 方案选择:跨库 SQL + 管理端点 + +**核心决策**:新旧库在同一 SQL Server 实例(`118.89.70.199:1433`),使用**跨数据库 SQL** 直接从新库连接执行 `UPDATE ... FROM G3HY2025.dbo.s_columnLib`,无需单独连接旧库。 + +```sql +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 = m.b_viewtable + AND sc.col_FieldName = sf.b_field +WHERE m.b_viewtable = ? + AND sc.col_Caption IS NOT NULL + AND LTRIM(RTRIM(sc.col_Caption)) != ''; +``` + +**为何需要新端点而非复用通用端点**: + +- 通用 `saveObjectApi` 只能操作当前 org 数据库的单表 insert/update/delete,无法表达跨库 JOIN 逻辑 +- 通用 `loadDataApi` 只能查询当前 org 的表,无法读取旧库 `s_columnLib` +- 此操作为管理类批量数据同步,不在业务 CRUD 范畴内 + +### 实现架构 + +```mermaid +flowchart LR + A[前端/管理员] -->|POST /api/admin/sync-field-names| B[AdminController] + B -->|校验 viewName 参数| C[FieldNameSyncService] + C -->|获取新库 Connection| D[OrgRoutingDataSource] + D -->|fms 连接| E[(SQL Server)] + E -->|跨库 JOIN| F[G3HY2025.dbo.s_columnLib] + E -->|UPDATE| G[fms.dbo.s_module_field] + C -->|返回同步数量| B + B -->|ApiResponse| A +``` + +### 模块划分 + +| 模块 | 文件 | 职责 | +| --- | --- | --- | +| 配置修复 | `config/dbconfigs/g3hd-old.properties` | 移除 `sslProtocol=TLSv1` | +| 同步服务 | `service/FieldNameSyncService.java` | 执行跨库 UPDATE,校验参数,返回影响行数 | +| 管理控制器 | `controller/AdminController.java` | `POST /api/admin/sync-field-names` 端点,JWT 保护 | +| 参考脚本 | `config/migrations/023_sync_field_names.sql` | 纯 SQL 版本供手动执行 | + + +### 数据流 + +1. 前端/管理员发送 `POST /api/admin/sync-field-names`,请求体 `{ "viewName": "v_b_othercompany" }` +2. `AdminController` 校验 `viewName` 为合法标识符(字母/下划线/数字,最长128) +3. `FieldNameSyncService` 通过 `@Resource DataSource` 获取新库连接 +4. 执行跨库 PreparedStatement UPDATE +5. 返回 `{ "updatedCount": 42, "message": "v_b_othercompany 同步完成,共更新 42 个字段名称" }` + +### 安全措施 + +- `viewName` 参数:白名单正则 `^[A-Za-z_][A-Za-z0-9_]{0,127},校验后作为 PreparedStatement 参数绑定(非字符串拼接) +- JWT 认证:`AdminController` 路径 `/api/admin/**` 经 `JwtAuthFilter` 保护,仅登录用户可调用 +- 失败不影响现有数据:UPDATE 在一条 SQL 内执行,单条匹配失败不阻断其他行 + +### 执行说明 + +- **热路径**:单条 UPDATE SQL 跨库 JOIN,性能取决于 `s_module_field` 行数和索引;对单个视图(~50 字段)毫秒级完成 +- **日志**:服务层记录同步开始/完成/失败日志,使用 `@Slf4j`,级别 INFO/ERROR +- **事务**:使用 Spring 声明式事务 `@Transactional`,确保整批同步原子提交或回滚 +- **向后兼容**:不修改现有 `DataController` 和通用端点,不影响业务数据流 + +## Agent Extensions + +### SubAgent + +- **code-explorer** +- 目的:探索旧库连接方式、现有 Service/Controller 模式、参数校验惯例 +- 预期结果:确认 `OrgDataSourceManager` 的 API 使用方式和现有异常处理/日志模式 \ No newline at end of file diff --git a/code/fms/fms-api/CheckEdicode.class b/code/fms/fms-api/CheckEdicode.class new file mode 100644 index 00000000..827067a6 Binary files /dev/null and b/code/fms/fms-api/CheckEdicode.class differ diff --git a/code/fms/fms-api/config/migrations/003_create_login_log.sql b/code/fms/fms-api/config/migrations/003_create_login_log.sql index b7dbb6b1..9dc12ade 100644 --- a/code/fms/fms-api/config/migrations/003_create_login_log.sql +++ b/code/fms/fms-api/config/migrations/003_create_login_log.sql @@ -1,5 +1,5 @@ /* - 登录日志表 s_log_login:记录每次登录尝试(成功/失败)的客户端环境与结果,用于登录审计。 + 登录日志表 s_login_log:记录每次登录尝试(成功/失败)的客户端环境与结果,用于登录审计。 后端在登录时统一采集 IP、IP 归属地、浏览器/版本、操作系统/版本、设备类型、原始 User-Agent, 并通过通用 saveobjt 接口写入本表;前端通过分页接口只读展示。 表名使用 s_ 前缀以区别于业务模块表(b_)。 @@ -10,14 +10,14 @@ SET NOCOUNT ON; BEGIN TRANSACTION; - IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = N's_log_login') + IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = N's_login_log') BEGIN - CREATE TABLE dbo.s_log_login ( + CREATE TABLE dbo.s_login_log ( b_id bigint NOT NULL, b_org_id nvarchar(50) NULL, b_user_id bigint NULL, b_account nvarchar(50) NULL, - b_login_time datetime NOT NULL CONSTRAINT DF_s_log_login_time DEFAULT (GETDATE()), + b_login_time datetime NOT NULL CONSTRAINT DF_s_login_log_time DEFAULT (GETDATE()), b_ip nvarchar(50) NULL, b_ip_location nvarchar(200) NULL, b_browser nvarchar(100) NULL, @@ -26,15 +26,15 @@ BEGIN TRANSACTION; b_os_version nvarchar(50) NULL, b_device_type nvarchar(20) NULL, b_user_agent nvarchar(max) NULL, - b_login_result int NOT NULL CONSTRAINT DF_s_log_login_result DEFAULT (0), + b_login_result int NOT NULL CONSTRAINT DF_s_login_log_result DEFAULT (0), b_fail_reason nvarchar(200) NULL, b_session_id nvarchar(50) NULL, - CONSTRAINT PK_s_log_login PRIMARY KEY (b_id) + CONSTRAINT PK_s_login_log PRIMARY KEY (b_id) ); - CREATE INDEX IX_s_log_login_time ON dbo.s_log_login (b_login_time DESC, b_id); - CREATE INDEX IX_s_log_login_account ON dbo.s_log_login (b_account, b_login_time DESC); - CREATE INDEX IX_s_log_login_result ON dbo.s_log_login (b_login_result, b_login_time DESC); + CREATE INDEX IX_s_login_log_time ON dbo.s_login_log (b_login_time DESC, b_id); + CREATE INDEX IX_s_login_log_account ON dbo.s_login_log (b_account, b_login_time DESC); + CREATE INDEX IX_s_login_log_result ON dbo.s_login_log (b_login_result, b_login_time DESC); END COMMIT TRANSACTION; diff --git a/code/fms/fms-api/config/migrations/022_add_otherdata_module.sql b/code/fms/fms-api/config/migrations/022_add_otherdata_module.sql new file mode 100644 index 00000000..ee59ad38 --- /dev/null +++ b/code/fms/fms-api/config/migrations/022_add_otherdata_module.sql @@ -0,0 +1,446 @@ +/* 通用 "其他数据" 模块配置。左侧树,右侧按数据模块加载不同表格(独立列定义)。*/ +SET NOCOUNT ON; +SET XACT_ABORT ON; + +BEGIN TRANSACTION; + + /* ========================================================================= + 1. 确定 base 目录 + ========================================================================= */ + DECLARE @BaseId bigint = ( + SELECT TOP (1) b_id + FROM dbo.s_module + WHERE b_code = 'base' AND b_module_type = 'directory' + ORDER BY b_id + ); + IF @BaseId IS NULL + THROW 52001, N'base directory not found. Run 012 / 002 migration first.', 1; + + /* ========================================================================= + 2. 创建 page 模块: m_base_otherdata + ========================================================================= */ + DECLARE @PageId bigint = 920000000000001100; + IF NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = 'm_base_otherdata') + BEGIN + IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @PageId) + THROW 52002, N'The reserved otherdata page ID is already in use.', 1; + + INSERT INTO dbo.s_module ( + b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type, + b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield, + b_canmenu, b_canuse, b_xh + ) VALUES ( + @PageId, 'm_base_otherdata', @BaseId, N'其他数据', + 'menu.base_otherdata', 'page', '/base/otherdata', + NULL, NULL, NULL, NULL, 1, 1, 30 + ); + END + ELSE + SET @PageId = (SELECT b_id FROM dbo.s_module WHERE b_code = 'm_base_otherdata'); + + /* ========================================================================= + 3. 创建 data 模块辅助存储过程 + ========================================================================= */ + DECLARE @DataId bigint; + DECLARE @DataCode varchar(100); + DECLARE @DataName nvarchar(200); + DECLARE @ViewTable varchar(100); + + /* ========================================================================= + 4. 港口 d_other_port → b_port + ========================================================================= */ + SET @DataCode = 'd_other_port'; + SET @DataName = N'港口'; + SET @ViewTable = 'b_port'; + SET @DataId = 920000000000001101; + + IF NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = @DataCode) + BEGIN + IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @DataId) + THROW 52003, N'The reserved port data ID is already in use.', 1; + INSERT INTO dbo.s_module ( + b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type, + b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield, + b_canmenu, b_canuse, b_xh + ) VALUES ( + @DataId, @DataCode, @PageId, @DataName, + 'module.d_other_port', 'data', NULL, + 'b_port', 'b_port', 'b_id', 'b_code ASC', 0, 1, 10 + ); + END + ELSE + SET @DataId = (SELECT b_id FROM dbo.s_module WHERE b_code = @DataCode); + + -- 港口字段 + DECLARE @PortFields TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_name nvarchar(200) NOT NULL, b_type varchar(30) NOT NULL, b_xh int NOT NULL); + INSERT INTO @PortFields VALUES + (920000000000010101, 'b_id', N'系统编号', 'input', 10), + (920000000000010102, 'b_code', N'港口代码', 'input', 20), + (920000000000010103, 'b_name_c', N'中文名称', 'input', 30), + (920000000000010104, 'b_name_e', N'英文名称', 'input', 40), + (920000000000010105, 'b_country', N'国家', 'input', 50), + (920000000000010106, 'b_oceanline', N'航线', 'input', 60), + (920000000000010107, 'b_edicode', N'EDI代码', 'input', 70), + (920000000000010108, 'b_bz', N'备注', 'input', 80), + (920000000000010109, 'b_tz', N'特征', 'input', 90); + + INSERT INTO dbo.s_module_field (b_id, b_module_id, b_field, b_name, b_i18n, b_type, b_canuse) + SELECT source.b_id, @DataId, source.b_field, source.b_name, + 'field.d_other_port.' + source.b_field, source.b_type, 1 + FROM @PortFields source + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field t WHERE t.b_module_id = @DataId AND t.b_field = source.b_field + ); + + -- 港口列表列 + DECLARE @PortList TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL, b_width int NOT NULL); + INSERT INTO @PortList VALUES + (920000000000020101, 'b_code', 10, 120), + (920000000000020102, 'b_name_c', 20, 150), + (920000000000020103, 'b_name_e', 30, 180), + (920000000000020104, 'b_country', 40, 120), + (920000000000020105, 'b_oceanline', 50, 120), + (920000000000020106, 'b_edicode', 60, 120); + + INSERT INTO dbo.s_module_field_list (b_id, b_module_id, b_field_id, b_i18n, b_visible, b_xh, b_width) + SELECT source.b_id, @DataId, f.b_id, NULL, 1, source.b_xh, source.b_width + FROM @PortList source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_list t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + -- 港口查询字段 + DECLARE @PortQuery TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL); + INSERT INTO @PortQuery VALUES + (920000000000030101, 'b_code', 10), + (920000000000030102, 'b_name_c', 20), + (920000000000030103, 'b_name_e', 30); + + INSERT INTO dbo.s_module_field_query (b_id, b_module_id, b_field_id, b_component, b_operator, b_default_value, b_xh, b_canuse) + SELECT source.b_id, @DataId, f.b_id, 'input', 'like', NULL, source.b_xh, 1 + FROM @PortQuery source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_query t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + /* ========================================================================= + 5. 城市 d_other_city → b_city + ========================================================================= */ + SET @DataCode = 'd_other_city'; + SET @DataName = N'城市'; + SET @ViewTable = 'b_city'; + SET @DataId = 920000000000001102; + + IF NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = @DataCode) + BEGIN + IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @DataId) + THROW 52004, N'The reserved city data ID is already in use.', 1; + INSERT INTO dbo.s_module ( + b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type, + b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield, + b_canmenu, b_canuse, b_xh + ) VALUES ( + @DataId, @DataCode, @PageId, @DataName, + 'module.d_other_city', 'data', NULL, + 'b_city', 'b_city', 'b_id', 'b_code ASC', 0, 1, 20 + ); + END + ELSE + SET @DataId = (SELECT b_id FROM dbo.s_module WHERE b_code = @DataCode); + + -- 城市字段 + DECLARE @CityFields TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_name nvarchar(200) NOT NULL, b_type varchar(30) NOT NULL, b_xh int NOT NULL); + INSERT INTO @CityFields VALUES + (920000000000010201, 'b_id', N'系统编号', 'input', 10), + (920000000000010202, 'b_code', N'城市代码', 'input', 20), + (920000000000010203, 'b_name_c', N'中文名称', 'input', 30), + (920000000000010204, 'b_name_e', N'英文名称', 'input', 40), + (920000000000010205, 'b_country_name_c', N'国家(中)', 'input', 50), + (920000000000010206, 'b_country_name_e', N'国家(英)', 'input', 60), + (920000000000010207, 'b_bz', N'备注', 'input', 70); + + INSERT INTO dbo.s_module_field (b_id, b_module_id, b_field, b_name, b_i18n, b_type, b_canuse) + SELECT source.b_id, @DataId, source.b_field, source.b_name, + 'field.d_other_city.' + source.b_field, source.b_type, 1 + FROM @CityFields source + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field t WHERE t.b_module_id = @DataId AND t.b_field = source.b_field + ); + + -- 城市列表列 + DECLARE @CityList TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL, b_width int NOT NULL); + INSERT INTO @CityList VALUES + (920000000000020201, 'b_code', 10, 120), + (920000000000020202, 'b_name_c', 20, 150), + (920000000000020203, 'b_name_e', 30, 180), + (920000000000020204, 'b_country_name_c', 40, 140), + (920000000000020205, 'b_country_name_e', 50, 140); + + INSERT INTO dbo.s_module_field_list (b_id, b_module_id, b_field_id, b_i18n, b_visible, b_xh, b_width) + SELECT source.b_id, @DataId, f.b_id, NULL, 1, source.b_xh, source.b_width + FROM @CityList source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_list t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + -- 城市查询字段 + DECLARE @CityQuery TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL); + INSERT INTO @CityQuery VALUES + (920000000000030201, 'b_code', 10), + (920000000000030202, 'b_name_c', 20); + + INSERT INTO dbo.s_module_field_query (b_id, b_module_id, b_field_id, b_component, b_operator, b_default_value, b_xh, b_canuse) + SELECT source.b_id, @DataId, f.b_id, 'input', 'like', NULL, source.b_xh, 1 + FROM @CityQuery source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_query t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + /* ========================================================================= + 6. 币种 d_other_money → b_money + ========================================================================= */ + SET @DataCode = 'd_other_money'; + SET @DataName = N'币种'; + SET @ViewTable = 'b_money'; + SET @DataId = 920000000000001103; + + IF NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = @DataCode) + BEGIN + IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @DataId) + THROW 52005, N'The reserved money data ID is already in use.', 1; + INSERT INTO dbo.s_module ( + b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type, + b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield, + b_canmenu, b_canuse, b_xh + ) VALUES ( + @DataId, @DataCode, @PageId, @DataName, + 'module.d_other_money', 'data', NULL, + 'b_money', 'b_money', 'b_id', 'b_code ASC', 0, 1, 30 + ); + END + ELSE + SET @DataId = (SELECT b_id FROM dbo.s_module WHERE b_code = @DataCode); + + -- 币种字段 + DECLARE @MoneyFields TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_name nvarchar(200) NOT NULL, b_type varchar(30) NOT NULL, b_xh int NOT NULL); + INSERT INTO @MoneyFields VALUES + (920000000000010301, 'b_id', N'系统编号', 'input', 10), + (920000000000010302, 'b_code', N'币种代码', 'input', 20), + (920000000000010303, 'b_name', N'名称', 'input', 30), + (920000000000010304, 'b_edicode', N'EDI代码', 'input', 40), + (920000000000010305, 'b_symbol', N'符号', 'input', 50), + (920000000000010306, 'b_hl', N'汇率', 'number', 60), + (920000000000010307, 'b_isconvert', N'可兑换', 'checkbox', 70), + (920000000000010308, 'b_canuse', N'启用', 'checkbox', 80), + (920000000000010309, 'b_xh', N'排序', 'number', 90), + (920000000000010310, 'b_bz', N'备注', 'input', 100); + + INSERT INTO dbo.s_module_field (b_id, b_module_id, b_field, b_name, b_i18n, b_type, b_canuse) + SELECT source.b_id, @DataId, source.b_field, source.b_name, + 'field.d_other_money.' + source.b_field, source.b_type, 1 + FROM @MoneyFields source + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field t WHERE t.b_module_id = @DataId AND t.b_field = source.b_field + ); + + -- 币种列表列 + DECLARE @MoneyList TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL, b_width int NOT NULL); + INSERT INTO @MoneyList VALUES + (920000000000020301, 'b_code', 10, 120), + (920000000000020302, 'b_name', 20, 150), + (920000000000020303, 'b_edicode', 30, 120), + (920000000000020304, 'b_symbol', 40, 80), + (920000000000020305, 'b_hl', 50, 120), + (920000000000020306, 'b_canuse', 60, 80); + + INSERT INTO dbo.s_module_field_list (b_id, b_module_id, b_field_id, b_i18n, b_visible, b_xh, b_width) + SELECT source.b_id, @DataId, f.b_id, NULL, 1, source.b_xh, source.b_width + FROM @MoneyList source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_list t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + -- 币种查询字段 + DECLARE @MoneyQuery TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL); + INSERT INTO @MoneyQuery VALUES + (920000000000030301, 'b_code', 10), + (920000000000030302, 'b_name', 20); + + INSERT INTO dbo.s_module_field_query (b_id, b_module_id, b_field_id, b_component, b_operator, b_default_value, b_xh, b_canuse) + SELECT source.b_id, @DataId, f.b_id, 'input', 'like', NULL, source.b_xh, 1 + FROM @MoneyQuery source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_query t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + /* ========================================================================= + 7. 航线 d_other_oceanline → b_oceanline + ========================================================================= */ + SET @DataCode = 'd_other_oceanline'; + SET @DataName = N'航线'; + SET @ViewTable = 'b_oceanline'; + SET @DataId = 920000000000001104; + + IF NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = @DataCode) + BEGIN + IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @DataId) + THROW 52006, N'The reserved oceanline data ID is already in use.', 1; + INSERT INTO dbo.s_module ( + b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type, + b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield, + b_canmenu, b_canuse, b_xh + ) VALUES ( + @DataId, @DataCode, @PageId, @DataName, + 'module.d_other_oceanline', 'data', NULL, + 'b_oceanline', 'b_oceanline', 'b_id', 'b_code ASC', 0, 1, 40 + ); + END + ELSE + SET @DataId = (SELECT b_id FROM dbo.s_module WHERE b_code = @DataCode); + + -- 航线字段 + DECLARE @OceanlineFields TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_name nvarchar(200) NOT NULL, b_type varchar(30) NOT NULL, b_xh int NOT NULL); + INSERT INTO @OceanlineFields VALUES + (920000000000010401, 'b_id', N'系统编号', 'input', 10), + (920000000000010402, 'b_code', N'航线代码', 'input', 20), + (920000000000010403, 'b_name', N'名称', 'input', 30), + (920000000000010404, 'b_edicode', N'EDI代码', 'input', 40), + (920000000000010405, 'b_bz', N'备注', 'input', 50); + + INSERT INTO dbo.s_module_field (b_id, b_module_id, b_field, b_name, b_i18n, b_type, b_canuse) + SELECT source.b_id, @DataId, source.b_field, source.b_name, + 'field.d_other_oceanline.' + source.b_field, source.b_type, 1 + FROM @OceanlineFields source + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field t WHERE t.b_module_id = @DataId AND t.b_field = source.b_field + ); + + -- 航线列表列 + DECLARE @OceanlineList TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL, b_width int NOT NULL); + INSERT INTO @OceanlineList VALUES + (920000000000020401, 'b_code', 10, 120), + (920000000000020402, 'b_name', 20, 180), + (920000000000020403, 'b_edicode', 30, 120); + + INSERT INTO dbo.s_module_field_list (b_id, b_module_id, b_field_id, b_i18n, b_visible, b_xh, b_width) + SELECT source.b_id, @DataId, f.b_id, NULL, 1, source.b_xh, source.b_width + FROM @OceanlineList source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_list t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + -- 航线查询字段 + DECLARE @OceanlineQuery TABLE (b_id bigint NOT NULL, b_field varchar(50) NOT NULL, b_xh int NOT NULL); + INSERT INTO @OceanlineQuery VALUES + (920000000000030401, 'b_code', 10), + (920000000000030402, 'b_name', 20); + + INSERT INTO dbo.s_module_field_query (b_id, b_module_id, b_field_id, b_component, b_operator, b_default_value, b_xh, b_canuse) + SELECT source.b_id, @DataId, f.b_id, 'input', 'like', NULL, source.b_xh, 1 + FROM @OceanlineQuery source + JOIN dbo.s_module_field f ON f.b_module_id = @DataId AND f.b_field = source.b_field + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_field_query t WHERE t.b_module_id = @DataId AND t.b_field_id = f.b_id + ); + + /* ========================================================================= + 8. s_module_power 权限 + ========================================================================= */ + DECLARE @Powers TABLE (b_id bigint NOT NULL, b_code varchar(50) NOT NULL, b_name nvarchar(200) NOT NULL, b_xh int NOT NULL); + INSERT INTO @Powers VALUES + (920000000000040101, 'create', N'新增', 10), + (920000000000040102, 'update', N'修改', 20), + (920000000000040103, 'delete', N'删除', 30); + + INSERT INTO dbo.s_module_power (b_id, b_module_id, b_code, b_name, b_i18n, b_xh, b_canuse) + SELECT source.b_id, @PageId, source.b_code, source.b_name, + 'power.base_otherdata.' + source.b_code, source.b_xh, 1 + FROM @Powers source + WHERE NOT EXISTS ( + SELECT 1 FROM dbo.s_module_power t + WHERE t.b_module_id = @PageId AND t.b_code = source.b_code + ); + + /* ========================================================================= + 9. Mock 测试数据 + ========================================================================= */ + -- 港口 + IF NOT EXISTS (SELECT 1 FROM dbo.b_port WHERE b_code = 'CNSHA') + INSERT INTO dbo.b_port (b_id, b_code, b_name_c, b_name_e, b_country, b_oceanline, b_edicode, b_bz, b_tz) VALUES + (920000000000110001, 'CNSHA', N'上海港', 'Shanghai', N'中国', 'ASIA', 'CNSHA', NULL, NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_port WHERE b_code = 'CNNGB') + INSERT INTO dbo.b_port (b_id, b_code, b_name_c, b_name_e, b_country, b_oceanline, b_edicode, b_bz, b_tz) VALUES + (920000000000110002, 'CNNGB', N'宁波港', 'Ningbo', N'中国', 'ASIA', 'CNNGB', NULL, NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_port WHERE b_code = 'HKKG') + INSERT INTO dbo.b_port (b_id, b_code, b_name_c, b_name_e, b_country, b_oceanline, b_edicode, b_bz, b_tz) VALUES + (920000000000110003, 'HKKG', N'香港港', 'Hong Kong', N'中国', 'ASIA', 'HKKG', NULL, NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_port WHERE b_code = 'USLAX') + INSERT INTO dbo.b_port (b_id, b_code, b_name_c, b_name_e, b_country, b_oceanline, b_edicode, b_bz, b_tz) VALUES + (920000000000110004, 'USLAX', N'洛杉矶港', 'Los Angeles', N'美国', 'AMERICA', 'USLAX', NULL, NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_port WHERE b_code = 'NLRTM') + INSERT INTO dbo.b_port (b_id, b_code, b_name_c, b_name_e, b_country, b_oceanline, b_edicode, b_bz, b_tz) VALUES + (920000000000110005, 'NLRTM', N'鹿特丹港', 'Rotterdam', N'荷兰', 'EUROPE', 'NLRTM', NULL, NULL); + + -- 城市 + IF NOT EXISTS (SELECT 1 FROM dbo.b_city WHERE b_code = 'SH') + INSERT INTO dbo.b_city (b_id, b_code, b_name_c, b_name_e, b_country_name_c, b_country_name_e, b_bz) VALUES + (920000000000110101, 'SH', N'上海', 'Shanghai', N'中国', 'China', NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_city WHERE b_code = 'SZ') + INSERT INTO dbo.b_city (b_id, b_code, b_name_c, b_name_e, b_country_name_c, b_country_name_e, b_bz) VALUES + (920000000000110102, 'SZ', N'深圳', 'Shenzhen', N'中国', 'China', NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_city WHERE b_code = 'NJ') + INSERT INTO dbo.b_city (b_id, b_code, b_name_c, b_name_e, b_country_name_c, b_country_name_e, b_bz) VALUES + (920000000000110103, 'NJ', N'南京', 'Nanjing', N'中国', 'China', NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_city WHERE b_code = 'NY') + INSERT INTO dbo.b_city (b_id, b_code, b_name_c, b_name_e, b_country_name_c, b_country_name_e, b_bz) VALUES + (920000000000110104, 'NY', N'纽约', 'New York', N'美国', 'America', NULL); + + -- 币种 + IF NOT EXISTS (SELECT 1 FROM dbo.b_money WHERE b_code = 'CNY') + INSERT INTO dbo.b_money (b_id, b_code, b_name, b_edicode, b_symbol, b_hl, b_isconvert, b_canuse, b_xh) VALUES + (920000000000110201, 'CNY', N'人民币', 'CNY', N'¥', 1.0000, '1', 1, 10); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_money WHERE b_code = 'USD') + INSERT INTO dbo.b_money (b_id, b_code, b_name, b_edicode, b_symbol, b_hl, b_isconvert, b_canuse, b_xh) VALUES + (920000000000110202, 'USD', N'美元', 'USD', N'$', 7.2500, '1', 1, 20); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_money WHERE b_code = 'EUR') + INSERT INTO dbo.b_money (b_id, b_code, b_name, b_edicode, b_symbol, b_hl, b_isconvert, b_canuse, b_xh) VALUES + (920000000000110203, 'EUR', N'欧元', 'EUR', N'€', 7.8900, '1', 1, 30); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_money WHERE b_code = 'JPY') + INSERT INTO dbo.b_money (b_id, b_code, b_name, b_edicode, b_symbol, b_hl, b_isconvert, b_canuse, b_xh) VALUES + (920000000000110204, 'JPY', N'日元', 'JPY', N'¥', 0.0490, '1', 1, 40); + + -- 航线 + IF NOT EXISTS (SELECT 1 FROM dbo.b_oceanline WHERE b_code = 'ASIA') + INSERT INTO dbo.b_oceanline (b_id, b_code, b_name, b_edicode, b_bz) VALUES + (920000000000110301, 'ASIA', N'亚洲线', 'ASIA', NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_oceanline WHERE b_code = 'EUROPE') + INSERT INTO dbo.b_oceanline (b_id, b_code, b_name, b_edicode, b_bz) VALUES + (920000000000110302, 'EUROPE', N'欧洲线', 'EUROPE', NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_oceanline WHERE b_code = 'AMERICA') + INSERT INTO dbo.b_oceanline (b_id, b_code, b_name, b_edicode, b_bz) VALUES + (920000000000110303, 'AMERICA', N'美洲线', 'AMERICA', NULL); + + IF NOT EXISTS (SELECT 1 FROM dbo.b_oceanline WHERE b_code = 'AFRICA') + INSERT INTO dbo.b_oceanline (b_id, b_code, b_name, b_edicode, b_bz) VALUES + (920000000000110304, 'AFRICA', N'非洲线', 'AFRICA', NULL); + +COMMIT TRANSACTION; diff --git a/code/fms/fms-api/config/migrations/023_add_otherdata_bmfl_module_code.sql b/code/fms/fms-api/config/migrations/023_add_otherdata_bmfl_module_code.sql new file mode 100644 index 00000000..93caabef --- /dev/null +++ b/code/fms/fms-api/config/migrations/023_add_otherdata_bmfl_module_code.sql @@ -0,0 +1,78 @@ +/* 扩展 b_other_bmfl 表,添加 b_module_code 字段关联 s_module 数据模块。 + 分类节点 b_module_code 为空(仅做分组),叶子节点指向对应的 data 模块。*/ +SET NOCOUNT ON; +SET XACT_ABORT ON; + +BEGIN TRANSACTION; + + /* ========================================================================= + 1. 添加 b_module_code 列 + ========================================================================= */ + IF NOT EXISTS ( + SELECT 1 FROM sys.columns + WHERE object_id = OBJECT_ID(N'dbo.b_other_bmfl') AND name = 'b_module_code' + ) + BEGIN + ALTER TABLE dbo.b_other_bmfl + ADD b_module_code nvarchar(100) NULL; + END; + + /* ========================================================================= + 2. 插入演示分类数据(基础信息 / 财务信息 / 物流信息 分组) + ========================================================================= */ + DECLARE @CatBasic bigint = 930000000000000101; + DECLARE @CatFinance bigint = 930000000000000102; + DECLARE @CatLogistics bigint = 930000000000000103; + + /* ---- 基础信息 ---- */ + IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatBasic) + INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code) + VALUES (@CatBasic, NULL, N'基础信息', 1, CAST(@CatBasic AS varchar(20)), 10, 1, NULL); + + /* ---- 财务信息 ---- */ + IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatFinance) + INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code) + VALUES (@CatFinance, NULL, N'财务信息', 1, CAST(@CatFinance AS varchar(20)), 20, 1, NULL); + + /* ---- 物流信息 ---- */ + IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatLogistics) + INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code) + VALUES (@CatLogistics, NULL, N'物流信息', 1, CAST(@CatLogistics AS varchar(20)), 30, 1, NULL); + + /* ========================================================================= + 3. 插入叶子节点(关联到已有 data 模块) + ========================================================================= */ + DECLARE @NodePort bigint = 930000000000000201; + DECLARE @NodeCity bigint = 930000000000000202; + DECLARE @NodeMoney bigint = 930000000000000203; + DECLARE @NodeOceanline bigint = 930000000000000204; + + /* ---- 基础信息 / 港口 ---- */ + IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodePort) + INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code) + VALUES (@NodePort, @CatBasic, N'港口', 2, + CAST(@CatBasic AS varchar(20)) + '/' + CAST(@NodePort AS varchar(20)), + 10, 1, 'd_other_port'); + + /* ---- 基础信息 / 城市 ---- */ + IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeCity) + INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code) + VALUES (@NodeCity, @CatBasic, N'城市', 2, + CAST(@CatBasic AS varchar(20)) + '/' + CAST(@NodeCity AS varchar(20)), + 20, 1, 'd_other_city'); + + /* ---- 财务信息 / 币种 ---- */ + IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeMoney) + INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code) + VALUES (@NodeMoney, @CatFinance, N'币种', 2, + CAST(@CatFinance AS varchar(20)) + '/' + CAST(@NodeMoney AS varchar(20)), + 10, 1, 'd_other_money'); + + /* ---- 物流信息 / 航线 ---- */ + IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeOceanline) + INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code) + VALUES (@NodeOceanline, @CatLogistics, N'航线', 2, + CAST(@CatLogistics AS varchar(20)) + '/' + CAST(@NodeOceanline AS varchar(20)), + 10, 1, 'd_other_oceanline'); + +COMMIT TRANSACTION; diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java index cd2649c0..7924d163 100644 --- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java +++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java @@ -16,7 +16,7 @@ import java.util.List; import java.util.Map; /** - * 登录日志服务:在登录成功或失败时采集客户端信息,并通过通用 saveobjt 机制写入 s_log_login 表。 + * 登录日志服务:在登录成功或失败时采集客户端信息,并通过通用 saveobjt 机制写入 s_login_log 表。 * 写日志失败不影响登录主流程;机构上下文通过 OrgContext 临时设置并在 finally 中清理。 */ @Service @@ -66,7 +66,7 @@ public class LoginLogService { row.put("b_session_id", sessionId); Map tableOp = new LinkedHashMap<>(); - tableOp.put("table", "s_log_login"); + tableOp.put("table", "s_login_log"); tableOp.put("key_field", "b_id"); tableOp.put("inserts", List.of(row)); diff --git a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class index f0513ed5..3cdce4b9 100644 Binary files a/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class and b/code/fms/fms-api/target/classes/cn/g3soft/fmsapi/service/LoginLogService.class differ diff --git a/code/fms/fms-api/tools/migration/FieldNameSync.java b/code/fms/fms-api/tools/migration/FieldNameSync.java new file mode 100644 index 00000000..65ce7661 --- /dev/null +++ b/code/fms/fms-api/tools/migration/FieldNameSync.java @@ -0,0 +1,264 @@ +import java.sql.*; + +/** + * 从旧库 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(编辑显示,不存在则插入记录) + * + * 用法: java -cp ".;mssql-jdbc.jar" FieldNameSync + */ +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.."; + + // 同步字段名称 + 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) + """; + + // 查询模块 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); + } + + 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); + } + } + if (moduleId == null) { + System.err.println("模块 " + viewName + " 不存在!"); + 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); + 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); + } + } + } + } +} diff --git a/code/fms/fms-api/tools/migration/README.md b/code/fms/fms-api/tools/migration/README.md new file mode 100644 index 00000000..cadb7028 --- /dev/null +++ b/code/fms/fms-api/tools/migration/README.md @@ -0,0 +1,112 @@ +# FieldNameSync — 模块字段配置迁移工具 + +从旧库 `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=隐藏) | + +## 匹配规则 + +通过视图名和字段名关联新旧库: + +``` +旧库 s_columnLib.col_TableName = 新库 s_module.b_viewtable +旧库 s_columnLib.col_FieldName = 新库 s_module_field.b_field +``` + +- 旧库字段匹配不上的,对应配置**不做修改**(保留原值) +- 列表配置或编辑配置记录不存在时,**自动插入**新记录 + +## 前置条件 + +### 1. JDK 21 + +工具使用 Java 文本块语法(`"""`),需要 JDK 21+ 编译。 + +### 2. mssql-jdbc 驱动 + +确保本地 Maven 仓库存在驱动 jar: +``` +%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar +``` + +如果不存在,从项目根目录 `fms-api/` 执行 `mvnw dependency:resolve` 下载。 + +### 3. 数据库连接 + +工具硬编码连接 `118.89.70.199:1433`,数据库 `fms`,直连新旧两库(同一实例)。 + +### 4. 新库已有 `s_module_field_edit.b_visible` 列 + +如果报 `Invalid column name 'b_visible'`,执行以下 SQL 添加列: +```sql +ALTER TABLE dbo.s_module_field_edit ADD b_visible int NOT NULL DEFAULT 1; +``` + +### 5. 模块已在 s_module 中注册 + +目标模块的 `b_viewtable` 必须已在 `s_module` 表中配置,且已有 `s_module_field` 记录。 + +## 编译和运行 + +```bash +# 进入迁移工具目录 +cd fms-api/tools/migration + +# 设置 JDK 21 +export JAVA_HOME=/path/to/jdk-21 # Linux/Mac +set JAVA_HOME=D:\devtool\jdk\jdk-21 # Windows + +# 编译 +javac -cp "%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar" FieldNameSync.java + +# 运行(以 v_b_othercompany 为例) +java -cp ".;%USERPROFILE%\.m2\repository\com\microsoft\sqlserver\mssql-jdbc\13.4.0.jre11\mssql-jdbc-13.4.0.jre11.jar" FieldNameSync v_b_othercompany +``` + +## 输出示例 + +``` +=== 同步前 === + b_field b_name list edit + ------------------------------------------------------------------ + b_id 系统编号 显示 显示 + b_name 公司全称 显示 显示 + ... + +=== 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 条 + +=== 事务已提交 === +``` + +## 用 AI 助手调用 + +如果通过 AI 助手(如 CodeBuddy)执行,告诉 AI 以下步骤即可: + +> 帮我用 FieldNameSync 工具同步视图 `` 的字段配置。 +> +> 1. 进入 `fms-api/tools/migration/` 目录 +> 2. 用 JDK 21 编译:`javac -cp "mssql-jdbc 驱动 jar 路径" FieldNameSync.java` +> 3. 运行:`java -cp ".;驱动 jar 路径" FieldNameSync ` +> 4. 运行完毕后删除 `FieldNameSync.class` 文件 + +## 已有同步记录 + +| 视图名 | 同步日期 | 字段数 | 备注 | +| --- | --- | --- | --- | +| `v_b_othercompany` | 2026-07-30 | 50 | 已同步 | +| `v_b_contact` | 2026-07-30 | 36 | 已同步 | diff --git a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue index 907dc7fa..04d6005c 100644 --- a/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue +++ b/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue @@ -1,5 +1,15 @@ + + + + diff --git a/code/fms/fms-vue/src/views/base/othercompany/index.vue b/code/fms/fms-vue/src/views/base/othercompany/index.vue index 733005f7..22540d69 100644 --- a/code/fms/fms-vue/src/views/base/othercompany/index.vue +++ b/code/fms/fms-vue/src/views/base/othercompany/index.vue @@ -390,7 +390,7 @@ onMounted(loadCategories);
diff --git a/code/fms/fms-vue/src/views/base/otherdata/index.vue b/code/fms/fms-vue/src/views/base/otherdata/index.vue new file mode 100644 index 00000000..4d67155a --- /dev/null +++ b/code/fms/fms-vue/src/views/base/otherdata/index.vue @@ -0,0 +1,456 @@ + + + + + diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue index 89b4df4c..3779bc95 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleEditConfigPanel.vue @@ -47,12 +47,12 @@ function onRowOrderChange(rows) { emitUpdate(); } -const availableFields = computed(() => { - const options = []; +const fieldNameMap = computed(() => { + const map = new Map(); for (const field of props.fields || []) { - options.push({ label: field.b_name || field.b_field, value: String(field.b_id) }); + map.set(String(field.b_id), field.b_name || field.b_field); } - return options; + return map; }); const groupOptions = computed(() => { @@ -119,9 +119,10 @@ const columns = shallowRef([ dataIndex: "b_field_id", title: "字段", width: 160, - editor: { - type: "select", - options: () => availableFields.value, + customCell: ({ row }) => { + const id = row.b_field_id; + if (id == null) return ""; + return fieldNameMap.value.get(String(id)) || id; }, }, { diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue index 63b1b740..1d22ecfc 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleI18nPanel.vue @@ -302,6 +302,15 @@ function flatten(rows) { return flat; } +// 由父级「同步字段」调用:移除匹配指定 _managedId 的透视行(字段定义已删除时清理残留翻译)。 +function removeByManagedIds(ids) { + if (!ids || ids.size === 0) return; + const idSet = new Set(Array.from(ids).map(String)); + pivotRows.value = pivotRows.value.filter( + (row) => !(row._managedId && idSet.has(String(row._managedId))), + ); +} + // 由父级「同步字段」调用:把默认翻译条目({ key, locale, value, managedId })并入草稿行。 // 已存在同 b_key 的行只补全空值(不覆盖用户配置),避免新增重复键;逻辑见 utils/i18n.mergeI18nDefaults。 function mergeDefaults(entries) { @@ -374,7 +383,7 @@ function markSaved() { originalsFlat.value = cloneData(flatten(pivotRows.value)); } -defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey, reconcileManagedRows }); +defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey, reconcileManagedRows, removeByManagedIds }); const displayedData = computed(() => { const kw = searchKey.value.toLowerCase(); diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue index c3ac0aab..a0a39b2b 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleListConfigPanel.vue @@ -47,12 +47,12 @@ function onRowOrderChange(rows) { emitUpdate(); } -const availableFields = computed(() => { - const options = []; +const fieldNameMap = computed(() => { + const map = new Map(); for (const field of props.fields || []) { - options.push({ label: field.b_name || field.b_field, value: String(field.b_id) }); + map.set(String(field.b_id), field.b_name || field.b_field); } - return options; + return map; }); const groupOptions = computed(() => { @@ -119,12 +119,18 @@ const columns = shallowRef([ dataIndex: "b_field_id", title: "字段", width: 160, - editor: { - type: "select", - options: () => availableFields.value, + customCell: ({ row }) => { + const id = row.b_field_id; + if (id == null) return ""; + return fieldNameMap.value.get(String(id)) || id; }, }, - { dataIndex: "b_width", title: "列宽", width: 80 }, + { + dataIndex: "b_width", + title: "列宽", + width: 80, + editor: { type: "input" }, + }, { dataIndex: "b_group_id", title: "分组", diff --git a/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue b/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue index ea85198c..4f9b60b0 100644 --- a/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue +++ b/code/fms/fms-vue/src/views/module-management/components/ModuleQueryConfigPanel.vue @@ -56,12 +56,12 @@ function onRowOrderChange(rows) { emitUpdate(); } -const availableFields = computed(() => { - const options = []; +const fieldNameMap = computed(() => { + const map = new Map(); for (const field of props.fields || []) { - options.push({ label: field.b_name || field.b_field, value: String(field.b_id) }); + map.set(String(field.b_id), field.b_name || field.b_field); } - return options; + return map; }); function addRow() { @@ -84,9 +84,10 @@ const columns = shallowRef([ dataIndex: "b_field_id", title: "字段", width: 160, - editor: { - type: "select", - options: () => availableFields.value, + customCell: ({ row }) => { + const id = row.b_field_id; + if (id == null) return ""; + return fieldNameMap.value.get(String(id)) || id; }, }, { diff --git a/code/fms/fms-vue/src/views/module-management/index.vue b/code/fms/fms-vue/src/views/module-management/index.vue index f955e905..3e1a79c1 100644 --- a/code/fms/fms-vue/src/views/module-management/index.vue +++ b/code/fms/fms-vue/src/views/module-management/index.vue @@ -38,6 +38,7 @@ const treeLoading = ref(false); const expandedTreeKeys = ref([]); const selectedModuleId = ref(null); const skipLoad = ref(false); +const expandTimer = ref(null); const activeTab = ref("basic"); function buildTreeContextMenu(node) { const items = []; @@ -442,24 +443,54 @@ async function loadModuleConfig(moduleId) { } // ---- 树节点选择 ---- +function clearExpandTimer() { + if (expandTimer.value != null) { + clearTimeout(expandTimer.value); + expandTimer.value = null; + } +} + function onTreeSelect(keys, info) { if (busy.value) return; if (info?.node?.isFmsRoot) { selectedModuleId.value = null; + clearExpandTimer(); return; } const nodeKey = info?.node?.key ?? keys[0]; if (nodeKey == null) return; const key = String(nodeKey); - selectedModuleId.value = key; - const node = findNode(key); - if (!node?.children?.length) return; + const hasChildren = node?.children?.length; + + clearExpandTimer(); + + if (!hasChildren) { + // 叶子节点:立即加载数据 + selectedModuleId.value = key; + return; + } + + // 父节点:处理展开/收起 const expandedKeys = expandedTreeKeys.value.map(String); - expandedTreeKeys.value = expandedKeys.includes(key) - ? expandedKeys.filter((item) => item !== key) - : [...expandedKeys, key]; + const isCurrentlyExpanded = expandedKeys.includes(key); + + if (isCurrentlyExpanded) { + // 收起:立即生效,同时加载数据 + expandedTreeKeys.value = expandedKeys.filter((item) => item !== key); + selectedModuleId.value = key; + } else { + // 展开:立即高亮选中节点,动画完毕后再加载数据 + expandedTreeKeys.value = [...expandedKeys, key]; + skipLoad.value = true; + selectedModuleId.value = key; // 立即高亮,watch 中 skipLoad=true 跳过加载 + expandTimer.value = setTimeout(() => { + expandTimer.value = null; + loadedModuleId.value = null; + void loadModuleConfig(key); + }, 300); + } } watch(selectedModuleId, (id) => { @@ -1009,11 +1040,26 @@ async function syncFields() { } } - // 视图中不存在的已有字段标记为不可用 + // 视图中不存在的已有字段:从字段定义中移除,并清理其在列表/编辑/查询配置中的残留行。 + const removedFieldIds = new Set(); for (const existing of existingFields) { const fieldName = existing.b_field ? existing.b_field.toLowerCase() : ""; if (!viewColumnSet.has(fieldName)) { - mergedFields.push({ ...existing, b_canuse: 0 }); + removedFieldIds.add(String(existing.b_id)); + } + } + if (removedFieldIds.size > 0) { + for (const section of ["listConfig", "editConfig", "queryConfig"]) { + drafts.value[section] = (drafts.value[section] || []).filter( + (row) => !removedFieldIds.has(String(row.b_field_id)), + ); + } + // 多语言面板中移除旧字段对应的受管翻译行(_managedId 格式为 field:) + if (i18nPanelRef.value) { + const managedIds = new Set( + Array.from(removedFieldIds).map((id) => `field:${id}`), + ); + i18nPanelRef.value.removeByManagedIds(managedIds); } } @@ -1125,12 +1171,8 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa :selected-keys="selectedModuleId ? [selectedModuleId] : []" :context-menu="buildTreeContextMenu" :context-menu-disabled="busy" - draggable - :allow-drop="allowDrop" @select="onTreeSelect" @menu-click="onTreeMenuClick" - @drop="onTreeDrop" - @drag-start="onTreeDragStart" >