diff --git a/docs/数据总线模块大致功能与调用介绍.md b/docs/数据总线模块大致功能与调用介绍.md index 41a8e332..2ad33b78 100644 --- a/docs/数据总线模块大致功能与调用介绍.md +++ b/docs/数据总线模块大致功能与调用介绍.md @@ -8,8 +8,8 @@ - **核心特性**: 1. API 全生命周期管理(定义、版本、回滚、发布缓存刷新)。 2. 编排引擎基于 Spring Integration 动态装配,支持 Start/HTTP/RPC/Script/End 步骤及 JSON 变换链路。 - 3. 多重安全防护:IP 白/黑名单、应用凭证、时间戳 + 随机串、报文加解密、签名、防重放、租户隔离、匿名固定用户等。 - 4. QoS 能力:可插拔限流策略(Redis 固定窗口计数)、审计日志、追踪 ID & Step 级结果入库。 + 3. 多重安全防护:**精确 IP 白/黑名单(仅支持单 IP,不支持 CIDR 段)**、应用凭证、时间戳 + 随机串、报文加解密、签名、防重放、租户隔离、匿名固定用户等。 + 4. QoS 能力:可插拔限流策略(Redis 固定窗口计数)、访问日志、追踪 ID & Step 级结果入库。 5. Debug 支持:管理端 `POST /databus/gateway/invoke` 可注入任意参数模拟真实调用。 ## 2. 运行时架构概览 @@ -86,16 +86,17 @@ ## 7. 配置项(`application.yml`)重点 +> 说明:下表均有默认值,除非特别标注“必填”,其余可按需覆盖或直接使用默认。未配置 `allowed-ips` 表示放行所有来源;如开启多租户透传才需设置 `tenant-header`。 + ```yaml databus: api-portal: base-path: /admin-api/databus/api/portal - allowed-ips: [10.0.0.0/24] # 可为空表示全放行 + # 仅支持精确 IP;留空表示全放行。示例:允许两个固定出口 IP + allowed-ips: [10.0.0.12, 10.0.0.13] denied-ips: [] enable-tenant-header: true - tenant-header: ZT-Tenant-Id - enable-audit: true - enable-rate-limit: true + tenant-header: ZT-Tenant-Id # 仅当 enable-tenant-header=true 时才需要配置 security: enabled: true signature-type: MD5 # 或 SHA256 @@ -109,6 +110,12 @@ databus: connection-pool-enabled: true # 默认启用 Reactor Netty 连接池,可在排查连接复用/长连接异常时设为 false ``` +### 必填/必选提示 + +- 无需在 yml 中显式写入必填项;若需要启用租户透传,请同步配置 `tenant-header`。 +- 安全校验依赖“客户端凭证”里的 `encryptionKey/encryptionType/signatureType`;当 `require-body-encryption=true` 且凭证缺少密钥时,调用会失败(HTTP 500 `应用未配置加密密钥`)。 +- 限流能力取决于 API 绑定的策略;未绑定即不做限流。`connection-pool-enabled` 仅用于排障,可保持默认。 + > `GatewaySecurityFilter` 会自动注册到最高优先级 +10,确保该路径的请求先经过安全校验。 关闭连接池后,每次 HTTP Step 请求都会新建 TCP 连接,适合短期定位“连接被复用导致 Reset/超时”的场景,但会带来额外的握手开销;切换时可关注启动日志中的 `Databus gateway WebClient pooling` 提示。 @@ -133,7 +140,7 @@ databus: | 1 | 生成时间戳 | `timestamp = System.currentTimeMillis()`,与服务器时间差 ≤ 300s。 | | 2 | 生成随机串 | `nonce` 长度≥8,可使用 `UUID.randomUUID().toString().replace("-", "")`。 | | 3 | 准备明文 Body | 例如 `{"orderNo":"SO20251120001"}`,记为 `plainBody`。 | -| 4 | 计算签名 | 将所有签名字段放入 Map(详见下节),调用 `CryptoSignatureUtils.verifySignature` 同样的规则:对 key 排序、跳过 `signature` 字段、使用 `&` 连接 `key=value`,再用 `MD5/SHA256` 计算;结果赋值给 `ZT-Signature`。*注意:签名使用明文 body。* | +| 4 | 计算签名 | 使用**明文**请求数据构造签名载荷:Query 参数 + JSON Body 字段(若 Body 非 JSON 则整体放入 `body` 字段)+ `ZT-App-Id` + `ZT-Timestamp` + `ZT-Nonce`,按字典序拼接 `key=value` 以 `&` 连接,使用 `MD5/SHA256` 计算;结果写入 `ZT-Signature`。 | | 5 | 加密请求体 | 使用凭证的 `encryptionKey + encryptionType` 对 `plainBody` 进行对称加密,Base64 结果作为 HTTP Body;Content-Type 可设 `text/plain` 或 `application/json`。 | | 6 | 组装请求头 | `ZT-App-Id`, `ZT-Timestamp`, `ZT-Nonce`, `ZT-Signature`, `ZT-Tenant-Id`(可选), `X-Client-Id`(建议,与限流相关),如有自带 JWT 则设置 `Authorization`。 | | 7 | 发送请求 | URL = `https://{host}{basePath}/{apiCode}/{version}`,方法与 API 定义保持一致。 | @@ -141,14 +148,14 @@ databus: #### 签名字段示例 ```text -appId=demo-app -&body={"orderNo":"SO20251120001"} -&nonce=0c5e2df9a1 -×tamp=1732070400000 +ZT-App-Id=demo-app +&ZT-Nonce=0c5e2df9a1 +&ZT-Timestamp=1732070400000 +&orderNo=SO20251120001 ``` - Query 参数将被拼接为 `key=value`(多值以逗号连接),自动忽略 `signature` 字段。 -- Request Body 若非 JSON,则退化为字符串整体签名。 +- Body 为 JSON 时会按字段展开参与签名;仅在非 JSON 场景下才使用整体报文字符串作为 `body` 字段参与签名。 #### cURL 示例 @@ -183,18 +190,17 @@ curl -X POST "https://gw.example.com/admin-api/databus/api/portal/order.create/v ## 9. 限流策略配置 -- 存储在 `ApiPolicyRateLimitDO.config`,JSON 结构示例: +- 存储在 `ApiPolicyRateLimitDO.config`,仅支持字段: ```json { "limit": 1000, - "windowSeconds": 60, - "keyTemplate": "${apiCode}:${tenantId}:${header.X-Client-Id}" // 预留扩展 + "windowSeconds": 60 } ``` -- 当前默认实现读取 `limit`(默认 100)与 `windowSeconds`(默认 60)。 -- Redis Key 格式:`databus:api:rl:{apiCode}:{version}:{X-Client-Id}`,当计数首次出现时自动设置过期。 +- 当前实现只读取上述两个字段;Key 模板不可配置。 +- Redis Key 固定为:`databus:api:rl:{apiCode}:{version}:{X-Client-Id}`,`X-Client-Id` 缺省时使用 `anonymous`,计数首次出现会自动设置过期。 - 限流拦截后会抛出 `API_RATE_LIMIT_EXCEEDED`,在访问日志中标记 `status=1/2`。 ## 10. 访问日志字段对照 diff --git a/sql/dm/ruoyi-vue-pro-dm8.sql b/sql/dm/ruoyi-vue-pro-dm8.sql index 12a37c4f..6d2da9d3 100644 --- a/sql/dm/ruoyi-vue-pro-dm8.sql +++ b/sql/dm/ruoyi-vue-pro-dm8.sql @@ -688,6 +688,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (75, 1, '接收成功', '10', 'system_sms_receive_status', 0, 'success', '', NULL, '1', '2021-04-11 20:29:25', '1', '2022-02-16 10:28:28', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (76, 2, '接收失败', '20', 'system_sms_receive_status', 0, 'danger', '', NULL, '1', '2021-04-11 20:29:31', '1', '2022-02-16 10:28:32', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (77, 0, '调试(钉钉)', 'DEBUG_DING_TALK', 'system_sms_channel_code', 0, 'info', '', NULL, '1', '2021-04-13 00:20:37', '1', '2022-02-16 10:10:00', '0'); +INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (1592, 5, '鸿联九五', 'HL95', 'system_sms_channel_code', 0, '', '', '', '1', '2025-12-10 00:00:00', '1', '2025-12-10 00:00:00', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (80, 100, '账号登录', '100', 'system_login_type', 0, 'primary', '', '账号登录', '1', '2021-10-06 00:52:02', '1', '2022-02-16 13:11:34', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (81, 101, '社交登录', '101', 'system_login_type', 0, 'info', '', '社交登录', '1', '2021-10-06 00:52:17', '1', '2022-02-16 13:11:40', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (83, 200, '主动登出', '200', 'system_login_type', 0, 'primary', '', '主动登出', '1', '2021-10-06 00:52:58', '1', '2022-02-16 13:11:49', '0'); @@ -1625,7 +1626,7 @@ INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:41', '0'); INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (101, '角色管理', '', 2, 2, 1, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-05-01 18:35:29', '0'); INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (102, '菜单管理', '', 2, 3, 1, 'menu', 'ep:menu', 'system/menu/index', 'SystemMenu', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:03:50', '0'); -INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (103, '部门管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', '0'); +INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (103, '组织管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', '0'); INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (104, '岗位管理', '', 2, 5, 1, 'post', 'fa:address-book-o', 'system/post/index', 'SystemPost', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:39', '0'); INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (105, '字典管理', '', 2, 6, 1, 'dict', 'ep:collection', 'system/dict/index', 'SystemDictType', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:07:12', '0'); INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (106, '配置管理', '', 2, 8, 2, 'config', 'fa:connectdevelop', 'infra/config/index', 'InfraConfig', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-04-23 00:02:45', '0'); @@ -3936,6 +3937,7 @@ COMMIT; CREATE TABLE system_sms_channel ( id bigint NOT NULL PRIMARY KEY, signature varchar(12) NOT NULL, + epid varchar(64) DEFAULT NULL NULL, code varchar(63) NOT NULL, status smallint NOT NULL, remark varchar(255) DEFAULT NULL NULL, @@ -3951,6 +3953,7 @@ CREATE TABLE system_sms_channel ( COMMENT ON COLUMN system_sms_channel.id IS '编号'; COMMENT ON COLUMN system_sms_channel.signature IS '短信签名'; +COMMENT ON COLUMN system_sms_channel.epid IS '企业编号(epid)'; COMMENT ON COLUMN system_sms_channel.code IS '渠道编码'; COMMENT ON COLUMN system_sms_channel.status IS '开启状态'; COMMENT ON COLUMN system_sms_channel.remark IS '备注'; @@ -3969,9 +3972,9 @@ COMMENT ON TABLE system_sms_channel IS '短信渠道'; -- ---------------------------- -- @formatter:off -- SET IDENTITY_INSERT system_sms_channel ON; -INSERT INTO system_sms_channel (id, signature, code, status, remark, api_key, api_secret, callback_url, creator, create_time, updater, update_time, deleted) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', '0'); -INSERT INTO system_sms_channel (id, signature, code, status, remark, api_key, api_secret, callback_url, creator, create_time, updater, update_time, deleted) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', '0'); -INSERT INTO system_sms_channel (id, signature, code, status, remark, api_key, api_secret, callback_url, creator, create_time, updater, update_time, deleted) VALUES (7, 'mock腾讯云', 'TENCENT', 0, '', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2024-09-30 08:55:01', '0'); +INSERT INTO system_sms_channel (id, signature, epid, code, status, remark, api_key, api_secret, callback_url, creator, create_time, updater, update_time, deleted) VALUES (2, 'Ballcat', NULL, 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', '0'); +INSERT INTO system_sms_channel (id, signature, epid, code, status, remark, api_key, api_secret, callback_url, creator, create_time, updater, update_time, deleted) VALUES (4, '测试渠道', NULL, 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', '0'); +INSERT INTO system_sms_channel (id, signature, epid, code, status, remark, api_key, api_secret, callback_url, creator, create_time, updater, update_time, deleted) VALUES (7, 'mock腾讯云', NULL, 'TENCENT', 0, '', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2024-09-30 08:55:01', '0'); COMMIT; -- SET IDENTITY_INSERT system_sms_channel OFF; -- @formatter:on diff --git a/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql b/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql new file mode 100644 index 00000000..ab9dcc19 --- /dev/null +++ b/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql @@ -0,0 +1,45 @@ +/* + * Databus API 凭证绑定与访问日志补充字段(DM8) + * Generated on 2025-12-09 + */ + +-- ---------------------------- +-- Table structure for databus_api_definition_credential +-- ---------------------------- +CREATE TABLE databus_api_definition_credential ( + id BIGINT NOT NULL PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + api_id BIGINT NOT NULL, + credential_id BIGINT NOT NULL, + app_id VARCHAR(128), + creator VARCHAR(64) DEFAULT '' NOT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater VARCHAR(64) DEFAULT '' NOT NULL, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted BIT DEFAULT '0' NOT NULL +); + +CREATE UNIQUE INDEX uk_databus_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted); +CREATE INDEX idx_databus_api_definition_credential_api ON databus_api_definition_credential (api_id); +CREATE INDEX idx_databus_api_definition_credential_cred ON databus_api_definition_credential (credential_id); + +COMMENT ON TABLE databus_api_definition_credential IS 'Databus API 凭证绑定表'; +COMMENT ON COLUMN databus_api_definition_credential.id IS '主键 ID'; +COMMENT ON COLUMN databus_api_definition_credential.tenant_id IS '租户编号'; +COMMENT ON COLUMN databus_api_definition_credential.api_id IS 'API 定义 ID'; +COMMENT ON COLUMN databus_api_definition_credential.credential_id IS '凭证 ID'; +COMMENT ON COLUMN databus_api_definition_credential.app_id IS '凭证应用标识冗余'; +COMMENT ON COLUMN databus_api_definition_credential.creator IS '创建者'; +COMMENT ON COLUMN databus_api_definition_credential.create_time IS '创建时间'; +COMMENT ON COLUMN databus_api_definition_credential.updater IS '更新者'; +COMMENT ON COLUMN databus_api_definition_credential.update_time IS '更新时间'; +COMMENT ON COLUMN databus_api_definition_credential.deleted IS '逻辑删除标记'; + +-- ---------------------------- +-- Alter databus_api_access_log add credential columns +-- ---------------------------- +ALTER TABLE databus_api_access_log ADD credential_app_id VARCHAR(128); +COMMENT ON COLUMN databus_api_access_log.credential_app_id IS '调用凭证应用标识'; + +ALTER TABLE databus_api_access_log ADD credential_id BIGINT; +COMMENT ON COLUMN databus_api_access_log.credential_id IS '调用凭证 ID'; diff --git a/sql/dm/短信渠道鸿联九五支持_DM8_20251210.sql b/sql/dm/短信渠道鸿联九五支持_DM8_20251210.sql new file mode 100644 index 00000000..008661ad --- /dev/null +++ b/sql/dm/短信渠道鸿联九五支持_DM8_20251210.sql @@ -0,0 +1,9 @@ +-- 短信渠道新增鸿联九五支持(达梦8) +-- 1) system_sms_channel 表新增 epid 字段 +-- 2) system_sms_channel_code 字典新增 HL95 选项 + +ALTER TABLE system_sms_channel ADD COLUMN epid VARCHAR(64); +COMMENT ON COLUMN system_sms_channel.epid IS '企业编号(epid)'; + +INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) +VALUES (1592, 5, '鸿联九五', 'HL95', 'system_sms_channel_code', 0, '', '', '', '1', '2025-12-10 00:00:00', '1', '2025-12-10 00:00:00', '0'); diff --git a/sql/mysql/databus_api_credential_20250512.sql b/sql/mysql/databus_api_credential_20250512.sql new file mode 100644 index 00000000..f7355dd9 --- /dev/null +++ b/sql/mysql/databus_api_credential_20250512.sql @@ -0,0 +1,25 @@ +-- Databus API 凭证绑定 & 访问日志补充字段 + +-- API 与客户凭证绑定表 +CREATE TABLE IF NOT EXISTS `databus_api_definition_credential` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `api_id` bigint NOT NULL COMMENT 'API 定义 ID', + `credential_id` bigint NOT NULL COMMENT '凭证 ID', + `app_id` varchar(128) DEFAULT NULL COMMENT '凭证应用标识冗余', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_api_credential` (`api_id`, `credential_id`, `deleted`), + KEY `idx_api_id` (`api_id`), + KEY `idx_credential_id` (`credential_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Databus API 凭证绑定表'; + +-- 访问日志补充凭证信息 +ALTER TABLE `databus_api_access_log` + ADD COLUMN IF NOT EXISTS `credential_app_id` varchar(128) NULL COMMENT '调用凭证应用标识' AFTER `api_version`; +ALTER TABLE `databus_api_access_log` + ADD COLUMN IF NOT EXISTS `credential_id` bigint NULL COMMENT '调用凭证 ID' AFTER `credential_app_id`; diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 56b702bb..c53fcb8f 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -871,6 +871,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1589, 10, 'BPMN 设计器', '10', 'bpm_model_type', 0, 'primary', '', '', '1', '2024-08-26 15:22:17', '1', '2024-08-26 16:46:02', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1590, 20, 'SIMPLE 设计器', '20', 'bpm_model_type', 0, 'success', '', '', '1', '2024-08-26 15:22:27', '1', '2024-08-26 16:45:58', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1591, 4, '七牛云', 'QINIU', 'system_sms_channel_code', 0, '', '', '', '1', '2024-08-31 08:45:03', '1', '2024-08-31 08:45:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1592, 5, '鸿联九五', 'HL95', 'system_sms_channel_code', 0, '', '', '', '1', '2025-12-10 00:00:00', '1', '2025-12-10 00:00:00', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1592, 3, '新人券', '3', 'promotion_coupon_take_type', 0, 'info', '', '新人注册后,自动发放', '1', '2024-09-03 11:57:16', '1', '2024-09-03 11:57:28', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1593, 5, '微信零钱', '5', 'brokerage_withdraw_type', 0, '', '', 'API 打款', '1', '2024-10-13 11:06:48', '1', '2025-05-10 08:24:55', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1683, 10, '字节豆包', 'DouBao', 'ai_platform', 0, '', '', '', '1', '2025-02-23 19:51:40', '1', '2025-02-23 19:52:02', b'0'); @@ -3509,6 +3510,7 @@ DROP TABLE IF EXISTS `system_sms_channel`; CREATE TABLE `system_sms_channel` ( `id` bigint NOT NULL COMMENT '编号', `signature` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信签名', + `epid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '企业编号(epid)', `code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '渠道编码', `status` tinyint NOT NULL COMMENT '开启状态', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', @@ -3527,9 +3529,9 @@ CREATE TABLE `system_sms_channel` ( -- Records of system_sms_channel -- ---------------------------- BEGIN; -INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', b'0'); -INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0'); -INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'mock腾讯云', 'TENCENT', 0, '', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2024-09-30 08:55:01', b'0'); +INSERT INTO `system_sms_channel` (`id`, `signature`, `epid`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', NULL, 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', b'0'); +INSERT INTO `system_sms_channel` (`id`, `signature`, `epid`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', NULL, 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0'); +INSERT INTO `system_sms_channel` (`id`, `signature`, `epid`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'mock腾讯云', NULL, 'TENCENT', 0, '', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2024-09-30 08:55:01', b'0'); COMMIT; -- ---------------------------- diff --git a/zt-dependencies/pom.xml b/zt-dependencies/pom.xml index cf5b81fe..b0a132d9 100644 --- a/zt-dependencies/pom.xml +++ b/zt-dependencies/pom.xml @@ -32,8 +32,6 @@ 3.4.5 2024.0.1 2023.0.3.2 - - 2.4.0 2.8.3 4.6.0 @@ -89,8 +87,6 @@ 1.2.5 0.9.0 4.12.0 - 11.4.7 - 11.4.7 2.15.1 4.5.13 @@ -136,20 +132,6 @@ import - - - - - org.apache.seata - seata-all - ${seata.version} - - - org.apache.seata - seata-spring-boot-starter - ${seata.version} - - io.github.mouzt diff --git a/zt-framework/zt-spring-boot-starter-test/src/main/java/com/zt/plat/framework/test/core/ut/BaseDbUnitTest.java b/zt-framework/zt-spring-boot-starter-test/src/main/java/com/zt/plat/framework/test/core/ut/BaseDbUnitTest.java index 1bbad906..7e2535c5 100644 --- a/zt-framework/zt-spring-boot-starter-test/src/main/java/com/zt/plat/framework/test/core/ut/BaseDbUnitTest.java +++ b/zt-framework/zt-spring-boot-starter-test/src/main/java/com/zt/plat/framework/test/core/ut/BaseDbUnitTest.java @@ -5,6 +5,7 @@ import com.zt.plat.framework.common.biz.system.sequence.SequenceCommonApi; import com.zt.plat.framework.datasource.config.ZtDataSourceAutoConfiguration; import com.zt.plat.framework.mybatis.config.ZtMybatisAutoConfiguration; import com.zt.plat.framework.test.config.SqlInitializationTestConfiguration; +import com.zt.plat.module.system.mq.producer.databus.DatabusChangeProducer; import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration; @@ -35,6 +36,12 @@ public class BaseDbUnitTest { @MockBean private FeignClientFactory feignClientFactory; + /** + * Databus 变更生产者在多数 Service 中依赖,但单测无需真正发送 MQ,使用 MockBean 降低装配成本。 + */ + @MockBean + private DatabusChangeProducer databusChangeProducer; + @Import({ // DB 配置类 ZtDataSourceAutoConfiguration.class, // 自己的 DB 配置类 diff --git a/zt-framework/zt-spring-boot-starter-test/src/main/java/com/zt/plat/module/system/mq/producer/databus/DatabusChangeProducer.java b/zt-framework/zt-spring-boot-starter-test/src/main/java/com/zt/plat/module/system/mq/producer/databus/DatabusChangeProducer.java new file mode 100644 index 00000000..a0f10b15 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-test/src/main/java/com/zt/plat/module/system/mq/producer/databus/DatabusChangeProducer.java @@ -0,0 +1,8 @@ +package com.zt.plat.module.system.mq.producer.databus; + +/** + * Minimal placeholder to allow test starter to mock DatabusChangeProducer without depending on system module. + * Real implementation lives in zt-module-system-server. + */ +public class DatabusChangeProducer { +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java index cc2736bd..3bc46e3d 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java @@ -7,6 +7,7 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.*; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCredentialBinding; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; @@ -18,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; @Mapper @@ -48,6 +50,11 @@ public interface ApiDefinitionConvert { detail.setApiLevelTransforms(convertTransforms(aggregate.getDefinition().getId(), aggregate.getApiLevelTransforms().values())); detail.setSteps(convertSteps(aggregate.getSteps())); detail.setPublication(convert(aggregate.getPublication())); + detail.setCredentialBindings(convertCredentialBindings(aggregate.getCredentialBindings())); + detail.setCredentialIds(detail.getCredentialBindings().stream() + .map(ApiCredentialBindingRespVO::getCredentialId) + .filter(Objects::nonNull) + .collect(Collectors.toList())); return detail; } @@ -99,6 +106,15 @@ public interface ApiDefinitionConvert { return publication == null ? null : BeanUtils.toBean(publication, ApiDefinitionPublicationRespVO.class); } + default List convertCredentialBindings(List bindings) { + if (CollUtil.isEmpty(bindings)) { + return new ArrayList<>(); + } + return bindings.stream() + .map(binding -> BeanUtils.toBean(binding, ApiCredentialBindingRespVO.class)) + .collect(Collectors.toList()); + } + /** * 转换步骤列表(DO -> SaveReqVO) */ diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java index 7d012a93..879304a6 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java @@ -47,6 +47,12 @@ public class ApiAccessLogPageReqVO extends PageParam { @Schema(description = "请求路径", example = "/gateway/api/user/query") private String requestPath; + @Schema(description = "应用标识", example = "app-portal-01") + private String credentialAppId; + + @Schema(description = "凭证主键", example = "10086") + private Long credentialId; + @Schema(description = "请求时间区间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] requestTime; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java index 00126a9a..a439724c 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java @@ -33,6 +33,12 @@ public class ApiAccessLogRespVO { @Schema(description = "请求路径", example = "/gateway/api/user/query") private String requestPath; + @Schema(description = "应用标识", example = "app-portal-01") + private String credentialAppId; + + @Schema(description = "凭证主键", example = "10086") + private Long credentialId; + @Schema(description = "查询参数(JSON)") private String requestQuery; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiCredentialBindingRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiCredentialBindingRespVO.java new file mode 100644 index 00000000..1eb4f7ed --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiCredentialBindingRespVO.java @@ -0,0 +1,17 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.definition; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ApiCredentialBindingRespVO { + + @Schema(description = "凭证主键", example = "10086") + private Long credentialId; + + @Schema(description = "应用标识", example = "app-portal-01") + private String appId; + + @Schema(description = "应用名称") + private String appName; +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java index c64ba1ec..37384ad2 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java @@ -53,6 +53,12 @@ public class ApiDefinitionDetailRespVO { @Schema(description = "API 级别变换列表") private List apiLevelTransforms = new ArrayList<>(); + @Schema(description = "授权凭证 ID 列表") + private List credentialIds = new ArrayList<>(); + + @Schema(description = "授权凭证详情列表") + private List credentialBindings = new ArrayList<>(); + @Schema(description = "步骤列表") private List steps = new ArrayList<>(); diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java index afd54dca..a0375294 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java @@ -46,6 +46,9 @@ public class ApiDefinitionSaveReqVO { @Valid private List apiLevelTransforms = new ArrayList<>(); + @Schema(description = "授权的客户端凭证 ID 列表") + private List credentialIds = new ArrayList<>(); + @Schema(description = "步骤列表") @NotEmpty(message = "编排步骤不能为空") @Valid diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java index f6ab6f90..e46b5507 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java @@ -52,6 +52,16 @@ public class ApiAccessLogDO extends TenantBaseDO { */ private String requestPath; + /** + * 调用使用的应用标识 + */ + private String credentialAppId; + + /** + * 调用使用的凭证主键 + */ + private Long credentialId; + /** * 查询参数(JSON 字符串) */ diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionCredentialDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionCredentialDO.java new file mode 100644 index 00000000..bfd11c89 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionCredentialDO.java @@ -0,0 +1,37 @@ +package com.zt.plat.module.databus.dal.dataobject.gateway; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zt.plat.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * API 与客户端凭证的授权关联。 + */ +@Data +@TableName("databus_api_definition_credential") +@KeySequence("databus_api_definition_credential_seq") +@EqualsAndHashCode(callSuper = true) +public class ApiDefinitionCredentialDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * API 主键 + */ + private Long apiId; + + /** + * 客户端凭证主键 + */ + private Long credentialId; + + /** + * 绑定时的应用标识冗余,便于快速校验 + */ + private String appId; +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java index 8d58239b..8fe6d4d9 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java @@ -20,6 +20,8 @@ public interface ApiAccessLogMapper extends BaseMapperX { .eqIfPresent(ApiAccessLogDO::getResponseStatus, reqVO.getResponseStatus()) .eqIfPresent(ApiAccessLogDO::getStatus, reqVO.getStatus()) .likeIfPresent(ApiAccessLogDO::getClientIp, reqVO.getClientIp()) + .eqIfPresent(ApiAccessLogDO::getCredentialAppId, reqVO.getCredentialAppId()) + .eqIfPresent(ApiAccessLogDO::getCredentialId, reqVO.getCredentialId()) .eqIfPresent(ApiAccessLogDO::getTenantId, reqVO.getTenantId()) .likeIfPresent(ApiAccessLogDO::getRequestPath, reqVO.getRequestPath()); if (ArrayUtil.isNotEmpty(reqVO.getRequestTime()) && reqVO.getRequestTime().length == 2) { diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionCredentialMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionCredentialMapper.java new file mode 100644 index 00000000..6ed1552d --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionCredentialMapper.java @@ -0,0 +1,32 @@ +package com.zt.plat.module.databus.dal.mysql.gateway; + +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionCredentialDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ApiDefinitionCredentialMapper extends BaseMapperX { + + default List selectByApiId(Long apiId) { + return selectList(new LambdaQueryWrapperX() + .eq(ApiDefinitionCredentialDO::getApiId, apiId)); + } + + default void deleteByApiId(Long apiId) { + delete(new LambdaQueryWrapperX() + .eq(ApiDefinitionCredentialDO::getApiId, apiId)); + } + + /** + * 按 API 逻辑删除已有绑定,保留操作记录。 + */ + default void logicDeleteByApiId(Long apiId) { + ApiDefinitionCredentialDO entity = new ApiDefinitionCredentialDO(); + entity.setDeleted(Boolean.TRUE); + update(entity, new LambdaQueryWrapperX() + .eq(ApiDefinitionCredentialDO::getApiId, apiId)); + } +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java index bce32b79..b0ed9987 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java @@ -83,7 +83,7 @@ public class ApiGatewayAccessLogger { try { ApiAccessLogDO update = new ApiAccessLogDO(); update.setId(logId); - int responseStatus = resolveHttpStatus(context); + Integer responseStatus = resolveHttpStatus(context); context.setResponseStatus(responseStatus); update.setResponseStatus(responseStatus); String responseMessage = resolveResponseMessage(context, responseStatus); @@ -193,6 +193,8 @@ public class ApiGatewayAccessLogger { logDO.setRequestBody(toJson(context.getRequestBody())); logDO.setClientIp(context.getClientIp()); logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT)); + logDO.setCredentialAppId(context.getCredentialAppId()); + logDO.setCredentialId(context.getCredentialId()); logDO.setStatus(3); logDO.setRequestTime(toLocalDateTime(context.getRequestTime())); logDO.setTenantId(parseTenantId(context.getTenantId())); @@ -231,7 +233,7 @@ public class ApiGatewayAccessLogger { return 3; } - private String resolveErrorMessage(ApiInvocationContext context, int responseStatus) { + private String resolveErrorMessage(ApiInvocationContext context, Integer responseStatus) { if (!isErrorStatus(responseStatus)) { return null; } @@ -248,7 +250,7 @@ public class ApiGatewayAccessLogger { return null; } - private String extractErrorCode(Object responseBody, int responseStatus) { + private String extractErrorCode(Object responseBody, Integer responseStatus) { if (!isErrorStatus(responseStatus)) { return null; } @@ -259,16 +261,14 @@ public class ApiGatewayAccessLogger { return null; } - private int resolveHttpStatus(ApiInvocationContext context) { - Integer status = context.getResponseStatus(); - if (status != null) { - return status; - } - // 默认兜底为 200,避免日志中出现空的 HTTP 状态码 - return HttpStatus.OK.value(); + private Integer resolveHttpStatus(ApiInvocationContext context) { + return context.getResponseStatus(); } - private String resolveResponseMessage(ApiInvocationContext context, int responseStatus) { + private String resolveResponseMessage(ApiInvocationContext context, Integer responseStatus) { + if (responseStatus == null) { + return null; + } if (StringUtils.hasText(context.getResponseMessage())) { return truncate(context.getResponseMessage()); } @@ -276,8 +276,8 @@ public class ApiGatewayAccessLogger { return resolved != null ? resolved.getReasonPhrase() : null; } - private boolean isErrorStatus(int responseStatus) { - return responseStatus >= 400; + private boolean isErrorStatus(Integer responseStatus) { + return responseStatus != null && responseStatus >= 400; } private Map buildExtra(ApiInvocationContext context) { diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java index e7b9dbcb..f24dfc08 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.Map; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_AUTH_UNAUTHORIZED; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_UNAUTHORIZED; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED; /** @@ -80,6 +81,9 @@ public class ApiGatewayErrorProcessor { if (API_RATE_LIMIT_EXCEEDED.getCode().equals(code)) { return HttpStatus.TOO_MANY_REQUESTS.value(); } + if (API_CREDENTIAL_UNAUTHORIZED.getCode().equals(code)) { + return HttpStatus.FORBIDDEN.value(); + } } return HttpStatus.INTERNAL_SERVER_ERROR.value(); } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java index 8f442153..14a5ed11 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java @@ -7,6 +7,7 @@ import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.framework.common.util.monitor.TracerUtils; import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO; import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCredentialBinding; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; @@ -19,6 +20,7 @@ import org.springframework.http.*; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.util.UriComponentsBuilder; @@ -29,12 +31,12 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_UNAUTHORIZED; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_DEFINITION_NOT_FOUND; /** - * Orchestrates API portal request mapping, dispatch and response building so that - * management-side debug invocations and external HTTP requests share identical - * behaviour (other than security concerns handled by {@link GatewaySecurityFilter}). + * 统一处理 API 门户的请求映射、分发与响应构建。 + * 管理端调试与外部 HTTP 请求共享同一套逻辑,安全校验由 {@link GatewaySecurityFilter} 执行。 */ @Slf4j @Component @@ -58,7 +60,7 @@ public class ApiGatewayExecutionService { private final ApiDefinitionService apiDefinitionService; /** - * Maps a raw HTTP message (as provided by Spring Integration) into a context message. + * 将 Spring Integration 提供的原始消息映射为网关上下文消息。 */ public Message mapRequest(Message message) { ApiInvocationContext context = requestMapper.map(message.getPayload(), message.getHeaders()); @@ -70,7 +72,7 @@ public class ApiGatewayExecutionService { } /** - * Dispatches the API invocation and applies gateway error processing rules on failure scenarios. + * 分发 API 调用,并在异常场景套用统一错误处理。 */ public ApiInvocationContext dispatch(Message message) { ApiInvocationContext context = message.getPayload(); @@ -78,6 +80,7 @@ public class ApiGatewayExecutionService { ApiInvocationContext responseContext; ApiDefinitionAggregate debugAggregate = null; try { + enforceCredentialAuthorization(context); if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) { debugAggregate = resolveDebugAggregate(context); } @@ -128,7 +131,7 @@ public class ApiGatewayExecutionService { Message rawMessage = buildDebugMessage(reqVO); Message mappedMessage = mapRequest(rawMessage); ApiInvocationContext context = mappedMessage.getPayload(); - // Ensure query parameters & headers from debug payload are reflected after mapping. + // 将调试透传的查询参数、请求头重新合并到上下文,避免映射阶段丢失 mergeDebugMetadata(context, reqVO); context.getAttributes().put(ATTR_DEBUG_INVOKE, Boolean.TRUE); ApiInvocationContext responseContext = dispatch(mappedMessage); @@ -155,7 +158,7 @@ public class ApiGatewayExecutionService { builder.setHeader(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables); builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, HttpMethod.POST.name()); - String basePath = normalizeBasePath(properties.getBasePath()); + String basePath = properties.getBasePath(); String rawQuery = buildQueryString(reqVO.getQueryParams()); String requestUri = basePath + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); if (StringUtils.hasText(rawQuery)) { @@ -223,8 +226,7 @@ public class ApiGatewayExecutionService { context.setHttpMethod(HttpMethod.POST.name()); } if (!StringUtils.hasText(context.getRequestPath())) { - String basePath = normalizeBasePath(properties.getBasePath()); - String path = basePath + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); + String path = properties.getBasePath() + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); context.setRequestPath(path); } } @@ -245,15 +247,29 @@ public class ApiGatewayExecutionService { .build(); } - private String normalizeBasePath(String basePath) { - if (!StringUtils.hasText(basePath)) { - return ApiGatewayProperties.DEFAULT_BASE_PATH; + /** + * 调用前校验凭证白名单,非调试调用需匹配绑定的 appId。 + */ + private void enforceCredentialAuthorization(ApiInvocationContext context) { + if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) { + return; } - String normalized = basePath.startsWith("/") ? basePath : "/" + basePath; - while (normalized.endsWith("/") && normalized.length() > 1) { - normalized = normalized.substring(0, normalized.length() - 1); + ApiDefinitionAggregate aggregate = apiDefinitionService.findByCodeAndVersion(context.getApiCode(), context.getApiVersion()) + .orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND)); + if (CollectionUtils.isEmpty(aggregate.getCredentialBindings())) { + return; + } + String appId = context.getCredentialAppId(); + if (!StringUtils.hasText(appId)) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED); + } + boolean matched = aggregate.getCredentialBindings().stream() + .map(ApiCredentialBinding::getAppId) + .filter(StringUtils::hasText) + .anyMatch(boundAppId -> appId.trim().equalsIgnoreCase(boundAppId)); + if (!matched) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED); } - return normalized; } private String buildQueryString(Map queryParams) { @@ -322,4 +338,5 @@ public class ApiGatewayExecutionService { } return null; } + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java index 5d813d52..1c4ba0da 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java @@ -35,6 +35,7 @@ public class ApiGatewayRequestMapper { private static final String HEADER_REQUEST_HEADERS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders"; private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri"; private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress"; + private static final String HEADER_CREDENTIAL_ID = "X-Databus-Credential-Id"; @SuppressWarnings("unchecked") public ApiInvocationContext map(Object payload, Map headers) { @@ -79,18 +80,29 @@ public class ApiGatewayRequestMapper { } Map requestHeaders = (Map) headers.get(HEADER_REQUEST_HEADERS); - GatewayHeaderUtils.mergeNormalizedHeaders(requestHeaders, context.getRequestHeaders()); + if (requestHeaders != null) { + context.getRequestHeaders().putAll(requestHeaders); + } headers.forEach((key, value) -> { - if (isInternalHeader(key)) { + if (isInternalHeader(key) || value == null) { return; } - String normalized = GatewayHeaderUtils.normalizeHeaderValue(value); - if (normalized != null) { - context.getRequestHeaders().putIfAbsent(key, normalized); - } + context.getRequestHeaders().putIfAbsent(key, String.valueOf(value)); }); context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT)); context.setClientIp(resolveClientIp(headers, context.getRequestHeaders())); + String appId = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), ApiGatewayProperties.APP_ID_HEADER); + if (StringUtils.hasText(appId)) { + context.setCredentialAppId(appId.trim()); + } + String credentialIdHeader = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HEADER_CREDENTIAL_ID); + if (StringUtils.hasText(credentialIdHeader)) { + try { + context.setCredentialId(Long.valueOf(credentialIdHeader.trim())); + } catch (NumberFormatException ignored) { + context.setCredentialId(null); + } + } captureAccessLogId(context); populateQueryParams(headers, context, originalRequestUri); if (properties.isEnableTenantHeader()) { diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiCredentialBinding.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiCredentialBinding.java new file mode 100644 index 00000000..6fd70b47 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiCredentialBinding.java @@ -0,0 +1,18 @@ +package com.zt.plat.module.databus.framework.integration.gateway.domain; + +import lombok.Builder; +import lombok.Value; + +/** + * API 授权绑定的凭证信息。 + */ +@Value +@Builder +public class ApiCredentialBinding { + + Long credentialId; + + String appId; + + String appName; +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java index 24121989..730bc36a 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java @@ -26,6 +26,8 @@ public class ApiDefinitionAggregate { ApiFlowPublication publication; + List credentialBindings; + public List getSteps() { return steps == null ? Collections.emptyList() : steps; } @@ -34,4 +36,8 @@ public class ApiDefinitionAggregate { return apiLevelTransforms == null ? Collections.emptyMap() : apiLevelTransforms; } + public List getCredentialBindings() { + return credentialBindings == null ? Collections.emptyList() : credentialBindings; + } + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java index a17dc89f..f27b4fbf 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java @@ -29,6 +29,10 @@ public class ApiInvocationContext { private String userAgent; + private String credentialAppId; + + private Long credentialId; + private String httpMethod; private String requestPath; @@ -72,6 +76,8 @@ public class ApiInvocationContext { copy.tenantId = this.tenantId; copy.clientIp = this.clientIp; copy.userAgent = this.userAgent; + copy.credentialAppId = this.credentialAppId; + copy.credentialId = this.credentialId; copy.httpMethod = this.httpMethod; copy.requestPath = this.requestPath; copy.requestBody = this.requestBody; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java index f722c8f1..0048bf19 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java @@ -15,11 +15,11 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -104,7 +104,7 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { return; } if (!StringUtils.hasText(value)) { - additionalHeaders.remove(name); + removeHeader(name); return; } additionalHeaders.put(name, new ArrayList<>(Collections.singletonList(value))); @@ -115,7 +115,7 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { return; } if (CollectionUtils.isEmpty(values)) { - additionalHeaders.remove(name); + removeHeader(name); return; } additionalHeaders.put(name, new ArrayList<>(values)); @@ -125,7 +125,7 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { if (!StringUtils.hasText(name) || !StringUtils.hasText(value)) { return; } - additionalHeaders.compute(name, (key, existing) -> { + additionalHeaders.compute(name, (k, existing) -> { List list = existing == null ? new ArrayList<>() : new ArrayList<>(existing); list.add(value); return list; @@ -152,23 +152,20 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { @Override public Enumeration getHeaders(String name) { - List combined = new ArrayList<>(); if (StringUtils.hasText(name)) { List custom = additionalHeaders.get(name); + // 如果自定义头已写入,则直接返回,避免与原始头部合并造成重复 if (!CollectionUtils.isEmpty(custom)) { - combined.addAll(custom); + return Collections.enumeration(custom); } } - Enumeration parent = super.getHeaders(name); - while (parent.hasMoreElements()) { - combined.add(parent.nextElement()); - } - return Collections.enumeration(combined); + return super.getHeaders(name); } @Override public Enumeration getHeaderNames() { - Set names = new LinkedHashSet<>(); + // 使用忽略大小写的集合,避免父请求头名与自定义头名大小写不同而重复 + Set names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); Enumeration parent = super.getHeaderNames(); while (parent.hasMoreElements()) { names.add(parent.nextElement()); diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java index 6698602b..6692f40d 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java @@ -61,6 +61,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private static final TypeReference> MAP_TYPE = new TypeReference<>() { }; + public static final String HEADER_CREDENTIAL_ID = "X-Databus-Credential-Id"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -133,6 +134,8 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { // 使用可重复读取的请求包装,供后续过滤器继续消费 CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody); + securedRequest.setHeader(APP_ID_HEADER, credential.getAppId()); + securedRequest.setHeader(HEADER_CREDENTIAL_ID, credential.getId() != null ? String.valueOf(credential.getId()) : null); ApiGatewayAccessLogger.propagateLogIdHeader(securedRequest, accessLogId); if (StringUtils.hasText(request.getCharacterEncoding())) { securedRequest.setCharacterEncoding(request.getCharacterEncoding()); @@ -283,7 +286,8 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { try { boolean valid = CryptoSignatureUtils.verifySignature(signaturePayload, signatureType); if (!valid) { -// throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "签名校验失败"); + log.error("[API-PORTAL] 签名校验失败"); + return; } } catch (IllegalArgumentException ex) { throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "签名算法配置异常"); diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java index 4c147b19..9347fe3d 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java @@ -4,8 +4,6 @@ import cn.hutool.core.collection.CollUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.LoadingCache; import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.util.object.BeanUtils; @@ -19,6 +17,7 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefi import com.zt.plat.module.databus.dal.dataobject.gateway.*; import com.zt.plat.module.databus.dal.mysql.gateway.*; import com.zt.plat.module.databus.enums.gateway.ApiStatusEnum; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCredentialBinding; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; @@ -26,20 +25,17 @@ import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiTransf import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; import com.zt.plat.module.databus.service.gateway.ApiVersionService; import com.zt.plat.module.databus.service.gateway.ApiVersionSnapshotContextHolder; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.dao.DataAccessException; -import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.time.Duration; import java.util.*; -import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.*; @@ -48,27 +44,16 @@ import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErro @RequiredArgsConstructor public class ApiDefinitionServiceImpl implements ApiDefinitionService { - private static final String REDIS_CACHE_PREFIX = "databus:api:def:"; - private final ApiDefinitionMapper apiDefinitionMapper; private final ApiStepMapper apiStepMapper; private final ApiTransformMapper apiTransformMapper; private final ApiPolicyRateLimitMapper apiPolicyRateLimitMapper; private final ApiFlowPublishMapper apiFlowPublishMapper; + private final ApiDefinitionCredentialMapper apiDefinitionCredentialMapper; + private final ApiClientCredentialMapper apiClientCredentialMapper; private final ObjectMapper objectMapper; - private final StringRedisTemplate stringRedisTemplate; private final ObjectProvider apiVersionServiceProvider; - private LoadingCache> definitionCache; - - @PostConstruct - public void initCache() { - definitionCache = Caffeine.newBuilder() - .maximumSize(512) - .expireAfterWrite(Duration.ofMinutes(5)) - .build(this::loadAggregateSync); - } - @Override public List loadActiveDefinitions() { List definitions = apiDefinitionMapper.selectActiveDefinitions(Collections.singletonList(ApiStatusEnum.ONLINE.getStatus())); @@ -84,12 +69,9 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { @Override public Optional findByCodeAndVersion(String apiCode, String version) { - String cacheKey = buildCacheKey(apiCode, version); - try { - return definitionCache.get(cacheKey); - } catch (RuntimeException ex) { - throw ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND); - } + return apiDefinitionMapper.selectByCodeAndVersion(apiCode, version) + .filter(definition -> ApiStatusEnum.isOnline(definition.getStatus())) + .map(this::buildAggregate); } @Override @@ -100,16 +82,12 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { @Override public Optional refresh(String apiCode, String version) { - String cacheKey = buildCacheKey(apiCode, version); - definitionCache.invalidate(cacheKey); - deleteRedis(cacheKey); return findByCodeAndVersion(apiCode, version); } @Override public void refreshAllCache() { - definitionCache.invalidateAll(); - clearRedisCacheForTenant(TenantContextHolder.getTenantId()); + // 缓存已移除,此处留空以保持接口兼容 } @Override @@ -129,6 +107,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { validateDuplication(reqVO, null); validateStructure(reqVO); validatePolicies(reqVO); + validateCredentials(reqVO.getCredentialIds()); ApiDefinitionDO definition = buildDefinitionDO(reqVO, null); apiDefinitionMapper.insert(definition); @@ -136,6 +115,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { persistApiLevelTransforms(apiId, reqVO.getApiLevelTransforms()); persistSteps(apiId, reqVO.getSteps()); + persistCredentialBindings(apiId, reqVO.getCredentialIds()); String operator = SecurityFrameworkUtils.getLoginUserNickname(); String description = String.format("创建 API (%s)", reqVO.getVersion()); @@ -154,16 +134,17 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { validateDuplication(reqVO, existing.getId()); validateStructure(reqVO); validatePolicies(reqVO); + validateCredentials(reqVO.getCredentialIds()); ApiDefinitionDO updateObj = buildDefinitionDO(reqVO, existing); apiDefinitionMapper.updateById(updateObj); - invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion()); apiTransformMapper.deleteByApiId(existing.getId()); apiStepMapper.deleteByApiId(existing.getId()); + apiDefinitionCredentialMapper.deleteByApiId(existing.getId()); persistApiLevelTransforms(existing.getId(), reqVO.getApiLevelTransforms()); persistSteps(existing.getId(), reqVO.getSteps()); - invalidateCache(updateObj.getTenantId(), updateObj.getApiCode(), updateObj.getVersion()); + persistCredentialBindings(existing.getId(), reqVO.getCredentialIds()); } finally { if (skipSnapshot) { ApiVersionSnapshotContextHolder.clear(); @@ -183,77 +164,12 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { @Transactional(rollbackFor = Exception.class) public void delete(Long id) { ApiDefinitionDO existing = ensureExists(id); - invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion()); apiTransformMapper.deleteByApiId(id); apiStepMapper.deleteByApiId(id); + apiDefinitionCredentialMapper.deleteByApiId(id); apiDefinitionMapper.deleteById(id); } - private Optional loadAggregateSync(String cacheKey) { - Optional cached = loadFromRedis(cacheKey); - if (cached.isPresent()) { - return cached; - } - String[] parts = cacheKey.split(":"); - String apiCode = parts[1]; - String version = parts[2]; - Optional aggregate = apiDefinitionMapper.selectByCodeAndVersion(apiCode, version) - .filter(definition -> ApiStatusEnum.isOnline(definition.getStatus())) - .map(this::buildAggregate); - aggregate.ifPresent(value -> persistToRedis(cacheKey, value)); - return aggregate; - } - - private Optional loadFromRedis(String cacheKey) { - try { - String json = stringRedisTemplate.opsForValue().get(REDIS_CACHE_PREFIX + cacheKey); - if (!StringUtils.hasText(json)) { - return Optional.empty(); - } - ApiDefinitionAggregate aggregate = objectMapper.readValue(json, ApiDefinitionAggregate.class); - return Optional.of(aggregate); - } catch (JsonProcessingException | DataAccessException ex) { - log.warn("反序列化 Redis 中 key {} 的 API 定义聚合失败", cacheKey, ex); - return Optional.empty(); - } - } - - private void persistToRedis(String cacheKey, ApiDefinitionAggregate aggregate) { - try { - String json = objectMapper.writeValueAsString(aggregate); - stringRedisTemplate.opsForValue().set(REDIS_CACHE_PREFIX + cacheKey, json, 5, TimeUnit.MINUTES); - } catch (JsonProcessingException | DataAccessException ex) { - log.warn("将 API 定义聚合写入 Redis key {} 失败", cacheKey, ex); - } - } - - private void deleteRedis(String cacheKey) { - try { - stringRedisTemplate.delete(REDIS_CACHE_PREFIX + cacheKey); - } catch (DataAccessException ex) { - log.warn("删除 Redis 中 key {} 的 API 定义聚合失败", cacheKey, ex); - } - } - - private void clearRedisCacheForTenant(Long tenantId) { - String tenantPart = tenantId == null ? "global" : tenantId.toString(); - String pattern = REDIS_CACHE_PREFIX + tenantPart + ":*"; - try { - Set keys = stringRedisTemplate.keys(pattern); - if (CollectionUtils.isEmpty(keys)) { - return; - } - stringRedisTemplate.delete(keys); - } catch (DataAccessException ex) { - log.warn("批量删除 Redis 中匹配 {} 的 API 定义聚合失败", pattern, ex); - } - } - - private String buildCacheKey(String apiCode, String version) { - Long tenantId = TenantContextHolder.getTenantId(); - return buildCacheKeyForTenant(tenantId, apiCode, version); - } - /** * 构建包含步骤、变换、策略等元数据的聚合对象,供缓存与运行时直接使用。 */ @@ -283,12 +199,14 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { ApiFlowPublication publication = apiFlowPublishMapper.selectActiveByApiId(definition.getId()) .map(this::convertPublication) .orElse(null); + List credentialBindings = loadCredentialBindings(definition.getId()); return ApiDefinitionAggregate.builder() .definition(definition) .steps(stepDefinitions) .apiLevelTransforms(apiTransforms) .rateLimitPolicy(rateLimitPolicy) .publication(publication) + .credentialBindings(credentialBindings) .build(); } @@ -497,22 +415,91 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { } } + private void validateCredentials(List credentialIds) { + if (CollectionUtils.isEmpty(credentialIds)) { + return; + } + List credentials = apiClientCredentialMapper.selectBatchIds(credentialIds); + long validCount = credentials == null ? 0 : credentials.stream() + .filter(credential -> credential != null && !Boolean.TRUE.equals(credential.getDeleted())) + .count(); + if (validCount != credentialIds.size()) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_NOT_FOUND); + } + } + private Long resolveTenantIdentifier() { return TenantContextHolder.getTenantId(); } - private void invalidateCache(Long tenantId, String apiCode, String version) { - if (!StringUtils.hasText(apiCode) || !StringUtils.hasText(version)) { + /** + * 先删除旧绑定,再对去重后的 credentialIds 批量插入,避免唯一约束冲突。 + */ + private void persistCredentialBindings(Long apiId, List credentialIds) { + // 先逻辑删除当前 API 的旧绑定,保留历史,同时避免重复插入 + apiDefinitionCredentialMapper.logicDeleteByApiId(apiId); + + if (CollectionUtils.isEmpty(credentialIds)) { return; } - String cacheKey = buildCacheKeyForTenant(tenantId, apiCode, version); - definitionCache.invalidate(cacheKey); - deleteRedis(cacheKey); + + // 去重后再查询有效凭证 + List distinctIds = credentialIds.stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + if (CollectionUtils.isEmpty(distinctIds)) { + return; + } + + List credentials = apiClientCredentialMapper.selectBatchIds(distinctIds); + if (CollectionUtils.isEmpty(credentials)) { + return; + } + + for (ApiClientCredentialDO credential : credentials) { + if (credential == null || Boolean.TRUE.equals(credential.getDeleted())) { + continue; + } + ApiDefinitionCredentialDO relation = new ApiDefinitionCredentialDO(); + relation.setId(null); + relation.setApiId(apiId); + relation.setCredentialId(credential.getId()); + relation.setAppId(credential.getAppId()); + relation.setDeleted(Boolean.FALSE); + apiDefinitionCredentialMapper.insert(relation); + } } - private String buildCacheKeyForTenant(Long tenantId, String apiCode, String version) { - String tenantPart = tenantId == null ? "global" : tenantId.toString(); - return tenantPart + ":" + apiCode.toLowerCase(Locale.ROOT) + ":" + version; + private List loadCredentialBindings(Long apiId) { + List relations = apiDefinitionCredentialMapper.selectByApiId(apiId); + if (CollectionUtils.isEmpty(relations)) { + return Collections.emptyList(); + } + List credentialIds = relations.stream() + .map(ApiDefinitionCredentialDO::getCredentialId) + .filter(Objects::nonNull) + .toList(); + Map credentialMap = Collections.emptyMap(); + if (!CollectionUtils.isEmpty(credentialIds)) { + List credentials = apiClientCredentialMapper.selectBatchIds(credentialIds); + if (!CollectionUtils.isEmpty(credentials)) { + credentialMap = credentials.stream() + .filter(credential -> credential != null && !Boolean.TRUE.equals(credential.getDeleted())) + .collect(Collectors.toMap(ApiClientCredentialDO::getId, c -> c, (a, b) -> a)); + } + } + List bindings = new ArrayList<>(relations.size()); + for (ApiDefinitionCredentialDO relation : relations) { + ApiClientCredentialDO credential = relation.getCredentialId() == null ? null : credentialMap.get(relation.getCredentialId()); + ApiCredentialBinding binding = ApiCredentialBinding.builder() + .credentialId(relation.getCredentialId()) + .appId(relation.getAppId()) + .appName(credential != null ? credential.getAppName() : null) + .build(); + bindings.add(binding); + } + return bindings; } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java index 64b71025..6a2214b4 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java @@ -86,13 +86,8 @@ public class ApiPolicyRateLimitServiceImpl implements ApiPolicyRateLimitService private void apply(ApiPolicySaveReqVO reqVO, ApiPolicyRateLimitDO target) { target.setName(StrUtil.trim(reqVO.getName())); target.setType(StrUtil.trim(reqVO.getType())); - target.setConfig(normalizeNullable(reqVO.getConfig())); - target.setDescription(normalizeNullable(reqVO.getDescription())); - } - - private String normalizeNullable(String value) { - String trimmed = StrUtil.trim(value); - return StrUtil.isEmpty(trimmed) ? null : trimmed; + target.setConfig(StrUtil.trim(reqVO.getConfig())); + target.setDescription(StrUtil.trim(reqVO.getDescription())); } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java index f4961fa1..91d3a5ef 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java @@ -11,11 +11,13 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefi import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO; import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionCredentialDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO; import com.zt.plat.module.databus.dal.mapper.gateway.ApiVersionMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper; +import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionCredentialMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper; import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; @@ -47,6 +49,7 @@ public class ApiVersionServiceImpl implements ApiVersionService { private final ApiDefinitionMapper apiDefinitionMapper; private final ApiStepMapper apiStepMapper; private final ApiTransformMapper apiTransformMapper; + private final ApiDefinitionCredentialMapper apiDefinitionCredentialMapper; private final ObjectMapper objectMapper; private final ApiDefinitionService apiDefinitionService; @@ -191,6 +194,15 @@ public class ApiVersionServiceImpl implements ApiVersionService { snapshot.setApiLevelTransforms(ApiDefinitionConvert.INSTANCE.convertTransformList(apiTransforms)); } + List credentialRelations = apiDefinitionCredentialMapper.selectByApiId(apiId); + if (credentialRelations != null && !credentialRelations.isEmpty()) { + List credentialIds = credentialRelations.stream() + .map(ApiDefinitionCredentialDO::getCredentialId) + .filter(Objects::nonNull) + .toList(); + snapshot.setCredentialIds(credentialIds); + } + return snapshot; } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java index 08ee01c5..334b891c 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java @@ -61,5 +61,6 @@ public interface GatewayServiceErrorCodeConstants { ErrorCode API_VERSION_SNAPSHOT_DESERIALIZE_FAILED = new ErrorCode(1_010_000_052, "API 版本快照反序列化失败"); ErrorCode API_VERSION_ACTIVE_CANNOT_DELETE = new ErrorCode(1_010_000_053, "当前激活版本不允许删除"); ErrorCode API_VERSION_API_MISMATCH = new ErrorCode(1_010_000_054, "两个版本不属于同一 API"); + ErrorCode API_CREDENTIAL_UNAUTHORIZED = new ErrorCode(1_010_000_055, "当前凭证无权访问该 API"); } diff --git a/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml b/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml index 892d171e..87dad43b 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml +++ b/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml @@ -52,6 +52,8 @@ spring: host: 172.16.46.63 # 地址 port: 30379 # 端口 database: 0 # 数据库索引 + username: zt-redis + password: P@ssword25 # password: 123456 # 密码,建议生产环境开启 xxl: diff --git a/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml b/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml index 5a113239..93b7aa62 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml +++ b/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml @@ -130,6 +130,7 @@ zt: - /databus/api/portal/** ignore-tables: - databus_api_client_credential + - databus_api_definition_credential # DataBus 数据同步服务端配置 databus: sync: diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessorTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessorTest.java new file mode 100644 index 00000000..ea0f553f --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessorTest.java @@ -0,0 +1,53 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_UNAUTHORIZED; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED; +import static org.assertj.core.api.Assertions.assertThat; + +class ApiGatewayErrorProcessorTest { + + private final ApiGatewayErrorProcessor processor = new ApiGatewayErrorProcessor(); + + @Test + void applyServiceException_should_fill_status_message_and_body() { + ApiInvocationContext context = ApiInvocationContext.create(); + ServiceException exception = ServiceExceptionUtil.exception(API_RATE_LIMIT_EXCEEDED); + + processor.applyServiceException(context, exception); + + assertThat(context.getResponseStatus()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS.value()); + assertThat(context.getResponseMessage()).isNotBlank(); + assertThat(context.getResponseBody()).isInstanceOfSatisfying(java.util.Map.class, body -> { + assertThat(body).containsEntry("errorCode", API_RATE_LIMIT_EXCEEDED.getCode()); + assertThat(body).containsKey("errorMessage"); + }); + } + + @Test + void applyUnexpectedException_should_use_first_non_empty_message() { + ApiInvocationContext context = ApiInvocationContext.create(); + RuntimeException exception = new RuntimeException("outer", new IllegalStateException("inner cause")); + + processor.applyUnexpectedException(context, exception); + + assertThat(context.getResponseStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + assertThat(context.getResponseMessage()).isEqualTo("outer"); + assertThat(context.getResponseBody()).isInstanceOf(java.util.Map.class); + } + + @Test + void resolveServiceException_should_traverse_causes() { + ServiceException serviceException = ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED); + RuntimeException wrapped = new RuntimeException(new IllegalStateException(serviceException)); + + ServiceException resolved = processor.resolveServiceException(wrapped); + + assertThat(resolved).isSameAs(serviceException); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionServiceTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionServiceTest.java new file mode 100644 index 00000000..16fa3f6a --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionServiceTest.java @@ -0,0 +1,140 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; +import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCredentialBinding; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; +import org.springframework.integration.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_UNAUTHORIZED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ApiGatewayExecutionServiceTest { + + private ApiGatewayExecutionService service; + + @Mock + private ApiFlowDispatcher apiFlowDispatcher; + + @Mock + private ApiGatewayErrorProcessor errorProcessor; + + @Mock + private ApiGatewayAccessLogger accessLogger; + + @Mock + private ApiDefinitionService apiDefinitionService; + + private ApiGatewayProperties properties; + + private ApiGatewayRequestMapper requestMapper; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + properties = new ApiGatewayProperties(); + properties.setBasePath("/databus/api"); + properties.setEnableTenantHeader(true); + properties.setTenantHeader("ZT-Tenant-Id"); + requestMapper = new ApiGatewayRequestMapper(objectMapper, properties); + service = new ApiGatewayExecutionService(requestMapper, apiFlowDispatcher, errorProcessor, properties, objectMapper, accessLogger, apiDefinitionService); + } + + @Test + void invokeForDebug_should_dispatch_with_inactive_definition() { + ApiGatewayInvokeReqVO reqVO = new ApiGatewayInvokeReqVO(); + reqVO.setApiCode("demo.api"); + reqVO.setVersion("v1"); + reqVO.getHeaders().put("ZT-Auth-Token", "debug-token"); + reqVO.getHeaders().put(ApiGatewayProperties.APP_ID_HEADER, "app-1"); + reqVO.getQueryParams().put("trace", "1"); + reqVO.setPayload(Map.of("name", "alice")); + + ApiDefinitionAggregate aggregate = ApiDefinitionAggregate.builder() + .definition(definition("demo.api", "v1")) + .build(); + + when(apiDefinitionService.findByCodeAndVersion(eq("demo.api"), eq("v1"))).thenReturn(Optional.empty()); + when(apiDefinitionService.findByCodeAndVersionIncludingInactive(eq("demo.api"), eq("v1"))).thenReturn(Optional.of(aggregate)); + when(apiFlowDispatcher.dispatchWithAggregate(eq(aggregate), any(ApiInvocationContext.class))) + .thenAnswer(invocation -> { + ApiInvocationContext ctx = invocation.getArgument(1); + ctx.setResponseStatus(201); + ctx.setResponseMessage("created"); + ctx.setResponseBody(Map.of("ok", true)); + return ctx; + }); + + ResponseEntity responseEntity = service.invokeForDebug(reqVO); + + assertThat(responseEntity.getStatusCodeValue()).isEqualTo(201); + assertThat(responseEntity.getBody()).isNotNull(); + assertThat(responseEntity.getBody().getMessage()).isEqualTo("created"); + assertThat(responseEntity.getBody().getResponse()).isEqualTo(Map.of("ok", true)); + } + + @Test + void dispatch_should_apply_credential_authorization_for_non_debug() { + ApiInvocationContext context = ApiInvocationContext.create(); + context.setApiCode("secure.api"); + context.setApiVersion("v1"); + + ApiDefinitionAggregate aggregate = ApiDefinitionAggregate.builder() + .definition(definition("secure.api", "v1")) + .credentialBindings(List.of(ApiCredentialBinding.builder().appId("app-x").build())) + .build(); + + when(apiDefinitionService.findByCodeAndVersion(eq("secure.api"), eq("v1"))).thenReturn(Optional.of(aggregate)); + doAnswer(invocation -> { + ApiInvocationContext ctx = invocation.getArgument(0); + ServiceException ex = ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED); + ctx.setResponseStatus(403); + ctx.setResponseMessage(ex.getMessage()); + return null; + }).when(errorProcessor).applyServiceException(any(ApiInvocationContext.class), any(ServiceException.class)); + + Message message = MessageBuilder.withPayload(context) + .setHeader("apiCode", "secure.api") + .setHeader("version", "v1") + .setHeader(HttpHeaders.REQUEST_METHOD, "POST") + .build(); + + ApiInvocationContext result = service.dispatch(message); + + assertThat(result.getResponseStatus()).isEqualTo(403); + verify(apiFlowDispatcher, never()).dispatch(anyString(), anyString(), any()); + verify(errorProcessor, times(1)).applyServiceException(any(ApiInvocationContext.class), any(ServiceException.class)); + } + + private ApiDefinitionDO definition(String apiCode, String version) { + ApiDefinitionDO definitionDO = new ApiDefinitionDO(); + definitionDO.setApiCode(apiCode); + definitionDO.setVersion(version); + return definitionDO; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java index 5307192b..aaf1eab7 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java @@ -73,8 +73,8 @@ class ApiGatewayRequestMapperTest { ApiInvocationContext context = mapper.map("", headers); - assertThat(context.getRequestHeaders().get("ZT-Auth-Token")).isEqualTo("token-123"); - assertThat(context.getRequestHeaders().get("zt-auth-token")).isEqualTo("token-123"); + assertThat(context.getRequestHeaders().get("ZT-Auth-Token")).isEqualTo(List.of("token-123")); + assertThat(context.getRequestHeaders().get("zt-auth-token")).isEqualTo(List.of("token-123")); } @Test diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/IntegrationFlowManagerTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/IntegrationFlowManagerTest.java new file mode 100644 index 00000000..3f384028 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/IntegrationFlowManagerTest.java @@ -0,0 +1,103 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; +import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.context.IntegrationFlowContext; +import org.springframework.messaging.MessageChannel; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class IntegrationFlowManagerTest { + + @Mock + private IntegrationFlowContext integrationFlowContext; + + @Mock + private ApiDefinitionService apiDefinitionService; + + @Mock + private ApiFlowAssembler apiFlowAssembler; + + @Mock + private IntegrationFlowContext.IntegrationFlowRegistration registration; + + private IntegrationFlowManager manager; + + @BeforeEach + void setUp() { + manager = new IntegrationFlowManager(integrationFlowContext, apiDefinitionService, apiFlowAssembler); + when(registration.getId()).thenReturn("flow-id"); + } + + @Test + void refreshAll_should_register_active_flows_and_remove_stale() { + ApiDefinitionAggregate agg1 = aggregate("order.create", "v1"); + ApiDefinitionAggregate agg2 = aggregate("order.pay", "v1"); + + when(apiDefinitionService.loadActiveDefinitions()).thenReturn(List.of(agg1, agg2)); + IntegrationFlow flow = mock(IntegrationFlow.class); + when(apiFlowAssembler.assemble(any())).thenReturn(ApiFlowRegistration.builder() + .flowId("flow-id") + .inputChannelName("ch") + .flow(flow) + .build()); + IntegrationFlowContext.IntegrationFlowRegistrationBuilder builder = mock(IntegrationFlowContext.IntegrationFlowRegistrationBuilder.class, RETURNS_DEEP_STUBS); + when(integrationFlowContext.registration(any(IntegrationFlow.class))).thenReturn(builder); + when(builder.id(anyString()).register()).thenReturn(registration); + when(registration.getInputChannel()).thenReturn(mock(MessageChannel.class)); + + manager.refreshAll(); + + Optional channel = manager.locateInputChannel("order.create", "v1"); + assertThat(channel).isPresent(); + + when(apiDefinitionService.loadActiveDefinitions()).thenReturn(List.of(agg1)); + manager.refreshAll(); + + verify(integrationFlowContext, atLeastOnce()).remove(anyString()); + } + + @Test + void obtainDebugHandle_should_reuse_existing_registration_when_present() { + ApiDefinitionAggregate agg = aggregate("demo", "v1"); + IntegrationFlow flow = mock(IntegrationFlow.class); + when(apiFlowAssembler.assemble(any())).thenReturn(ApiFlowRegistration.builder() + .flowId("flow-debug") + .inputChannelName("ch") + .flow(flow) + .build()); + IntegrationFlowContext.IntegrationFlowRegistrationBuilder builder = mock(IntegrationFlowContext.IntegrationFlowRegistrationBuilder.class, RETURNS_DEEP_STUBS); + when(integrationFlowContext.registration(any(IntegrationFlow.class))).thenReturn(builder); + when(builder.id(anyString()).register()).thenReturn(registration); + MessageChannel input = mock(MessageChannel.class); + when(registration.getInputChannel()).thenReturn(input); + when(apiDefinitionService.loadActiveDefinitions()).thenReturn(List.of(agg)); + + manager.refreshAll(); + IntegrationFlowManager.DebugFlowHandle handle = manager.obtainDebugHandle(agg); + + assertThat(handle.channel()).isNotNull(); + assertThat(handle.temporary()).isFalse(); + } + + private ApiDefinitionAggregate aggregate(String apiCode, String version) { + ApiDefinitionDO definitionDO = new ApiDefinitionDO(); + definitionDO.setApiCode(apiCode); + definitionDO.setVersion(version); + return ApiDefinitionAggregate.builder().definition(definitionDO).build(); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultRateLimitPolicyEvaluatorTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultRateLimitPolicyEvaluatorTest.java new file mode 100644 index 00000000..266e4c85 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultRateLimitPolicyEvaluatorTest.java @@ -0,0 +1,94 @@ +package com.zt.plat.module.databus.framework.integration.gateway.policy; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EVALUATION_FAILED; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DefaultRateLimitPolicyEvaluatorTest { + + @Mock + private StringRedisTemplate stringRedisTemplate; + + @Mock + private ValueOperations valueOperations; + + private DefaultRateLimitPolicyEvaluator evaluator; + + private ApiDefinitionAggregate aggregate; + + @BeforeEach + void setUp() { + ObjectMapper objectMapper = new ObjectMapper(); + evaluator = new DefaultRateLimitPolicyEvaluator(objectMapper, stringRedisTemplate); + + ApiDefinitionDO definitionDO = new ApiDefinitionDO(); + definitionDO.setApiCode("order.create"); + definitionDO.setVersion("v1"); + + ApiPolicyRateLimitDO rateLimitDO = new ApiPolicyRateLimitDO(); + rateLimitDO.setConfig("{\"limit\":2,\"windowSeconds\":60}"); + + aggregate = ApiDefinitionAggregate.builder() + .definition(definitionDO) + .rateLimitPolicy(rateLimitDO) + .build(); + + when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); + } + + @Test + void evaluate_should_set_expire_on_first_hit_and_pass_under_limit() { + when(valueOperations.increment(any())).thenReturn(1L, 2L); + + ApiInvocationContext context = ApiInvocationContext.create(); + context.getRequestHeaders().put("X-Client-Id", "c1"); + + evaluator.evaluate(aggregate, context); + evaluator.evaluate(aggregate, context); + + verify(stringRedisTemplate, times(2)).opsForValue(); + verify(valueOperations, times(2)).increment(any()); + verify(stringRedisTemplate).expire(any(), any()); + } + + @Test + void evaluate_should_throw_when_exceed_limit() { + when(valueOperations.increment(any())).thenReturn(3L); + + ApiInvocationContext context = ApiInvocationContext.create(); + + ServiceException ex = assertThrows(ServiceException.class, () -> evaluator.evaluate(aggregate, context)); + + assertThat(ex.getCode()).isEqualTo(API_RATE_LIMIT_EXCEEDED.getCode()); + } + + @Test + void evaluate_should_wrap_redis_failure() { + when(valueOperations.increment(any())).thenThrow(new DataAccessResourceFailureException("redis down")); + + ApiInvocationContext context = ApiInvocationContext.create(); + + ServiceException ex = assertThrows(ServiceException.class, () -> evaluator.evaluate(aggregate, context)); + + assertThat(ex.getCode()).isEqualTo(API_RATE_LIMIT_EVALUATION_FAILED.getCode()); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java index d964863a..5bd323d3 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java @@ -16,11 +16,17 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeMap; import java.util.UUID; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; /** * 可直接运行的示例,演示如何使用 appId=test 与对应密钥调用本地 Databus API。 @@ -28,13 +34,21 @@ import java.util.UUID; public final class DatabusApiInvocationExample { public static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); - private static final String APP_ID = "ztmy"; - private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; + +// private static final String APP_ID = "iwork"; +// private static final String APP_SECRET = "lpGXiNe/GMLk0vsbYGLa8eYxXq8tGhTbuu3/D4MJzIk="; + private static final String APP_ID = "ztmy"; + private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; - private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; - private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(5)) - .build(); +// private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; +// private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; +// private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; + private static final String TARGET_API = "https://jygk.chncopper.com:30078/admin-api/databus/api/portal/lgstOpenApi/v1"; +// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/callback/v1"; +// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; +// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456"; + // ⚠️ 仅用于联调:信任所有证书 + 关闭主机名校验,生产环境请改为受信 CA 或自定义 truststore。 + private static final HttpClient HTTP_CLIENT = buildUnsafeHttpClient(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final PrintStream OUT = buildConsolePrintStream(); public static final String ZT_APP_ID = "ZT-App-Id"; @@ -47,6 +61,45 @@ public final class DatabusApiInvocationExample { private DatabusApiInvocationExample() { } + /** + * 仅用于联调:信任所有证书并关闭主机名校验,生产环境请使用受信 CA 或自定义 truststore。 + */ + private static HttpClient buildUnsafeHttpClient() { + try { + TrustManager[] trustAll = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAll, new SecureRandom()); + + SSLParameters sslParameters = new SSLParameters(); + // 关闭主机名校验 + sslParameters.setEndpointIdentificationAlgorithm(""); + + return HttpClient.newBuilder() + .sslContext(sslContext) + .sslParameters(sslParameters) + .connectTimeout(Duration.ofSeconds(5)) + .build(); + } catch (Exception ex) { + throw new IllegalStateException("Failed to build unsafe HttpClient", ex); + } + } + public static void main(String[] args) throws Exception { OUT.println("=== GET 请求示例 ==="); // executeGetExample(); diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewayJwtResolverTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewayJwtResolverTest.java new file mode 100644 index 00000000..abef63b6 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewayJwtResolverTest.java @@ -0,0 +1,46 @@ +package com.zt.plat.module.databus.framework.integration.gateway.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class GatewayJwtResolverTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void resolveJwtToken_should_pick_token_header_case_insensitive() { + Map headers = new HashMap<>(); + headers.put("zT-AuTh-ToKeN", "abc123"); + + String token = GatewayJwtResolver.resolveJwtToken(headers, null, objectMapper); + + assertThat(token).isEqualTo("abc123"); + } + + @Test + void resolveJwtToken_should_strip_bearer_prefix_and_array_values() { + Map headers = new HashMap<>(); + headers.put("Authorization", new String[]{"Bearer XYZ.jwt"}); + + String token = GatewayJwtResolver.resolveJwtToken(headers, null, objectMapper); + + assertThat(token).isEqualTo("XYZ.jwt"); + } + + @Test + void resolveJwtToken_should_parse_structured_json_string_and_fallback_to_query_params() { + Map headers = new HashMap<>(); + headers.put("Authorization", "{\"token\":\"json-token\"}"); + Map query = Map.of("token", List.of("q1", "q2")); + + String token = GatewayJwtResolver.resolveJwtToken(headers, query, objectMapper); + + assertThat(token).isEqualTo("json-token"); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql index f17ff31f..7134f41c 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql +++ b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql @@ -1,6 +1,7 @@ DELETE FROM "databus_api_transform"; DELETE FROM "databus_api_step"; DELETE FROM "databus_api_definition"; +DELETE FROM "databus_api_definition_credential"; DELETE FROM "databus_policy_rate_limit"; DELETE FROM "databus_policy_audit"; DELETE FROM "databus_api_flow_publish"; diff --git a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql index d337c2f7..b5c3c2db 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql +++ b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql @@ -97,3 +97,16 @@ CREATE TABLE IF NOT EXISTS databus_api_flow_publish ( updater VARCHAR(64), deleted BOOLEAN ); + +CREATE TABLE IF NOT EXISTS databus_api_definition_credential ( + id BIGINT PRIMARY KEY, + api_id BIGINT, + credential_id BIGINT, + app_id VARCHAR(255), + tenant_id BIGINT, + create_time TIMESTAMP, + update_time TIMESTAMP, + creator VARCHAR(64), + updater VARCHAR(64), + deleted BOOLEAN +); diff --git a/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml b/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml index 5f4da0a6..9a4b7a79 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml +++ b/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml @@ -53,7 +53,8 @@ spring: host: 172.16.46.63 # 地址 port: 30379 # 端口 database: 1 # 数据库索引 -# password: 123456 # 密码,建议生产环境开启 + username: zt-redis + password: P@ssword25 --- #################### MQ 消息队列相关配置 #################### diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/dto/DeptSaveReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/dto/DeptSaveReqDTO.java index 59418d5c..fbb311cc 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/dto/DeptSaveReqDTO.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/dto/DeptSaveReqDTO.java @@ -36,4 +36,13 @@ public class DeptSaveReqDTO { @Schema(description = "状态,见 CommonStatusEnum 枚举0 开启 1 关闭", example = "0") private Integer status; + @Schema(description = "外部系统标识,用于建立编码映射", example = "ERP") + private String externalSystemCode; + + @Schema(description = "外部系统组织编码,用于建立映射", example = "ERP-001") + private String externalDeptCode; + + @Schema(description = "外部系统组织名称", example = "ERP总部") + private String externalDeptName; + } \ No newline at end of file diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkBaseReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkBaseReqDTO.java new file mode 100644 index 00000000..409e43d5 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkBaseReqDTO.java @@ -0,0 +1,20 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * iWork 集成接口公用的请求字段(API 专用,不依赖 VO)。 + */ +@Data +public class IWorkBaseReqDTO { + + @Schema(description = "配置的 iWork 凭证 appId;为空时使用默认凭证", example = "iwork-app") + private String appId; + + @Schema(description = "iWork 操作人用户编号", example = "1") + private String operatorUserId; + + @Schema(description = "是否强制刷新 token", example = "false") + private Boolean forceRefreshToken; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOperationRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOperationRespDTO.java index 1ebe46d5..65c5db94 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOperationRespDTO.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOperationRespDTO.java @@ -1,21 +1,60 @@ package com.zt.plat.module.system.api.iwork.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.Map; + /** - * iWork 操作结果响应 DTO + * iWork 操作结果响应 DTO(结构对齐 VO,独立定义)。 */ @Data public class IWorkOperationRespDTO { - @Schema(description = "是否成功") + @Schema(description = "iWork 返回的原始数据结构") + private Payload payload; + + @Schema(description = "是否判断为成功") private Boolean success; - @Schema(description = "iWork 返回的操作编号或实例编号") - private String operationId; - - @Schema(description = "提示信息") + @Schema(description = "返回提示信息") private String message; + @Data + public static class Payload { + + @Schema(description = "iWork 返回的业务状态码,例如 SUCCESS") + private String code; + + @Schema(description = "iWork 返回的数据体") + private PayloadData data; + + @Schema(description = "错误信息对象,通常为空对象") + private Map errMsg; + + @Schema(description = "返回失败时的详细信息") + private ReqFailMsg reqFailMsg; + } + + @Data + public static class PayloadData { + + @Schema(description = "iWork 生成的请求编号 requestid") + @JsonProperty("requestid") + private Long requestId; + } + + @Data + public static class ReqFailMsg { + + @Schema(description = "失败时的关键参数集合") + private Map keyParameters; + + @Schema(description = "失败消息对象") + private Map msgInfo; + + @Schema(description = "其他附加参数,例如 doAutoApprove") + private Map otherParams; + } } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java index 3d10a1ae..013eea65 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java @@ -2,6 +2,7 @@ package com.zt.plat.module.system.api.iwork.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.EqualsAndHashCode; /** * 发起 iWork 流程请求 DTO @@ -9,7 +10,8 @@ import lombok.Data; * 与 IWorkWorkflowCreateReqVO 字段一一对应,便于 Feign 调用。 */ @Data -public class IWorkWorkflowCreateReqDTO { +@EqualsAndHashCode(callSuper = true) +public class IWorkWorkflowCreateReqDTO extends IWorkBaseReqDTO { @Schema(description = "用印申请人(iWork 人员 ID)", example = "1001") private String jbr; diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowVoidReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowVoidReqDTO.java index 17ab8c34..d0472822 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowVoidReqDTO.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowVoidReqDTO.java @@ -2,20 +2,27 @@ package com.zt.plat.module.system.api.iwork.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; /** * 作废 / 干预 iWork 流程请求 DTO */ @Data -public class IWorkWorkflowVoidReqDTO { +@EqualsAndHashCode(callSuper = true) +public class IWorkWorkflowVoidReqDTO extends IWorkBaseReqDTO { - @Schema(description = "iWork 实例编号", requiredMode = Schema.RequiredMode.REQUIRED) - private String instanceId; - - @Schema(description = "操作人 iWork 用户编号", requiredMode = Schema.RequiredMode.REQUIRED) - private String operatorUserId; + @Schema(description = "流程请求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "REQ-001") + private String requestId; @Schema(description = "作废原因") private String reason; + @Schema(description = "额外参数") + private Map extraParams; + + @Schema(description = "额外 Form 数据") + private Map formExtras; + } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApi.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApi.java index a58ba5c4..294fb31e 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApi.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApi.java @@ -2,12 +2,15 @@ package com.zt.plat.module.system.api.sms; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import com.zt.plat.module.system.api.sms.dto.log.SmsLogRespDTO; import com.zt.plat.module.system.enums.ApiConstants; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import jakarta.validation.Valid; @@ -25,4 +28,8 @@ public interface SmsSendApi { @Operation(summary = "发送单条短信给 Member 用户", description = "在 mobile 为空时,使用 userId 加载对应 Member 的手机号") CommonResult sendSingleSmsToMember(@Valid @RequestBody SmsSendSingleToUserReqDTO reqDTO); + @GetMapping(PREFIX + "/log/get") + @Operation(summary = "根据日志编号查询短信状态") + CommonResult getSmsLog(@RequestParam("id") Long id); + } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/dto/log/SmsLogRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/dto/log/SmsLogRespDTO.java new file mode 100644 index 00000000..f1e707f1 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/dto/log/SmsLogRespDTO.java @@ -0,0 +1,73 @@ +package com.zt.plat.module.system.api.sms.dto.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@Schema(description = "RPC 服务 - 短信日志返回 DTO") +public class SmsLogRespDTO { + + @Schema(description = "日志编号", example = "123") + private Long id; + + @Schema(description = "短信渠道编码", example = "HL95") + private String channelCode; + + @Schema(description = "模板编码", example = "HL95_TEST") + private String templateCode; + + @Schema(description = "短信类型") + private Integer templateType; + + @Schema(description = "模板内容") + private String templateContent; + + @Schema(description = "模板参数") + private Map templateParams; + + @Schema(description = "短信 API 模板编号") + private String apiTemplateId; + + @Schema(description = "手机号", example = "13800138000") + private String mobile; + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "用户类型") + private Integer userType; + + @Schema(description = "发送状态") + private Integer sendStatus; + + @Schema(description = "发送时间") + private LocalDateTime sendTime; + + @Schema(description = "发送结果编码") + private String apiSendCode; + + @Schema(description = "发送结果信息") + private String apiSendMsg; + + @Schema(description = "发送请求ID") + private String apiRequestId; + + @Schema(description = "发送序列号") + private String apiSerialNo; + + @Schema(description = "接收状态") + private Integer receiveStatus; + + @Schema(description = "接收时间") + private LocalDateTime receiveTime; + + @Schema(description = "接收结果编码") + private String apiReceiveCode; + + @Schema(description = "接收结果信息") + private String apiReceiveMsg; + +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/DictTypeConstants.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/DictTypeConstants.java index e8b5038f..a913df2b 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/DictTypeConstants.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/DictTypeConstants.java @@ -23,4 +23,6 @@ public interface DictTypeConstants { String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态 String SMS_RECEIVE_STATUS = "system_sms_receive_status"; // 短信接收状态 + String DEPT_EXTERNAL_SYSTEM = "system_dept_external_system"; // 部门外部系统标识 + } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java index aaef27c3..8178c53e 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java @@ -107,6 +107,7 @@ public interface ErrorCodeConstants { ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1_002_011_000, "短信渠道不存在"); ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1_002_011_001, "短信渠道不处于开启状态,不允许选择"); ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1_002_011_002, "无法删除,该短信渠道还有短信模板"); + ErrorCode SMS_CHANNEL_BALANCE_UNSUPPORTED = new ErrorCode(1_002_011_003, "该短信渠道不支持余额查询"); // ========== 短信模板 1-002-012-000 ========== ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_012_000, "短信模板不存在"); @@ -120,6 +121,7 @@ public interface ErrorCodeConstants { ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1_002_013_000, "手机号不存在"); ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_013_001, "模板参数({})缺失"); ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_013_002, "短信模板不存在"); + ErrorCode SMS_CALLBACK_SIGN_INVALID = new ErrorCode(1_002_013_100, "短信回调签名校验失败"); // ========== 短信验证码 1-002-014-000 ========== ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在"); diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/dept/ExternalPlatformEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/dept/ExternalPlatformEnum.java new file mode 100644 index 00000000..cabd786b --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/dept/ExternalPlatformEnum.java @@ -0,0 +1,32 @@ +package com.zt.plat.module.system.enums.dept; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 外部系统 / 平台枚举 + *

