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 73b29247..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'); @@ -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/短信渠道鸿联九五支持_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-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/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 f1f39fbb..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 @@ -11,6 +11,7 @@ import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCreden 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.framework.integration.gateway.security.GatewayJwtResolver; import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter; import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; import lombok.RequiredArgsConstructor; @@ -171,6 +172,7 @@ public class ApiGatewayExecutionService { if (reqVO.getHeaders() != null) { requestHeaders.putAll(reqVO.getHeaders()); } + normalizeJwtHeaders(requestHeaders, reqVO.getQueryParams()); requestHeaders.putIfAbsent(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); builder.setHeader(HEADER_REQUEST_HEADERS, requestHeaders); requestHeaders.forEach((key, value) -> { @@ -304,4 +306,37 @@ public class ApiGatewayExecutionService { builder.queryParam(key, value); } + private void normalizeJwtHeaders(Map headers, Map queryParams) { + String token = GatewayJwtResolver.resolveJwtToken(headers, queryParams, objectMapper); + if (!StringUtils.hasText(token)) { + return; + } + ensureHeaderValue(headers, GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token); + ensureHeaderValue(headers, HttpHeaders.AUTHORIZATION, "Bearer " + token); + } + + private void ensureHeaderValue(Map headers, String headerName, String value) { + if (!StringUtils.hasText(headerName) || value == null) { + return; + } + String existingKey = findHeaderKey(headers, headerName); + if (existingKey != null) { + headers.put(existingKey, value); + } else { + headers.put(headerName, value); + } + } + + private String findHeaderKey(Map headers, String headerName) { + if (headers == null || !StringUtils.hasText(headerName)) { + return null; + } + for (String key : headers.keySet()) { + if (headerName.equalsIgnoreCase(key)) { + return key; + } + } + return null; + } + } 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 77511c9a..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; @@ -27,20 +25,16 @@ 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.*; @@ -50,8 +44,6 @@ 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; @@ -60,19 +52,8 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { 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())); @@ -88,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 @@ -104,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 @@ -165,14 +139,12 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { 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()); persistCredentialBindings(existing.getId(), reqVO.getCredentialIds()); - invalidateCache(updateObj.getTenantId(), updateObj.getApiCode(), updateObj.getVersion()); } finally { if (skipSnapshot) { ApiVersionSnapshotContextHolder.clear(); @@ -192,78 +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); - } - /** * 构建包含步骤、变换、策略等元数据的聚合对象,供缓存与运行时直接使用。 */ @@ -526,20 +432,6 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { return TenantContextHolder.getTenantId(); } - private void invalidateCache(Long tenantId, String apiCode, String version) { - if (!StringUtils.hasText(apiCode) || !StringUtils.hasText(version)) { - return; - } - String cacheKey = buildCacheKeyForTenant(tenantId, apiCode, version); - definitionCache.invalidate(cacheKey); - deleteRedis(cacheKey); - } - - private String buildCacheKeyForTenant(Long tenantId, String apiCode, String version) { - String tenantPart = tenantId == null ? "global" : tenantId.toString(); - return tenantPart + ":" + apiCode.toLowerCase(Locale.ROOT) + ":" + version; - } - /** * 先删除旧绑定,再对去重后的 credentialIds 批量插入,避免唯一约束冲突。 */ 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 03a2040a..d0bd9492 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 @@ -29,15 +29,17 @@ public final class DatabusApiInvocationExample { public static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); - 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 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 String TARGET_API = "http://172.16.46.195:48080/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 = "http://localhost:48080/admin-api/databus/api/portal/callback/v1"; - private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456"; + 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"; private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); 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-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/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-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/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/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/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/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 91bf4b7d..7d430fcd 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 @@ -197,9 +197,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/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/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/create_tables.sql b/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql index fc9bc7a0..e27aa063 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 @@ -242,6 +242,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 +268,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 { }