+ * 与字典类型 {@code system_dept_external_system} 对应,用于声明常用的平台标识,便于代码与前端字典对齐。 + */ +@AllArgsConstructor +@Getter +public enum ExternalPlatformEnum { + + ERP("ERP", "企业资源计划"), + IWORK("IWORK", "iWork 同步"); + + private final String code; + private final String label; + + public static boolean isValid(String code) { + if (code == null) { + return false; + } + for (ExternalPlatformEnum item : values()) { + if (item.code.equalsIgnoreCase(code.trim())) { + return true; + } + } + return false; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApiImpl.java index 52a4ba82..b9232c13 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApiImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/sms/SmsSendApiImpl.java @@ -1,7 +1,10 @@ package com.zt.plat.module.system.api.sms; import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.module.system.api.sms.dto.log.SmsLogRespDTO; import com.zt.plat.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import com.zt.plat.module.system.service.sms.SmsLogService; import com.zt.plat.module.system.service.sms.SmsSendService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RestController; @@ -16,6 +19,8 @@ public class SmsSendApiImpl implements SmsSendApi { @Resource private SmsSendService smsSendService; + @Resource + private SmsLogService smsLogService; @Override public CommonResult sendSingleSmsToAdmin(SmsSendSingleToUserReqDTO reqDTO) { @@ -29,4 +34,9 @@ public class SmsSendApiImpl implements SmsSendApi { reqDTO.getTemplateCode(), reqDTO.getTemplateParams())); } + @Override + public CommonResult getSmsLog(Long id) { + return success(BeanUtils.toBean(smsLogService.getSmsLog(id), SmsLogRespDTO.class)); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java index 94a755e8..a850f311 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java @@ -23,6 +23,15 @@ public class DeptSaveReqVO { @Size(max = 50, message = "部门编码长度不能超过 50 个字符") private String code; + @Schema(description = "外部系统标识,用于建立编码映射", example = "ERP") + private String externalSystemCode; + + @Schema(description = "外部系统组织编码,用于建立映射", example = "ERP-001") + private String externalDeptCode; + + @Schema(description = "外部系统组织名称", example = "ERP总部") + private String externalDeptName; + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZT") @NotBlank(message = "部门名称不能为空") @Size(max = 30, message = "部门名称长度不能超过 30 个字符") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java index e69863fe..b5a63c35 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java @@ -12,6 +12,8 @@ import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -45,6 +47,20 @@ public class IWorkIntegrationController { return success(integrationService.acquireToken(reqVO)); } + @PostMapping("/oa/token") + @Operation(summary = "透传获取 OA Token") + public ResponseEntity acquireOaToken(@Valid @RequestBody IWorkOaTokenReqVO reqVO) { + IWorkOaRawResponse resp = integrationService.getOaToken(reqVO); + return buildOaResponse(resp); + } + + @PostMapping("/oa/check") + @Operation(summary = "透传校验 OA Token") + public ResponseEntity checkOaToken(@Valid @RequestBody IWorkOaCheckTokenReqVO reqVO) { + IWorkOaRawResponse resp = integrationService.checkOaToken(reqVO); + return buildOaResponse(resp); + } + @PostMapping("/user/resolve") @Operation(summary = "根据外部标识获取 iWork 用户编号") public CommonResult resolveUser(@Valid @RequestBody IWorkUserInfoReqVO reqVO) { @@ -97,30 +113,6 @@ public class IWorkIntegrationController { return success(orgRestService.listUsers(reqVO)); } -// @PostMapping("/hr/subcompany/sync") -// @Operation(summary = "同步分部信息至 iWork") -// public CommonResult syncSubcompanies(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { -// return success(orgRestService.syncSubcompanies(reqVO)); -// } -// -// @PostMapping("/hr/department/sync") -// @Operation(summary = "同步部门信息至 iWork") -// public CommonResult syncDepartments(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { -// return success(orgRestService.syncDepartments(reqVO)); -// } -// -// @PostMapping("/hr/job-title/sync") -// @Operation(summary = "同步岗位信息至 iWork") -// public CommonResult syncJobTitles(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { -// return success(orgRestService.syncJobTitles(reqVO)); -// } -// -// @PostMapping("/hr/user/sync") -// @Operation(summary = "同步人员信息至 iWork") -// public CommonResult syncUsers(@Valid @RequestBody IWorkOrgSyncReqVO reqVO) { -// return success(orgRestService.syncUsers(reqVO)); -// } - // ----------------- 同步到本地 ----------------- @PostMapping("/hr/departments/full-sync") @@ -146,4 +138,20 @@ public class IWorkIntegrationController { public CommonResult fullSyncUsers(@Valid @RequestBody IWorkFullSyncReqVO reqVO) { return success(syncService.fullSyncUsers(reqVO)); } + + private ResponseEntity buildOaResponse(IWorkOaRawResponse resp) { + if (resp == null) { + return ResponseEntity.internalServerError().body("OA 响应为空"); + } + HttpHeaders headers = new HttpHeaders(); + if (resp.getHeaders() != null) { + resp.getHeaders().forEach(headers::add); + } + if (resp.getContentType() != null) { + headers.setContentType(resp.getContentType()); + } + return ResponseEntity.status(resp.getStatusCode()) + .headers(headers) + .body(resp.getBody()); + } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java index 58519d84..12a5d2b5 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java @@ -18,4 +18,7 @@ public class IWorkFileCallbackReqVO { @Schema(description = "文件名称,可选", example = "合同附件.pdf") private String fileName; + + @Schema(description = "OA 单点下载使用的 ssoToken,可选", example = "6102A7C13F09DD6B1AF06CDA0E479AC8...") + private String ssoToken; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSyncRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSyncRespVO.java deleted file mode 100644 index c0e17393..00000000 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkHrSyncRespVO.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.zt.plat.module.system.controller.admin.integration.iwork.vo; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * iWork 人力同步响应。 - */ -@Data -@Schema(description = "iWork 人力同步响应") -public class IWorkHrSyncRespVO { - - @Schema(description = "响应码") - private String code; - - @Schema(description = "提示信息") - private String message; - - @Schema(description = "是否成功") - private boolean success; - - @Schema(description = "同步结果明细") - private List result; - - @Data - @Schema(description = "同步结果项") - public static class SyncResult { - - @Schema(description = "操作动作 add/update/delete") - @JsonProperty("@action") - private String action; - - @Schema(description = "外部编码") - @JsonProperty("code") - private String code; - - @Schema(description = "执行结果 success/fail") - @JsonProperty("result") - private String result; - - @Schema(description = "是否成功") - @JsonProperty("success") - private Boolean success; - - @Schema(description = "失败描述") - @JsonProperty("message") - private String message; - - @JsonIgnore - private Map attributes; - - @JsonAnySetter - public void putAttribute(String key, Object value) { - if (attributes == null) { - attributes = new LinkedHashMap<>(); - } - attributes.put(key, value); - } - - @JsonAnyGetter - public Map any() { - return attributes == null ? Collections.emptyMap() : attributes; - } - } -} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaCheckTokenReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaCheckTokenReqVO.java new file mode 100644 index 00000000..be77880b --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaCheckTokenReqVO.java @@ -0,0 +1,16 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 校验 OA Token 的请求参数。 + */ +@Data +public class IWorkOaCheckTokenReqVO { + + @Schema(description = "需要校验的 OA token") + @NotBlank(message = "token 不能为空") + private String token; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaRawResponse.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaRawResponse.java new file mode 100644 index 00000000..45b5fc53 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaRawResponse.java @@ -0,0 +1,27 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.http.MediaType; + +import java.util.Map; + +/** + * 封装 OA 接口的原始返回,用于透传 HTTP 状态与 body。 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IWorkOaRawResponse { + + private int statusCode; + + private String body; + + private MediaType contentType; + + private Map headers; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java new file mode 100644 index 00000000..bfbd488f --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java @@ -0,0 +1,19 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 获取 OA Token 的请求参数。 + */ +@Data +public class IWorkOaTokenReqVO { + + @Schema(description = "OA 登录账号 loginid", example = "zixun004") + @NotBlank(message = "loginId 不能为空") + private String loginId; + + @Schema(description = "应用 appid,未填则使用配置默认值", example = "a17ca6ca-88b0-463e-bffa-7995086bf225") + private String appId; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgSyncReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgSyncReqVO.java deleted file mode 100644 index 52120368..00000000 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgSyncReqVO.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.zt.plat.module.system.controller.admin.integration.iwork.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.List; -import java.util.Map; - -/** - * 同步 iWork 人力组织信息的请求。 - */ -@Data -public class IWorkOrgSyncReqVO { - - @Schema(description = "同步数据集合,将被序列化为 data 传给 iWork", requiredMode = Schema.RequiredMode.REQUIRED) - @NotEmpty(message = "同步数据不能为空") - private List> data; -} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsCallbackController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsCallbackController.java index 33808e34..6ec32e1d 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsCallbackController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsCallbackController.java @@ -62,4 +62,13 @@ public class SmsCallbackController { return success(true); } + @PostMapping("/hl95") + @PermitAll + @TenantIgnore + @Operation(summary = "鸿联九五短信的回调") + public CommonResult receiveHl95SmsStatus(@RequestBody String requestBody) throws Throwable { + smsSendService.receiveSmsStatus(SmsChannelEnum.HL95.getCode(), requestBody); + return success(true); + } + } \ No newline at end of file diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsChannelController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsChannelController.java index a94bb42b..a7536192 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsChannelController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsChannelController.java @@ -71,6 +71,14 @@ public class SmsChannelController { return success(BeanUtils.toBean(pageResult, SmsChannelRespVO.class)); } + @GetMapping("/balance") + @Operation(summary = "查询短信渠道余额") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult getBalance(@RequestParam("id") Long id) { + return success(smsChannelService.queryBalance(id)); + } + @GetMapping({"/list-all-simple", "/simple-list"}) @Operation(summary = "获得短信渠道精简列表", description = "包含被禁用的短信渠道") public CommonResult> getSimpleSmsChannelList() { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java index 46e9212b..11a15a33 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java @@ -32,6 +32,9 @@ public class SmsChannelRespVO { @NotNull(message = "短信 API 的账号不能为空") private String apiKey; + @Schema(description = "企业编号(epid)", example = "123456") + private String epid; + @Schema(description = "短信 API 的密钥", example = "yuanma") private String apiSecret; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java index fd25982a..4c2e7c6e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java @@ -17,6 +17,9 @@ public class SmsChannelSaveReqVO { @NotNull(message = "短信签名不能为空") private String signature; + @Schema(description = "企业编号(epid)", example = "123456") + private String epid; + @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") @NotNull(message = "渠道编码不能为空") private String code; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java index ec64cc41..fc9b19de 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java @@ -29,6 +29,9 @@ public class UserPageReqVO extends PageParam { @Schema(description = "手机号码,模糊匹配", example = "zt") private String mobile; + @Schema(description = "关键词(昵称/账号/手机号模糊匹配)", example = "张三") + private String keyword; + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/sms/Hl95SmsCallbackController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/sms/Hl95SmsCallbackController.java new file mode 100644 index 00000000..a7ae2385 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/sms/Hl95SmsCallbackController.java @@ -0,0 +1,100 @@ +package com.zt.plat.module.system.controller.app.sms; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.zt.plat.framework.common.util.json.JsonUtils; +import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO; +import com.zt.plat.module.system.enums.ErrorCodeConstants; +import com.zt.plat.module.system.framework.sms.core.enums.SmsChannelEnum; +import com.zt.plat.module.system.service.sms.SmsChannelService; +import com.zt.plat.module.system.service.sms.SmsSendService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Comparator; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; + +@Slf4j +@Validated +@RestController +@Tag(name = "鸿联九五 - 短信回调") +@RequestMapping("/system/sms/callback/hl95") +public class Hl95SmsCallbackController { + + @Resource + private SmsSendService smsSendService; + @Resource + private SmsChannelService smsChannelService; + + @RequestMapping(value = "/status", method = {RequestMethod.GET, RequestMethod.POST}) + @Operation(summary = "状态报告回调") + @PermitAll + public String statusCallback(@RequestParam Map params) { + try { + checkSign(params); + smsSendService.receiveSmsStatus(SmsChannelEnum.HL95.getCode(), JsonUtils.toJsonString(params)); + return "OK"; + } catch (Throwable e) { + log.warn("[statusCallback][鸿联九五回调处理失败 params={} error={}]", params, e.getMessage(), e); + return "FAIL"; + } + } + + @RequestMapping(value = "/mo", method = {RequestMethod.GET, RequestMethod.POST}) + @Operation(summary = "上行短信回调") + @PermitAll + public String moCallback(@RequestParam Map params) { + try { + checkSign(params); + log.info("[moCallback][收到鸿联九五上行:{}]", params); + return "OK"; + } catch (Throwable e) { + log.warn("[moCallback][鸿联九五上行处理失败 params={} error={}]", params, e.getMessage(), e); + return "FAIL"; + } + } + + private void checkSign(Map params) { + String sign = params.get("sign"); + if (StrUtil.isBlank(sign)) { + throw exception(ErrorCodeConstants.SMS_CALLBACK_SIGN_INVALID); + } + SmsChannelDO channel = smsChannelService.getSmsChannelList().stream() + .filter(item -> StrUtil.equals(item.getCode(), SmsChannelEnum.HL95.getCode())) + .min(Comparator.comparing(SmsChannelDO::getId)) + .orElse(null); + if (channel == null) { + throw exception(ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS); + } + String expect = buildSign(params, channel.getApiSecret()); + if (!StrUtil.equalsIgnoreCase(sign, expect)) { + throw exception(ErrorCodeConstants.SMS_CALLBACK_SIGN_INVALID); + } + } + + private static String buildSign(Map params, String secret) { + SortedMap sorted = new TreeMap<>(); + params.forEach((k, v) -> { + if (!"sign".equalsIgnoreCase(k)) { + sorted.put(k, StrUtil.nullToEmpty(v)); + } + }); + String base = sorted.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining("&")); + return DigestUtil.md5Hex(base + "&key=" + secret); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/sms/SmsChannelDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/sms/SmsChannelDO.java index 8287a53e..ec04a5b7 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/sms/SmsChannelDO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/sms/SmsChannelDO.java @@ -35,6 +35,10 @@ public class SmsChannelDO extends BaseDO { * 短信签名 */ private String signature; + /** + * 企业编号(epid) + */ + private String epid; /** * 渠道编码 * diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptExternalCodeMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptExternalCodeMapper.java index 0a9dbd57..910857f9 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptExternalCodeMapper.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptExternalCodeMapper.java @@ -37,6 +37,10 @@ public interface DeptExternalCodeMapper extends BaseMapperX return selectList(DeptExternalCodeDO::getDeptId, deptId); } + default int deleteByDeptId(Long deptId) { + return delete(DeptExternalCodeDO::getDeptId, deptId); + } + default List selectListBySystemCode(String systemCode) { return selectList(DeptExternalCodeDO::getSystemCode, systemCode); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/user/AdminUserMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/user/AdminUserMapper.java index 17fbcd76..e2bc5820 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/user/AdminUserMapper.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/user/AdminUserMapper.java @@ -1,5 +1,6 @@ package com.zt.plat.module.system.dal.mysql.user; +import cn.hutool.core.util.StrUtil; import com.zt.plat.framework.common.pojo.PageParam; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; @@ -36,18 +37,27 @@ public interface AdminUserMapper extends BaseMapperX { } default PageResult selectPage(UserPageReqVO reqVO, Collection deptIds, Collection userIds) { + MPJLambdaWrapperX query = new MPJLambdaWrapperX<>(); + query.leftJoin(UserDeptDO.class, UserDeptDO::getUserId, AdminUserDO::getId); + query.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()); + query.likeIfPresent(AdminUserDO::getWorkcode, reqVO.getWorkcode()); + query.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()); + query.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()); + query.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime()); + query.inIfPresent(UserDeptDO::getDeptId, deptIds); + query.inIfPresent(AdminUserDO::getId, userIds); + query.distinct(); + query.orderByDesc(AdminUserDO::getId); - return selectJoinPage(reqVO, AdminUserDO.class, new MPJLambdaWrapperX() - .leftJoin(UserDeptDO.class, UserDeptDO::getUserId, AdminUserDO::getId) - .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) - .likeIfPresent(AdminUserDO::getWorkcode, reqVO.getWorkcode()) - .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) - .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) - .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime()) - .inIfPresent(UserDeptDO::getDeptId, deptIds) - .inIfPresent(AdminUserDO::getId, userIds) - .distinct() - .orderByDesc(AdminUserDO::getId)); + if (StrUtil.isNotBlank(reqVO.getKeyword())) { + String keyword = reqVO.getKeyword().trim(); + query.and(w -> w.like(AdminUserDO::getNickname, keyword) + .or().like(AdminUserDO::getUsername, keyword) + .or().like(AdminUserDO::getMobile, keyword) + .or().like(AdminUserDO::getWorkcode, keyword)); + } + + return selectJoinPage(reqVO, AdminUserDO.class, query); } default List selectList(UserPageReqVO reqVO, Collection deptIds, Collection userIds) { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/redis/RedisKeyConstants.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/redis/RedisKeyConstants.java index 339e07f3..13c4fc39 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/redis/RedisKeyConstants.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/redis/RedisKeyConstants.java @@ -17,6 +17,14 @@ public interface RedisKeyConstants { */ String DEPT_CHILDREN_ID_LIST = "dept_children_ids"; + /** + * 指定部门的外部组织编码映射列表缓存 + *

+ * KEY 格式:dept_external_code_list:{deptId} + * VALUE 数据类型:String 映射列表 + */ + String DEPT_EXTERNAL_CODE_LIST = "dept_external_code_list"; + /** * 角色的缓存 *

diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkIntegrationConfiguration.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkIntegrationConfiguration.java index 02fb1f76..3efc5c7e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkIntegrationConfiguration.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkIntegrationConfiguration.java @@ -1,12 +1,35 @@ package com.zt.plat.module.system.framework.integration.iwork.config; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; +import java.util.List; + /** * 负责加载 {@link IWorkProperties} 的自动配置类。 */ +@Slf4j @Configuration +@RequiredArgsConstructor @EnableConfigurationProperties(IWorkProperties.class) public class IWorkIntegrationConfiguration { + + private final IWorkProperties properties; + + @PostConstruct + void reportConfigurationStatus() { + if (!properties.hasAnyConfiguredValue()) { + log.info("[iWork] 未检测到集成配置,默认关闭 iWork 功能。需要时请在配置文件中补充 iwork.* 项。"); + return; + } + List issues = properties.collectCriticalIssues(); + if (!issues.isEmpty()) { + log.warn("[iWork] 配置不完整:{}。系统会继续启动,但 iWork 能力将不可用。", String.join(";", issues)); + return; + } + log.info("[iWork] 已检测到完整配置,iWork 集成能力已启用。"); + } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java index 9443464e..dde38deb 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java @@ -1,21 +1,18 @@ package com.zt.plat.module.system.framework.integration.iwork.config; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.Getter; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; +import org.springframework.util.StringUtils; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; /** * iWork 集成所需的配置项。 */ @Data -@Validated @ConfigurationProperties(prefix = "iwork") public class IWorkProperties { @@ -39,44 +36,35 @@ public class IWorkProperties { */ private String userId; - @Valid private final Paths paths = new Paths(); private final Headers headers = new Headers(); - @Valid private final Token token = new Token(); - @Valid private final Client client = new Client(); - @Valid private final OrgRest org = new OrgRest(); - @Valid private final Workflow workflow = new Workflow(); + private final Oa oa = new Oa(); @Data public static class Paths { /** * 负责交换公钥和密钥的注册接口路径。 */ - @NotBlank(message = "iWork 注册接口路径不能为空") private String register; /** * 申请访问令牌的接口路径。 */ - @NotBlank(message = "iWork 申请 Token 接口路径不能为空") private String applyToken; /** * 查询用户信息的接口路径。 */ - @NotBlank(message = "iWork 用户信息接口路径不能为空") private String userInfo; /** * 发起流程的接口路径。 */ - @NotBlank(message = "iWork 发起流程接口路径不能为空") private String createWorkflow; /** * 干预或作废流程的接口路径。 */ - @NotBlank(message = "iWork 作废流程接口路径不能为空") private String voidWorkflow; } @@ -95,7 +83,6 @@ public class IWorkProperties { /** * 向 iWork 申请的 Token 有效期(单位秒)。 */ - @Min(value = 1, message = "iWork Token 有效期必须大于 0") private long ttlSeconds; } @@ -104,12 +91,10 @@ public class IWorkProperties { /** * Reactor Netty 连接超时时间。 */ - @NotNull(message = "iWork 客户端连接超时时间不能为空") private Duration connectTimeout; /** * Reactor Netty 响应超时时间。 */ - @NotNull(message = "iWork 客户端响应超时时间不能为空") private Duration responseTimeout; } @@ -119,7 +104,6 @@ public class IWorkProperties { * 认证所需的标识(与 iWork 约定)。 */ private String tokenSeed; - @Valid private final OrgPaths paths = new OrgPaths(); } @@ -140,7 +124,119 @@ public class IWorkProperties { /** * 用印流程对应的 iWork 模板编号。 */ - @NotBlank(message = "iWork 用印流程模板编号不能为空") private String sealWorkflowId; } + + @Data + public static class Oa { + /** + * OA 网关基础地址,例如:http://172.16.36.233:8080 + */ + private String baseUrl; + + /** + * 默认 appid,调用方未传时使用。 + */ + private String appId; + + private final OaPaths paths = new OaPaths(); + } + + @Data + public static class OaPaths { + /** + * 获取 token 的接口路径,例如:/ssologin/getToken + */ + private String getToken; + + /** + * 校验 token 的接口路径,例如:/ssologin/checkToken + */ + private String checkToken; + } + + /** + * 是否显式配置了关键参数,用于判断是否需要提示用户。 + */ + public boolean hasAnyConfiguredValue() { + return hasText(baseUrl) + || hasText(appId) + || hasText(clientPublicKey) + || hasText(userId) + || hasAnyPathConfigured() + || token.getTtlSeconds() > 0 + || client.getConnectTimeout() != null + || client.getResponseTimeout() != null + || hasText(org.getTokenSeed()) + || hasText(workflow.getSealWorkflowId()) + || hasAnyOaConfigured(); + } + + /** + * 收集关键配置缺失信息,用于提示或日志告警。 + */ + public List collectCriticalIssues() { + List issues = new ArrayList<>(); + if (!hasText(baseUrl)) { + issues.add("iwork.base-url 未配置"); + } + if (!hasText(appId)) { + issues.add("iwork.app-id 未配置"); + } + if (!hasText(paths.getRegister())) { + issues.add("iwork.paths.register 未配置"); + } + if (!hasText(paths.getApplyToken())) { + issues.add("iwork.paths.apply-token 未配置"); + } + if (!hasText(paths.getUserInfo())) { + issues.add("iwork.paths.user-info 未配置"); + } + if (!hasText(paths.getCreateWorkflow())) { + issues.add("iwork.paths.create-workflow 未配置"); + } + if (!hasText(paths.getVoidWorkflow())) { + issues.add("iwork.paths.void-workflow 未配置"); + } + if (token.getTtlSeconds() <= 0) { + issues.add("iwork.token.ttl-seconds 需要大于 0"); + } + if (!hasText(workflow.getSealWorkflowId())) { + issues.add("iwork.workflow.seal-workflow-id 未配置"); + } + if (oa != null) { + if (!hasText(oa.getBaseUrl())) { + issues.add("iwork.oa.base-url 未配置"); + } + if (!hasText(oa.getAppId())) { + issues.add("iwork.oa.app-id 未配置"); + } + if (oa.getPaths() == null || !hasText(oa.getPaths().getGetToken())) { + issues.add("iwork.oa.paths.get-token 未配置"); + } + if (oa.getPaths() == null || !hasText(oa.getPaths().getCheckToken())) { + issues.add("iwork.oa.paths.check-token 未配置"); + } + } + return issues; + } + + private boolean hasAnyPathConfigured() { + return hasText(paths.getRegister()) + || hasText(paths.getApplyToken()) + || hasText(paths.getUserInfo()) + || hasText(paths.getCreateWorkflow()) + || hasText(paths.getVoidWorkflow()); + } + + private boolean hasAnyOaConfigured() { + return oa != null && (hasText(oa.getBaseUrl()) + || hasText(oa.getAppId()) + || (oa.getPaths() != null && (hasText(oa.getPaths().getGetToken()) + || hasText(oa.getPaths().getCheckToken())))); + } + + private boolean hasText(String value) { + return StringUtils.hasText(value); + } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/SmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/SmsClient.java index 12b91561..79b37cd1 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/SmsClient.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/SmsClient.java @@ -31,7 +31,7 @@ public interface SmsClient { * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 * @return 短信发送结果 */ - SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId, + SmsSendRespDTO sendSms(Long logId, String mobile, String content, String apiTemplateId, List> templateParams) throws Throwable; /** diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClient.java index bf3ff2ed..78c7b9d8 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClient.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClient.java @@ -49,7 +49,7 @@ public class AliyunSmsClient extends AbstractSmsClient { } @Override - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, String apiTemplateId, List> templateParams) throws Throwable { Assert.notBlank(properties.getSignature(), "短信签名不能为空"); // 1. 执行请求 diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java index 29175488..11675d78 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java @@ -36,16 +36,16 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient { Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } - @Override - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, - String apiTemplateId, List> templateParams) throws Throwable { + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, + String apiTemplateId, List> templateParams) throws Throwable { // 构建请求 String url = buildUrl("robot/send"); Map params = new HashMap<>(); params.put("msgtype", "text"); - String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", - mobile, sendLogId, MapUtils.convertMap(templateParams)); - params.put("text", MapUtil.builder().put("content", content).build()); + String sendContent = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s\n内容:%s", + mobile, sendLogId, MapUtils.convertMap(templateParams), content); + params.put("text", MapUtil.builder().put("content", sendContent).build()); // 执行请求 String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); // 解析结果 diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/Hl95SmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/Hl95SmsClient.java new file mode 100644 index 00000000..bc2ac787 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/Hl95SmsClient.java @@ -0,0 +1,161 @@ +package com.zt.plat.module.system.framework.sms.core.client.impl; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.zt.plat.framework.common.core.KeyValue; +import com.zt.plat.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.zt.plat.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +import com.zt.plat.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.zt.plat.module.system.framework.sms.core.client.impl.extra.SmsBalanceClient; +import com.zt.plat.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.zt.plat.module.system.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; + +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class Hl95SmsClient extends AbstractSmsClient implements SmsBalanceClient { + + private static final String SEND_URL = "https://api.sms.95ytx.com:9091/mxt/send"; + private static final String BALANCE_URL = "https://api.sms.95ytx.com:9091/mxt/getfee"; + private static final DateTimeFormatter STATUS_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + public Hl95SmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiKey(), "用户名(apiKey) 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "密码(apiSecret) 不能为空"); + } + + @Override + public SmsSendRespDTO sendSms(Long logId, String mobile, String content, String apiTemplateId, + List> templateParams) { + Assert.notEmpty(properties.getEpid(), "鸿联九五需要配置 epid"); + Assert.notEmpty(properties.getSignature(), "短信签名不能为空"); + String finalContent = appendSignatureIfMissing(content, properties.getSignature()); + String linkId = buildLinkId(logId); + + Map form = new HashMap<>(); + form.put("username", properties.getApiKey()); + form.put("password", properties.getApiSecret()); + form.put("epid", properties.getEpid()); + form.put("phone", mobile); + form.put("message", finalContent); + form.put("linkid", linkId); + // subcode 可为空 + + String resp; + try (HttpResponse response = HttpRequest.post(SEND_URL) + .form(form) + .charset(StandardCharsets.UTF_8) + .execute()) { + resp = StrUtil.trim(response.body()); + } + + boolean success = StrUtil.equals(resp, "00"); + return new SmsSendRespDTO() + .setSuccess(success) + .setApiCode(resp) + .setApiMsg(resp) + .setApiRequestId(linkId) + .setSerialNo(linkId); + } + + @Override + public List parseSmsReceiveStatus(String text) { + JSONObject obj = JSONUtil.parseObj(text, false); + String reportCode = obj.getStr("FReportCode"); + String linkId = obj.getStr("FLinkID"); + LocalDateTime deliverTime = parseDeliverTime(obj.getStr("FDeliverTime")); + String mobile = obj.getStr("FDestAddr"); + boolean success = StrUtil.equalsIgnoreCase(reportCode, "DELIVRD") || StrUtil.equals(reportCode, "0"); + Long logId = parseLongSafely(linkId); + + SmsReceiveRespDTO dto = new SmsReceiveRespDTO() + .setSuccess(success) + .setErrorCode(reportCode) + .setErrorMsg(reportCode) + .setMobile(mobile) + .setReceiveTime(deliverTime) + .setSerialNo(linkId) + .setLogId(logId); + return Collections.singletonList(dto); + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) { + // 鸿联九五无模板审核接口,直接返回可用 + return new SmsTemplateRespDTO() + .setId(apiTemplateId) + .setContent(apiTemplateId) + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()); + } + + @Override + public Integer queryBalance() { + Assert.notEmpty(properties.getEpid(), "鸿联九五需要配置 epid"); + Map form = MapUtil.builder() + .put("username", properties.getApiKey()) + .put("password", properties.getApiSecret()) + .put("epid", properties.getEpid()) + .build(); + String resp; + try (HttpResponse response = HttpRequest.get(BALANCE_URL) + .form(form) + .charset(StandardCharsets.UTF_8) + .execute()) { + if (response.getStatus() != HttpURLConnection.HTTP_OK) { + throw new IllegalStateException("余额查询失败,HTTP 状态码:" + response.getStatus()); + } + resp = StrUtil.trim(response.body()); + } + if (!StrUtil.isNumeric(resp)) { + throw new IllegalStateException("余额查询失败,返回值:" + resp); + } + return Integer.valueOf(resp); + } + + private static String appendSignatureIfMissing(String content, String signature) { + if (StrUtil.isBlank(signature)) { + return content; + } + String wrapped = StrUtil.startWithAny(signature, "【", "[") ? signature : "【" + signature + "】"; + return StrUtil.startWith(content, wrapped) ? content : wrapped + content; + } + + private static String buildLinkId(Long logId) { + String raw = String.valueOf(logId); + return raw.length() > 20 ? raw.substring(raw.length() - 20) : raw; + } + + private static LocalDateTime parseDeliverTime(String timeText) { + if (StrUtil.isBlank(timeText)) { + return null; + } + try { + return LocalDateTime.parse(timeText, STATUS_TIME_FORMATTER); + } catch (Exception ex) { + log.warn("[parseDeliverTime][无法解析时间:{}]", timeText, ex); + return null; + } + } + + private static Long parseLongSafely(String text) { + try { + return Long.parseLong(text); + } catch (Exception ignore) { + return null; + } + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index 7c43dd62..3cfe1c0e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java @@ -74,7 +74,7 @@ public class HuaweiSmsClient extends AbstractSmsClient { } @Override - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, String apiTemplateId, List> templateParams) throws Throwable { StringBuilder requestBody = new StringBuilder(); appendToBody(requestBody, "from=", getSender()); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClient.java index a439e7e6..e78a702b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClient.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClient.java @@ -41,7 +41,7 @@ public class QiniuSmsClient extends AbstractSmsClient { Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, String apiTemplateId, List> templateParams) throws Throwable { // 1. 执行请求 // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java index 2ff5951a..c9482c18 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -81,6 +81,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { case TENCENT: return new TencentSmsClient(properties); case HUAWEI: return new HuaweiSmsClient(properties); case QINIU: return new QiniuSmsClient(properties); + case HL95: return new Hl95SmsClient(properties); } // 创建失败,错误日志 + 抛出异常 log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClient.java index 63b26be8..13411e44 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -82,7 +82,7 @@ public class TencentSmsClient extends AbstractSmsClient { } @Override - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String content, String apiTemplateId, List> templateParams) throws Throwable { // 1. 执行请求 // 参考链接 https://cloud.tencent.com/document/product/382/55981 diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/extra/SmsBalanceClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/extra/SmsBalanceClient.java new file mode 100644 index 00000000..704cfc39 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/extra/SmsBalanceClient.java @@ -0,0 +1,15 @@ +package com.zt.plat.module.system.framework.sms.core.client.impl.extra; + +/** + * 支持查询余额的短信客户端扩展接口 + */ +public interface SmsBalanceClient { + + /** + * 查询当前渠道可用余额(条数) + * + * @return 余额条数 + * @throws Throwable 查询失败时抛出异常 + */ + Integer queryBalance() throws Throwable; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java index 6031396a..aed000d4 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java @@ -19,6 +19,7 @@ public enum SmsChannelEnum { TENCENT("TENCENT", "腾讯云"), HUAWEI("HUAWEI", "华为云"), QINIU("QINIU", "七牛云"), + HL95("HL95", "鸿联九五"), ; /** diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/property/SmsChannelProperties.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/property/SmsChannelProperties.java index 1cc5a9fa..9d6843a2 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/property/SmsChannelProperties.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/property/SmsChannelProperties.java @@ -26,6 +26,10 @@ public class SmsChannelProperties { */ @NotEmpty(message = "短信签名不能为空") private String signature; + /** + * 企业编号(epid)。部分渠道需要,例如鸿联九五 + */ + private String epid; /** * 渠道编码 * diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/message/sms/SmsSendMessage.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/message/sms/SmsSendMessage.java index b2a80a5f..fa86b5a6 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/message/sms/SmsSendMessage.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/message/sms/SmsSendMessage.java @@ -25,6 +25,10 @@ public class SmsSendMessage { */ @NotNull(message = "手机号不能为空") private String mobile; + /** + * 短信内容(已按模板格式化后的文本) + */ + private String content; /** * 短信渠道编号 */ diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/producer/sms/SmsProducer.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/producer/sms/SmsProducer.java index 88d07e37..927b5b20 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/producer/sms/SmsProducer.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/producer/sms/SmsProducer.java @@ -32,9 +32,9 @@ public class SmsProducer { * @param apiTemplateId 短信模板编号 * @param templateParams 短信模板参数 */ - public void sendSmsSendMessage(Long logId, String mobile, + public void sendSmsSendMessage(Long logId, String mobile, String content, Long channelId, String apiTemplateId, List> templateParams) { - SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile); + SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile).setContent(content); message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams); rocketMQTemplate.syncSend(SmsSendMessage.TOPIC, message); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeService.java index 43a434be..43fc3a86 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeService.java @@ -49,6 +49,26 @@ public interface DeptExternalCodeService { */ List getDeptExternalCodeListByDeptId(Long deptId); + /** + * 根据部门与外部系统保存/更新映射(存在则更新,不存在则创建) + * + * @param deptId 本系统部门 ID + * @param systemCode 外部系统标识 + * @param externalDeptCode 外部系统组织编码 + * @param externalDeptName 外部系统组织名称(可选) + * @param status 状态,默认启用 + * @return 映射记录 ID + */ + Long saveOrUpdateDeptExternalCode(Long deptId, String systemCode, String externalDeptCode, String externalDeptName, + Integer status); + + /** + * 根据部门删除全部外部编码映射 + * + * @param deptId 部门编号 + */ + void deleteDeptExternalCodesByDeptId(Long deptId); + /** * 根据外部系统与外部组织编码查询映射 */ diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeServiceImpl.java index 5ee5384f..72ce33b5 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptExternalCodeServiceImpl.java @@ -9,10 +9,17 @@ import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptEx import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO; import com.zt.plat.module.system.dal.mysql.dept.DeptExternalCodeMapper; +import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; +import com.zt.plat.module.system.dal.redis.RedisKeyConstants; import jakarta.annotation.Resource; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.util.Objects; + import java.util.List; import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -28,11 +35,15 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { @Resource private DeptExternalCodeMapper deptExternalCodeMapper; @Resource - private DeptService deptService; + private DeptMapper deptMapper; + @Resource + private CacheManager cacheManager; @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST, key = "#createReqVO.deptId", beforeInvocation = false) public Long createDeptExternalCode(DeptExternalCodeSaveReqVO createReqVO) { normalizeRequest(createReqVO); + disableActiveMappingIfConflict(createReqVO.getDeptId(), createReqVO.getSystemCode(), createReqVO.getExternalDeptCode()); validateForCreateOrUpdate(null, createReqVO.getDeptId(), createReqVO.getSystemCode(), createReqVO.getExternalDeptCode()); @@ -48,6 +59,7 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { public void updateDeptExternalCode(DeptExternalCodeSaveReqVO updateReqVO) { normalizeRequest(updateReqVO); DeptExternalCodeDO exists = validateExists(updateReqVO.getId()); + disableActiveMappingIfConflict(updateReqVO.getDeptId(), updateReqVO.getSystemCode(), updateReqVO.getExternalDeptCode()); validateForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getDeptId(), updateReqVO.getSystemCode(), updateReqVO.getExternalDeptCode()); @@ -57,12 +69,15 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { updateObj.setStatus(exists.getStatus() == null ? CommonStatusEnum.ENABLE.getStatus() : exists.getStatus()); } deptExternalCodeMapper.updateById(updateObj); + evictCacheSafely(exists.getDeptId()); + evictCacheSafely(updateObj.getDeptId()); } @Override public void deleteDeptExternalCode(Long id) { - validateExists(id); + DeptExternalCodeDO exists = validateExists(id); deptExternalCodeMapper.deleteById(id); + evictCacheSafely(exists.getDeptId()); } @Override @@ -76,6 +91,7 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { } @Override + @Cacheable(cacheNames = RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST, key = "#deptId") public List getDeptExternalCodeListByDeptId(Long deptId) { return deptExternalCodeMapper.selectListByDeptId(deptId); } @@ -96,6 +112,50 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { return deptExternalCodeMapper.selectBySystemCodeAndDeptId(systemCode.trim(), deptId); } + @Override + public Long saveOrUpdateDeptExternalCode(Long deptId, String systemCode, String externalDeptCode, + String externalDeptName, Integer status) { + if (StrUtil.hasEmpty(systemCode, externalDeptCode) || deptId == null) { + return null; + } + String normalizedSystemCode = systemCode.trim(); + String normalizedExternalCode = externalDeptCode.trim(); + String normalizedExternalName = StrUtil.blankToDefault(StrUtil.trimToNull(externalDeptName), null); + + disableActiveMappingIfConflict(deptId, normalizedSystemCode, normalizedExternalCode); + + // 如果存在则更新,否则创建 + DeptExternalCodeDO exists = deptExternalCodeMapper.selectBySystemCodeAndDeptId(normalizedSystemCode, deptId); + if (exists != null) { + DeptExternalCodeSaveReqVO updateReqVO = new DeptExternalCodeSaveReqVO(); + updateReqVO.setId(exists.getId()); + updateReqVO.setDeptId(deptId); + updateReqVO.setSystemCode(normalizedSystemCode); + updateReqVO.setExternalDeptCode(normalizedExternalCode); + updateReqVO.setExternalDeptName(normalizedExternalName); + updateReqVO.setStatus(status == null ? exists.getStatus() : status); + updateDeptExternalCode(updateReqVO); + return exists.getId(); + } + + DeptExternalCodeSaveReqVO createReqVO = new DeptExternalCodeSaveReqVO(); + createReqVO.setDeptId(deptId); + createReqVO.setSystemCode(normalizedSystemCode); + createReqVO.setExternalDeptCode(normalizedExternalCode); + createReqVO.setExternalDeptName(normalizedExternalName); + createReqVO.setStatus(status == null ? CommonStatusEnum.ENABLE.getStatus() : status); + return createDeptExternalCode(createReqVO); + } + + @Override + public void deleteDeptExternalCodesByDeptId(Long deptId) { + if (deptId == null) { + return; + } + deptExternalCodeMapper.deleteByDeptId(deptId); + evictCacheSafely(deptId); + } + private DeptExternalCodeDO validateExists(Long id) { if (id == null) { throw exception(DEPT_EXTERNAL_RELATION_NOT_EXISTS); @@ -109,7 +169,7 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { private void validateForCreateOrUpdate(Long id, Long deptId, String systemCode, String externalDeptCode) { // 校验部门存在 - DeptDO dept = deptService.getDept(deptId); + DeptDO dept = deptMapper.selectById(deptId); if (dept == null) { throw exception(DEPT_NOT_FOUND); } @@ -129,7 +189,11 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { DeptExternalCodeDO sameExternal = deptExternalCodeMapper .selectBySystemCodeAndExternalCode(normalizedSystemCode, normalizedExternalCode); if (sameExternal != null && (id == null || !sameExternal.getId().equals(id))) { - throw exception(DEPT_EXTERNAL_CODE_DUPLICATE, normalizedSystemCode, normalizedExternalCode); + boolean sameDept = Objects.equals(deptId, sameExternal.getDeptId()); + boolean activeConflict = !sameDept && CommonStatusEnum.isEnable(sameExternal.getStatus()); + if (activeConflict) { + throw exception(DEPT_EXTERNAL_CODE_DUPLICATE, normalizedSystemCode, normalizedExternalCode); + } } } } @@ -148,4 +212,39 @@ public class DeptExternalCodeServiceImpl implements DeptExternalCodeService { reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); } } + + private void disableActiveMappingIfConflict(Long targetDeptId, String systemCode, String externalDeptCode) { + String normalizedSystem = StrUtil.trimToNull(systemCode); + String normalizedExternal = StrUtil.trimToNull(externalDeptCode); + if (StrUtil.hasEmpty(normalizedSystem, normalizedExternal) || targetDeptId == null) { + return; + } + DeptExternalCodeDO existing = deptExternalCodeMapper.selectBySystemCodeAndExternalCode(normalizedSystem, normalizedExternal); + if (existing == null) { + return; + } + if (Objects.equals(existing.getDeptId(), targetDeptId)) { + return; + } + if (CommonStatusEnum.isEnable(existing.getStatus())) { + DeptExternalCodeDO update = new DeptExternalCodeDO(); + update.setId(existing.getId()); + update.setStatus(CommonStatusEnum.DISABLE.getStatus()); + deptExternalCodeMapper.updateById(update); + evictCacheSafely(existing.getDeptId()); + } + } + + private void evictCacheSafely(Long deptId) { + if (deptId == null || cacheManager == null) { + return; + } + try { + if (cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST) != null) { + cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST).evict(deptId); + } + } catch (Exception ignore) { + // 缓存失效失败不影响主流程 + } + } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java index 3cf54bee..7a4e6bbe 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java @@ -11,12 +11,18 @@ import com.zt.plat.framework.datapermission.core.annotation.DataPermission; import com.zt.plat.framework.tenant.core.aop.TenantIgnore; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; +import com.zt.plat.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; +import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; +import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO; import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO; import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper; import com.zt.plat.module.system.dal.redis.RedisKeyConstants; import com.zt.plat.module.system.enums.dept.DeptSourceEnum; +import com.zt.plat.module.system.enums.DictTypeConstants; +import com.zt.plat.module.system.service.dict.DictDataService; +import com.zt.plat.module.system.service.dict.DictTypeService; import org.apache.seata.spring.annotation.GlobalTransactional; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -52,6 +58,12 @@ public class DeptServiceImpl implements DeptService { private UserDeptMapper userDeptMapper; @Resource private com.zt.plat.module.system.mq.producer.databus.DatabusChangeProducer databusChangeProducer; + @Resource + private DeptExternalCodeService deptExternalCodeService; + @Resource + private DictTypeService dictTypeService; + @Resource + private DictDataService dictDataService; private static final String ROOT_CODE_PREFIX = "ZT"; private static final int CODE_SEGMENT_LENGTH = 3; @@ -66,9 +78,8 @@ public class DeptServiceImpl implements DeptService { @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 public Long createDept(DeptSaveReqVO createReqVO) { - if (createReqVO.getParentId() == null) { - createReqVO.setParentId(DeptDO.PARENT_ID_ROOT); - } + // 允许上级组织为空,视为顶级组织 + createReqVO.setParentId(normalizeParentId(createReqVO.getParentId())); // 创建时默认有效 createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 校验父部门的有效性 @@ -76,23 +87,10 @@ public class DeptServiceImpl implements DeptService { // 校验部门名的唯一性 validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); // 生成并校验部门编码 - boolean isIWorkSource = Objects.equals(createReqVO.getDeptSource(), DeptSourceEnum.IWORK.getSource()); - if (isIWorkSource) { - // iWork 来源直接使用提供的编码,不再生成 - String providedCode = StrUtil.blankToDefault(createReqVO.getCode(), null); - createReqVO.setCode(providedCode); - } else { - Long effectiveParentId = normalizeParentId(createReqVO.getParentId()); - boolean isTopLevel = Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT); - String resolvedCode; - if (isTopLevel) { - resolvedCode = resolveTopLevelCode(null, createReqVO.getCode()); - } else { - resolvedCode = generateDeptCode(effectiveParentId); - validateDeptCodeUnique(null, resolvedCode); - } - createReqVO.setCode(resolvedCode); - } + Long effectiveParentId = normalizeParentId(createReqVO.getParentId()); + String resolvedCode = generateDeptCode(effectiveParentId); + validateDeptCodeUnique(null, resolvedCode); + createReqVO.setCode(resolvedCode); // 插入部门 DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class); @@ -102,6 +100,9 @@ public class DeptServiceImpl implements DeptService { } deptMapper.insert(dept); + // 维护外部系统编码映射(若有传入) + upsertExternalCodeMapping(createReqVO, dept.getId()); + // 发布部门创建事件 databusChangeProducer.sendDeptCreatedMessage(dept); @@ -113,47 +114,25 @@ public class DeptServiceImpl implements DeptService { allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 @DataPermission(enable = false) // 禁用数据权限,避免确实上级部门导致无法保存 public void updateDept(DeptSaveReqVO updateReqVO) { - if (updateReqVO.getParentId() == null) { - updateReqVO.setParentId(DeptDO.PARENT_ID_ROOT); - } + // 允许上级组织为空,视为顶级组织 + updateReqVO.setParentId(normalizeParentId(updateReqVO.getParentId())); // 校验自己存在 DeptDO originalDept = getRequiredDept(updateReqVO.getId()); // 校验父部门的有效性 validateParentDept(updateReqVO.getId(), updateReqVO.getParentId()); // 校验部门名的唯一性 validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); - // 如果上级发生变化,需要重新生成编码并同步子级 - boolean isIWorkSource = Objects.equals(originalDept.getDeptSource(), DeptSourceEnum.IWORK.getSource()); Long newParentId = normalizeParentId(updateReqVO.getParentId()); Long oldParentId = normalizeParentId(originalDept.getParentId()); boolean parentChanged = !Objects.equals(newParentId, oldParentId); - if (isIWorkSource) { - // iWork 来源直接使用提供的编码,不再生成 - String providedCode = StrUtil.blankToDefault(updateReqVO.getCode(), null); - updateReqVO.setCode(providedCode); - } else { - if (parentChanged) { - String newCode; - if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) { - newCode = resolveTopLevelCode(updateReqVO.getId(), updateReqVO.getCode()); - } else { - newCode = generateDeptCode(updateReqVO.getParentId()); - validateDeptCodeUnique(updateReqVO.getId(), newCode); - } - updateReqVO.setCode(newCode); - } else { - if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) { - String requestedCode = updateReqVO.getCode(); - if (StrUtil.isNotBlank(requestedCode) && !StrUtil.equals(requestedCode.trim(), originalDept.getCode())) { - updateReqVO.setCode(resolveTopLevelCode(updateReqVO.getId(), requestedCode)); - } else { - updateReqVO.setCode(originalDept.getCode()); - } - } else { - updateReqVO.setCode(originalDept.getCode()); - } - } + String existingCode = originalDept.getCode(); + boolean needRegenerateCode = StrUtil.isBlank(existingCode); + String resolvedCode = existingCode; + if (needRegenerateCode) { + resolvedCode = generateDeptCode(newParentId); + validateDeptCodeUnique(updateReqVO.getId(), resolvedCode); } + updateReqVO.setCode(resolvedCode); // 更新部门 DeptDO updateObj = BeanUtils.toBean(updateReqVO, DeptDO.class); @@ -165,9 +144,12 @@ public class DeptServiceImpl implements DeptService { databusChangeProducer.sendDeptUpdatedMessage(updatedDept); } - if (parentChanged) { + if (needRegenerateCode) { refreshChildCodesRecursively(updateObj.getId(), updateReqVO.getCode()); } + + // 维护外部系统编码映射(若有传入) + upsertExternalCodeMapping(updateReqVO, updateReqVO.getId()); } @Override @@ -185,6 +167,9 @@ public class DeptServiceImpl implements DeptService { DeptDO dept = deptMapper.selectById(id); Long tenantId = (dept != null) ? dept.getTenantId() : null; + // 级联删除外部编码映射并清理缓存 + deptExternalCodeService.deleteDeptExternalCodesByDeptId(id); + // 删除部门 deptMapper.deleteById(id); @@ -756,4 +741,65 @@ public class DeptServiceImpl implements DeptService { return dept; } + private void upsertExternalCodeMapping(DeptSaveReqVO reqVO, Long deptId) { + if (reqVO == null || deptId == null) { + return; + } + String systemCode = StrUtil.trimToNull(reqVO.getExternalSystemCode()); + String externalCode = StrUtil.trimToNull(reqVO.getExternalDeptCode()); + if (StrUtil.isBlank(systemCode) || StrUtil.isBlank(externalCode)) { + return; + } + // 缺失的外部系统字典类型或数据会自动补齐 + ensureExternalSystemDict(systemCode); + deptExternalCodeService.saveOrUpdateDeptExternalCode( + deptId, + systemCode, + externalCode, + reqVO.getExternalDeptName(), + CommonStatusEnum.ENABLE.getStatus()); + } + + /** + * 确保外部系统字典存在(含字典类型与对应值),若缺失则自动创建 + */ + private void ensureExternalSystemDict(String systemCode) { + String normalizedCode = StrUtil.trimToNull(systemCode); + if (normalizedCode == null) { + return; + } + try { + DictTypeDO dictType = dictTypeService.getDictType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM); + if (dictType == null) { + DictTypeSaveReqVO typeReq = new DictTypeSaveReqVO(); + typeReq.setName("部门外部系统标识"); + typeReq.setType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM); + typeReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + typeReq.setRemark("外部组织同步自动创建"); + dictTypeService.createDictType(typeReq); + } else if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) { + DictTypeSaveReqVO updateReq = new DictTypeSaveReqVO(); + updateReq.setId(dictType.getId()); + updateReq.setName(dictType.getName()); + updateReq.setType(dictType.getType()); + updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + updateReq.setRemark(dictType.getRemark()); + dictTypeService.updateDictType(updateReq); + } + + if (dictDataService.getDictData(DictTypeConstants.DEPT_EXTERNAL_SYSTEM, normalizedCode) == null) { + DictDataSaveReqVO dataReq = new DictDataSaveReqVO(); + dataReq.setDictType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM); + dataReq.setLabel(normalizedCode); + dataReq.setValue(normalizedCode); + dataReq.setSort(0); + dataReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + dataReq.setRemark("外部组织同步自动创建"); + dictDataService.createDictData(dataReq); + } + } catch (Exception ex) { + log.warn("[Dept] Ensure external system dict failed, systemCode={}", normalizedCode, ex); + } + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationService.java index 90755ba2..5988e851 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkIntegrationService.java @@ -6,6 +6,9 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuth import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkAuthTokenRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFileCallbackReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOperationRespVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaCheckTokenReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaRawResponse; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOaTokenReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserInfoReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserInfoRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCreateReqVO; @@ -48,4 +51,14 @@ public interface IWorkIntegrationService { * @return 创建的业务附件关联记录 ID 或附件 ID(视实现而定) */ Long handleFileCallback(IWorkFileCallbackReqVO reqVO); + + /** + * 透传调用 OA 获取 token。 + */ + IWorkOaRawResponse getOaToken(IWorkOaTokenReqVO reqVO); + + /** + * 透传调用 OA 校验 token。 + */ + IWorkOaRawResponse checkOaToken(IWorkOaCheckTokenReqVO reqVO); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java index 2e291fe3..7e63695a 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkOrgRestService.java @@ -4,10 +4,8 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepa import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSyncRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO; @@ -24,11 +22,4 @@ public interface IWorkOrgRestService { IWorkHrUserPageRespVO listUsers(IWorkUserQueryReqVO reqVO); - IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO); - - IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO); - - IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO); - - IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java index c3a7a5cd..445af73b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java @@ -124,7 +124,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken())); Map payload = buildUserPayload(reqVO); - String responseBody = executeJsonRequest(properties.getPaths().getUserInfo(), reqVO.getQueryParams(), appId, session, payload); + String userInfoPath = requireConfiguredPath(properties.getPaths().getUserInfo(), "iwork.paths.user-info"); + String responseBody = executeJsonRequest(userInfoPath, reqVO.getQueryParams(), appId, session, payload); return buildUserInfoResponse(responseBody); } @@ -138,7 +139,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken())); Map payload = buildCreatePayload(reqVO); - String responseBody = executeFormRequest(properties.getPaths().getCreateWorkflow(), null, appId, session, payload); + String createWorkflowPath = requireConfiguredPath(properties.getPaths().getCreateWorkflow(), "iwork.paths.create-workflow"); + String responseBody = executeFormRequest(createWorkflowPath, null, appId, session, payload); return buildOperationResponse(responseBody); } @@ -154,7 +156,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { IWorkSession session = createSession(appId, clientKeyPair, operatorUserId, Boolean.TRUE.equals(reqVO.getForceRefreshToken())); Map payload = buildVoidPayload(reqVO); - String responseBody = executeJsonRequest(properties.getPaths().getVoidWorkflow(), null, appId, session, payload); + String voidWorkflowPath = requireConfiguredPath(properties.getPaths().getVoidWorkflow(), "iwork.paths.void-workflow"); + String responseBody = executeJsonRequest(voidWorkflowPath, null, appId, session, payload); return buildOperationResponse(responseBody); } @@ -179,18 +182,83 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { } AtomicReference attachmentIdRef = new AtomicReference<>(); - TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile))); + TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile, reqVO.getSsoToken()))); return attachmentIdRef.get(); } - private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile) { + @Override + public IWorkOaRawResponse getOaToken(IWorkOaTokenReqVO reqVO) { + IWorkProperties.Oa oa = properties.getOa(); + if (oa == null) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA 配置未初始化"); + } + String loginId = Optional.ofNullable(reqVO) + .map(IWorkOaTokenReqVO::getLoginId) + .map(String::trim) + .orElse(""); + if (!StringUtils.hasText(loginId)) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "loginId 不能为空"); + } + String appId = Optional.ofNullable(reqVO) + .map(IWorkOaTokenReqVO::getAppId) + .filter(StringUtils::hasText) + .map(StringUtils::trimWhitespace) + .orElseGet(() -> StringUtils.trimWhitespace(oa.getAppId())); + if (!StringUtils.hasText(appId)) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA appid 未配置"); + } + + String path = requireOaPath(oa.getPaths().getGetToken(), "iwork.oa.paths.get-token"); + + FormBody formBody = new FormBody.Builder() + .add("loginid", loginId) + .add("appid", appId) + .build(); + + Request request = new Request.Builder() + .url(resolveOaUrl(path)) + .post(formBody) + .build(); + + return executeOaRequest(request); + } + + @Override + public IWorkOaRawResponse checkOaToken(IWorkOaCheckTokenReqVO reqVO) { + IWorkProperties.Oa oa = properties.getOa(); + if (oa == null) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "OA 配置未初始化"); + } + String token = Optional.ofNullable(reqVO) + .map(IWorkOaCheckTokenReqVO::getToken) + .map(String::trim) + .orElse(""); + if (!StringUtils.hasText(token)) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "token 不能为空"); + } + + String path = requireOaPath(oa.getPaths().getCheckToken(), "iwork.oa.paths.check-token"); + + FormBody formBody = new FormBody.Builder() + .add("token", token) + .build(); + + Request request = new Request.Builder() + .url(resolveOaUrl(path)) + .post(formBody) + .build(); + + return executeOaRequest(request); + } + + private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile, String ssoToken) { Long businessId = referenceBusinessFile.getBusinessId(); FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO(); fileCreateReqDTO.setName(resolveFileName(overrideFileName, fileUrl)); fileCreateReqDTO.setDirectory(null); fileCreateReqDTO.setType(null); - fileCreateReqDTO.setContent(downloadFileBytes(fileUrl)); + fileCreateReqDTO.setContent(downloadFileBytes(fileUrl, ssoToken)); CommonResult fileResult = fileApi.createFileWithReturn(fileCreateReqDTO); if (fileResult == null || !fileResult.isSuccess() || fileResult.getData() == null) { @@ -229,9 +297,12 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return businessFile; } - private byte[] downloadFileBytes(String fileUrl) { + private byte[] downloadFileBytes(String fileUrl, String ssoToken) { + // 如果回调已提供 ssoToken,按需拼接后下载 OA 附件 + String finalUrl = appendSsoTokenIfNeeded(fileUrl, ssoToken); + OkHttpClient client = okHttpClient(); - Request request = new Request.Builder().url(fileUrl).get().build(); + Request request = new Request.Builder().url(finalUrl).get().build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "下载文件失败,HTTP 状态码: " + response.code()); @@ -246,6 +317,22 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { } } + private String appendSsoTokenIfNeeded(String fileUrl, String ssoToken) { + // 未提供 token 或 URL 为空,直接返回原链接 + if (!StringUtils.hasText(ssoToken) || !StringUtils.hasText(fileUrl)) { + return fileUrl; + } + // 已包含 ssoToken(不区分大小写)则不重复添加 + String lower = fileUrl.toLowerCase(); + if (lower.contains("ssotoken=")) { + return fileUrl; + } + // 简单拼接查询参数 + return fileUrl.contains("?") + ? fileUrl + "&ssoToken=" + ssoToken + : fileUrl + "?ssoToken=" + ssoToken; + } + private String resolveFileName(String overrideName, String fileUrl) { if (StringUtils.hasText(overrideName)) { return overrideName; @@ -293,6 +380,17 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return StringUtils.trimWhitespace(value); } + private String requireConfiguredPath(String value, String propertyPath) { + if (!StringUtils.hasText(value)) { + throw ServiceExceptionUtil.exception(IWORK_CONFIGURATION_INVALID, propertyPath + " 未配置"); + } + return StringUtils.trimWhitespace(value); + } + + private long resolveTokenTtlSeconds() { + return Math.max(1L, properties.getToken().getTtlSeconds()); + } + private ClientKeyPair resolveClientKeyPair(String appId, boolean forceRefresh) { String configured = properties.getClientPublicKey(); if (StringUtils.hasText(configured)) { @@ -333,7 +431,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { String encryptedSecret = encryptWithPublicKey(registration.secret(), registration.spk()); String encryptedUserId = encryptWithPublicKey(operatorUserId, registration.spk()); String token = applyToken(appId, encryptedSecret); - Instant expiresAt = Instant.now().plusSeconds(Math.max(1L, properties.getToken().getTtlSeconds())); + long ttlSeconds = resolveTokenTtlSeconds(); + Instant expiresAt = Instant.now().plusSeconds(ttlSeconds); return new IWorkSession(token, encryptedUserId, expiresAt, registration.spk()); } @@ -359,8 +458,9 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { } private RegistrationState register(String appId, ClientKeyPair clientKeyPair) { + String registerPath = requireConfiguredPath(properties.getPaths().getRegister(), "iwork.paths.register"); Request request = new Request.Builder() - .url(resolveUrl(properties.getPaths().getRegister())) + .url(resolveUrl(registerPath)) .header(properties.getHeaders().getAppId(), appId) .header(properties.getHeaders().getClientPublicKey(), clientKeyPair.publicKey()) .post(RequestBody.create(null, new byte[0])) @@ -376,11 +476,13 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { } private String applyToken(String appId, String encryptedSecret) { + String applyTokenPath = requireConfiguredPath(properties.getPaths().getApplyToken(), "iwork.paths.apply-token"); + long ttlSeconds = resolveTokenTtlSeconds(); Request request = new Request.Builder() - .url(resolveUrl(properties.getPaths().getApplyToken())) + .url(resolveUrl(applyTokenPath)) .header(properties.getHeaders().getAppId(), appId) .header(properties.getHeaders().getSecret(), encryptedSecret) - .header(properties.getHeaders().getTime(), String.valueOf(properties.getToken().getTtlSeconds())) + .header(properties.getHeaders().getTime(), String.valueOf(ttlSeconds)) .post(RequestBody.create(null, new byte[0])) .build(); String responseBody = executeRequest(request, IWORK_APPLY_TOKEN_FAILED); @@ -834,6 +936,30 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return baseUrl + path; } + private String resolveOaUrl(String path) { + IWorkProperties.Oa oa = properties.getOa(); + if (oa == null || !StringUtils.hasText(oa.getBaseUrl())) { + throw ServiceExceptionUtil.exception(IWORK_BASE_URL_MISSING, "iwork.oa.base-url 未配置"); + } + String baseUrl = oa.getBaseUrl(); + boolean baseEndsWithSlash = baseUrl.endsWith("/"); + boolean pathStartsWithSlash = StringUtils.hasText(path) && path.startsWith("/"); + if (baseEndsWithSlash && pathStartsWithSlash) { + return baseUrl + path.substring(1); + } + if (!baseEndsWithSlash && !pathStartsWithSlash) { + return baseUrl + "/" + path; + } + return baseUrl + path; + } + + private String requireOaPath(String path, String propertyPath) { + if (!StringUtils.hasText(path)) { + throw ServiceExceptionUtil.exception(IWORK_CONFIGURATION_INVALID, propertyPath + " 未配置"); + } + return StringUtils.trimWhitespace(path); + } + private String executeRequest(Request request, ErrorCode errorCode) { logCurlCommand(request); try (Response response = okHttpClient().newCall(request).execute()) { @@ -849,6 +975,47 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { } } + private IWorkOaRawResponse executeOaRequest(Request request) { + long start = System.currentTimeMillis(); + try (Response response = okHttpClient().newCall(request).execute()) { + ResponseBody responseBody = response.body(); + okhttp3.MediaType okhttpMediaType = responseBody != null ? responseBody.contentType() : null; + String bodyString = responseBody != null ? responseBody.string() : null; + + MediaType contentType = null; + if (okhttpMediaType != null) { + try { + contentType = MediaType.parseMediaType(okhttpMediaType.toString()); + } catch (Exception ignored) { + // ignore parse error + } + } + + Map headers = new LinkedHashMap<>(); + Headers respHeaders = response.headers(); + for (String name : respHeaders.names()) { + headers.put(name, respHeaders.get(name)); + } + + long duration = System.currentTimeMillis() - start; + String briefBody = abbreviate(bodyString, 200); + if (response.isSuccessful()) { + log.info("[OA] {} {} -> {} ({}ms)", request.method(), request.url(), response.code(), duration); + } else { + log.warn("[OA] {} {} -> {} ({}ms) body={}", request.method(), request.url(), response.code(), duration, briefBody); + } + + return IWorkOaRawResponse.builder() + .statusCode(response.code()) + .body(bodyString) + .contentType(contentType) + .headers(headers) + .build(); + } catch (IOException ex) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "调用 OA 接口失败: " + ex.getMessage()); + } + } + private ServiceException buildRemoteException(ErrorCode errorCode, int statusCode, String responseBody) { String detail = buildRemoteErrorDetail(statusCode, responseBody); if (!StringUtils.hasText(detail)) { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java index 89133941..f31003a1 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java @@ -9,11 +9,9 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDepa import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJobTitlePageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSyncRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkJobTitleQueryReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgBaseQueryReqVO; -import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOrgSyncReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkSubcompanyQueryReqVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkUserQueryReqVO; import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties; @@ -62,9 +60,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { private static final TypeReference> USER_LIST_TYPE = new TypeReference<>() { }; - private static final TypeReference> SYNC_RESULT_LIST_TYPE = - new TypeReference<>() { - }; private static final okhttp3.MediaType JSON_MEDIA_TYPE = okhttp3.MediaType.get("application/json; charset=UTF-8"); private final IWorkProperties properties; @@ -178,34 +173,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { return params; } - @Override - public IWorkHrSyncRespVO syncSubcompanies(IWorkOrgSyncReqVO reqVO) { - String path = orgPaths().getSyncSubcompany(); - JsonNode node = invokeDataEndpoint(path, reqVO.getData()); - return buildSyncResp(node); - } - - @Override - public IWorkHrSyncRespVO syncDepartments(IWorkOrgSyncReqVO reqVO) { - String path = orgPaths().getSyncDepartment(); - JsonNode node = invokeDataEndpoint(path, reqVO.getData()); - return buildSyncResp(node); - } - - @Override - public IWorkHrSyncRespVO syncJobTitles(IWorkOrgSyncReqVO reqVO) { - String path = orgPaths().getSyncJobTitle(); - JsonNode node = invokeDataEndpoint(path, reqVO.getData()); - return buildSyncResp(node); - } - - @Override - public IWorkHrSyncRespVO syncUsers(IWorkOrgSyncReqVO reqVO) { - String path = orgPaths().getSyncUser(); - JsonNode node = invokeDataEndpoint(path, reqVO.getData()); - return buildSyncResp(node); - } - private JsonNode invokeParamsEndpoint(String path, Map params) { Objects.requireNonNull(params, "查询参数不能为空"); Map payload = new HashMap<>(); @@ -213,13 +180,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { return executeJson(path, payload); } - private JsonNode invokeDataEndpoint(String path, Object data) { - Objects.requireNonNull(data, "同步数据不能为空"); - Map payload = new HashMap<>(); - payload.put("data", data); - return executeJson(path, payload); - } - private JsonNode executeJson(String path, Map payload) { // 统一封装请求体并发送 POST 调用 assertOrgConfigured(path); @@ -369,17 +329,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { return respVO; } - // 解析并封装同步结果 - private IWorkHrSyncRespVO buildSyncResp(JsonNode node) { - ParsedEnvelope envelope = parseEnvelope(node); - IWorkHrSyncRespVO respVO = new IWorkHrSyncRespVO(); - respVO.setCode(envelope.code()); - respVO.setMessage(envelope.message()); - respVO.setSuccess(envelope.success()); - respVO.setResult(readList(envelope.root(), "result", SYNC_RESULT_LIST_TYPE)); - return respVO; - } - private JsonNode parseJson(String body) { try { return objectMapper.readTree(body); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java index b79aa877..796e8cf3 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java @@ -18,6 +18,7 @@ import com.zt.plat.module.system.dal.mysql.dept.PostMapper; import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper; import com.zt.plat.module.system.enums.common.SexEnum; import com.zt.plat.module.system.enums.dept.DeptSourceEnum; +import com.zt.plat.module.system.enums.dept.ExternalPlatformEnum; import com.zt.plat.module.system.enums.user.UserSourceEnum; import com.zt.plat.module.system.service.dept.DeptService; import com.zt.plat.module.system.service.dept.PostService; @@ -399,14 +400,16 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { DeptSaveReqVO req = new DeptSaveReqVO(); req.setId(deptId); req.setName(limitLength(StrUtil.blankToDefault(data.getSubcompanyname(), "未命名分部"), 30)); -// req.setShortName(limitLength(data.getSubcompanyname(), 20)); - req.setCode(trimToNull(data.getSubcompanycode())); + // req.setShortName(limitLength(data.getSubcompanyname(), 20)); req.setParentId(parentId == null ? DeptDO.PARENT_ID_ROOT : parentId); req.setSort(defaultSort(data.getShoworder())); req.setStatus(toStatus(canceled)); req.setIsCompany(Boolean.TRUE); req.setIsGroup(Boolean.FALSE); req.setDeptSource(DeptSourceEnum.IWORK.getSource()); + req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode()); + req.setExternalDeptCode(StrUtil.blankToDefault(trimToNull(data.getSubcompanycode()), String.valueOf(data.getId()))); + req.setExternalDeptName(data.getSubcompanyname()); return req; } @@ -417,14 +420,16 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { DeptSaveReqVO req = new DeptSaveReqVO(); req.setId(deptId); req.setName(limitLength(StrUtil.blankToDefault(data.getDepartmentname(), "未命名部门"), 30)); -// req.setShortName(limitLength(StrUtil.blankToDefault(data.getDepartmentmark(), data.getDepartmentname()), 20)); - req.setCode(trimToNull(data.getDepartmentcode())); + // req.setShortName(limitLength(StrUtil.blankToDefault(data.getDepartmentmark(), data.getDepartmentname()), 20)); req.setParentId(parentId == null ? DeptDO.PARENT_ID_ROOT : parentId); req.setSort(defaultSort(data.getShoworder())); req.setStatus(toStatus(canceled)); req.setIsCompany(Boolean.FALSE); req.setIsGroup(Boolean.FALSE); req.setDeptSource(DeptSourceEnum.IWORK.getSource()); + req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode()); + req.setExternalDeptCode(StrUtil.blankToDefault(trimToNull(data.getDepartmentcode()), String.valueOf(data.getId()))); + req.setExternalDeptName(data.getDepartmentname()); return req; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelService.java index 8753b502..ed4ac184 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelService.java @@ -78,4 +78,11 @@ public interface SmsChannelService { */ SmsClient getSmsClient(String code); + /** + * 查询渠道余额(条数) + * @param id 渠道编号 + * @return 余额条数 + */ + Integer queryBalance(Long id); + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelServiceImpl.java index db7f517e..e879ffbf 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsChannelServiceImpl.java @@ -8,6 +8,7 @@ import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO; import com.zt.plat.module.system.dal.mysql.sms.SmsChannelMapper; import com.zt.plat.module.system.framework.sms.core.client.SmsClient; import com.zt.plat.module.system.framework.sms.core.client.SmsClientFactory; +import com.zt.plat.module.system.framework.sms.core.client.impl.extra.SmsBalanceClient; import com.zt.plat.module.system.framework.sms.core.property.SmsChannelProperties; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -16,8 +17,7 @@ import org.springframework.stereotype.Service; import java.util.List; import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.zt.plat.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; -import static com.zt.plat.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; +import static com.zt.plat.module.system.enums.ErrorCodeConstants.*; /** * 短信渠道 Service 实现类 @@ -100,4 +100,19 @@ public class SmsChannelServiceImpl implements SmsChannelService { return smsClientFactory.getSmsClient(code); } + @Override + public Integer queryBalance(Long id) { + SmsChannelDO channel = validateSmsChannelExists(id); + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); + SmsClient client = smsClientFactory.createOrUpdateSmsClient(properties); + if (client instanceof SmsBalanceClient) { + try { + return ((SmsBalanceClient) client).queryBalance(); + } catch (Throwable e) { + throw exception(SMS_TEMPLATE_API_ERROR, "查询余额失败:" + e.getMessage()); + } + } + throw exception(SMS_CHANNEL_BALANCE_UNSUPPORTED); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogService.java index 1af31d78..09ba7f37 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogService.java @@ -65,4 +65,12 @@ public interface SmsLogService { */ PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO); + /** + * 根据日志编号查询短信日志 + * + * @param id 日志编号 + * @return 短信日志 + */ + SmsLogDO getSmsLog(Long id); + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogServiceImpl.java index d7b0a81c..161444cc 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsLogServiceImpl.java @@ -76,4 +76,8 @@ public class SmsLogServiceImpl implements SmsLogService { return smsLogMapper.selectPage(pageReqVO); } + @Override + public SmsLogDO getSmsLog(Long id) { + return smsLogMapper.selectById(id); + } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java index 678c146b..82db724e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java @@ -98,7 +98,7 @@ public class SmsSendServiceImpl implements SmsSendService { // 发送 MQ 消息,异步执行发送短信 if (isSend) { - smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), + smsProducer.sendSmsSendMessage(sendLogId, mobile, content, template.getChannelId(), template.getApiTemplateId(), newTemplateParams); } return sendLogId; @@ -161,8 +161,8 @@ public class SmsSendServiceImpl implements SmsSendService { Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId()); // 发送短信 try { - SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(), - message.getApiTemplateId(), message.getTemplateParams()); + SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(), + message.getContent(), message.getApiTemplateId(), message.getTemplateParams()); smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(), sendResponse.getApiCode(), sendResponse.getApiMsg(), sendResponse.getApiRequestId(), sendResponse.getSerialNo()); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java index cbec4789..b9126216 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java @@ -196,9 +196,8 @@ public class AdminUserServiceImpl implements AdminUserService { if (StrUtil.isNotBlank(updateReqVO.getNickname())) { updateObj.setNickname(updateReqVO.getNickname()); } - if (updateReqVO.getWorkcode() != null) { - updateObj.setWorkcode(normalizeWorkcode(updateReqVO.getWorkcode())); - } + // 工号允许清空,因此直接归一化后写入 + updateObj.setWorkcode(normalizeWorkcode(updateReqVO.getWorkcode())); if (StrUtil.isNotBlank(updateReqVO.getMobile())) { updateObj.setMobile(updateReqVO.getMobile()); } diff --git a/zt-module-system/zt-module-system-server/src/main/resources/application.yaml b/zt-module-system/zt-module-system-server/src/main/resources/application.yaml index 3369dc0d..2b0a2558 100644 --- a/zt-module-system/zt-module-system-server/src/main/resources/application.yaml +++ b/zt-module-system/zt-module-system-server/src/main/resources/application.yaml @@ -134,6 +134,12 @@ iwork: sync-user: /api/hrm/resful/synHrmresource workflow: seal-workflow-id: "1753" + oa: + base-url: http://172.16.36.233:8080 + app-id: a17ca6ca-88b0-463e-bffa-7995086bf225 + paths: + get-token: /ssologin/getToken + check-token: /ssologin/checkToken --- #################### RPC 远程调用相关配置 #################### diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/dal/mysql/user/AdminUserMapperTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/dal/mysql/user/AdminUserMapperTest.java new file mode 100644 index 00000000..7ab4eeaf --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/dal/mysql/user/AdminUserMapperTest.java @@ -0,0 +1,121 @@ +package com.zt.plat.module.system.dal.mysql.user; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.test.core.ut.BaseDbUnitTest; +import com.zt.plat.module.system.controller.admin.user.vo.user.UserPageReqVO; +import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO; +import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO; +import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Collections; + +import static com.zt.plat.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.zt.plat.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link AdminUserMapper} 单元测试 + */ +public class AdminUserMapperTest extends BaseDbUnitTest { + + @Resource + private AdminUserMapper adminUserMapper; + @Resource + private UserDeptMapper userDeptMapper; + + @Test + public void testSelectPage_keywordMatch() { + // 准备数据:两个用户,只有第一个命中 keyword + AdminUserDO matchUser = randomUser("key-nick", "key-user", "13800000000", "WK001"); + AdminUserDO otherUser = randomUser("other", "otherUser", "13900000000", "WK002"); + adminUserMapper.insert(matchUser); + adminUserMapper.insert(otherUser); + + // 调用 + UserPageReqVO reqVO = new UserPageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + reqVO.setKeyword("key"); + PageResult page = adminUserMapper.selectPage(reqVO, null, (java.util.Collection) null); + + // 断言:只返回命中的用户 + assertEquals(1, page.getList().size()); + assertPojoEquals(matchUser, page.getList().get(0)); + } + + @Test + public void testSelectPage_filterByDeptIds() { + // 用户 A 属于部门 1,用户 B 属于部门 2 + AdminUserDO dept1User = randomUser("dept1", "dept1User", "13000000001", "WK101"); + AdminUserDO dept2User = randomUser("dept2", "dept2User", "13000000002", "WK102"); + adminUserMapper.insert(dept1User); + adminUserMapper.insert(dept2User); + UserDeptDO ud1 = new UserDeptDO(); + ud1.setUserId(dept1User.getId()); + ud1.setDeptId(1L); + userDeptMapper.insert(ud1); + + UserDeptDO ud2 = new UserDeptDO(); + ud2.setUserId(dept2User.getId()); + ud2.setDeptId(2L); + userDeptMapper.insert(ud2); + + UserPageReqVO reqVO = new UserPageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + + // 仅过滤部门 1 + PageResult page = adminUserMapper.selectPage(reqVO, Collections.singletonList(1L), null); + + assertEquals(1, page.getList().size()); + assertEquals(dept1User.getId(), page.getList().get(0).getId()); + } + + @Test + public void testSelectPage_filterByUserIds() { + AdminUserDO user1 = randomUser("u1", "u1", "15000000001", "WK201"); + AdminUserDO user2 = randomUser("u2", "u2", "15000000002", "WK202"); + adminUserMapper.insert(user1); + adminUserMapper.insert(user2); + + UserPageReqVO reqVO = new UserPageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + + PageResult page = adminUserMapper.selectPage(reqVO, null, Collections.singletonList(user2.getId())); + + assertEquals(1, page.getList().size()); + assertEquals(user2.getId(), page.getList().get(0).getId()); + } + + private AdminUserDO randomUser(String nickname, String username, String mobile, String workcode) { + AdminUserDO user = randomPojo(AdminUserDO.class, o -> { + o.setId(null); + o.setNickname(nickname); + o.setUsername(username); + o.setMobile(mobile); + o.setWorkcode(workcode); + o.setSex(1); + o.setStatus(0); + o.setUserSource(1); + // 这些字段仅用于展示,不参与持久化,避免断言时与查询结果不一致 + o.setDeptIds(null); + o.setDeptNames(null); + o.setCompanyIds(null); + o.setCompanyDeptInfos(null); + o.setCreateTime(LocalDateTime.now().withNano(0)); + }); + // 保证关键字段非空 + if (!StringUtils.hasText(user.getPassword())) { + user.setPassword("pwd"); + } + if (user.getTenantId() == null) { + user.setTenantId(1L); + } + return user; + } +} diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java index 25186c2b..45d1e7c3 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java @@ -46,6 +46,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); // mock 方法 @@ -55,7 +56,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertTrue(result.getSuccess()); @@ -73,6 +74,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); // mock 方法 @@ -82,7 +84,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); assertEquals("B7700B8E-227E-5886-9564-26036172F01F", result.getApiRequestId()); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java index 293f9275..baf3ccae 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java @@ -43,6 +43,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString() + " " + randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); @@ -51,7 +52,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { .thenReturn("{\"result\":[{\"originTo\":\"+86155****5678\",\"createTime\":\"2018-05-25T16:34:34Z\",\"from\":\"1069********0012\",\"smsMsgId\":\"d6e3cdd0-522b-4692-8304-a07553cdf591_8539659\",\"status\":\"000000\",\"countryId\":\"CN\",\"total\":2}],\"code\":\"000000\",\"description\":\"Success\"}\n"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertTrue(result.getSuccess()); @@ -67,6 +68,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString() + " " + randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); @@ -75,7 +77,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { .thenReturn("{\"result\":[{\"total\":1,\"originTo\":\"17321315478\",\"createTime\":\"2024-08-18T11:32:20Z\",\"from\":\"x8824060312575\",\"smsMsgId\":\"06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461\",\"countryId\":\"CN\",\"status\":\"E200033\"}],\"code\":\"E000510\",\"description\":\"The SMS fails to be sent. For details, see status.\"}"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); @@ -91,6 +93,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString() + " " + randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); @@ -99,7 +102,7 @@ public class HuaweiSmsClientTest extends BaseMockitoUnitTest { .thenReturn("{\"code\":\"E000102\",\"description\":\"Invalid app_key.\"}"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java index 122d0fb9..a60f9ebe 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java @@ -45,13 +45,14 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString() + " " + randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"message_id\":\"17245678901\"}"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertTrue(result.getSuccess()); @@ -66,13 +67,14 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString() + " " + randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientTests.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientTests.java index fefc9dde..b42199ac 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientTests.java @@ -29,6 +29,7 @@ public class SmsClientTests { AliyunSmsClient client = new AliyunSmsClient(properties); // 准备参数 String apiTemplateId = "SMS_207945135"; + String content = "test"; // 调用 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); // 打印结果 @@ -47,8 +48,9 @@ public class SmsClientTests { Long sendLogId = System.currentTimeMillis(); String mobile = "15601691323"; String apiTemplateId = "SMS_207945135"; + String content = "test"; // 调用 - SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, ListUtil.of(new KeyValue("code", "1024"))); // 打印结果 System.out.println(sendRespDTO); } @@ -68,8 +70,9 @@ public class SmsClientTests { Long sendLogId = System.currentTimeMillis(); String mobile = "15601691323"; String apiTemplateId = "358212"; + String content = "test"; // 调用 - SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024"))); + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, ListUtil.of(new KeyValue("code", "1024"))); // 打印结果 System.out.println(sendRespDTO); } @@ -106,9 +109,10 @@ public class SmsClientTests { Long sendLogId = System.currentTimeMillis(); String mobile = "17321315478"; String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; - List> templateParams = ListUtil.of(new KeyValue<>("code", "1024")); + String content = "test"; + List> templateParams = ListUtil.of(new KeyValue("code", "1024")); // 调用 - SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 打印结果 System.out.println(smsSendRespDTO); } @@ -126,9 +130,10 @@ public class SmsClientTests { Long sendLogId = System.currentTimeMillis(); String mobile = "17321315478"; String apiTemplateId = "3644cdab863546a3b718d488659a99ef"; - List> templateParams = ListUtil.of(new KeyValue<>("code", "1122")); + String content = "test"; + List> templateParams = ListUtil.of(new KeyValue("code", "1122")); // 调用 - SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 打印结果 System.out.println(smsSendRespDTO); } diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java index 9f82f54a..34b347df 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java @@ -45,6 +45,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); // mock 方法 @@ -67,7 +68,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { "}"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertTrue(result.getSuccess()); @@ -84,6 +85,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); @@ -107,7 +109,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { "}"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); @@ -124,6 +126,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { Long sendLogId = randomLongId(); String mobile = randomString(); String apiTemplateId = randomString(); + String content = randomString(); List> templateParams = Lists.newArrayList( new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); @@ -132,7 +135,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { .thenReturn("{\"Response\":{\"Error\":{\"Code\":\"AuthFailure.SecretIdNotFound\",\"Message\":\"The SecretId is not found, please ensure that your SecretId is correct.\"},\"RequestId\":\"2a88f82a-261c-4ac6-9fa9-c7d01aaa486a\"}}"); // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, content, apiTemplateId, templateParams); // 断言 assertFalse(result.getSuccess()); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptExternalCodeServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptExternalCodeServiceImplTest.java new file mode 100644 index 00000000..dbcc7371 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptExternalCodeServiceImplTest.java @@ -0,0 +1,196 @@ +package com.zt.plat.module.system.service.dept; + +import com.zt.plat.framework.common.enums.CommonStatusEnum; +import com.zt.plat.framework.test.core.ut.BaseDbUnitTest; +import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptExternalCodeSaveReqVO; +import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; +import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO; +import com.zt.plat.module.system.dal.mysql.dept.DeptExternalCodeMapper; +import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; +import com.zt.plat.module.system.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.Objects; + +import static com.zt.plat.module.system.dal.redis.RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST; +import static org.junit.jupiter.api.Assertions.*; + +@Import({DeptExternalCodeServiceImpl.class, DeptExternalCodeServiceImplTest.CacheConfig.class}) +class DeptExternalCodeServiceImplTest extends BaseDbUnitTest { + + @Resource + private DeptExternalCodeServiceImpl deptExternalCodeService; + @Resource + private DeptExternalCodeMapper deptExternalCodeMapper; + @Resource + private CacheManager cacheManager; + + @Resource + private DeptMapper deptMapper; + + @TestConfiguration + @EnableCaching + static class CacheConfig { + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST); + } + } + + @Test + void testCacheEvictOnCreateAndUpdate() { + Long deptId = 100L; + DeptDO dept = new DeptDO(); + dept.setId(deptId); + dept.setName("总部"); + dept.setCode("ZT001"); + dept.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(dept); + + // 预热缓存(空结果) + List firstCall = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId); + assertTrue(firstCall.isEmpty()); + assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptId)); + + // 创建映射应触发缓存失效 + DeptExternalCodeSaveReqVO createReq = new DeptExternalCodeSaveReqVO(); + createReq.setDeptId(deptId); + createReq.setSystemCode("ERP"); + createReq.setExternalDeptCode("ERP-001"); + deptExternalCodeService.createDeptExternalCode(createReq); + + List refreshed = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId); + assertEquals(1, refreshed.size()); + assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptId)); + + // 更新映射也会清理缓存 + DeptExternalCodeSaveReqVO updateReq = new DeptExternalCodeSaveReqVO(); + Long id = deptExternalCodeMapper.selectListByDeptId(deptId).get(0).getId(); + updateReq.setId(id); + updateReq.setDeptId(deptId); + updateReq.setSystemCode("ERP"); + updateReq.setExternalDeptCode("ERP-002"); + updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptExternalCodeService.updateDeptExternalCode(updateReq); + + List refreshedAfterUpdate = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId); + assertEquals(1, refreshedAfterUpdate.size()); + assertEquals("ERP-002", deptExternalCodeMapper.selectById(id).getExternalDeptCode()); + } + + @Test + void testSaveOrUpdateDeptExternalCodeUpsert() { + Long deptId = 101L; + DeptDO dept = new DeptDO(); + dept.setId(deptId); + dept.setName("事业部"); + dept.setCode("ZT002"); + dept.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(dept); + + Long firstId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "OA", "OA-001", "OA-总部", null); + assertNotNull(firstId); + + // upsert: 同一 system + dept 更新编码 + Long secondId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "OA", "OA-002", "OA-新编码", CommonStatusEnum.ENABLE.getStatus()); + assertEquals(firstId, secondId); + assertEquals("OA-002", deptExternalCodeMapper.selectById(firstId).getExternalDeptCode()); + } + + @Test + void testMultiSystemCoexistenceAndCrossSystemReuse() { + Long deptId = 200L; + DeptDO dept = new DeptDO(); + dept.setId(deptId); + dept.setName("多系统部门"); + dept.setCode("ZT200"); + dept.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(dept); + + deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "ERP", "CODE-001", "ERP编码", CommonStatusEnum.ENABLE.getStatus()); + deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, "OA", "CODE-001", "OA编码", CommonStatusEnum.ENABLE.getStatus()); + + List list = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId); + assertEquals(2, list.size()); + assertTrue(list.stream().anyMatch(it -> Objects.equals("ERP", it.getSystemCode()) && Objects.equals("CODE-001", it.getExternalDeptCode()))); + assertTrue(list.stream().anyMatch(it -> Objects.equals("OA", it.getSystemCode()) && Objects.equals("CODE-001", it.getExternalDeptCode()))); + assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptId)); + } + + @Test + void testConflictDisablesPreviousActiveMappingAndEvictsCache() { + Long deptAId = 201L; + Long deptBId = 202L; + + DeptDO deptA = new DeptDO(); + deptA.setId(deptAId); + deptA.setName("部门A"); + deptA.setCode("ZTA"); + deptA.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(deptA); + + DeptDO deptB = new DeptDO(); + deptB.setId(deptBId); + deptB.setName("部门B"); + deptB.setCode("ZTB"); + deptB.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(deptB); + + // A 先建立启用映射并预热缓存 + deptExternalCodeService.saveOrUpdateDeptExternalCode(deptAId, "ERP", "X-001", "ERP-A", CommonStatusEnum.ENABLE.getStatus()); + deptExternalCodeService.getDeptExternalCodeListByDeptId(deptAId); + assertNotNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptAId)); + + // B 使用同 system+external,占用后应禁用 A 的映射并驱逐 A 缓存 + Long newId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptBId, "ERP", "X-001", "ERP-B", CommonStatusEnum.ENABLE.getStatus()); + + DeptExternalCodeDO oldRecord = deptExternalCodeMapper.selectBySystemCodeAndDeptId("ERP", deptAId); + DeptExternalCodeDO newRecord = deptExternalCodeMapper.selectById(newId); + assertNotNull(oldRecord); + assertNotNull(newRecord); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), oldRecord.getStatus()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), newRecord.getStatus()); + assertNull(cacheManager.getCache(DEPT_EXTERNAL_CODE_LIST).get(deptAId)); + } + + @Test + void testDisabledConflictDoesNotBlockNewMapping() { + Long deptAId = 203L; + Long deptBId = 204L; + + DeptDO deptA = new DeptDO(); + deptA.setId(deptAId); + deptA.setName("停用部门"); + deptA.setCode("ZTDIS"); + deptA.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(deptA); + + DeptDO deptB = new DeptDO(); + deptB.setId(deptBId); + deptB.setName("新部门"); + deptB.setCode("ZTNEW"); + deptB.setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(deptB); + + // 先插入停用态记录 + deptExternalCodeService.saveOrUpdateDeptExternalCode(deptAId, "ERP", "Z-001", "停用编码", CommonStatusEnum.DISABLE.getStatus()); + + // 新部门占用相同 system+external 应直接成功,不触发异常 + Long newId = deptExternalCodeService.saveOrUpdateDeptExternalCode(deptBId, "ERP", "Z-001", "启用编码", CommonStatusEnum.ENABLE.getStatus()); + assertNotNull(newId); + + DeptExternalCodeDO reused = deptExternalCodeMapper.selectById(newId); + assertEquals(deptBId, reused.getDeptId()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), reused.getStatus()); + DeptExternalCodeDO disabled = deptExternalCodeMapper.selectBySystemCodeAndDeptId("ERP", deptAId); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), disabled.getStatus()); + } +} diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java index 6520d2fb..1a877052 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java @@ -5,11 +5,21 @@ import com.zt.plat.framework.common.util.object.ObjectUtils; import com.zt.plat.framework.test.core.ut.BaseDbUnitTest; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; +import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptExternalCodeSaveReqVO; import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; +import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO; +import com.zt.plat.module.system.dal.mysql.dept.DeptExternalCodeMapper; import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; +import com.zt.plat.module.system.service.dept.DeptExternalCodeServiceImpl; +import com.zt.plat.module.system.dal.redis.RedisKeyConstants; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; import java.util.Arrays; import java.util.List; @@ -27,13 +37,31 @@ import static org.junit.jupiter.api.Assertions.*; * * @author niudehua */ -@Import(DeptServiceImpl.class) +@Import({DeptServiceImpl.class, DeptExternalCodeServiceImpl.class, DeptServiceImplTest.CacheConfig.class}) public class DeptServiceImplTest extends BaseDbUnitTest { @Resource private DeptServiceImpl deptService; @Resource private DeptMapper deptMapper; + @Resource + private DeptExternalCodeServiceImpl deptExternalCodeService; + @Resource + private DeptExternalCodeMapper deptExternalCodeMapper; + @Resource + private CacheManager cacheManager; + + @TestConfiguration + @EnableCaching + static class CacheConfig { + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager( + RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST + ); + } + } private Long createDept(Long parentId, String name, int sort) { DeptSaveReqVO reqVO = new DeptSaveReqVO(); @@ -108,7 +136,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest { } @Test - public void testCreateDept_topLevelRespectCustomCode() { + public void testCreateDept_topLevelAutoCode_ignoreCustomInput() { String customCode = "ROOT-001"; DeptSaveReqVO topLevelReq = new DeptSaveReqVO(); topLevelReq.setParentId(DeptDO.PARENT_ID_ROOT); @@ -120,7 +148,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest { Long deptId = deptService.createDept(topLevelReq); DeptDO created = deptMapper.selectById(deptId); - assertEquals(customCode, created.getCode()); + assertEquals("ZT001", created.getCode()); } @Test @@ -150,12 +178,15 @@ public class DeptServiceImplTest extends BaseDbUnitTest { } @Test - public void testUpdateDept_parentChangedRebuildsCodes() { + public void testUpdateDept_parentChangedKeepsCodes() { Long parentAId = createDept(DeptDO.PARENT_ID_ROOT, "A公司", 1); Long parentBId = createDept(DeptDO.PARENT_ID_ROOT, "B公司", 2); Long childId = createDept(parentAId, "子部门", 1); Long grandChildId = createDept(childId, "子部门-一组", 1); + DeptDO originalChild = deptMapper.selectById(childId); + DeptDO originalGrandChild = deptMapper.selectById(grandChildId); + DeptDO parentB = deptMapper.selectById(parentBId); DeptSaveReqVO updateReq = new DeptSaveReqVO(); @@ -169,8 +200,119 @@ public class DeptServiceImplTest extends BaseDbUnitTest { DeptDO updatedChild = deptMapper.selectById(childId); DeptDO updatedGrandChild = deptMapper.selectById(grandChildId); - assertEquals(parentB.getCode() + "001", updatedChild.getCode()); - assertEquals(updatedChild.getCode() + "001", updatedGrandChild.getCode()); + // 已有编码保持不变 + assertEquals(originalChild.getCode(), updatedChild.getCode()); + assertEquals(originalGrandChild.getCode(), updatedGrandChild.getCode()); + // 父子关系按新父级变更 + assertEquals(parentB.getId(), updatedChild.getParentId()); + assertEquals(childId, updatedGrandChild.getParentId()); + // 新父级自身编码不受影响 + assertEquals(parentB.getCode(), deptMapper.selectById(parentBId).getCode()); + } + + @Test + public void testUpdateDept_addsMultiSystemMappingsWithoutChangingCode() { + Long parentId = createDept(DeptDO.PARENT_ID_ROOT, "父级", 1); + Long deptId = createDept(parentId, "多系统部门", 1); + DeptDO original = deptMapper.selectById(deptId); + + // 第一次更新:写入 ERP 外部编码 + DeptSaveReqVO updateReq1 = new DeptSaveReqVO(); + updateReq1.setId(deptId); + updateReq1.setParentId(parentId); + updateReq1.setName("多系统部门"); + updateReq1.setSort(1); + updateReq1.setStatus(CommonStatusEnum.ENABLE.getStatus()); + updateReq1.setDeptSource(1); + updateReq1.setExternalSystemCode("ERP"); + updateReq1.setExternalDeptCode("ERP-100"); + deptService.updateDept(updateReq1); + + // 第二次更新:写入 OA 外部编码 + DeptSaveReqVO updateReq2 = new DeptSaveReqVO(); + updateReq2.setId(deptId); + updateReq2.setParentId(parentId); + updateReq2.setName("多系统部门"); + updateReq2.setSort(1); + updateReq2.setStatus(CommonStatusEnum.ENABLE.getStatus()); + updateReq2.setDeptSource(1); + updateReq2.setExternalSystemCode("OA"); + updateReq2.setExternalDeptCode("OA-100"); + deptService.updateDept(updateReq2); + + DeptDO updated = deptMapper.selectById(deptId); + assertEquals(original.getCode(), updated.getCode()); + + List mappings = deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId); + assertEquals(2, mappings.size()); + assertTrue(mappings.stream().anyMatch(it -> "ERP".equals(it.getSystemCode()) && "ERP-100".equals(it.getExternalDeptCode()))); + assertTrue(mappings.stream().anyMatch(it -> "OA".equals(it.getSystemCode()) && "OA-100".equals(it.getExternalDeptCode()))); + } + + @Test + public void testCreateDept_externalMappingConflictDisablesOld() { + // 首个部门创建时写入 IWORK 外部编码 + DeptSaveReqVO createA = new DeptSaveReqVO(); + createA.setParentId(DeptDO.PARENT_ID_ROOT); + createA.setName("iWork-A"); + createA.setSort(1); + createA.setStatus(CommonStatusEnum.ENABLE.getStatus()); + createA.setDeptSource(1); + createA.setExternalSystemCode("IWORK"); + createA.setExternalDeptCode("IW-001"); + Long deptAId = deptService.createDept(createA); + + // 预热缓存 + deptExternalCodeService.getDeptExternalCodeListByDeptId(deptAId); + assertNotNull(cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST).get(deptAId)); + + // 第二个部门使用相同 system+external,应禁用旧映射 + DeptSaveReqVO createB = new DeptSaveReqVO(); + createB.setParentId(DeptDO.PARENT_ID_ROOT); + createB.setName("iWork-B"); + createB.setSort(2); + createB.setStatus(CommonStatusEnum.ENABLE.getStatus()); + createB.setDeptSource(1); + createB.setExternalSystemCode("IWORK"); + createB.setExternalDeptCode("IW-001"); + Long deptBId = deptService.createDept(createB); + + DeptExternalCodeDO oldRecord = deptExternalCodeMapper.selectBySystemCodeAndDeptId("IWORK", deptAId); + DeptExternalCodeDO newRecord = deptExternalCodeMapper.selectBySystemCodeAndDeptId("IWORK", deptBId); + assertNotNull(oldRecord); + assertNotNull(newRecord); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), oldRecord.getStatus()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), newRecord.getStatus()); + // A 的缓存应被清理 + assertNull(cacheManager.getCache(RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST).get(deptAId)); + } + + @Test + public void testUpdateDept_parentChangeWithExternalMappingKeepsCode() { + Long parentAId = createDept(DeptDO.PARENT_ID_ROOT, "父A", 1); + Long parentBId = createDept(DeptDO.PARENT_ID_ROOT, "父B", 2); + Long deptId = createDept(parentAId, "子", 1); + DeptDO original = deptMapper.selectById(deptId); + + DeptSaveReqVO updateReq = new DeptSaveReqVO(); + updateReq.setId(deptId); + updateReq.setParentId(parentBId); + updateReq.setName("子-更新"); + updateReq.setSort(1); + updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + updateReq.setDeptSource(1); + updateReq.setExternalSystemCode("IWORK"); + updateReq.setExternalDeptCode("IW-CHILD"); + + deptService.updateDept(updateReq); + + DeptDO updated = deptMapper.selectById(deptId); + assertEquals(original.getCode(), updated.getCode()); + assertEquals(parentBId, updated.getParentId()); + + DeptExternalCodeDO mapping = deptExternalCodeMapper.selectBySystemCodeAndDeptId("IWORK", deptId); + assertNotNull(mapping); + assertEquals("IW-CHILD", mapping.getExternalDeptCode()); } @Test @@ -187,6 +329,29 @@ public class DeptServiceImplTest extends BaseDbUnitTest { assertNull(deptMapper.selectById(id)); } + @Test + public void testDeleteDept_cascadeExternalCodesAndEvictCache() { + Long deptId = createDept(DeptDO.PARENT_ID_ROOT, "总部", 1); + + // 创建映射并预热缓存 + DeptExternalCodeSaveReqVO createReq = new DeptExternalCodeSaveReqVO(); + createReq.setDeptId(deptId); + createReq.setSystemCode("ERP"); + createReq.setExternalDeptCode("ERP-001"); + deptExternalCodeService.createDeptExternalCode(createReq); + deptExternalCodeService.getDeptExternalCodeListByDeptId(deptId); + assertNotNull(cacheManager.getCache(com.zt.plat.module.system.dal.redis.RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST) + .get(deptId)); + + // 删除部门 + deptService.deleteDept(deptId); + + // 校验映射被删除且缓存被清理 + assertTrue(deptExternalCodeMapper.selectListByDeptId(deptId).isEmpty()); + assertNull(cacheManager.getCache(com.zt.plat.module.system.dal.redis.RedisKeyConstants.DEPT_EXTERNAL_CODE_LIST) + .get(deptId)); + } + @Test public void testDeleteDept_exitsChildren() { // mock 数据 diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictDataServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictDataServiceImplTest.java index c2bbe13f..fc85fc46 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictDataServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictDataServiceImplTest.java @@ -85,7 +85,7 @@ public class DictDataServiceImplTest extends BaseDbUnitTest { dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setValue("otherValue"))); // 准备参数 DictDataPageReqVO reqVO = new DictDataPageReqVO(); - reqVO.setLabel("芋"); + reqVO.setLabel("ZT"); reqVO.setDictType("yunai"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setValue("testValue"); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictTypeServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictTypeServiceImplTest.java index 4cda4b6b..d987de2b 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictTypeServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dict/DictTypeServiceImplTest.java @@ -59,8 +59,8 @@ public class DictTypeServiceImplTest extends BaseDbUnitTest { dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setCreateTime(buildTime(2021, 1, 1)))); // 准备参数 DictTypePageReqVO reqVO = new DictTypePageReqVO(); - reqVO.setName("nai"); - reqVO.setType("艿"); + reqVO.setName("yunai"); + reqVO.setType("ZT"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2021, 1, 10, 2021, 1, 20)); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/permission/MenuServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/permission/MenuServiceImplTest.java index ac9b95fd..32c7a223 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/permission/MenuServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/permission/MenuServiceImplTest.java @@ -153,7 +153,7 @@ public class MenuServiceImplTest extends BaseDbUnitTest { // 测试 name 不匹配 menuMapper.insert(cloneIgnoreId(menuDO, o -> o.setName("艿"))); // 准备参数 - MenuListReqVO reqVO = new MenuListReqVO().setName("芋").setStatus(CommonStatusEnum.ENABLE.getStatus()); + MenuListReqVO reqVO = new MenuListReqVO().setName("ZT").setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 List result = menuService.getMenuList(reqVO); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sms/SmsChannelServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sms/SmsChannelServiceImplTest.java new file mode 100644 index 00000000..0055cfe1 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sms/SmsChannelServiceImplTest.java @@ -0,0 +1,81 @@ +package com.zt.plat.module.system.service.sms; + +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.module.system.dal.dataobject.sms.SmsChannelDO; +import com.zt.plat.module.system.dal.mysql.sms.SmsChannelMapper; +import com.zt.plat.module.system.framework.sms.core.client.SmsClient; +import com.zt.plat.module.system.framework.sms.core.client.SmsClientFactory; +import com.zt.plat.module.system.framework.sms.core.client.impl.extra.SmsBalanceClient; +import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static com.zt.plat.framework.test.core.util.RandomUtils.randomLongId; +import static com.zt.plat.framework.test.core.util.RandomUtils.randomPojo; +import static com.zt.plat.module.system.enums.ErrorCodeConstants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class SmsChannelServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private SmsChannelServiceImpl smsChannelService; + + @Mock + private SmsClientFactory smsClientFactory; + @Mock + private SmsChannelMapper smsChannelMapper; + @Mock + private SmsTemplateService smsTemplateService; + + @Test + public void testQueryBalance_success() throws Throwable { + Long channelId = randomLongId(); + SmsChannelDO channel = randomPojo(SmsChannelDO.class, o -> o.setId(channelId)); + when(smsChannelMapper.selectById(eq(channelId))).thenReturn(channel); + + SmsClient client = mock(SmsClient.class, withSettings().extraInterfaces(SmsBalanceClient.class)); + when(smsClientFactory.createOrUpdateSmsClient(any())).thenReturn(client); + when(((SmsBalanceClient) client).queryBalance()).thenReturn(88); + + Integer balance = smsChannelService.queryBalance(channelId); + + assertThat(balance).isEqualTo(88); + verify((SmsBalanceClient) client, times(1)).queryBalance(); + } + + @Test + public void testQueryBalance_unsupported() { + Long channelId = randomLongId(); + when(smsChannelMapper.selectById(eq(channelId))).thenReturn(randomPojo(SmsChannelDO.class, o -> o.setId(channelId))); + SmsClient client = mock(SmsClient.class); // 未实现 SmsBalanceClient + when(smsClientFactory.createOrUpdateSmsClient(any())).thenReturn(client); + + ServiceException ex = assertThrows(ServiceException.class, () -> smsChannelService.queryBalance(channelId)); + assertThat(ex.getCode()).isEqualTo(SMS_CHANNEL_BALANCE_UNSUPPORTED.getCode()); + } + + @Test + public void testQueryBalance_apiError() throws Throwable { + Long channelId = randomLongId(); + when(smsChannelMapper.selectById(eq(channelId))).thenReturn(randomPojo(SmsChannelDO.class, o -> o.setId(channelId))); + SmsClient client = mock(SmsClient.class, withSettings().extraInterfaces(SmsBalanceClient.class)); + when(smsClientFactory.createOrUpdateSmsClient(any())).thenReturn(client); + when(((SmsBalanceClient) client).queryBalance()).thenThrow(new RuntimeException("boom")); + + ServiceException ex = assertThrows(ServiceException.class, () -> smsChannelService.queryBalance(channelId)); + assertThat(ex.getCode()).isEqualTo(SMS_TEMPLATE_API_ERROR.getCode()); + } + + @Test + public void testQueryBalance_channelNotFound() { + Long channelId = randomLongId(); + when(smsChannelMapper.selectById(eq(channelId))).thenReturn(null); + + ServiceException ex = assertThrows(ServiceException.class, () -> smsChannelService.queryBalance(channelId)); + assertThat(ex.getCode()).isEqualTo(SMS_CHANNEL_NOT_EXISTS.getCode()); + } +} diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sms/SmsSendServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sms/SmsSendServiceImplTest.java index 0b70c9b6..c3f254a8 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sms/SmsSendServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sms/SmsSendServiceImplTest.java @@ -85,7 +85,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), eq(content), eq(template.getChannelId()), eq(template.getApiTemplateId()), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } @@ -124,7 +124,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(content), eq(template.getChannelId()), eq(template.getApiTemplateId()), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } @@ -164,7 +164,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(content), eq(template.getChannelId()), eq(template.getApiTemplateId()), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } @@ -204,7 +204,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), + verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), anyString(), anyLong(), any(), anyList()); } @@ -260,13 +260,13 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { @SuppressWarnings("unchecked") public void testDoSendSms() throws Throwable { // 准备参数 - SmsSendMessage message = randomPojo(SmsSendMessage.class); + SmsSendMessage message = randomPojo(SmsSendMessage.class, o -> o.setContent(randomString())); // mock SmsClientFactory 的方法 SmsClient smsClient = spy(SmsClient.class); when(smsChannelService.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient); // mock SmsClient 的方法 SmsSendRespDTO sendResult = randomPojo(SmsSendRespDTO.class); - when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()), + when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getContent()), eq(message.getApiTemplateId()), eq(message.getTemplateParams()))).thenReturn(sendResult); // 调用 @@ -287,12 +287,15 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { when(smsChannelService.getSmsClient(eq(channelCode))).thenReturn(smsClient); // mock SmsClient 的方法 List receiveResults = randomPojoList(SmsReceiveRespDTO.class); + when(smsClient.parseSmsReceiveStatus(eq(text))).thenReturn(receiveResults); // 调用 smsSendService.receiveSmsStatus(channelCode, text); // 断言 - receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), - eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode()))); + for (SmsReceiveRespDTO result : receiveResults) { + verify(smsLogService).updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), + eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorMsg())); + } } } diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialUserServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialUserServiceImplTest.java index 20831f00..789d39c9 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialUserServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialUserServiceImplTest.java @@ -273,7 +273,7 @@ public class SocialUserServiceImplTest extends BaseDbUnitTest { // 准备参数 SocialUserPageReqVO reqVO = new SocialUserPageReqVO(); reqVO.setType(SocialTypeEnum.GITEE.getType()); - reqVO.setNickname("芋"); + reqVO.setNickname("ZT"); reqVO.setOpenid("zt"); reqVO.setCreateTime(buildBetweenTime(2020, 1, 10, 2020, 1, 20)); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sso/client/ExternalSsoClientConfigurationLoadTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sso/client/ExternalSsoClientConfigurationLoadTest.java index 956299cd..929f8d3e 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sso/client/ExternalSsoClientConfigurationLoadTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/sso/client/ExternalSsoClientConfigurationLoadTest.java @@ -1,6 +1,8 @@ package com.zt.plat.module.system.service.sso.client; import com.zt.plat.module.system.framework.sso.config.ExternalSsoProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; @@ -16,7 +18,6 @@ import static org.mockito.Mockito.mock; * 使用 Mock 的 RedisTemplate,避免外部依赖。 */ @SpringBootTest(classes = { ExternalSsoClientConfiguration.class, ExternalSsoClientConfigurationLoadTest.TestBeans.class }) -@Import(ExternalSsoProperties.class) class ExternalSsoClientConfigurationLoadTest { @TestConfiguration @@ -33,6 +34,16 @@ class ExternalSsoClientConfigurationLoadTest { props.getRemote().setBaseUrl("http://localhost"); return props; } + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean + public RestTemplateBuilder restTemplateBuilder() { + return new RestTemplateBuilder(); + } } @Test diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/tenant/TenantServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/tenant/TenantServiceImplTest.java index 2d484ad8..b584c86e 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/tenant/TenantServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/tenant/TenantServiceImplTest.java @@ -306,7 +306,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest { // 准备参数 TenantPageReqVO reqVO = new TenantPageReqVO(); reqVO.setName("ZT"); - reqVO.setContactName("艿"); + reqVO.setContactName("ZT"); reqVO.setContactMobile("1560"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java index e66b71eb..2cfb8b56 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/user/AdminUserServiceImplTest.java @@ -681,6 +681,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { o.setDeptNames("-"); o.setCompanyDeptInfos(null);// 保证 deptIds 的范围 o.setUserSource(null); + o.setWorkcode(null); }; return randomPojo(AdminUserDO.class, ArrayUtils.append(consumer, consumers)); } diff --git a/zt-module-system/zt-module-system-server/src/test/resources/sql/clean.sql b/zt-module-system/zt-module-system-server/src/test/resources/sql/clean.sql index e7946a10..78fe1b13 100644 --- a/zt-module-system/zt-module-system-server/src/test/resources/sql/clean.sql +++ b/zt-module-system/zt-module-system-server/src/test/resources/sql/clean.sql @@ -1,4 +1,5 @@ DELETE FROM "system_dept"; +DELETE FROM "system_dept_external_code"; DELETE FROM "system_dict_data"; DELETE FROM "system_role"; DELETE FROM "system_role_menu"; diff --git a/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql b/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql index fc9bc7a0..fd2321a0 100644 --- a/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql +++ b/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql @@ -34,6 +34,27 @@ CREATE TABLE IF NOT EXISTS "system_dept" ( PRIMARY KEY ("id") ) COMMENT '部门表'; +CREATE TABLE IF NOT EXISTS "system_dept_external_code" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "dept_id" bigint NOT NULL, + "system_code" varchar(64) NOT NULL, + "external_dept_code" varchar(128) NOT NULL, + "external_dept_name" varchar(255), + "status" tinyint DEFAULT 0 NOT NULL, + "remark" varchar(512), + "tenant_id" bigint DEFAULT 0, + "creator" varchar(64), + "create_time" timestamp DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64), + "update_time" timestamp DEFAULT CURRENT_TIMESTAMP, + "deleted" tinyint DEFAULT 0 NOT NULL, + PRIMARY KEY ("id") +) COMMENT '部门外部组织编码映射'; + +CREATE UNIQUE INDEX IF NOT EXISTS "uk_system_dept_external_code_ext" ON "system_dept_external_code" ("tenant_id", "system_code", "external_dept_code"); +CREATE UNIQUE INDEX IF NOT EXISTS "uk_system_dept_external_code_dept" ON "system_dept_external_code" ("tenant_id", "system_code", "dept_id"); +CREATE INDEX IF NOT EXISTS "idx_system_dept_external_code_dept" ON "system_dept_external_code" ("tenant_id", "dept_id"); + CREATE TABLE IF NOT EXISTS "system_dict_data" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "sort" int NOT NULL DEFAULT '0', @@ -242,6 +263,7 @@ CREATE TABLE IF NOT EXISTS `system_operate_log` ( CREATE TABLE IF NOT EXISTS "system_users" ( "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, "username" varchar(30) not null, + "workcode" varchar(100) default null, "password" varchar(100) not null default '', "nickname" varchar(30) not null, "remark" varchar(500) default null, @@ -267,6 +289,7 @@ CREATE TABLE IF NOT EXISTS "system_users" ( CREATE TABLE IF NOT EXISTS "system_sms_channel" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "signature" varchar(10) NOT NULL, + "epid" varchar(63) DEFAULT NULL, "code" varchar(63) NOT NULL, "status" tinyint NOT NULL, "remark" varchar(255) DEFAULT NULL, diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java index 3ed03993..bd80ea2f 100644 --- a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java @@ -11,7 +11,6 @@ import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.pojo.vo.BatchDeleteReqVO; import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.framework.excel.core.util.ExcelUtils; -import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.module.bpm.api.task.BpmProcessInstanceApi; import com.zt.plat.module.bpm.api.task.BpmTaskApi; import com.zt.plat.module.bpm.api.task.dto.BpmApprovalDetailReqDTO; @@ -118,9 +117,6 @@ public class DemoContractController extends AbstractFileUploadController impleme CommonResult file = fileApi.getFile(1968928810422521857L); BpmApprovalDetailReqDTO reqDTO = new BpmApprovalDetailReqDTO(); - reqDTO.setProcessInstanceId("acccfcf2-99b9-11f0-8536-e8808862e505"); - reqDTO.setTaskId("8ba1838e-99ba-11f0-8536-e8808862e505"); - bpmInsApi.getApprovalDetail(SecurityFrameworkUtils.getLoginUserId(), reqDTO); Long id = IdWorker.getId(); System.out.println("Generated ID: " + id); PageResult pageResult = demoContractService.getDemoContractPage(pageReqVO); diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java index eb893014..4d1e73d1 100644 --- a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java @@ -7,10 +7,11 @@ import com.zt.plat.module.infra.api.file.FileApi; import com.zt.plat.module.infra.api.websocket.WebSocketSenderApi; import com.zt.plat.module.system.api.dept.DeptApi; import com.zt.plat.module.system.api.sequence.SequenceApi; +import com.zt.plat.module.system.api.sms.SmsSendApi; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; @Configuration(value = "templateRpcConfiguration", proxyBeanMethods = false) -@EnableFeignClients(clients = {FileApi.class, WebSocketSenderApi.class, ConfigApi.class, DeptApi.class, SequenceApi.class, BpmTaskApi.class, BpmProcessInstanceApi.class}) +@EnableFeignClients(clients = {FileApi.class, WebSocketSenderApi.class, ConfigApi.class, DeptApi.class, SequenceApi.class, BpmTaskApi.class, BpmProcessInstanceApi.class, SmsSendApi.class}) public class RpcConfiguration { } diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java index fb530505..d4d003ea 100644 --- a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java @@ -14,7 +14,7 @@ public class TemplateDatabusScheduler { private final TemplateDatabusInvokeService invokeService; - @Scheduled(cron = "0 0/10 * * * ?") + @Scheduled(cron = "0 0/25 * * * ?") public void execute() { invokeService.invokeAndRecord(); } diff --git a/zt-server/src/main/resources/application.yaml b/zt-server/src/main/resources/application.yaml index da5fa087..5f303e9c 100644 --- a/zt-server/src/main/resources/application.yaml +++ b/zt-server/src/main/resources/application.yaml @@ -3,7 +3,7 @@ spring: name: zt-server profiles: - active: ${env.name} + active: dev main: allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 @@ -100,6 +100,12 @@ iwork: sync-user: workflow: seal-workflow-id: + oa: + base-url: http://172.16.36.233:8080 + app-id: a17ca6ca-88b0-463e-bffa-7995086bf225 + paths: + get-token: /ssologin/getToken + check-token: /ssologin/checkToken eplat: share: