Commit 4e8e491035180f3c86e4a9f0f6233d6bd43dbe08

Authored by YunaiV
2 parents 98e62211 a6e5b288

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/bpm

# Conflicts:
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
Showing 283 changed files with 5197 additions and 2493 deletions
sql/mysql/ruoyi-vue-pro.sql
... ... @@ -11,7 +11,7 @@
11 11 Target Server Version : 80200 (8.2.0)
12 12 File Encoding : 65001
13 13  
14   - Date: 28/07/2024 23:20:28
  14 + Date: 31/08/2024 09:22:45
15 15 */
16 16  
17 17 SET NAMES utf8mb4;
... ... @@ -62,7 +62,7 @@ COMMIT;
62 62 -- ----------------------------
63 63 DROP TABLE IF EXISTS `infra_api_error_log`;
64 64 CREATE TABLE `infra_api_error_log` (
65   - `id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
  65 + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
66 66 `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '链路追踪编号',
67 67 `user_id` int NOT NULL DEFAULT 0 COMMENT '用户编号',
68 68 `user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型',
... ... @@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log` (
91 91 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
92 92 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
93 93 PRIMARY KEY (`id`) USING BTREE
94   -) ENGINE = InnoDB AUTO_INCREMENT = 19166 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
  94 +) ENGINE = InnoDB AUTO_INCREMENT = 20014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
95 95  
96 96 -- ----------------------------
97 97 -- Records of infra_api_error_log
... ... @@ -128,7 +128,7 @@ CREATE TABLE `infra_codegen_column` (
128 128 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
129 129 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
130 130 PRIMARY KEY (`id`) USING BTREE
131   -) ENGINE = InnoDB AUTO_INCREMENT = 2470 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
  131 +) ENGINE = InnoDB AUTO_INCREMENT = 2483 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
132 132  
133 133 -- ----------------------------
134 134 -- Records of infra_codegen_column
... ... @@ -166,7 +166,7 @@ CREATE TABLE `infra_codegen_table` (
166 166 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
167 167 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
168 168 PRIMARY KEY (`id`) USING BTREE
169   -) ENGINE = InnoDB AUTO_INCREMENT = 186 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
  169 +) ENGINE = InnoDB AUTO_INCREMENT = 187 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
170 170  
171 171 -- ----------------------------
172 172 -- Records of infra_codegen_table
... ... @@ -179,7 +179,7 @@ COMMIT;
179 179 -- ----------------------------
180 180 DROP TABLE IF EXISTS `infra_config`;
181 181 CREATE TABLE `infra_config` (
182   - `id` int NOT NULL AUTO_INCREMENT COMMENT '参数主键',
  182 + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '参数主键',
183 183 `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数分组',
184 184 `type` tinyint NOT NULL COMMENT '参数类型',
185 185 `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '参数名称',
... ... @@ -250,7 +250,7 @@ CREATE TABLE `infra_file` (
250 250 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
251 251 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
252 252 PRIMARY KEY (`id`) USING BTREE
253   -) ENGINE = InnoDB AUTO_INCREMENT = 1447 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
  253 +) ENGINE = InnoDB AUTO_INCREMENT = 1472 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
254 254  
255 255 -- ----------------------------
256 256 -- Records of infra_file
... ... @@ -438,7 +438,7 @@ CREATE TABLE `system_dict_data` (
438 438 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
439 439 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
440 440 PRIMARY KEY (`id`) USING BTREE
441   -) ENGINE = InnoDB AUTO_INCREMENT = 1588 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
  441 +) ENGINE = InnoDB AUTO_INCREMENT = 1592 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
442 442  
443 443 -- ----------------------------
444 444 -- Records of system_dict_data
... ... @@ -858,6 +858,10 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
858 858 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 (1585, 2, '回复', '2', 'ai_write_type', 0, '', '', '', '1', '2024-07-10 21:26:06', '1', '2024-07-10 21:26:06', b'0');
859 859 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 (1586, 2, '腾讯云', 'TENCENT', 'system_sms_channel_code', 0, '', '', '', '1', '2024-07-22 22:23:16', '1', '2024-07-22 22:23:16', b'0');
860 860 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 (1587, 3, '华为云', 'HUAWEI', 'system_sms_channel_code', 0, '', '', '', '1', '2024-07-22 22:23:46', '1', '2024-07-22 22:23:53', b'0');
  861 +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 (1588, 1, 'OpenAI 微软', 'AzureOpenAI', 'ai_platform', 0, '', '', '', '1', '2024-08-10 14:07:41', '1', '2024-08-10 14:07:41', b'0');
  862 +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');
  863 +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');
  864 +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');
861 865 COMMIT;
862 866  
863 867 -- ----------------------------
... ... @@ -877,7 +881,7 @@ CREATE TABLE `system_dict_type` (
877 881 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
878 882 `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
879 883 PRIMARY KEY (`id`) USING BTREE
880   -) ENGINE = InnoDB AUTO_INCREMENT = 629 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
  884 +) ENGINE = InnoDB AUTO_INCREMENT = 630 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
881 885  
882 886 -- ----------------------------
883 887 -- Records of system_dict_type
... ... @@ -975,6 +979,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
975 979 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (626, '写作长度', 'ai_write_length', 0, '', '1', '2024-07-07 15:18:41', '1', '2024-07-07 15:18:41', b'0', '1970-01-01 00:00:00');
976 980 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (627, '写作格式', 'ai_write_format', 0, '', '1', '2024-07-07 15:14:34', '1', '2024-07-07 15:14:34', b'0', '1970-01-01 00:00:00');
977 981 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (628, 'AI 写作类型', 'ai_write_type', 0, '', '1', '2024-07-10 21:25:29', '1', '2024-07-10 21:25:29', b'0', '1970-01-01 00:00:00');
  982 +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (629, 'BPM 流程模型类型', 'bpm_model_type', 0, '', '1', '2024-08-26 15:21:43', '1', '2024-08-26 15:21:43', b'0', '1970-01-01 00:00:00');
978 983 COMMIT;
979 984  
980 985 -- ----------------------------
... ... @@ -998,7 +1003,7 @@ CREATE TABLE `system_login_log` (
998 1003 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
999 1004 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
1000 1005 PRIMARY KEY (`id`) USING BTREE
1001   -) ENGINE = InnoDB AUTO_INCREMENT = 3261 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
  1006 +) ENGINE = InnoDB AUTO_INCREMENT = 3289 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
1002 1007  
1003 1008 -- ----------------------------
1004 1009 -- Records of system_login_log
... ... @@ -1129,7 +1134,7 @@ CREATE TABLE `system_menu` (
1129 1134 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
1130 1135 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
1131 1136 PRIMARY KEY (`id`) USING BTREE
1132   -) ENGINE = InnoDB AUTO_INCREMENT = 2798 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
  1137 +) ENGINE = InnoDB AUTO_INCREMENT = 2808 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
1133 1138  
1134 1139 -- ----------------------------
1135 1140 -- Records of system_menu
... ... @@ -1293,7 +1298,6 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
1293 1298 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1193, '流程模型', '', 2, 1, 1186, 'model', 'fa-solid:project-diagram', 'bpm/model/index', 'BpmModel', 0, b'1', b'1', b'1', '1', '2021-12-31 23:24:58', '1', '2024-03-19 12:25:19', b'0');
1294 1299 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1194, '模型查询', 'bpm:model:query', 3, 1, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:10', '1', '2022-04-20 17:03:10', b'0');
1295 1300 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1195, '模型创建', 'bpm:model:create', 3, 2, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:24', '1', '2022-04-20 17:03:10', b'0');
1296   -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1196, '模型导入', 'bpm:model:import', 3, 3, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:35', '1', '2022-04-20 17:03:10', b'0');
1297 1301 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1197, '模型更新', 'bpm:model:update', 3, 4, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:28', '1', '2022-04-20 17:03:10', b'0');
1298 1302 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1198, '模型删除', 'bpm:model:delete', 3, 5, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:43', '1', '2022-04-20 17:03:10', b'0');
1299 1303 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1199, '模型发布', 'bpm:model:deploy', 3, 6, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:03:24', '1', '2022-04-20 17:03:10', b'0');
... ... @@ -1952,7 +1956,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
1952 1956 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2784, '绘画管理', '', 2, 11, 2760, 'image', 'fa:file-image-o', 'ai/image/manager/index.vue', 'AiImageManager', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 21:37:13', b'0');
1953 1957 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2785, '绘画查询', 'ai:image:query', 3, 1, 2784, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 22:21:57', b'0');
1954 1958 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2786, '绘画删除', 'ai:image:delete', 3, 4, 2784, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-06-26 13:32:31', '1', '2024-06-26 22:22:08', b'0');
1955   -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2787, '会话更新公开状态', 'ai:image:update-public-status', 3, 2, 2784, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-06-26 22:47:56', '1', '2024-06-26 22:47:56', b'0');
  1959 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2787, '绘图更新', 'ai:image:update', 3, 2, 2784, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-06-26 22:47:56', '1', '2024-08-31 09:21:35', b'0');
1956 1960 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2788, '音乐管理', '', 2, 12, 2760, 'music', 'fa:music', 'ai/music/manager/index.vue', 'AiMusicManager', 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '1', '2024-06-27 23:04:19', b'0');
1957 1961 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2789, '音乐查询', 'ai:music:query', 3, 1, 2788, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '', '2024-06-27 15:03:33', b'0');
1958 1962 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2790, '音乐更新', 'ai:music:update', 3, 3, 2788, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-06-27 15:03:33', '', '2024-06-27 15:03:33', b'0');
... ... @@ -1961,8 +1965,18 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
1961 1965 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2793, '写作管理', '', 2, 13, 2760, 'write', 'fa:bookmark-o', 'ai/write/manager/index.vue', 'AiWriteManager', 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '1', '2024-07-10 21:31:59', b'0');
1962 1966 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2794, 'AI 写作查询', 'ai:write:query', 3, 1, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
1963 1967 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
1964   -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiWrite', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0');
  1968 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-29 21:11:52', b'0');
1965 1969 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', b'0');
  1970 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2798, 'AI 思维导图', '', 2, 5, 2758, 'mind-map', 'fa:sitemap', 'ai/mindmap/index/index.vue', 'AiMindMap', 0, b'1', b'1', b'1', '1', '2024-07-29 21:31:59', '1', '2024-07-29 21:33:20', b'0');
  1971 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2799, '导图管理', '', 2, 14, 2760, 'mind-map', 'fa:map', 'ai/mindmap/manager/index', 'AiMindMapManager', 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '1', '2024-08-10 17:24:28', b'0');
  1972 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2800, '思维导图查询', 'ai:mind-map:query', 3, 1, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0');
  1973 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2801, '思维导图删除', 'ai:mind-map:delete', 3, 4, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0');
  1974 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2802, '会话查询', 'promotion:kefu-conversation:query', 3, 1, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:17:52', '1', '2024-08-31 09:18:52', b'0');
  1975 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2803, '会话更新', 'promotion:kefu-conversation:update', 3, 2, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:18:15', '1', '2024-08-31 09:19:29', b'0');
  1976 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2804, '消息查询', 'promotion:kefu-message:query', 3, 10, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:18:42', '1', '2024-08-31 09:18:42', b'0');
  1977 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2805, '会话删除', 'promotion:kefu-conversation:delete', 3, 3, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:19:51', '1', '2024-08-31 09:20:32', b'0');
  1978 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2806, '消息发送', 'promotion:kefu-message:send', 3, 12, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:20:06', '1', '2024-08-31 09:20:06', b'0');
  1979 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2807, '消息更新', 'promotion:kefu-message:update', 3, 11, 2797, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-08-31 09:20:22', '1', '2024-08-31 09:20:22', b'0');
1966 1980 COMMIT;
1967 1981  
1968 1982 -- ----------------------------
... ... @@ -2084,7 +2098,7 @@ CREATE TABLE `system_oauth2_access_token` (
2084 2098 PRIMARY KEY (`id`) USING BTREE,
2085 2099 INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
2086 2100 INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
2087   -) ENGINE = InnoDB AUTO_INCREMENT = 8784 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
  2101 +) ENGINE = InnoDB AUTO_INCREMENT = 9563 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
2088 2102  
2089 2103 -- ----------------------------
2090 2104 -- Records of system_oauth2_access_token
... ... @@ -2206,7 +2220,7 @@ CREATE TABLE `system_oauth2_refresh_token` (
2206 2220 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
2207 2221 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
2208 2222 PRIMARY KEY (`id`) USING BTREE
2209   -) ENGINE = InnoDB AUTO_INCREMENT = 1598 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
  2223 +) ENGINE = InnoDB AUTO_INCREMENT = 1620 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
2210 2224  
2211 2225 -- ----------------------------
2212 2226 -- Records of system_oauth2_refresh_token
... ... @@ -2239,7 +2253,7 @@ CREATE TABLE `system_operate_log` (
2239 2253 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
2240 2254 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
2241 2255 PRIMARY KEY (`id`) USING BTREE
2242   -) ENGINE = InnoDB AUTO_INCREMENT = 9053 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
  2256 +) ENGINE = InnoDB AUTO_INCREMENT = 9056 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
2243 2257  
2244 2258 -- ----------------------------
2245 2259 -- Records of system_operate_log
... ... @@ -2298,7 +2312,7 @@ CREATE TABLE `system_role` (
2298 2312 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
2299 2313 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
2300 2314 PRIMARY KEY (`id`) USING BTREE
2301   -) ENGINE = InnoDB AUTO_INCREMENT = 153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
  2315 +) ENGINE = InnoDB AUTO_INCREMENT = 154 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
2302 2316  
2303 2317 -- ----------------------------
2304 2318 -- Records of system_role
... ... @@ -2307,9 +2321,10 @@ BEGIN;
2307 2321 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '超级管理员', 'super_admin', 1, 1, '', 0, 1, '超级管理员', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:21', b'0', 1);
2308 2322 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '普通角色', 'common', 2, 2, '', 0, 1, '普通角色', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:20', b'0', 1);
2309 2323 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 'CRM 管理员', 'crm_admin', 2, 1, '', 0, 1, 'CRM 专属角色', '1', '2024-02-24 10:51:13', '1', '2024-02-24 02:51:32', b'0', 1);
2310   -INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '测试账号', 'test', 0, 2, '[105,106,107]', 0, 2, '', '', '2021-01-06 13:49:35', '1', '2024-07-27 23:30:47', b'0', 1);
  2324 +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '测试账号', 'test', 0, 1, '[]', 0, 2, '', '', '2021-01-06 13:49:35', '1', '2024-08-11 10:41:10', b'0', 1);
2311 2325 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121);
2312 2326 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122);
  2327 +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (153, '某角色', 'tt', 4, 1, '', 0, 2, '', '1', '2024-08-17 14:09:35', '1', '2024-08-17 14:09:35', b'0', 1);
2313 2328 COMMIT;
2314 2329  
2315 2330 -- ----------------------------
... ... @@ -2390,7 +2405,6 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
2390 2405 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1623, 101, 1193, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
2391 2406 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1624, 101, 1194, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
2392 2407 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1625, 101, 1195, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
2393   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1626, 101, 1196, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
2394 2408 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1627, 101, 1197, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
2395 2409 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1628, 101, 1198, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
2396 2410 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1629, 101, 1199, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1);
... ... @@ -3195,9 +3209,8 @@ CREATE TABLE `system_sms_channel` (
3195 3209 -- Records of system_sms_channel
3196 3210 -- ----------------------------
3197 3211 BEGIN;
3198   -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', '2023-12-02 22:10:17', b'0');
  3212 +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');
3199 3213 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');
3200   -INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, '测试演示', 'DEBUG_DING_TALK', 0, '仅测试', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2022-04-10 23:07:59', '1', '2023-12-02 22:10:08', b'0');
3201 3214 COMMIT;
3202 3215  
3203 3216 -- ----------------------------
... ... @@ -3222,7 +3235,7 @@ CREATE TABLE `system_sms_code` (
3222 3235 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
3223 3236 PRIMARY KEY (`id`) USING BTREE,
3224 3237 INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号'
3225   -) ENGINE = InnoDB AUTO_INCREMENT = 628 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
  3238 +) ENGINE = InnoDB AUTO_INCREMENT = 632 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
3226 3239  
3227 3240 -- ----------------------------
3228 3241 -- Records of system_sms_code
... ... @@ -3263,7 +3276,7 @@ CREATE TABLE `system_sms_log` (
3263 3276 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
3264 3277 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
3265 3278 PRIMARY KEY (`id`) USING BTREE
3266   -) ENGINE = InnoDB AUTO_INCREMENT = 987 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
  3279 +) ENGINE = InnoDB AUTO_INCREMENT = 1088 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
3267 3280  
3268 3281 -- ----------------------------
3269 3282 -- Records of system_sms_log
... ... @@ -3293,24 +3306,25 @@ CREATE TABLE `system_sms_template` (
3293 3306 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
3294 3307 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
3295 3308 PRIMARY KEY (`id`) USING BTREE
3296   -) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
  3309 +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
3297 3310  
3298 3311 -- ----------------------------
3299 3312 -- Records of system_sms_template
3300 3313 -- ----------------------------
3301 3314 BEGIN;
3302   -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', '测试备注', '4383920', 6, 'DEBUG_DING_TALK', '', '2021-03-31 10:49:38', '1', '2023-12-02 22:32:47', b'0');
  3315 +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', '测试备注', '4383920', 4, 'DEBUG_DING_TALK', '', '2021-03-31 10:49:38', '1', '2024-08-18 11:57:18', b'0');
3303 3316 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, 1, 0, 'test_02', '公告通知', '您的验证码{code},该验证码5分钟内有效,请勿泄漏于他人!', '[\"code\"]', NULL, 'SMS_207945135', 2, 'ALIYUN', '', '2021-03-31 11:56:30', '1', '2021-04-10 01:22:02', b'0');
3304   -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, 3, 0, 'test-01', '测试模板', '哈哈哈 {name}', '[\"name\"]', 'f哈哈哈', '4383920', 6, 'DEBUG_DING_TALK', '1', '2021-04-10 01:07:21', '1', '2022-12-10 21:26:09', b'0');
  3317 +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, 3, 0, 'test-01', '测试模板', '哈哈哈 {name}', '[\"name\"]', 'f哈哈哈', '4383920', 4, 'DEBUG_DING_TALK', '1', '2021-04-10 01:07:21', '1', '2024-08-18 11:57:07', b'0');
3305 3318 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 3, 0, 'test-04', '测试下', '老鸡{name},牛逼{code}', '[\"name\",\"code\"]', '哈哈哈哈', 'suibian', 4, 'DEBUG_DING_TALK', '1', '2021-04-13 00:29:53', '1', '2023-12-02 22:35:34', b'0');
3306   -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, 0, 'user-sms-login', '前台用户短信登录', '您的验证码是{code}', '[\"code\"]', NULL, '4372216', 6, 'DEBUG_DING_TALK', '1', '2021-10-11 08:10:00', '1', '2022-12-10 21:25:59', b'0');
  3319 +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, 0, 'user-sms-login', '前台用户短信登录', '您的验证码是{code}', '[\"code\"]', NULL, '4372216', 4, 'DEBUG_DING_TALK', '1', '2021-10-11 08:10:00', '1', '2024-08-18 11:57:06', b'0');
3307 3320 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 2, 0, 'bpm_task_assigned', '【工作流】任务被分配', '您收到了一条新的待办任务:{processInstanceName}-{taskName},申请人:{startUserNickname},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"startUserNickname\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-21 22:31:19', '1', '2022-01-22 00:03:36', b'0');
3308 3321 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (10, 2, 0, 'bpm_process_instance_reject', '【工作流】流程被不通过', '您的流程被审批不通过:{processInstanceName},原因:{reason},查看链接:{detailUrl}', '[\"processInstanceName\",\"reason\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:03:31', '1', '2022-05-01 12:33:14', b'0');
3309 3322 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 2, 0, 'bpm_process_instance_approve', '【工作流】流程被通过', '您的流程被审批通过:{processInstanceName},查看链接:{detailUrl}', '[\"processInstanceName\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:04:31', '1', '2022-03-27 20:32:21', b'0');
3310   -INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 2, 0, 'demo', '演示模板', '我就是测试一下下', '[]', NULL, 'biubiubiu', 6, 'DEBUG_DING_TALK', '1', '2022-04-10 23:22:49', '1', '2023-03-24 23:45:07', b'0');
  3323 +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 2, 0, 'demo', '演示模板', '我就是测试一下下', '[]', NULL, 'biubiubiu', 4, 'DEBUG_DING_TALK', '1', '2022-04-10 23:22:49', '1', '2024-08-18 11:57:04', b'0');
3311 3324 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (14, 1, 0, 'user-update-mobile', '会员用户 - 修改手机', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:04', b'0');
3312 3325 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, 1, 0, 'user-update-password', '会员用户 - 修改密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:18', b'0');
3313 3326 INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 1, 0, 'user-reset-password', '会员用户 - 重置密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-12-02 22:35:27', b'0');
  3327 +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, 2, 0, 'bpm_task_timeout', '【工作流】任务审批超时', '您收到了一条超时的待办任务:{processInstanceName}-{taskName},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"detailUrl\"]', '', 'X', 4, 'DEBUG_DING_TALK', '1', '2024-08-16 21:59:15', '1', '2024-08-16 21:59:34', b'0');
3314 3328 COMMIT;
3315 3329  
3316 3330 -- ----------------------------
... ... @@ -3367,7 +3381,7 @@ CREATE TABLE `system_social_user` (
3367 3381 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
3368 3382 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
3369 3383 PRIMARY KEY (`id`) USING BTREE
3370   -) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
  3384 +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
3371 3385  
3372 3386 -- ----------------------------
3373 3387 -- Records of system_social_user
... ... @@ -3392,7 +3406,7 @@ CREATE TABLE `system_social_user_bind` (
3392 3406 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
3393 3407 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
3394 3408 PRIMARY KEY (`id`) USING BTREE
3395   -) ENGINE = InnoDB AUTO_INCREMENT = 119 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
  3409 +) ENGINE = InnoDB AUTO_INCREMENT = 120 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
3396 3410  
3397 3411 -- ----------------------------
3398 3412 -- Records of system_social_user_bind
... ... @@ -3559,10 +3573,10 @@ CREATE TABLE `system_users` (
3559 3573 -- Records of system_users
3560 3574 -- ----------------------------
3561 3575 BEGIN;
3562   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-07-28 11:35:00', 'admin', '2021-01-05 17:03:47', NULL, '2024-07-28 11:35:00', b'0', 1);
3563   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
3564   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-03-18 21:09:04', '', '2021-01-13 23:50:35', NULL, '2024-03-18 21:09:04', b'0', 1);
3565   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-07-13 23:13:16', '', '2021-01-21 02:13:53', NULL, '2024-07-13 23:13:16', b'0', 1);
  3576 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-08-26 16:54:00', 'admin', '2021-01-05 17:03:47', NULL, '2024-08-26 16:54:00', b'0', 1);
  3577 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 0, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', '1', '2024-08-17 11:06:13', b'0', 1);
  3578 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', NULL, '2024-08-11 17:48:12', b'0', 1);
  3579 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 09:38:08', '', '2021-01-21 02:13:53', NULL, '2024-08-11 09:38:08', b'0', 1);
3566 3580 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2022-02-27 08:26:51', b'0', 118);
3567 3581 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2022-02-27 08:26:53', b'0', 119);
3568 3582 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2022-02-27 08:26:56', b'0', 120);
... ... @@ -3572,7 +3586,7 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
3572 3586 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', NULL, '2022-03-19 18:38:51', b'0', 122);
3573 3587 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-03-24 22:21:05', '1', '2022-03-19 21:50:58', NULL, '2024-03-24 22:21:05', b'0', 1);
3574 3588 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$04$GcyP0Vyzb2F2Yni5PuIK9ueGxM0tkZGMtDwVRwrNbtMvorzbpNsV2', '阿呆', '11222', 102, '[1,2]', '7648@qq.com', '15601691229', 2, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2024-04-04 09:37:14', b'0', 1);
3575   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2022-07-09 17:40:26', b'0', 1);
  3589 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号02', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2024-08-11 10:12:03', b'0', 1);
3576 3590 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$04$OB1SuphCdiLVRpiYRKeqH.8NYS7UIp5vmIv1W7U4w6toiFeOAATVK', '狗蛋', NULL, 103, '[1]', '', '15601691239', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-03-17 09:10:27', '1', '2022-07-09 17:44:43', '1', '2024-04-04 09:48:05', b'0', 1);
3577 3591 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (131, 'hh', '$2a$04$jyH9h6.gaw8mpOjPfHIpx.8as2Rzfcmdlj5rlJFwgCw4rsv/MTb2K', '呵呵', NULL, 100, '[]', '777@qq.com', '15601882312', 1, '', 0, '', NULL, '1', '2024-04-27 08:45:56', '1', '2024-04-27 08:45:56', b'0', 1);
3578 3592 COMMIT;
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
... ... @@ -111,7 +111,7 @@ public class HttpUtils {
111 111 authorization = Base64.decodeStr(authorization);
112 112 clientId = StrUtil.subBefore(authorization, ":", false);
113 113 clientSecret = StrUtil.subAfter(authorization, ":", false);
114   - // 再从 Param 中获取
  114 + // 再从 Param 中获取
115 115 } else {
116 116 clientId = request.getParameter("client_id");
117 117 clientSecret = request.getParameter("client_secret");
... ... @@ -143,4 +143,21 @@ public class HttpUtils {
143 143 }
144 144 }
145 145  
  146 + /**
  147 + * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
  148 + *
  149 + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
  150 + *
  151 + * @param url URL
  152 + * @param headers 请求头
  153 + * @return 请求结果
  154 + */
  155 + public static String get(String url, Map<String, String> headers) {
  156 + try (HttpResponse response = HttpRequest.get(url)
  157 + .addHeaders(headers)
  158 + .execute()) {
  159 + return response.body();
  160 + }
  161 + }
  162 +
146 163 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.tenant.core.job;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.exceptions.ExceptionUtil;
  5 +import cn.hutool.core.util.StrUtil;
5 6 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6 7 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
7 8 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
... ... @@ -44,8 +45,10 @@ public class TenantJobAspect {
44 45 // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
45 46 TenantUtils.execute(tenantId, () -> {
46 47 try {
47   - joinPoint.proceed();
  48 + Object result = joinPoint.proceed();
  49 + results.put(tenantId, StrUtil.toStringOrNull(result));
48 50 } catch (Throwable e) {
  51 + log.error("[execute][租户({}) 执行 Job 发生异常", tenantId, e);
49 52 results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
50 53 }
51 54 });
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
... ... @@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
7 7 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
8 8 import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
9 9 import com.baomidou.mybatisplus.extension.incrementer.*;
  10 +import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
  11 +import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
10 12 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
11 13 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
12 14 import org.apache.ibatis.annotations.Mapper;
... ... @@ -16,6 +18,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
16 18 import org.springframework.context.annotation.Bean;
17 19 import org.springframework.core.env.ConfigurableEnvironment;
18 20  
  21 +import java.util.concurrent.TimeUnit;
  22 +
19 23 /**
20 24 * MyBaits 配置类
21 25 *
... ... @@ -26,6 +30,14 @@ import org.springframework.core.env.ConfigurableEnvironment;
26 30 lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
27 31 public class YudaoMybatisAutoConfiguration {
28 32  
  33 + static {
  34 + // 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存
  35 + JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(
  36 + (cache) -> cache.maximumSize(1024)
  37 + .expireAfterWrite(5, TimeUnit.SECONDS))
  38 + );
  39 + }
  40 +
29 41 @Bean
30 42 public MybatisPlusInterceptor mybatisPlusInterceptor() {
31 43 MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
... ... @@ -34,7 +46,7 @@ public class YudaoMybatisAutoConfiguration {
34 46 }
35 47  
36 48 @Bean
37   - public MetaObjectHandler defaultMetaObjectHandler(){
  49 + public MetaObjectHandler defaultMetaObjectHandler() {
38 50 return new DefaultDBFieldHandler(); // 自动填充参数类
39 51 }
40 52  
... ...
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java
... ... @@ -69,7 +69,7 @@ public class ApiSignatureAspect {
69 69  
70 70 // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
71 71 String nonce = request.getHeader(signature.nonce());
72   - signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
  72 + signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit());
73 73 return true;
74 74 }
75 75  
... ... @@ -113,7 +113,7 @@ public class ApiSignatureAspect {
113 113 }
114 114  
115 115 // 3. 检查 nonce 是否存在,有且仅能使用一次
116   - return signatureRedisDAO.getNonce(nonce) == null;
  116 + return signatureRedisDAO.getNonce(appId, nonce) == null;
117 117 }
118 118  
119 119 /**
... ... @@ -165,5 +165,4 @@ public class ApiSignatureAspect {
165 165 return sortedMap;
166 166 }
167 167  
168   -}
169   -
  168 +}
170 169 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java
... ... @@ -22,7 +22,7 @@ public class ApiSignatureRedisDAO {
22 22 * VALUE 格式:String
23 23 * 过期时间:不固定
24 24 */
25   - private static final String SIGNATURE_NONCE = "api_signature_nonce:%s";
  25 + private static final String SIGNATURE_NONCE = "api_signature_nonce:%s:%s";
26 26  
27 27 /**
28 28 * 签名密钥
... ... @@ -36,16 +36,16 @@ public class ApiSignatureRedisDAO {
36 36  
37 37 // ========== 验签随机数 ==========
38 38  
39   - public String getNonce(String nonce) {
40   - return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
  39 + public String getNonce(String appId, String nonce) {
  40 + return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce));
41 41 }
42 42  
43   - public void setNonce(String nonce, int time, TimeUnit timeUnit) {
44   - stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
  43 + public void setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
  44 + stringRedisTemplate.opsForValue().set(formatNonceKey(appId, nonce), "", time, timeUnit);
45 45 }
46 46  
47   - private static String formatNonceKey(String key) {
48   - return String.format(SIGNATURE_NONCE, key);
  47 + private static String formatNonceKey(String appId, String nonce) {
  48 + return String.format(SIGNATURE_NONCE, appId, nonce);
49 49 }
50 50  
51 51 // ========== 签名密钥 ==========
... ...
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java
... ... @@ -69,7 +69,7 @@ public class ApiSignatureTest {
69 69 // 断言结果
70 70 assertTrue(result);
71 71 // 断言调用
72   - verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS));
  72 + verify(signatureRedisDAO).setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS));
73 73 }
74 74  
75 75 -}
  76 +}
76 77 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java
... ... @@ -29,18 +29,22 @@ public class LogRecordServiceImpl implements ILogRecordService {
29 29  
30 30 @Override
31 31 public void record(LogRecord logRecord) {
32   - // 1. 补全通用字段
33 32 OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO();
34   - reqDTO.setTraceId(TracerUtils.getTraceId());
35   - // 补充用户信息
36   - fillUserFields(reqDTO);
37   - // 补全模块信息
38   - fillModuleFields(reqDTO, logRecord);
39   - // 补全请求信息
40   - fillRequestFields(reqDTO);
  33 + try {
  34 + reqDTO.setTraceId(TracerUtils.getTraceId());
  35 + // 补充用户信息
  36 + fillUserFields(reqDTO);
  37 + // 补全模块信息
  38 + fillModuleFields(reqDTO, logRecord);
  39 + // 补全请求信息
  40 + fillRequestFields(reqDTO);
41 41  
42   - // 2. 异步记录日志
43   - operateLogApi.createOperateLog(reqDTO);
  42 + // 2. 异步记录日志
  43 + operateLogApi.createOperateLogAsync(reqDTO);
  44 + } catch (Throwable ex) {
  45 + // 由于 @Async 异步调用,这里打印下日志,更容易跟进
  46 + log.error("[record][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
  47 + }
44 48 }
45 49  
46 50 private static void fillUserFields(OperateLogCreateReqDTO reqDTO) {
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java
... ... @@ -2,15 +2,10 @@ package cn.iocoder.yudao.framework.apilog.config;
2 2  
3 3 import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter;
4 4 import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
5   -import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
6   -import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
7   -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
8   -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
9 5 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
10 6 import cn.iocoder.yudao.framework.web.config.WebProperties;
11 7 import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
12 8 import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
13   -import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
14 9 import jakarta.servlet.Filter;
15 10 import org.springframework.beans.factory.annotation.Value;
16 11 import org.springframework.boot.autoconfigure.AutoConfiguration;
... ... @@ -23,18 +18,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
23 18 @AutoConfiguration(after = YudaoWebAutoConfiguration.class)
24 19 public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
25 20  
26   - @Bean
27   - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
28   - public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
29   - return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
30   - }
31   -
32   - @Bean
33   - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
34   - public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
35   - return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
36   - }
37   -
38 21 /**
39 22 * 创建 ApiAccessLogFilter Bean,记录 API 请求日志
40 23 */
... ... @@ -42,8 +25,8 @@ public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
42 25 @ConditionalOnProperty(prefix = "yudao.access-log", value = "enable", matchIfMissing = true) // 允许使用 yudao.access-log.enable=false 禁用访问日志
43 26 public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
44 27 @Value("${spring.application.name}") String applicationName,
45   - ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
46   - ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
  28 + ApiAccessLogApi apiAccessLogApi) {
  29 + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi);
47 30 return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
48 31 }
49 32  
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java
... ... @@ -9,7 +9,6 @@ import cn.hutool.core.util.BooleanUtil;
9 9 import cn.hutool.core.util.StrUtil;
10 10 import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
11 11 import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
12   -import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
13 12 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
14 13 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
15 14 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
... ... @@ -18,6 +17,7 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
18 17 import cn.iocoder.yudao.framework.web.config.WebProperties;
19 18 import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
20 19 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
  20 +import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
21 21 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
22 22 import com.fasterxml.jackson.databind.JsonNode;
23 23 import io.swagger.v3.oas.annotations.Operation;
... ... @@ -36,7 +36,7 @@ import java.time.temporal.ChronoUnit;
36 36 import java.util.Iterator;
37 37 import java.util.Map;
38 38  
39   -import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*;
  39 +import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD;
40 40 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
41 41  
42 42 /**
... ... @@ -53,12 +53,12 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
53 53  
54 54 private final String applicationName;
55 55  
56   - private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
  56 + private final ApiAccessLogApi apiAccessLogApi;
57 57  
58   - public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
  58 + public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogApi apiAccessLogApi) {
59 59 super(webProperties);
60 60 this.applicationName = applicationName;
61   - this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
  61 + this.apiAccessLogApi = apiAccessLogApi;
62 62 }
63 63  
64 64 @Override
... ... @@ -91,7 +91,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
91 91 if (!enable) {
92 92 return;
93 93 }
94   - apiAccessLogFrameworkService.createApiAccessLog(accessLog);
  94 + apiAccessLogApi.createApiAccessLogAsync(accessLog);
95 95 } catch (Throwable th) {
96 96 log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
97 97 }
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java deleted
1   -package cn.iocoder.yudao.framework.apilog.core.service;
2   -
3   -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
4   -
5   -/**
6   - * API 访问日志 Framework Service 接口
7   - *
8   - * @author 芋道源码
9   - */
10   -public interface ApiAccessLogFrameworkService {
11   -
12   - /**
13   - * 创建 API 访问日志
14   - *
15   - * @param reqDTO API 访问日志
16   - */
17   - void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO);
18   -
19   -}
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java deleted
1   -package cn.iocoder.yudao.framework.apilog.core.service;
2   -
3   -import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
4   -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
5   -import lombok.RequiredArgsConstructor;
6   -import lombok.extern.slf4j.Slf4j;
7   -import org.springframework.scheduling.annotation.Async;
8   -
9   -/**
10   - * API 访问日志 Framework Service 实现类
11   - *
12   - * 基于 {@link ApiAccessLogApi} 服务,记录访问日志
13   - *
14   - * @author 芋道源码
15   - */
16   -@RequiredArgsConstructor
17   -@Slf4j
18   -public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
19   -
20   - private final ApiAccessLogApi apiAccessLogApi;
21   -
22   - @Override
23   - @Async
24   - public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) {
25   - try {
26   - apiAccessLogApi.createApiAccessLog(reqDTO);
27   - } catch (Throwable ex) {
28   - // 由于 @Async 异步调用,这里打印下日志,更容易跟进
29   - log.error("[createApiAccessLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
30   - }
31   - }
32   -
33   -}
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java deleted
1   -package cn.iocoder.yudao.framework.apilog.core.service;
2   -
3   -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
4   -
5   -/**
6   - * API 错误日志 Framework Service 接口
7   - *
8   - * @author 芋道源码
9   - */
10   -public interface ApiErrorLogFrameworkService {
11   -
12   - /**
13   - * 创建 API 错误日志
14   - *
15   - * @param reqDTO API 错误日志
16   - */
17   - void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO);
18   -
19   -}
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java deleted
1   -package cn.iocoder.yudao.framework.apilog.core.service;
2   -
3   -import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
4   -import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
5   -import lombok.RequiredArgsConstructor;
6   -import lombok.extern.slf4j.Slf4j;
7   -import org.springframework.scheduling.annotation.Async;
8   -
9   -/**
10   - * API 错误日志 Framework Service 实现类
11   - *
12   - * 基于 {@link ApiErrorLogApi} 服务,记录错误日志
13   - *
14   - * @author 芋道源码
15   - */
16   -@RequiredArgsConstructor
17   -@Slf4j
18   -public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
19   -
20   - private final ApiErrorLogApi apiErrorLogApi;
21   -
22   - @Override
23   - @Async
24   - public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) {
25   - try {
26   - apiErrorLogApi.createApiErrorLog(reqDTO);
27   - } catch (Throwable ex) {
28   - // 由于 @Async 异步调用,这里打印下日志,更容易跟进
29   - log.error("[createApiErrorLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
30   - }
31   - }
32   -
33   -}
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
1 1 package cn.iocoder.yudao.framework.web.config;
2 2  
3   -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
4 3 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
5 4 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
6 5 import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
7 6 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
8 7 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
9 8 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
  9 +import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
  10 +import jakarta.annotation.Resource;
  11 +import jakarta.servlet.Filter;
10 12 import org.springframework.beans.factory.annotation.Value;
11 13 import org.springframework.boot.autoconfigure.AutoConfiguration;
12 14 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
... ... @@ -25,9 +27,6 @@ import org.springframework.web.filter.CorsFilter;
25 27 import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
26 28 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
27 29  
28   -import jakarta.annotation.Resource;
29   -import jakarta.servlet.Filter;
30   -
31 30 @AutoConfiguration
32 31 @EnableConfigurationProperties(WebProperties.class)
33 32 public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
... ... @@ -59,8 +58,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
59 58 }
60 59  
61 60 @Bean
62   - public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) {
63   - return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService);
  61 + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
  62 + public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogApi apiErrorLogApi) {
  63 + return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
64 64 }
65 65  
66 66 @Bean
... ...
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
... ... @@ -4,7 +4,7 @@ import cn.hutool.core.exceptions.ExceptionUtil;
4 4 import cn.hutool.core.map.MapUtil;
5 5 import cn.hutool.core.util.ObjUtil;
6 6 import cn.hutool.core.util.StrUtil;
7   -import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
  7 +import cn.hutool.extra.servlet.JakartaServletUtil;
8 8 import cn.iocoder.yudao.framework.common.exception.ServiceException;
9 9 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
10 10 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
... ... @@ -13,14 +13,16 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
13 13 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
14 14 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
15 15 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
  16 +import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
16 17 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
  18 +import com.fasterxml.jackson.databind.exc.InvalidFormatException;
17 19 import jakarta.servlet.http.HttpServletRequest;
18 20 import jakarta.validation.ConstraintViolation;
19 21 import jakarta.validation.ConstraintViolationException;
20 22 import jakarta.validation.ValidationException;
21 23 import lombok.AllArgsConstructor;
22 24 import lombok.extern.slf4j.Slf4j;
23   -import org.apache.commons.lang3.exception.ExceptionUtils;
  25 +import org.springframework.http.converter.HttpMessageNotReadableException;
24 26 import org.springframework.security.access.AccessDeniedException;
25 27 import org.springframework.util.Assert;
26 28 import org.springframework.validation.BindException;
... ... @@ -58,7 +60,7 @@ public class GlobalExceptionHandler {
58 60 @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
59 61 private final String applicationName;
60 62  
61   - private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
  63 + private final ApiErrorLogApi apiErrorLogApi;
62 64  
63 65 /**
64 66 * 处理所有异常,主要是提供给 Filter 使用
... ... @@ -88,7 +90,7 @@ public class GlobalExceptionHandler {
88 90 return validationException((ValidationException) ex);
89 91 }
90 92 if (ex instanceof NoHandlerFoundException) {
91   - return noHandlerFoundExceptionHandler(request, (NoHandlerFoundException) ex);
  93 + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
92 94 }
93 95 if (ex instanceof NoResourceFoundException) {
94 96 return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex);
... ... @@ -123,7 +125,7 @@ public class GlobalExceptionHandler {
123 125 */
124 126 @ExceptionHandler(MethodArgumentTypeMismatchException.class)
125 127 public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
126   - log.warn("[missingServletRequestParameterExceptionHandler]", ex);
  128 + log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex);
127 129 return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
128 130 }
129 131  
... ... @@ -150,6 +152,22 @@ public class GlobalExceptionHandler {
150 152 }
151 153  
152 154 /**
  155 + * 处理 SpringMVC 请求参数类型错误
  156 + *
  157 + * 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String
  158 + */
  159 + @ExceptionHandler(HttpMessageNotReadableException.class)
  160 + public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
  161 + log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
  162 + if(ex.getCause() instanceof InvalidFormatException) {
  163 + InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause();
  164 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
  165 + }else {
  166 + return defaultExceptionHandler(ServletUtils.getRequest(), ex);
  167 + }
  168 + }
  169 +
  170 + /**
153 171 * 处理 Validator 校验不通过产生的异常
154 172 */
155 173 @ExceptionHandler(value = ConstraintViolationException.class)
... ... @@ -177,7 +195,7 @@ public class GlobalExceptionHandler {
177 195 * 2. spring.mvc.static-path-pattern 为 /statics/**
178 196 */
179 197 @ExceptionHandler(NoHandlerFoundException.class)
180   - public CommonResult<?> noHandlerFoundExceptionHandler(HttpServletRequest req, NoHandlerFoundException ex) {
  198 + public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
181 199 log.warn("[noHandlerFoundExceptionHandler]", ex);
182 200 return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
183 201 }
... ... @@ -253,7 +271,7 @@ public class GlobalExceptionHandler {
253 271 // 情况二:处理异常
254 272 log.error("[defaultExceptionHandler]", ex);
255 273 // 插入异常日志
256   - this.createExceptionLog(req, ex);
  274 + createExceptionLog(req, ex);
257 275 // 返回 ERROR CommonResult
258 276 return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
259 277 }
... ... @@ -265,7 +283,7 @@ public class GlobalExceptionHandler {
265 283 // 初始化 errorLog
266 284 buildExceptionLog(errorLog, req, e);
267 285 // 执行插入 errorLog
268   - apiErrorLogFrameworkService.createApiErrorLog(errorLog);
  286 + apiErrorLogApi.createApiErrorLogAsync(errorLog);
269 287 } catch (Throwable th) {
270 288 log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
271 289 }
... ... @@ -279,7 +297,7 @@ public class GlobalExceptionHandler {
279 297 errorLog.setExceptionName(e.getClass().getName());
280 298 errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
281 299 errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
282   - errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e));
  300 + errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
283 301 StackTraceElement[] stackTraceElements = e.getStackTrace();
284 302 Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
285 303 StackTraceElement stackTraceElement = stackTraceElements[0];
... ... @@ -292,12 +310,12 @@ public class GlobalExceptionHandler {
292 310 errorLog.setApplicationName(applicationName);
293 311 errorLog.setRequestUrl(request.getRequestURI());
294 312 Map<String, Object> requestParams = MapUtil.<String, Object>builder()
295   - .put("query", ServletUtils.getParamMap(request))
296   - .put("body", ServletUtils.getBody(request)).build();
  313 + .put("query", JakartaServletUtil.getParamMap(request))
  314 + .put("body", JakartaServletUtil.getBody(request)).build();
297 315 errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
298 316 errorLog.setRequestMethod(request.getMethod());
299 317 errorLog.setUserAgent(ServletUtils.getUserAgent(request));
300   - errorLog.setUserIp(ServletUtils.getClientIP(request));
  318 + errorLog.setUserIp(JakartaServletUtil.getClientIP(request));
301 319 errorLog.setExceptionTime(LocalDateTime.now());
302 320 }
303 321  
... ... @@ -314,51 +332,51 @@ public class GlobalExceptionHandler {
314 332 }
315 333 // 1. 数据报表
316 334 if (message.contains("report_")) {
317   - log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
  335 + log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
318 336 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
319   - "[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
  337 + "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
320 338 }
321 339 // 2. 工作流
322 340 if (message.contains("bpm_")) {
323   - log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
  341 + log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
324 342 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325   - "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
  343 + "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
326 344 }
327 345 // 3. 微信公众号
328 346 if (message.contains("mp_")) {
329   - log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
  347 + log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
330 348 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
331   - "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
  349 + "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
332 350 }
333 351 // 4. 商城系统
334 352 if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
335   - log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
  353 + log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
336 354 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
337   - "[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
  355 + "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
338 356 }
339 357 // 5. ERP 系统
340 358 if (message.contains("erp_")) {
341   - log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
  359 + log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
342 360 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
343   - "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
  361 + "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
344 362 }
345 363 // 6. CRM 系统
346 364 if (message.contains("crm_")) {
347   - log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
  365 + log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
348 366 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
349   - "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
  367 + "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
350 368 }
351 369 // 7. 支付平台
352 370 if (message.contains("pay_")) {
353   - log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
  371 + log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
354 372 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
355   - "[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
  373 + "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
356 374 }
357 375 // 8. AI 大模型
358 376 if (message.contains("ai_")) {
359   - log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://doc.iocoder.cn/ai/build/ 开启]");
  377 + log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
360 378 return CommonResult.error(NOT_IMPLEMENTED.getCode(),
361   - "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://doc.iocoder.cn/ai/build/ 开启]");
  379 + "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
362 380 }
363 381 return null;
364 382 }
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
5 5 import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
6 6 import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
7 7 import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
  8 +import cn.iocoder.yudao.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer;
8 9 import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
9 10 import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
10 11 import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
... ... @@ -76,6 +77,11 @@ public class YudaoWebSocketAutoConfiguration {
76 77 return new WebSocketSessionManagerImpl();
77 78 }
78 79  
  80 + @Bean
  81 + public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
  82 + return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
  83 + }
  84 +
79 85 // ==================== Sender 相关 ====================
80 86  
81 87 @Configuration
... ...
yudao-module-ai/pom.xml
... ... @@ -18,7 +18,7 @@
18 18  
19 19 <name>${project.artifactId}</name>
20 20 <description>
21   - ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
  21 + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。
22 22 目前已接入各种模型,不限于:
23 23 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
24 24 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
1 1 package cn.iocoder.yudao.module.ai.enums;
2 2  
3   -import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4 3 import lombok.AllArgsConstructor;
5 4 import lombok.Getter;
6 5  
7   -import java.util.Arrays;
8   -
9 6 /**
10 7 * AI 内置聊天角色的枚举
11 8 *
... ... @@ -13,16 +10,16 @@ import java.util.Arrays;
13 10 */
14 11 @AllArgsConstructor
15 12 @Getter
16   -public enum AiChatRoleEnum implements IntArrayValuable {
  13 +public enum AiChatRoleEnum {
17 14  
18   - AI_WRITE_ROLE(1, "写作助手", """
  15 + AI_WRITE_ROLE("写作助手", """
19 16 你是一位出色的写作助手,能够帮助用户生成创意和灵感,并在用户提供场景和提示词时生成对应的回复。你的任务包括:
20 17 1. 撰写建议:根据用户提供的主题或问题,提供详细的写作建议、情节发展方向、角色设定以及背景描写,确保内容结构清晰、有逻辑。
21 18 2. 回复生成:根据用户提供的场景和提示词,生成合适的对话或文字回复,确保语气和风格符合场景需求。
22 19 除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。
23 20 """),
24 21  
25   - AI_MIND_MAP_ROLE(2, "脑图助手", """
  22 + AI_MIND_MAP_ROLE("导图助手", """
26 23 你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
27 24 # Geek-AI 助手
28 25 ## 完整的开源系统
... ... @@ -39,11 +36,6 @@ public enum AiChatRoleEnum implements IntArrayValuable {
39 36 除此之外不要任何解释性语句。
40 37 """);
41 38  
42   - // TODO @xin:这个 role 是不是删除掉好点哈。= = 目前主要是没做角色枚举。这里多了 role 反倒容易误解哈
43   - /**
44   - * 角色
45   - */
46   - private final Integer role;
47 39 /**
48 40 * 角色名
49 41 */
... ... @@ -54,11 +46,4 @@ public enum AiChatRoleEnum implements IntArrayValuable {
54 46 */
55 47 private final String systemMessage;
56 48  
57   - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiChatRoleEnum::getRole).toArray();
58   -
59   - @Override
60   - public int[] array() {
61   - return ARRAYS;
62   - }
63   -
64 49 }
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
... ... @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
4 4  
5 5 /**
6 6 * AI 错误码枚举类
7   - *
  7 + * <p>
8 8 * ai 系统,使用 1-040-000-000 段
9 9 */
10 10 public interface ErrorCodeConstants {
... ... @@ -45,9 +45,16 @@ public interface ErrorCodeConstants {
45 45 // ========== API 音乐 1-040-006-000 ==========
46 46 ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
47 47  
48   -
49 48 // ========== API 写作 1-022-007-000 ==========
50 49 ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
51 50 ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!");
52 51  
  52 + // ========== API 思维导图 1-040-008-000 ==========
  53 + ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!");
  54 +
  55 + // ========== API 知识库 1-022-008-000 ==========
  56 + ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!");
  57 + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_001, "文档不存在!");
  58 + ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_002, "段落不存在!");
  59 +
53 60 }
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * AI 知识库-文档状态的枚举
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +@AllArgsConstructor
  15 +@Getter
  16 +public enum AiKnowledgeDocumentStatusEnum implements IntArrayValuable {
  17 +
  18 + IN_PROGRESS(10, "索引中"),
  19 + SUCCESS(20, "可用"),
  20 + FAIL(30, "失败");
  21 +
  22 + /**
  23 + * 状态
  24 + */
  25 + private final Integer status;
  26 +
  27 + /**
  28 + * 状态名
  29 + */
  30 + private final String name;
  31 +
  32 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiKnowledgeDocumentStatusEnum::getStatus).toArray();
  33 +
  34 + @Override
  35 + public int[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
  39 +}
... ...
yudao-module-ai/yudao-module-ai-biz/pom.xml
... ... @@ -12,7 +12,7 @@
12 12  
13 13 <name>${project.artifactId}</name>
14 14 <description>
15   - ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
  15 + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。
16 16 目前已接入各种模型,不限于:
17 17 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
18 18 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
... ... @@ -5,10 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
5 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
9   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
10   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
11   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
12 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
13 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
14 11 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -45,6 +42,13 @@ public class AiImageController {
45 42 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
46 43 }
47 44  
  45 + @GetMapping("/public-page")
  46 + @Operation(summary = "获取公开的绘图分页")
  47 + public CommonResult<PageResult<AiImageRespVO>> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
  48 + PageResult<AiImageDO> pageResult = imageService.getImagePagePublic(pageReqVO);
  49 + return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
  50 + }
  51 +
48 52 @GetMapping("/get-my")
49 53 @Operation(summary = "获取【我的】绘图记录")
50 54 @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 绘画公开的分页 Request VO")
  8 +@Data
  9 +public class AiImagePublicPageReqVO extends PageParam {
  10 +
  11 + @Schema(description = "提示词")
  12 + private String prompt;
  13 +
  14 +}
0 15 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
  9 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
  11 +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
  12 +import io.swagger.v3.oas.annotations.Operation;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.*;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  21 +
  22 +@Tag(name = "管理后台 - AI 知识库")
  23 +@RestController
  24 +@RequestMapping("/ai/knowledge")
  25 +@Validated
  26 +public class AiKnowledgeController {
  27 +
  28 + @Resource
  29 + private AiKnowledgeService knowledgeService;
  30 +
  31 + @GetMapping("/my-page")
  32 + @Operation(summary = "获取【我的】知识库分页")
  33 + public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePageMy(@Validated PageParam pageReqVO) {
  34 + PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePageMy(getLoginUserId(), pageReqVO);
  35 + return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class));
  36 + }
  37 +
  38 + @PostMapping("/create-my")
  39 + @Operation(summary = "创建【我的】知识库")
  40 + public CommonResult<Long> createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) {
  41 + return success(knowledgeService.createKnowledgeMy(createReqVO, getLoginUserId()));
  42 + }
  43 +
  44 + @PutMapping("/update-my")
  45 + @Operation(summary = "更新【我的】知识库")
  46 + public CommonResult<Boolean> updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) {
  47 + knowledgeService.updateKnowledgeMy(updateReqVO, getLoginUserId());
  48 + return success(true);
  49 + }
  50 +
  51 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
  9 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
  11 +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
  12 +import io.swagger.v3.oas.annotations.Operation;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.*;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "管理后台 - AI 知识库文档")
  22 +@RestController
  23 +@RequestMapping("/ai/knowledge/document")
  24 +@Validated
  25 +public class AiKnowledgeDocumentController {
  26 +
  27 + @Resource
  28 + private AiKnowledgeDocumentService documentService;
  29 +
  30 + @PostMapping("/create")
  31 + @Operation(summary = "新建文档")
  32 + public CommonResult<Long> createKnowledgeDocument(@Valid AiKnowledgeDocumentCreateReqVO reqVO) {
  33 + Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO);
  34 + return success(knowledgeDocumentId);
  35 + }
  36 +
  37 + @GetMapping("/page")
  38 + @Operation(summary = "获取文档分页")
  39 + public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPageMy(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
  40 + PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO);
  41 + return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
  42 + }
  43 +
  44 + @PutMapping("/update")
  45 + @Operation(summary = "更新文档")
  46 + public CommonResult<Boolean> updateKnowledgeDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) {
  47 + documentService.updateKnowledgeDocument(reqVO);
  48 + return success(true);
  49 + }
  50 +
  51 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentRespVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
  9 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
  11 +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
  12 +import io.swagger.v3.oas.annotations.Operation;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.*;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "管理后台 - AI 知识库段落")
  22 +@RestController
  23 +@RequestMapping("/ai/knowledge/segment")
  24 +@Validated
  25 +public class AiKnowledgeSegmentController {
  26 +
  27 + @Resource
  28 + private AiKnowledgeSegmentService segmentService;
  29 +
  30 + @GetMapping("/page")
  31 + @Operation(summary = "获取段落分页")
  32 + public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPageMy(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
  33 + PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
  34 + return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
  35 + }
  36 +
  37 + @PutMapping("/update")
  38 + @Operation(summary = "更新段落内容")
  39 + public CommonResult<Boolean> updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentUpdateReqVO reqVO) {
  40 + segmentService.updateKnowledgeSegment(reqVO);
  41 + return success(true);
  42 + }
  43 +
  44 + @PutMapping("/update-status")
  45 + @Operation(summary = "启禁用段落内容")
  46 + public CommonResult<Boolean> updateKnowledgeSegmentStatus(@Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
  47 + segmentService.updateKnowledgeSegmentStatus(reqVO);
  48 + return success(true);
  49 + }
  50 +
  51 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 知识库文档的分页 Request VO")
  8 +@Data
  9 +public class AiKnowledgeDocumentPageReqVO extends PageParam {
  10 +
  11 + @Schema(description = "文档名称", example = "Java 开发手册")
  12 + private String name;
  13 +
  14 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 知识库-文档 Response VO")
  8 +@Data
  9 +public class AiKnowledgeDocumentRespVO extends PageParam {
  10 +
  11 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  12 + private Long id;
  13 +
  14 + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  15 + private Long knowledgeId;
  16 +
  17 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
  18 + private String name;
  19 +
  20 + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 是一门面向对象的语言.....")
  21 + private String content;
  22 +
  23 + @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn")
  24 + private String url;
  25 +
  26 + @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  27 + private Integer tokens;
  28 +
  29 + @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008")
  30 + private Integer wordCount;
  31 +
  32 + @Schema(description = "切片状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  33 + private Integer sliceStatus;
  34 +
  35 + @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  36 + private Integer status;
  37 +
  38 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +
  10 +@Schema(description = "管理后台 - AI 更新 知识库-文档 Request VO")
  11 +@Data
  12 +public class AiKnowledgeDocumentUpdateReqVO {
  13 +
  14 +
  15 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
  16 + @NotNull(message = "编号不能为空")
  17 + private Long id;
  18 +
  19 + @Schema(description = "是否启用", example = "1")
  20 + @InEnum(CommonStatusEnum.class)
  21 + private Integer status;
  22 +
  23 + @Schema(description = "名称", example = "Java 开发手册")
  24 + private String name;
  25 +
  26 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateMyReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.util.List;
  9 +
  10 +@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO")
  11 +@Data
  12 +public class AiKnowledgeCreateMyReqVO {
  13 +
  14 + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
  15 + @NotBlank(message = "知识库名称不能为空")
  16 + private String name;
  17 +
  18 + @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档")
  19 + private String description;
  20 +
  21 + @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
  22 + private List<Long> visibilityPermissions;
  23 +
  24 + @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  25 + @NotNull(message = "嵌入模型不能为空")
  26 + private Long modelId;
  27 +
  28 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import org.hibernate.validator.constraints.URL;
  8 +
  9 +
  10 +@Schema(description = "管理后台 - AI 知识库文档的创建 Request VO")
  11 +@Data
  12 +public class AiKnowledgeDocumentCreateReqVO {
  13 +
  14 + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
  15 + @NotNull(message = "知识库编号不能为空")
  16 + private Long knowledgeId;
  17 +
  18 + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "三方登陆")
  19 + @NotBlank(message = "文档名称不能为空")
  20 + private String name;
  21 +
  22 + @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn")
  23 + @URL(message = "文档 URL 格式不正确")
  24 + private String url;
  25 +
  26 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +
  7 +@Schema(description = "管理后台 - AI 知识库 Response VO")
  8 +@Data
  9 +public class AiKnowledgeRespVO {
  10 +
  11 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  12 + private Long id;
  13 +
  14 + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
  15 + private String name;
  16 +
  17 + @Schema(description = "知识库描述", example = "帮助你快速构建系统")
  18 + private String description;
  19 +
  20 + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14")
  21 + private Long modelId;
  22 +
  23 + @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat")
  24 + private String model;
  25 +
  26 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateMyReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.util.List;
  9 +
  10 +@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO")
  11 +@Data
  12 +public class AiKnowledgeUpdateMyReqVO {
  13 +
  14 + @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
  15 + @NotNull(message = "知识库编号不能为空")
  16 + private Long id;
  17 +
  18 + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
  19 + @NotBlank(message = "知识库名称不能为空")
  20 + private String name;
  21 +
  22 + @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
  23 + private String description;
  24 +
  25 + @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
  26 + private List<Long> visibilityPermissions;
  27 +
  28 + @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  29 + @NotNull(message = "嵌入模型不能为空")
  30 + private Long modelId;
  31 +
  32 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 知识库分段的分页 Request VO")
  8 +@Data
  9 +public class AiKnowledgeSegmentPageReqVO extends PageParam {
  10 +
  11 + @Schema(description = "分段状态", example = "1")
  12 + private Integer status;
  13 +
  14 + @Schema(description = "文档编号", example = "1")
  15 + private Integer documentId;
  16 +
  17 + @Schema(description = "分段内容关键字", example = "Java 开发")
  18 + private String keyword;
  19 +
  20 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - AI 知识库-文档 Response VO")
  7 +@Data
  8 +public class AiKnowledgeSegmentRespVO {
  9 +
  10 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  11 + private Long id;
  12 +
  13 + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  14 + private Long documentId;
  15 +
  16 + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  17 + private Long knowledgeId;
  18 +
  19 + @Schema(description = "向量库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1858496a-1dde-4edf-a43e-0aed08f37f8c")
  20 + private String vectorId;
  21 +
  22 + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
  23 + private String content;
  24 +
  25 + @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  26 + private Integer tokens;
  27 +
  28 + @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008")
  29 + private Integer wordCount;
  30 +
  31 + @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + private Integer status;
  33 +
  34 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +
  7 +@Schema(description = "管理后台 - AI 更新 知识库-段落 request VO")
  8 +@Data
  9 +public class AiKnowledgeSegmentUpdateReqVO {
  10 +
  11 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  12 + private Long id;
  13 +
  14 + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
  15 + private String content;
  16 +
  17 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +
  10 +@Schema(description = "管理后台 - AI 知识库段落的更新状态 Request VO")
  11 +@Data
  12 +public class AiKnowledgeSegmentUpdateStatusReqVO {
  13 +
  14 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  15 + private Long id;
  16 +
  17 + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  18 + @NotNull(message = "是否启用不能为空")
  19 + @InEnum(CommonStatusEnum.class)
  20 + private Integer status;
  21 +
  22 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.mindmap;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 6 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapRespVO;
  9 +import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
5 10 import cn.iocoder.yudao.module.ai.service.mindmap.AiMindMapService;
6 11 import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
7 13 import io.swagger.v3.oas.annotations.tags.Tag;
8 14 import jakarta.annotation.Resource;
9 15 import jakarta.annotation.security.PermitAll;
10 16 import jakarta.validation.Valid;
11 17 import org.springframework.http.MediaType;
12   -import org.springframework.web.bind.annotation.PostMapping;
13   -import org.springframework.web.bind.annotation.RequestBody;
14   -import org.springframework.web.bind.annotation.RequestMapping;
15   -import org.springframework.web.bind.annotation.RestController;
  18 +import org.springframework.security.access.prepost.PreAuthorize;
  19 +import org.springframework.web.bind.annotation.*;
16 20 import reactor.core.publisher.Flux;
17 21  
  22 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
18 23 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
19 24  
20 25 @Tag(name = "管理后台 - AI 思维导图")
... ... @@ -26,10 +31,29 @@ public class AiMindMapController {
26 31 private AiMindMapService mindMapService;
27 32  
28 33 @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
29   - @Operation(summary = "脑图生成(流式)", description = "流式返回,响应较快")
  34 + @Operation(summary = "导图生成(流式)", description = "流式返回,响应较快")
30 35 @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
31 36 public Flux<CommonResult<String>> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) {
32 37 return mindMapService.generateMindMap(generateReqVO, getLoginUserId());
33 38 }
34 39  
  40 + // ================ 导图管理 ================
  41 +
  42 + @DeleteMapping("/delete")
  43 + @Operation(summary = "删除思维导图")
  44 + @Parameter(name = "id", description = "编号", required = true)
  45 + @PreAuthorize("@ss.hasPermission('ai:mind-map:delete')")
  46 + public CommonResult<Boolean> deleteMindMap(@RequestParam("id") Long id) {
  47 + mindMapService.deleteMindMap(id);
  48 + return success(true);
  49 + }
  50 +
  51 + @GetMapping("/page")
  52 + @Operation(summary = "获得思维导图分页")
  53 + @PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
  54 + public CommonResult<PageResult<AiMindMapRespVO>> getMindMapPage(@Valid AiMindMapPageReqVO pageReqVO) {
  55 + PageResult<AiMindMapDO> pageResult = mindMapService.getMindMapPage(pageReqVO);
  56 + return success(BeanUtils.toBean(pageResult, AiMindMapRespVO.class));
  57 + }
  58 +
35 59 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +import org.springframework.format.annotation.DateTimeFormat;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  13 +
  14 +@Schema(description = "管理后台 - AI 思维导图分页 Request VO")
  15 +@Data
  16 +@EqualsAndHashCode(callSuper = true)
  17 +@ToString(callSuper = true)
  18 +public class AiMindMapPageReqVO extends PageParam {
  19 +
  20 + @Schema(description = "用户编号", example = "4325")
  21 + private Long userId;
  22 +
  23 + @Schema(description = "生成内容提示", example = "Java 学习路线")
  24 + private String prompt;
  25 +
  26 + @Schema(description = "创建时间")
  27 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  28 + private LocalDateTime[] createTime;
  29 +
  30 +}
0 31 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI 思维导图 Response VO")
  9 +@Data
  10 +public class AiMindMapRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3373")
  13 + private Long id;
  14 +
  15 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4325")
  16 + private Long userId;
  17 +
  18 + @Schema(description = "生成内容提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
  19 + private String prompt;
  20 +
  21 + @Schema(description = "生成的思维导图内容")
  22 + private String generatedContent;
  23 +
  24 + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
  25 + private String platform;
  26 +
  27 + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125")
  28 + private String model;
  29 +
  30 + @Schema(description = "错误信息")
  31 + private String errorMessage;
  32 +
  33 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  34 + private LocalDateTime createTime;
  35 +
  36 +}
0 37 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java
... ... @@ -30,7 +30,7 @@ public class AiImageDO extends BaseDO {
30 30 /**
31 31 * 编号
32 32 */
33   - @TableId(type = IdType.AUTO)
  33 + @TableId
34 34 private Long id;
35 35  
36 36 /**
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import com.baomidou.mybatisplus.annotation.TableField;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
  9 +import lombok.Data;
  10 +
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI 知识库 DO
  15 + *
  16 + * @author xiaoxin
  17 + */
  18 +@TableName(value = "ai_knowledge", autoResultMap = true)
  19 +@Data
  20 +public class AiKnowledgeDO extends BaseDO {
  21 +
  22 + /**
  23 + * 编号
  24 + */
  25 + @TableId
  26 + private Long id;
  27 + /**
  28 + * 用户编号
  29 + * <p>
  30 + * 关联 AdminUserDO 的 userId 字段
  31 + */
  32 + private Long userId;
  33 + /**
  34 + * 知识库名称
  35 + */
  36 + private String name;
  37 + /**
  38 + * 知识库描述
  39 + */
  40 + private String description;
  41 + // TODO @新:如果全部可见,需要怎么设置?
  42 + /**
  43 + * 可见权限,只能选择哪些人可见
  44 + */
  45 + @TableField(typeHandler = JacksonTypeHandler.class)
  46 + private List<Long> visibilityPermissions;
  47 + /**
  48 + * 嵌入模型编号
  49 + */
  50 + private Long modelId;
  51 + /**
  52 + * 模型标识
  53 + */
  54 + private String model;
  55 + /**
  56 + * 状态
  57 + * <p>
  58 + * 枚举 {@link CommonStatusEnum}
  59 + */
  60 + private Integer status;
  61 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.Data;
  9 +
  10 +/**
  11 + * AI 知识库-文档 DO
  12 + *
  13 + * @author xiaoxin
  14 + */
  15 +@TableName(value = "ai_knowledge_document")
  16 +@Data
  17 +public class AiKnowledgeDocumentDO extends BaseDO {
  18 +
  19 + /**
  20 + * 编号
  21 + */
  22 + @TableId
  23 + private Long id;
  24 + /**
  25 + * 知识库编号
  26 + *
  27 + * 关联 {@link AiKnowledgeDO#getId()}
  28 + */
  29 + private Long knowledgeId;
  30 + /**
  31 + * 文件名称
  32 + */
  33 + private String name;
  34 + /**
  35 + * 内容
  36 + */
  37 + private String content;
  38 + /**
  39 + * 文件 URL
  40 + */
  41 + private String url;
  42 + /**
  43 + * token 数量
  44 + */
  45 + private Integer tokens;
  46 + /**
  47 + * 字符数
  48 + */
  49 + private Integer wordCount;
  50 + /**
  51 + * 切片状态
  52 + * <p>
  53 + * 枚举 {@link AiKnowledgeDocumentStatusEnum}
  54 + */
  55 + private Integer sliceStatus;
  56 +
  57 + /**
  58 + * 状态
  59 + * <p>
  60 + * 枚举 {@link CommonStatusEnum}
  61 + */
  62 + private Integer status;
  63 +
  64 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Data;
  8 +
  9 +/**
  10 + * AI 知识库-文档分段 DO
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +@TableName(value = "ai_knowledge_segment")
  15 +@Data
  16 +public class AiKnowledgeSegmentDO extends BaseDO {
  17 +
  18 + public static final String FIELD_KNOWLEDGE_ID = "knowledgeId";
  19 +
  20 + /**
  21 + * 编号
  22 + */
  23 + @TableId
  24 + private Long id;
  25 + /**
  26 + * 向量库的编号
  27 + */
  28 + private String vectorId;
  29 + /**
  30 + * 知识库编号
  31 + *
  32 + * 关联 {@link AiKnowledgeDO#getId()}
  33 + */
  34 + private Long knowledgeId;
  35 + /**
  36 + * 文档编号
  37 + *
  38 + * 关联 {@link AiKnowledgeDocumentDO#getId()}
  39 + */
  40 + private Long documentId;
  41 + /**
  42 + * 切片内容
  43 + */
  44 + private String content;
  45 + /**
  46 + * 字符数
  47 + */
  48 + private Integer wordCount;
  49 + /**
  50 + * token 数量
  51 + */
  52 + private Integer tokens;
  53 + /**
  54 + * 状态
  55 + *
  56 + * 枚举 {@link CommonStatusEnum}
  57 + */
  58 + private Integer status;
  59 +
  60 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java
... ... @@ -19,7 +19,7 @@ public class AiMindMapDO extends BaseDO {
19 19 /**
20 20 * 编号
21 21 */
22   - @TableId(type = IdType.AUTO)
  22 + @TableId
23 23 private Long id;
24 24  
25 25 /**
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
... ... @@ -25,7 +25,7 @@ public class AiMusicDO extends BaseDO {
25 25 /**
26 26 * 编号
27 27 */
28   - @TableId(type = IdType.AUTO)
  28 + @TableId
29 29 private Long id;
30 30  
31 31 /**
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java
... ... @@ -20,7 +20,7 @@ public class AiWriteDO extends BaseDO {
20 20 /**
21 21 * 编号
22 22 */
23   - @TableId(type = IdType.AUTO)
  23 + @TableId
24 24 private Long id;
25 25  
26 26 /**
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicPageReqVO;
7 8 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
8 9 import org.apache.ibatis.annotations.Mapper;
9 10  
... ... @@ -41,6 +42,13 @@ public interface AiImageMapper extends BaseMapperX&lt;AiImageDO&gt; {
41 42 .orderByDesc(AiImageDO::getId));
42 43 }
43 44  
  45 + default PageResult<AiImageDO> selectPage(AiImagePublicPageReqVO pageReqVO) {
  46 + return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
  47 + .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE)
  48 + .likeIfPresent(AiImageDO::getPrompt, pageReqVO.getPrompt())
  49 + .orderByDesc(AiImageDO::getId));
  50 + }
  51 +
44 52 default List<AiImageDO> selectListByStatusAndPlatform(Integer status, String platform) {
45 53 return selectList(AiImageDO::getStatus, status,
46 54 AiImageDO::getPlatform, platform);
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
  7 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +/**
  11 + * AI 知识库-文档 Mapper
  12 + *
  13 + * @author xiaoxin
  14 + */
  15 +@Mapper
  16 +public interface AiKnowledgeDocumentMapper extends BaseMapperX<AiKnowledgeDocumentDO> {
  17 +
  18 + default PageResult<AiKnowledgeDocumentDO> selectPage(AiKnowledgeDocumentPageReqVO reqVO) {
  19 + return selectPage(reqVO, new LambdaQueryWrapperX<AiKnowledgeDocumentDO>()
  20 + .likeIfPresent(AiKnowledgeDocumentDO::getName, reqVO.getName())
  21 + .orderByDesc(AiKnowledgeDocumentDO::getId));
  22 + }
  23 +
  24 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +
  11 +/**
  12 + * AI 知识库基础信息 Mapper
  13 + *
  14 + * @author xiaoxin
  15 + */
  16 +@Mapper
  17 +public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> {
  18 +
  19 + default PageResult<AiKnowledgeDO> selectPageByMy(Long userId, PageParam pageReqVO) {
  20 + return selectPage(pageReqVO, new LambdaQueryWrapperX<AiKnowledgeDO>()
  21 + .eq(AiKnowledgeDO::getUserId, userId)
  22 + .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
  23 + .orderByDesc(AiKnowledgeDO::getId));
  24 + }
  25 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
  7 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +/**
  11 + * AI 知识库-分片 Mapper
  12 + *
  13 + * @author xiaoxin
  14 + */
  15 +@Mapper
  16 +public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegmentDO> {
  17 +
  18 + default PageResult<AiKnowledgeSegmentDO> selectPage(AiKnowledgeSegmentPageReqVO reqVO) {
  19 + return selectPage(reqVO, new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
  20 + .eq(AiKnowledgeSegmentDO::getDocumentId, reqVO.getDocumentId())
  21 + .eqIfPresent(AiKnowledgeSegmentDO::getStatus, reqVO.getStatus())
  22 + .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword())
  23 + .orderByDesc(AiKnowledgeSegmentDO::getId));
  24 + }
  25 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java
1 1 package cn.iocoder.yudao.module.ai.dal.mysql.mindmap;
2 2  
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
4 7 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
5 8 import org.apache.ibatis.annotations.Mapper;
6 9  
... ... @@ -11,4 +14,13 @@ import org.apache.ibatis.annotations.Mapper;
11 14 */
12 15 @Mapper
13 16 public interface AiMindMapMapper extends BaseMapperX<AiMindMapDO> {
  17 +
  18 + default PageResult<AiMindMapDO> selectPage(AiMindMapPageReqVO reqVO) {
  19 + return selectPage(reqVO, new LambdaQueryWrapperX<AiMindMapDO>()
  20 + .eqIfPresent(AiMindMapDO::getUserId, reqVO.getUserId())
  21 + .eqIfPresent(AiMindMapDO::getPrompt, reqVO.getPrompt())
  22 + .betweenIfPresent(AiMindMapDO::getCreateTime, reqVO.getCreateTime())
  23 + .orderByDesc(AiMindMapDO::getId));
  24 + }
  25 +
14 26 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
... ... @@ -2,9 +2,7 @@ package cn.iocoder.yudao.module.ai.service.image;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
6   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
7   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
8 6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
9 7 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
10 8 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -29,6 +27,14 @@ public interface AiImageService {
29 27 PageResult<AiImageDO> getImagePageMy(Long userId, AiImagePageReqVO pageReqVO);
30 28  
31 29 /**
  30 + * 获取公开的绘图分页
  31 + *
  32 + * @param pageReqVO 分页条件
  33 + * @return 绘图分页
  34 + */
  35 + PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO);
  36 +
  37 + /**
32 38 * 获得绘图记录
33 39 *
34 40 * @param id 绘图编号
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
... ... @@ -12,9 +12,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
12 12 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
13 13 import cn.iocoder.yudao.framework.common.pojo.PageResult;
14 14 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
15   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
16   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
17   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
  15 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
18 16 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
19 17 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
20 18 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -71,6 +69,11 @@ public class AiImageServiceImpl implements AiImageService {
71 69 }
72 70  
73 71 @Override
  72 + public PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
  73 + return imageMapper.selectPage(pageReqVO);
  74 + }
  75 +
  76 + @Override
74 77 public AiImageDO getImage(Long id) {
75 78 return imageMapper.selectById(id);
76 79 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
  7 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
  8 +
  9 +/**
  10 + * AI 知识库-文档 Service 接口
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +public interface AiKnowledgeDocumentService {
  15 +
  16 + /**
  17 + * 创建文档
  18 + *
  19 + * @param createReqVO 文档创建 Request VO
  20 + * @return 文档编号
  21 + */
  22 + Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO);
  23 +
  24 +
  25 + /**
  26 + * 获取文档分页
  27 + *
  28 + * @param pageReqVO 分页参数
  29 + * @return 文档分页
  30 + */
  31 + PageResult<AiKnowledgeDocumentDO> getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO);
  32 +
  33 + /**
  34 + * 更新文档
  35 + *
  36 + * @param reqVO 更新信息
  37 + */
  38 + void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO);
  39 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.http.HttpUtil;
  5 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  9 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
  10 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
  11 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
  12 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
  13 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
  14 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
  15 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
  16 +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper;
  17 +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
  18 +import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum;
  19 +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
  20 +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
  21 +import jakarta.annotation.Resource;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.springframework.ai.document.Document;
  24 +import org.springframework.ai.reader.tika.TikaDocumentReader;
  25 +import org.springframework.ai.tokenizer.TokenCountEstimator;
  26 +import org.springframework.ai.transformer.splitter.TokenTextSplitter;
  27 +import org.springframework.ai.vectorstore.VectorStore;
  28 +import org.springframework.core.io.ByteArrayResource;
  29 +import org.springframework.stereotype.Service;
  30 +import org.springframework.transaction.annotation.Transactional;
  31 +
  32 +import java.util.List;
  33 +
  34 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  35 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS;
  36 +
  37 +/**
  38 + * AI 知识库文档 Service 实现类
  39 + *
  40 + * @author xiaoxin
  41 + */
  42 +@Service
  43 +@Slf4j
  44 +public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentService {
  45 +
  46 + @Resource
  47 + private AiKnowledgeDocumentMapper documentMapper;
  48 + @Resource
  49 + private AiKnowledgeSegmentMapper segmentMapper;
  50 +
  51 + @Resource
  52 + private TokenTextSplitter tokenTextSplitter;
  53 + @Resource
  54 + private TokenCountEstimator tokenCountEstimator;
  55 +
  56 + @Resource
  57 + private AiApiKeyService apiKeyService;
  58 + @Resource
  59 + private AiKnowledgeService knowledgeService;
  60 + @Resource
  61 + private AiChatModelService chatModelService;
  62 +
  63 + @Override
  64 + @Transactional(rollbackFor = Exception.class)
  65 + public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) {
  66 + // 0. 校验
  67 + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
  68 + AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
  69 +
  70 + // 1.1 下载文档
  71 + TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl()));
  72 + List<Document> documents = loader.get();
  73 + Document document = CollUtil.getFirst(documents);
  74 + // 1.2 文档记录入库
  75 + String content = document.getContent();
  76 + AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class)
  77 + .setTokens(tokenCountEstimator.estimate(content)).setWordCount(content.length())
  78 + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus());
  79 + documentMapper.insert(documentDO);
  80 + Long documentId = documentDO.getId();
  81 + if (CollUtil.isEmpty(documents)) {
  82 + return documentId;
  83 + }
  84 +
  85 + // 2.1 文档分段
  86 + List<Document> segments = tokenTextSplitter.apply(documents);
  87 + // 2.2 分段内容入库
  88 + List<AiKnowledgeSegmentDO> segmentDOList = CollectionUtils.convertList(segments,
  89 + segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId)
  90 + .setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId())
  91 + .setTokens(tokenCountEstimator.estimate(segment.getContent())).setWordCount(segment.getContent().length())
  92 + .setStatus(CommonStatusEnum.ENABLE.getStatus()));
  93 + segmentMapper.insertBatch(segmentDOList);
  94 +
  95 + // 3.1 获取向量存储实例
  96 + VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
  97 + // 3.2 向量化并存储
  98 + segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId()));
  99 + vectorStore.add(segments);
  100 + return documentId;
  101 + }
  102 +
  103 + @Override
  104 + public PageResult<AiKnowledgeDocumentDO> getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) {
  105 + return documentMapper.selectPage(pageReqVO);
  106 + }
  107 +
  108 + @Override
  109 + public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) {
  110 + // 1. 校验文档是否存在
  111 + validateKnowledgeDocumentExists(reqVO.getId());
  112 + // 2. 更新文档
  113 + AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class);
  114 + documentMapper.updateById(document);
  115 + }
  116 +
  117 + /**
  118 + * 校验文档是否存在
  119 + *
  120 + * @param id 文档编号
  121 + * @return 文档信息
  122 + */
  123 + private AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) {
  124 + AiKnowledgeDocumentDO knowledgeDocument = documentMapper.selectById(id);
  125 + if (knowledgeDocument == null) {
  126 + throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS);
  127 + }
  128 + return knowledgeDocument;
  129 + }
  130 +
  131 + private org.springframework.core.io.Resource downloadFile(String url) {
  132 + try {
  133 + byte[] bytes = HttpUtil.downloadBytes(url);
  134 + return new ByteArrayResource(bytes);
  135 + } catch (Exception e) {
  136 + log.error("[downloadFile][url({}) 下载失败]", url, e);
  137 + throw new RuntimeException(e);
  138 + }
  139 + }
  140 +
  141 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
  7 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
  8 +
  9 +/**
  10 + * AI 知识库段落 Service 接口
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +public interface AiKnowledgeSegmentService {
  15 +
  16 + /**
  17 + * 获取段落分页
  18 + *
  19 + * @param pageReqVO 分页查询
  20 + * @return 文档分页
  21 + */
  22 + PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO);
  23 +
  24 + /**
  25 + * 更新段落的内容
  26 + *
  27 + * @param reqVO 更新内容
  28 + */
  29 + void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO);
  30 +
  31 + /**
  32 + * 更新段落的状态
  33 + *
  34 + * @param reqVO 更新内容
  35 + */
  36 + void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
  37 +
  38 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
  8 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
  9 +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
  10 +import jakarta.annotation.Resource;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.springframework.stereotype.Service;
  13 +
  14 +/**
  15 + * AI 知识库分片 Service 实现类
  16 + *
  17 + * @author xiaoxin
  18 + */
  19 +@Service
  20 +@Slf4j
  21 +public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService {
  22 +
  23 + @Resource
  24 + private AiKnowledgeSegmentMapper segmentMapper;
  25 +
  26 + @Override
  27 + public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
  28 + return segmentMapper.selectPage(pageReqVO);
  29 + }
  30 +
  31 + @Override
  32 + public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) {
  33 + segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
  34 + // TODO @xin 重新向量化
  35 + }
  36 +
  37 + @Override
  38 + public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
  39 + segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
  40 + // TODO @xin 1.禁用删除向量 2.启用重新向量化
  41 + }
  42 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
  7 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
  8 +
  9 +/**
  10 + * AI 知识库-基础信息 Service 接口
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +public interface AiKnowledgeService {
  15 +
  16 + /**
  17 + * 创建【我的】知识库
  18 + *
  19 + * @param createReqVO 创建信息
  20 + * @param userId 用户编号
  21 + * @return 编号
  22 + */
  23 + Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId);
  24 +
  25 +
  26 + /**
  27 + * 创建【我的】知识库
  28 + *
  29 + * @param updateReqVO 更新信息
  30 + * @param userId 用户编号
  31 + */
  32 + void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId);
  33 +
  34 +
  35 + /**
  36 + * 校验知识库是否存在
  37 + *
  38 + * @param id 记录编号
  39 + */
  40 + AiKnowledgeDO validateKnowledgeExists(Long id);
  41 +
  42 + /**
  43 + * 获得【我的】知识库分页
  44 + *
  45 + * @param userId 用户编号
  46 + * @param pageReqVO 分页查询
  47 + * @return 知识库分页
  48 + */
  49 + PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO);
  50 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
  9 +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
  11 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
  12 +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper;
  13 +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
  14 +import jakarta.annotation.Resource;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +import org.springframework.stereotype.Service;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  19 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_EXISTS;
  20 +
  21 +/**
  22 + * AI 知识库-基础信息 Service 实现类
  23 + *
  24 + * @author xiaoxin
  25 + */
  26 +@Service
  27 +@Slf4j
  28 +public class AiKnowledgeServiceImpl implements AiKnowledgeService {
  29 +
  30 + @Resource
  31 + private AiChatModelService chatModalService;
  32 +
  33 + @Resource
  34 + private AiKnowledgeMapper knowledgeMapper;
  35 +
  36 + @Override
  37 + public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) {
  38 + // 1. 校验模型配置
  39 + AiChatModelDO model = chatModalService.validateChatModel(createReqVO.getModelId());
  40 +
  41 + // 2. 插入知识库
  42 + AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class)
  43 + .setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus());
  44 + knowledgeMapper.insert(knowledgeBase);
  45 + return knowledgeBase.getId();
  46 + }
  47 +
  48 + @Override
  49 + public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) {
  50 + // 1.1 校验知识库存在
  51 + AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId());
  52 + if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) {
  53 + throw exception(KNOWLEDGE_NOT_EXISTS);
  54 + }
  55 + // 1.2 校验模型配置
  56 + AiChatModelDO model = chatModalService.validateChatModel(updateReqVO.getModelId());
  57 +
  58 + // 2. 更新知识库
  59 + AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class);
  60 + updateDO.setModel(model.getModel());
  61 + knowledgeMapper.updateById(updateDO);
  62 + }
  63 +
  64 + @Override
  65 + public AiKnowledgeDO validateKnowledgeExists(Long id) {
  66 + AiKnowledgeDO knowledgeBase = knowledgeMapper.selectById(id);
  67 + if (knowledgeBase == null) {
  68 + throw exception(KNOWLEDGE_NOT_EXISTS);
  69 + }
  70 + return knowledgeBase;
  71 + }
  72 +
  73 + @Override
  74 + public PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO) {
  75 + return knowledgeMapper.selectPageByMy(userId, pageReqVO);
  76 + }
  77 +
  78 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java
1 1 package cn.iocoder.yudao.module.ai.service.mindmap;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
  7 +import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
5 8 import reactor.core.publisher.Flux;
6 9  
7 10 /**
... ... @@ -20,4 +23,19 @@ public interface AiMindMapService {
20 23 */
21 24 Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId);
22 25  
  26 + /**
  27 + * 删除思维导图
  28 + *
  29 + * @param id 编号
  30 + */
  31 + void deleteMindMap(Long id);
  32 +
  33 + /**
  34 + * 获得思维导图分页
  35 + *
  36 + * @param pageReqVO 分页查询
  37 + * @return 思维导图分页
  38 + */
  39 + PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO);
  40 +
23 41 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
... ... @@ -6,9 +6,11 @@ import cn.hutool.core.util.StrUtil;
6 6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
7 7 import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
8 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
9 10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
10 11 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
11 12 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
  13 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
12 14 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
13 15 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
14 16 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
... ... @@ -33,8 +35,10 @@ import reactor.core.publisher.Flux;
33 35 import java.util.ArrayList;
34 36 import java.util.List;
35 37  
  38 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
36 39 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
37 40 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  41 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_EXISTS;
38 42  
39 43 /**
40 44 * AI 思维导图 Service 实现类
... ... @@ -57,10 +61,10 @@ public class AiMindMapServiceImpl implements AiMindMapService {
57 61  
58 62 @Override
59 63 public Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId) {
60   - // 1. 获取脑图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
  64 + // 1. 获取导图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
61 65 AiChatRoleDO role = CollUtil.getFirst(
62 66 chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName()));
63   - // 1.1 获取脑图执行模型
  67 + // 1.1 获取导图执行模型
64 68 AiChatModelDO model = getModel(role);
65 69 // 1.2 获取角色设定消息
66 70 String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage())
... ... @@ -131,4 +135,23 @@ public class AiMindMapServiceImpl implements AiMindMapService {
131 135 return model;
132 136 }
133 137  
  138 + @Override
  139 + public void deleteMindMap(Long id) {
  140 + // 校验存在
  141 + validateMindMapExists(id);
  142 + // 删除
  143 + mindMapMapper.deleteById(id);
  144 + }
  145 +
  146 + private void validateMindMapExists(Long id) {
  147 + if (mindMapMapper.selectById(id) == null) {
  148 + throw exception(MIND_MAP_NOT_EXISTS);
  149 + }
  150 + }
  151 +
  152 + @Override
  153 + public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) {
  154 + return mindMapMapper.selectPage(pageReqVO);
  155 + }
  156 +
134 157 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
... ... @@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR
9 9 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
10 10 import jakarta.validation.Valid;
11 11 import org.springframework.ai.chat.model.ChatModel;
  12 +import org.springframework.ai.embedding.EmbeddingModel;
12 13 import org.springframework.ai.image.ImageModel;
  14 +import org.springframework.ai.vectorstore.VectorStore;
13 15  
14 16 import java.util.List;
15 17  
... ... @@ -111,4 +113,20 @@ public interface AiApiKeyService {
111 113 */
112 114 SunoApi getSunoApi();
113 115  
  116 + /**
  117 + * 获得 EmbeddingModel 对象
  118 + *
  119 + * @param id 编号
  120 + * @return EmbeddingModel 对象
  121 + */
  122 + EmbeddingModel getEmbeddingModel(Long id);
  123 +
  124 + /**
  125 + * 获得 VectorStore 对象
  126 + *
  127 + * @param id 编号
  128 + * @return VectorStore 对象
  129 + */
  130 + VectorStore getOrCreateVectorStore(Long id);
  131 +
114 132 }
115 133 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 4 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
  5 +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
5 6 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
6 7 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
7 8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
... ... @@ -13,7 +14,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
13 14 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
14 15 import jakarta.annotation.Resource;
15 16 import org.springframework.ai.chat.model.ChatModel;
  17 +import org.springframework.ai.embedding.EmbeddingModel;
16 18 import org.springframework.ai.image.ImageModel;
  19 +import org.springframework.ai.vectorstore.VectorStore;
17 20 import org.springframework.stereotype.Service;
18 21 import org.springframework.validation.annotation.Validated;
19 22  
... ... @@ -36,6 +39,8 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
36 39  
37 40 @Resource
38 41 private AiModelFactory modelFactory;
  42 + @Resource
  43 + private AiVectorStoreFactory vectorFactory;
39 44  
40 45 @Override
41 46 public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
... ... @@ -132,4 +137,19 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
132 137 }
133 138 return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
134 139 }
  140 +
  141 + @Override
  142 + public EmbeddingModel getEmbeddingModel(Long id) {
  143 + AiApiKeyDO apiKey = validateApiKey(id);
  144 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
  145 + return modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), apiKey.getUrl());
  146 + }
  147 +
  148 + @Override
  149 + public VectorStore getOrCreateVectorStore(Long id) {
  150 + AiApiKeyDO apiKey = validateApiKey(id);
  151 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
  152 + return vectorFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl());
  153 + }
  154 +
135 155 }
136 156 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
... ... @@ -14,42 +14,68 @@
14 14 <name>${project.artifactId}</name>
15 15 <description>AI 大模型拓展,接入国内外大模型</description>
16 16 <properties>
17   - <spring-ai.version>1.0.0-M1</spring-ai.version>
  17 + <spring-ai.groupId>group.springframework.ai</spring-ai.groupId>
  18 + <spring-ai.version>1.1.0</spring-ai.version>
18 19 </properties>
19 20  
20 21 <dependencies>
21 22 <dependency>
22   - <groupId>org.springframework.ai</groupId>
  23 + <groupId>${spring-ai.groupId}</groupId>
23 24 <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
24 25 <version>${spring-ai.version}</version>
25 26 </dependency>
26   -
27 27 <dependency>
28   - <groupId>org.springframework.ai</groupId>
  28 + <groupId>${spring-ai.groupId}</groupId>
29 29 <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
30 30 <version>${spring-ai.version}</version>
31 31 </dependency>
32 32 <dependency>
33   - <groupId>org.springframework.ai</groupId>
  33 + <groupId>${spring-ai.groupId}</groupId>
  34 + <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
  35 + <version>${spring-ai.version}</version>
  36 + </dependency>
  37 + <dependency>
  38 + <groupId>${spring-ai.groupId}</groupId>
34 39 <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
35 40 <version>${spring-ai.version}</version>
36 41 </dependency>
37 42 <dependency>
38   - <groupId>org.springframework.ai</groupId>
  43 + <groupId>${spring-ai.groupId}</groupId>
39 44 <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
40 45 <version>${spring-ai.version}</version>
41 46 </dependency>
42 47  
  48 + <!-- 向量化,基于 Redis 存储,Tika 解析内容 -->
  49 + <dependency>
  50 + <groupId>${spring-ai.groupId}</groupId>
  51 + <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
  52 + <version>${spring-ai.version}</version>
  53 + </dependency>
  54 + <dependency>
  55 + <groupId>${spring-ai.groupId}</groupId>
  56 + <artifactId>spring-ai-tika-document-reader</artifactId>
  57 + <version>${spring-ai.version}</version>
  58 + </dependency>
  59 + <dependency>
  60 + <groupId>${spring-ai.groupId}</groupId>
  61 + <artifactId>spring-ai-redis-store</artifactId>
  62 + <version>${spring-ai.version}</version>
  63 + </dependency>
  64 +
  65 + <dependency>
  66 + <groupId>cn.iocoder.boot</groupId>
  67 + <artifactId>yudao-spring-boot-starter-redis</artifactId>
  68 + </dependency>
  69 +
43 70 <dependency>
44 71 <groupId>cn.iocoder.boot</groupId>
45 72 <artifactId>yudao-common</artifactId>
46 73 </dependency>
47 74  
48   - <!-- TODO 芋艿:等 spring-ai 官方发布后,需要把 groupId 改下 -->
49 75 <dependency>
50   - <groupId>group.springframework.ai</groupId>
  76 + <groupId>${spring-ai.groupId}</groupId>
51 77 <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
52   - <version>1.1.0</version>
  78 + <version>${spring-ai.version}</version>
53 79 </dependency>
54 80  
55 81 <!-- 阿里云 通义千问 -->
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
... ... @@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.ai.config;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
4 4 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
  5 +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
  6 +import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactoryImpl;
5 7 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
6 8 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
7 9 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
... ... @@ -10,11 +12,15 @@ import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
10 12 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
11 13 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
12 14 import lombok.extern.slf4j.Slf4j;
  15 +import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
  16 +import org.springframework.ai.tokenizer.TokenCountEstimator;
  17 +import org.springframework.ai.transformer.splitter.TokenTextSplitter;
13 18 import org.springframework.boot.autoconfigure.AutoConfiguration;
14 19 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
15 20 import org.springframework.boot.context.properties.EnableConfigurationProperties;
16 21 import org.springframework.context.annotation.Bean;
17 22 import org.springframework.context.annotation.Import;
  23 +import org.springframework.context.annotation.Lazy;
18 24  
19 25 /**
20 26 * 芋道 AI 自动配置
... ... @@ -32,6 +38,12 @@ public class YudaoAiAutoConfiguration {
32 38 return new AiModelFactoryImpl();
33 39 }
34 40  
  41 + @Bean
  42 + public AiVectorStoreFactory aiVectorFactory() {
  43 + return new AiVectorStoreFactoryImpl();
  44 + }
  45 +
  46 +
35 47 // ========== 各种 AI Client 创建 ==========
36 48  
37 49 @Bean
... ... @@ -73,4 +85,43 @@ public class YudaoAiAutoConfiguration {
73 85 return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
74 86 }
75 87  
  88 + // ========== rag 相关 ==========
  89 + // TODO @xin 免费版本
  90 +// @Bean
  91 +// @Lazy // TODO 芋艿:临时注释,避免无法启动」
  92 +// public EmbeddingModel transformersEmbeddingClient() {
  93 +// return new TransformersEmbeddingModel(MetadataMode.EMBED);
  94 +// }
  95 +
  96 + /**
  97 + * TODO @xin 默认版本先不弄,目前都先取对应的 EmbeddingModel
  98 + */
  99 +// @Bean
  100 +// @Lazy // TODO 芋艿:临时注释,避免无法启动
  101 +// public RedisVectorStore vectorStore(TongYiTextEmbeddingModel tongYiTextEmbeddingModel, RedisVectorStoreProperties properties,
  102 +// RedisProperties redisProperties) {
  103 +// var config = RedisVectorStore.RedisVectorStoreConfig.builder()
  104 +// .withIndexName(properties.getIndex())
  105 +// .withPrefix(properties.getPrefix())
  106 +// .build();
  107 +//
  108 +// RedisVectorStore redisVectorStore = new RedisVectorStore(config, tongYiTextEmbeddingModel,
  109 +// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
  110 +// properties.isInitializeSchema());
  111 +// redisVectorStore.afterPropertiesSet();
  112 +// return redisVectorStore;
  113 +// }
  114 +
  115 + @Bean
  116 + @Lazy // TODO 芋艿:临时注释,避免无法启动
  117 + public TokenTextSplitter tokenTextSplitter() {
  118 + return new TokenTextSplitter(500, 100, 5, 10000, true);
  119 + }
  120 +
  121 + @Bean
  122 + @Lazy // TODO 芋艿:临时注释,避免无法启动
  123 + public TokenCountEstimator tokenCountEstimator() {
  124 + return new JTokkitTokenCountEstimator();
  125 + }
  126 +
76 127 }
77 128 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java
... ... @@ -22,7 +22,8 @@ public enum AiPlatformEnum {
22 22  
23 23 // ========== 国外平台 ==========
24 24  
25   - OPENAI("OpenAI", "OpenAI"),
  25 + OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
  26 + AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
26 27 OLLAMA("Ollama", "Ollama"),
27 28  
28 29 STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 4 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
5 5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
6 6 import org.springframework.ai.chat.model.ChatModel;
  7 +import org.springframework.ai.embedding.EmbeddingModel;
7 8 import org.springframework.ai.image.ImageModel;
8 9  
9 10 /**
... ... @@ -79,4 +80,16 @@ public interface AiModelFactory {
79 80 */
80 81 SunoApi getOrCreateSunoApi(String apiKey, String url);
81 82  
  83 + /**
  84 + * 基于指定配置,获得 EmbeddingModel 对象
  85 + *
  86 + * 如果不存在,则进行创建
  87 + *
  88 + * @param platform 平台
  89 + * @param apiKey API KEY
  90 + * @param url API URL
  91 + * @return ChatModel 对象
  92 + */
  93 + EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url);
  94 +
82 95 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java
... ... @@ -21,6 +21,11 @@ import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel;
21 21 import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties;
22 22 import com.alibaba.dashscope.aigc.generation.Generation;
23 23 import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
  24 +import com.alibaba.dashscope.embeddings.TextEmbedding;
  25 +import com.azure.ai.openai.OpenAIClient;
  26 +import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
  27 +import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
  28 +import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
24 29 import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
25 30 import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
26 31 import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
... ... @@ -31,7 +36,9 @@ import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
31 36 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties;
32 37 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties;
33 38 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties;
  39 +import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
34 40 import org.springframework.ai.chat.model.ChatModel;
  41 +import org.springframework.ai.embedding.EmbeddingModel;
35 42 import org.springframework.ai.image.ImageModel;
36 43 import org.springframework.ai.model.function.FunctionCallbackContext;
37 44 import org.springframework.ai.ollama.OllamaChatModel;
... ... @@ -82,6 +89,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
82 89 return buildXingHuoChatModel(apiKey);
83 90 case OPENAI:
84 91 return buildOpenAiChatModel(apiKey, url);
  92 + case AZURE_OPENAI:
  93 + return buildAzureOpenAiChatModel(apiKey, url);
85 94 case OLLAMA:
86 95 return buildOllamaChatModel(url);
87 96 default:
... ... @@ -106,6 +115,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
106 115 return SpringUtil.getBean(XingHuoChatModel.class);
107 116 case OPENAI:
108 117 return SpringUtil.getBean(OpenAiChatModel.class);
  118 + case AZURE_OPENAI:
  119 + return SpringUtil.getBean(AzureOpenAiChatModel.class);
109 120 case OLLAMA:
110 121 return SpringUtil.getBean(OllamaChatModel.class);
111 122 default:
... ... @@ -166,6 +177,20 @@ public class AiModelFactoryImpl implements AiModelFactory {
166 177 return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
167 178 }
168 179  
  180 + @Override
  181 + public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url) {
  182 + String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url);
  183 + return Singleton.get(cacheKey, (Func0<EmbeddingModel>) () -> {
  184 + // TODO @xin 先测试一个
  185 + switch (platform) {
  186 + case TONG_YI:
  187 + return buildTongYiEmbeddingModel(apiKey);
  188 + default:
  189 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  190 + }
  191 + });
  192 + }
  193 +
169 194 private static String buildClientCacheKey(Class<?> clazz, Object... params) {
170 195 if (ArrayUtil.isEmpty(params)) {
171 196 return clazz.getName();
... ... @@ -229,8 +254,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
229 254 }
230 255  
231 256 /**
232   - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(
233   - * ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)}
  257 + * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)}
234 258 */
235 259 private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
236 260 url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL);
... ... @@ -239,8 +263,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
239 263 }
240 264  
241 265 /**
242   - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(
243   - * ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)}
  266 + * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)}
244 267 */
245 268 private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
246 269 url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL);
... ... @@ -269,6 +292,21 @@ public class AiModelFactoryImpl implements AiModelFactory {
269 292 }
270 293  
271 294 /**
  295 + * 可参考 {@link AzureOpenAiAutoConfiguration}
  296 + */
  297 + private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
  298 + AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
  299 + // 创建 OpenAIClient 对象
  300 + AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
  301 + connectionProperties.setApiKey(apiKey);
  302 + connectionProperties.setEndpoint(url);
  303 + OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties);
  304 + // 获取 AzureOpenAiChatProperties 对象
  305 + AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
  306 + return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null);
  307 + }
  308 +
  309 + /**
272 310 * 可参考 {@link OpenAiAutoConfiguration}
273 311 */
274 312 private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
... ... @@ -291,4 +329,15 @@ public class AiModelFactoryImpl implements AiModelFactory {
291 329 return new StabilityAiImageModel(stabilityAiApi);
292 330 }
293 331  
  332 + // ========== 各种创建 EmbeddingModel 的方法 ==========
  333 +
  334 + /**
  335 + * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)}
  336 + */
  337 + private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) {
  338 + TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties();
  339 + connectionProperties.setApiKey(apiKey);
  340 + return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties);
  341 + }
  342 +
294 343 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.factory;
  2 +
  3 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import org.springframework.ai.embedding.EmbeddingModel;
  5 +import org.springframework.ai.vectorstore.VectorStore;
  6 +
  7 +// TODO @xin:也放到 AiModelFactory 里面好了,后续改成 AiFactory
  8 +/**
  9 + * AI Vector 模型工厂的接口类
  10 + *
  11 + * @author xiaoxin
  12 + */
  13 +public interface AiVectorStoreFactory {
  14 +
  15 + /**
  16 + * 基于指定配置,获得 VectorStore 对象
  17 + * <p>
  18 + * 如果不存在,则进行创建
  19 + *
  20 + * @param embeddingModel 嵌入模型
  21 + * @param platform 平台
  22 + * @param apiKey API KEY
  23 + * @param url API URL
  24 + * @return VectorStore 对象
  25 + */
  26 + VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url);
  27 +
  28 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.factory;
  2 +
  3 +import cn.hutool.core.lang.Singleton;
  4 +import cn.hutool.core.lang.func.Func0;
  5 +import cn.hutool.core.util.ArrayUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  8 +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
  9 +import org.springframework.ai.embedding.EmbeddingModel;
  10 +import org.springframework.ai.vectorstore.RedisVectorStore;
  11 +import org.springframework.ai.vectorstore.VectorStore;
  12 +import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
  13 +import redis.clients.jedis.JedisPooled;
  14 +
  15 +/**
  16 + * AI Vector 模型工厂的实现类
  17 + * 使用 redisVectorStore 实现 VectorStore
  18 + *
  19 + * @author xiaoxin
  20 + */
  21 +public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory {
  22 +
  23 + @Override
  24 + public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
  25 + String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
  26 + return Singleton.get(cacheKey, (Func0<VectorStore>) () -> {
  27 + // TODO 芋艿 @xin 这两个配置取哪好呢
  28 + // TODO 不同模型的向量维度可能会不一样,目前看貌似是以 index 来做区分的,维度不一样存不到一个 index 上
  29 + // TODO 回复:好的哈
  30 + String index = "default-index";
  31 + String prefix = "default:";
  32 + var config = RedisVectorStore.RedisVectorStoreConfig.builder()
  33 + .withIndexName(index)
  34 + .withPrefix(prefix)
  35 + .build();
  36 + RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
  37 + RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
  38 + new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
  39 + true);
  40 + redisVectorStore.afterPropertiesSet();
  41 + return redisVectorStore;
  42 + });
  43 + }
  44 +
  45 + private static String buildClientCacheKey(Class<?> clazz, Object... params) {
  46 + if (ArrayUtil.isEmpty(params)) {
  47 + return clazz.getName();
  48 + }
  49 + return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
  50 + }
  51 +
  52 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java
1 1 package cn.iocoder.yudao.framework.ai.core.model.deepseek;
2 2  
  3 +import cn.hutool.core.collection.ListUtil;
3 4 import cn.hutool.core.lang.Assert;
4 5 import lombok.extern.slf4j.Slf4j;
5 6 import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
... ... @@ -70,12 +71,12 @@ public class DeepSeekChatModel implements ChatModel {
70 71 OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
71 72 if (chatCompletion == null) {
72 73 log.warn("No chat completion returned for prompt: {}", prompt);
73   - return new ChatResponse(List.of());
  74 + return new ChatResponse(ListUtil.of());
74 75 }
75 76 List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
76 77 if (choices == null) {
77 78 log.warn("No choices returned for prompt: {}", prompt);
78   - return new ChatResponse(List.of());
  79 + return new ChatResponse(ListUtil.of());
79 80 }
80 81  
81 82 // 2. 转换 ChatResponse 返回
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java
1 1 package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
2 2  
  3 +import cn.hutool.core.collection.ListUtil;
3 4 import cn.hutool.core.lang.Assert;
4 5 import lombok.extern.slf4j.Slf4j;
5 6 import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
... ... @@ -72,12 +73,12 @@ public class XingHuoChatModel implements ChatModel {
72 73 OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
73 74 if (chatCompletion == null) {
74 75 log.warn("No chat completion returned for prompt: {}", prompt);
75   - return new ChatResponse(List.of());
  76 + return new ChatResponse(ListUtil.of());
76 77 }
77 78 List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
78 79 if (choices == null) {
79 80 log.warn("No choices returned for prompt: {}", prompt);
80   - return new ChatResponse(List.of());
  81 + return new ChatResponse(ListUtil.of());
81 82 }
82 83  
83 84 // 2. 转换 ChatResponse 返回
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
5 5 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
6 6 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
7 7 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
  8 +import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
8 9 import org.springframework.ai.chat.messages.*;
9 10 import org.springframework.ai.chat.prompt.ChatOptions;
10 11 import org.springframework.ai.ollama.api.OllamaOptions;
... ... @@ -35,6 +36,9 @@ public class AiUtils {
35 36 return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
36 37 case OPENAI:
37 38 return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  39 + case AZURE_OPENAI:
  40 + // TODO 芋艿:貌似没 model 字段???!
  41 + return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
38 42 case OLLAMA:
39 43 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
40 44 default:
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java
... ... @@ -16,6 +16,7 @@
16 16  
17 17 package com.alibaba.cloud.ai.tongyi.audio.transcription;
18 18  
  19 +import cn.hutool.core.collection.ListUtil;
19 20 import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels;
20 21 import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionPrompt;
21 22 import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResponse;
... ... @@ -82,7 +83,7 @@ public class TongYiAudioTranscriptionModel
82 83 try {
83 84 transcriptionParam = TranscriptionParam.builder()
84 85 .model(AudioTranscriptionModels.Paraformer_V1)
85   - .fileUrls(List.of(String.valueOf(instructions.getURL())))
  86 + .fileUrls(ListUtil.of(String.valueOf(instructions.getURL())))
86 87 .build();
87 88 }
88 89 catch (IOException e) {
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java
... ... @@ -16,6 +16,7 @@
16 16  
17 17 package com.alibaba.cloud.ai.tongyi.chat;
18 18  
  19 +import cn.hutool.core.collection.ListUtil;
19 20 import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
20 21 import com.alibaba.dashscope.aigc.conversation.ConversationParam;
21 22 import com.alibaba.dashscope.aigc.generation.Generation;
... ... @@ -207,7 +208,7 @@ public class TongYiChatModel extends
207 208 .getChoices()
208 209 .get(0)
209 210 ));
210   - return new ChatResponse(List.of(gen));
  211 + return new ChatResponse(ListUtil.of(gen));
211 212 })
212 213 )
213 214 .publishOn(Schedulers.parallel());
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java
... ... @@ -16,6 +16,7 @@
16 16  
17 17 package com.alibaba.cloud.ai.tongyi.embedding;
18 18  
  19 +import cn.hutool.core.collection.ListUtil;
19 20 import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
20 21 import com.alibaba.cloud.ai.tongyi.metadata.TongYiTextEmbeddingResponseMetadata;
21 22 import com.alibaba.dashscope.embeddings.TextEmbedding;
... ... @@ -100,7 +101,7 @@ public class TongYiTextEmbeddingModel extends AbstractEmbeddingModel {
100 101  
101 102 return this.call(
102 103 new EmbeddingRequest(
103   - List.of(document.getFormattedContent(this.metadataMode)),
  104 + ListUtil.of(document.getFormattedContent(this.metadataMode)),
104 105 null)
105 106 ).getResults().stream()
106 107 .map(Embedding::getOutput)
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java 0 → 100644
  1 +/*
  2 + * Copyright 2023 - 2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.springframework.ai.autoconfigure.vectorstore.redis;
  17 +
  18 +import org.springframework.ai.embedding.EmbeddingModel;
  19 +import org.springframework.ai.vectorstore.RedisVectorStore;
  20 +import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
  21 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  22 +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
  23 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  24 +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
  25 +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
  26 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  27 +import org.springframework.context.annotation.Bean;
  28 +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
  29 +import redis.clients.jedis.JedisPooled;
  30 +
  31 +/**
  32 + * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突
  33 + *
  34 + * TODO 这个官方,有说啥时候 fix 哇?
  35 + * TODO 看着是列在1.0.0-M2版本
  36 + *
  37 + * @author Christian Tzolov
  38 + * @author Eddú Meléndez
  39 + */
  40 +@AutoConfiguration(after = RedisAutoConfiguration.class)
  41 +@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class})
  42 +@ConditionalOnBean(JedisConnectionFactory.class)
  43 +@EnableConfigurationProperties(RedisVectorStoreProperties.class)
  44 +public class RedisVectorStoreAutoConfiguration {
  45 +
  46 + @Bean
  47 + @ConditionalOnMissingBean
  48 + public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
  49 + JedisConnectionFactory jedisConnectionFactory) {
  50 +
  51 + var config = RedisVectorStoreConfig.builder()
  52 + .withIndexName(properties.getIndex())
  53 + .withPrefix(properties.getPrefix())
  54 + .build();
  55 +
  56 + return new RedisVectorStore(config, embeddingModel,
  57 + new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()),
  58 + properties.isInitializeSchema());
  59 + }
  60 +
  61 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java 0 → 100644
  1 +/*
  2 + * Copyright 2023 - 2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.springframework.ai.vectorstore;
  17 +
  18 +import org.slf4j.Logger;
  19 +import org.slf4j.LoggerFactory;
  20 +import org.springframework.ai.document.Document;
  21 +import org.springframework.ai.embedding.EmbeddingModel;
  22 +import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
  23 +import org.springframework.beans.factory.InitializingBean;
  24 +import org.springframework.util.Assert;
  25 +import org.springframework.util.CollectionUtils;
  26 +import redis.clients.jedis.JedisPooled;
  27 +import redis.clients.jedis.Pipeline;
  28 +import redis.clients.jedis.json.Path2;
  29 +import redis.clients.jedis.search.*;
  30 +import redis.clients.jedis.search.Schema.FieldType;
  31 +import redis.clients.jedis.search.schemafields.*;
  32 +import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
  33 +
  34 +import java.text.MessageFormat;
  35 +import java.util.*;
  36 +import java.util.function.Function;
  37 +import java.util.function.Predicate;
  38 +import java.util.stream.Collectors;
  39 +
  40 +/**
  41 + * The RedisVectorStore is for managing and querying vector data in a Redis database. It
  42 + * offers functionalities like adding, deleting, and performing similarity searches on
  43 + * documents.
  44 + *
  45 + * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
  46 + * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
  47 + * efficient similarity searches. Additionally, it allows for custom metadata fields in
  48 + * the documents to be stored alongside the vector and content data.
  49 + *
  50 + * This class requires a RedisVectorStoreConfig configuration object for initialization,
  51 + * which includes settings like Redis URI, index name, field names, and vector algorithms.
  52 + * It also requires an EmbeddingModel to convert documents into embeddings before storing
  53 + * them.
  54 + *
  55 + * @author Julien Ruaux
  56 + * @author Christian Tzolov
  57 + * @author Eddú Meléndez
  58 + * @see VectorStore
  59 + * @see RedisVectorStoreConfig
  60 + * @see EmbeddingModel
  61 + */
  62 +public class RedisVectorStore implements VectorStore, InitializingBean {
  63 +
  64 + public enum Algorithm {
  65 +
  66 + FLAT, HSNW
  67 +
  68 + }
  69 +
  70 + public record MetadataField(String name, FieldType fieldType) {
  71 +
  72 + public static MetadataField text(String name) {
  73 + return new MetadataField(name, FieldType.TEXT);
  74 + }
  75 +
  76 + public static MetadataField numeric(String name) {
  77 + return new MetadataField(name, FieldType.NUMERIC);
  78 + }
  79 +
  80 + public static MetadataField tag(String name) {
  81 + return new MetadataField(name, FieldType.TAG);
  82 + }
  83 +
  84 + }
  85 +
  86 + /**
  87 + * Configuration for the Redis vector store.
  88 + */
  89 + public static final class RedisVectorStoreConfig {
  90 +
  91 + private final String indexName;
  92 +
  93 + private final String prefix;
  94 +
  95 + private final String contentFieldName;
  96 +
  97 + private final String embeddingFieldName;
  98 +
  99 + private final Algorithm vectorAlgorithm;
  100 +
  101 + private final List<MetadataField> metadataFields;
  102 +
  103 + private RedisVectorStoreConfig() {
  104 + this(builder());
  105 + }
  106 +
  107 + private RedisVectorStoreConfig(Builder builder) {
  108 + this.indexName = builder.indexName;
  109 + this.prefix = builder.prefix;
  110 + this.contentFieldName = builder.contentFieldName;
  111 + this.embeddingFieldName = builder.embeddingFieldName;
  112 + this.vectorAlgorithm = builder.vectorAlgorithm;
  113 + this.metadataFields = builder.metadataFields;
  114 + }
  115 +
  116 + /**
  117 + * Start building a new configuration.
  118 + * @return The entry point for creating a new configuration.
  119 + */
  120 + public static Builder builder() {
  121 +
  122 + return new Builder();
  123 + }
  124 +
  125 + /**
  126 + * {@return the default config}
  127 + */
  128 + public static RedisVectorStoreConfig defaultConfig() {
  129 +
  130 + return builder().build();
  131 + }
  132 +
  133 + public static class Builder {
  134 +
  135 + private String indexName = DEFAULT_INDEX_NAME;
  136 +
  137 + private String prefix = DEFAULT_PREFIX;
  138 +
  139 + private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME;
  140 +
  141 + private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME;
  142 +
  143 + private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM;
  144 +
  145 + private List<MetadataField> metadataFields = new ArrayList<>();
  146 +
  147 + private Builder() {
  148 + }
  149 +
  150 + /**
  151 + * Configures the Redis index name to use.
  152 + * @param name the index name to use
  153 + * @return this builder
  154 + */
  155 + public Builder withIndexName(String name) {
  156 + this.indexName = name;
  157 + return this;
  158 + }
  159 +
  160 + /**
  161 + * Configures the Redis key prefix to use (default: "embedding:").
  162 + * @param prefix the prefix to use
  163 + * @return this builder
  164 + */
  165 + public Builder withPrefix(String prefix) {
  166 + this.prefix = prefix;
  167 + return this;
  168 + }
  169 +
  170 + /**
  171 + * Configures the Redis content field name to use.
  172 + * @param name the content field name to use
  173 + * @return this builder
  174 + */
  175 + public Builder withContentFieldName(String name) {
  176 + this.contentFieldName = name;
  177 + return this;
  178 + }
  179 +
  180 + /**
  181 + * Configures the Redis embedding field name to use.
  182 + * @param name the embedding field name to use
  183 + * @return this builder
  184 + */
  185 + public Builder withEmbeddingFieldName(String name) {
  186 + this.embeddingFieldName = name;
  187 + return this;
  188 + }
  189 +
  190 + /**
  191 + * Configures the Redis vector algorithmto use.
  192 + * @param algorithm the vector algorithm to use
  193 + * @return this builder
  194 + */
  195 + public Builder withVectorAlgorithm(Algorithm algorithm) {
  196 + this.vectorAlgorithm = algorithm;
  197 + return this;
  198 + }
  199 +
  200 + public Builder withMetadataFields(MetadataField... fields) {
  201 + return withMetadataFields(Arrays.asList(fields));
  202 + }
  203 +
  204 + public Builder withMetadataFields(List<MetadataField> fields) {
  205 + this.metadataFields = fields;
  206 + return this;
  207 + }
  208 +
  209 + /**
  210 + * {@return the immutable configuration}
  211 + */
  212 + public RedisVectorStoreConfig build() {
  213 +
  214 + return new RedisVectorStoreConfig(this);
  215 + }
  216 +
  217 + }
  218 +
  219 + }
  220 +
  221 + private final boolean initializeSchema;
  222 +
  223 + public static final String DEFAULT_INDEX_NAME = "spring-ai-index";
  224 +
  225 + public static final String DEFAULT_CONTENT_FIELD_NAME = "content";
  226 +
  227 + public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding";
  228 +
  229 + public static final String DEFAULT_PREFIX = "embedding:";
  230 +
  231 + public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW;
  232 +
  233 + private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]";
  234 +
  235 + private static final Path2 JSON_SET_PATH = Path2.of("$");
  236 +
  237 + private static final String JSON_PATH_PREFIX = "$.";
  238 +
  239 + private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class);
  240 +
  241 + private static final Predicate<Object> RESPONSE_OK = Predicate.isEqual("OK");
  242 +
  243 + private static final Predicate<Object> RESPONSE_DEL_OK = Predicate.isEqual(1l);
  244 +
  245 + private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32";
  246 +
  247 + private static final String EMBEDDING_PARAM_NAME = "BLOB";
  248 +
  249 + public static final String DISTANCE_FIELD_NAME = "vector_score";
  250 +
  251 + private static final String DEFAULT_DISTANCE_METRIC = "COSINE";
  252 +
  253 + private final JedisPooled jedis;
  254 +
  255 + private final EmbeddingModel embeddingModel;
  256 +
  257 + private final RedisVectorStoreConfig config;
  258 +
  259 + private FilterExpressionConverter filterExpressionConverter;
  260 +
  261 + public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
  262 + boolean initializeSchema) {
  263 +
  264 + Assert.notNull(config, "Config must not be null");
  265 + Assert.notNull(embeddingModel, "Embedding model must not be null");
  266 + this.initializeSchema = initializeSchema;
  267 +
  268 + this.jedis = jedis;
  269 + this.embeddingModel = embeddingModel;
  270 + this.config = config;
  271 + this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields);
  272 + }
  273 +
  274 + public JedisPooled getJedis() {
  275 + return this.jedis;
  276 + }
  277 +
  278 + @Override
  279 + public void add(List<Document> documents) {
  280 + try (Pipeline pipeline = this.jedis.pipelined()) {
  281 + for (Document document : documents) {
  282 + var embedding = this.embeddingModel.embed(document);
  283 + document.setEmbedding(embedding);
  284 +
  285 + var fields = new HashMap<String, Object>();
  286 + fields.put(this.config.embeddingFieldName, embedding);
  287 + fields.put(this.config.contentFieldName, document.getContent());
  288 + fields.putAll(document.getMetadata());
  289 + pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
  290 + }
  291 + List<Object> responses = pipeline.syncAndReturnAll();
  292 + Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny();
  293 + if (errResponse.isPresent()) {
  294 + String message = MessageFormat.format("Could not add document: {0}", errResponse.get());
  295 + if (logger.isErrorEnabled()) {
  296 + logger.error(message);
  297 + }
  298 + throw new RuntimeException(message);
  299 + }
  300 + }
  301 + }
  302 +
  303 + private String key(String id) {
  304 + return this.config.prefix + id;
  305 + }
  306 +
  307 + @Override
  308 + public Optional<Boolean> delete(List<String> idList) {
  309 + try (Pipeline pipeline = this.jedis.pipelined()) {
  310 + for (String id : idList) {
  311 + pipeline.jsonDel(key(id));
  312 + }
  313 + List<Object> responses = pipeline.syncAndReturnAll();
  314 + Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny();
  315 + if (errResponse.isPresent()) {
  316 + if (logger.isErrorEnabled()) {
  317 + logger.error("Could not delete document: {}", errResponse.get());
  318 + }
  319 + return Optional.of(false);
  320 + }
  321 + return Optional.of(true);
  322 + }
  323 + }
  324 +
  325 + @Override
  326 + public List<Document> similaritySearch(SearchRequest request) {
  327 +
  328 + Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero");
  329 + Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1,
  330 + "The similarity score is bounded between 0 and 1; least to most similar respectively.");
  331 +
  332 + String filter = nativeExpressionFilter(request);
  333 +
  334 + String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName,
  335 + EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME);
  336 +
  337 + List<String> returnFields = new ArrayList<>();
  338 + this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
  339 + returnFields.add(this.config.embeddingFieldName);
  340 + returnFields.add(this.config.contentFieldName);
  341 + returnFields.add(DISTANCE_FIELD_NAME);
  342 + var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery()));
  343 + Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding))
  344 + .returnFields(returnFields.toArray(new String[0]))
  345 + .setSortBy(DISTANCE_FIELD_NAME, true)
  346 + .dialect(2);
  347 +
  348 + SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
  349 + return result.getDocuments()
  350 + .stream()
  351 + .filter(d -> similarityScore(d) >= request.getSimilarityThreshold())
  352 + .map(this::toDocument)
  353 + .toList();
  354 + }
  355 +
  356 + private Document toDocument(redis.clients.jedis.search.Document doc) {
  357 + var id = doc.getId().substring(this.config.prefix.length());
  358 + var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName)
  359 + : null;
  360 + Map<String, Object> metadata = this.config.metadataFields.stream()
  361 + .map(MetadataField::name)
  362 + .filter(doc::hasProperty)
  363 + .collect(Collectors.toMap(Function.identity(), doc::getString));
  364 + metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc));
  365 + return new Document(id, content, metadata);
  366 + }
  367 +
  368 + private float similarityScore(redis.clients.jedis.search.Document doc) {
  369 + return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2;
  370 + }
  371 +
  372 + private String nativeExpressionFilter(SearchRequest request) {
  373 + if (request.getFilterExpression() == null) {
  374 + return "*";
  375 + }
  376 + return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")";
  377 + }
  378 +
  379 + @Override
  380 + public void afterPropertiesSet() {
  381 +
  382 + if (!this.initializeSchema) {
  383 + return;
  384 + }
  385 +
  386 + // If index already exists don't do anything
  387 + if (this.jedis.ftList().contains(this.config.indexName)) {
  388 + return;
  389 + }
  390 +
  391 + String response = this.jedis.ftCreate(this.config.indexName,
  392 + FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields());
  393 + if (!RESPONSE_OK.test(response)) {
  394 + String message = MessageFormat.format("Could not create index: {0}", response);
  395 + throw new RuntimeException(message);
  396 + }
  397 + }
  398 +
  399 + private Iterable<SchemaField> schemaFields() {
  400 + Map<String, Object> vectorAttrs = new HashMap<>();
  401 + vectorAttrs.put("DIM", this.embeddingModel.dimensions());
  402 + vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC);
  403 + vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32);
  404 + List<SchemaField> fields = new ArrayList<>();
  405 + fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0));
  406 + fields.add(VectorField.builder()
  407 + .fieldName(jsonPath(this.config.embeddingFieldName))
  408 + .algorithm(vectorAlgorithm())
  409 + .attributes(vectorAttrs)
  410 + .as(this.config.embeddingFieldName)
  411 + .build());
  412 +
  413 + if (!CollectionUtils.isEmpty(this.config.metadataFields)) {
  414 + for (MetadataField field : this.config.metadataFields) {
  415 + fields.add(schemaField(field));
  416 + }
  417 + }
  418 + return fields;
  419 + }
  420 +
  421 + private SchemaField schemaField(MetadataField field) {
  422 + String fieldName = jsonPath(field.name);
  423 + switch (field.fieldType) {
  424 + case NUMERIC:
  425 + return NumericField.of(fieldName).as(field.name);
  426 + case TAG:
  427 + return TagField.of(fieldName).as(field.name);
  428 + case TEXT:
  429 + return TextField.of(fieldName).as(field.name);
  430 + default:
  431 + throw new IllegalArgumentException(
  432 + MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType));
  433 + }
  434 + }
  435 +
  436 + private VectorAlgorithm vectorAlgorithm() {
  437 + if (config.vectorAlgorithm == Algorithm.HSNW) {
  438 + return VectorAlgorithm.HNSW;
  439 + }
  440 + return VectorAlgorithm.FLAT;
  441 + }
  442 +
  443 + private String jsonPath(String field) {
  444 + return JSON_PATH_PREFIX + field;
  445 + }
  446 +
  447 + private static float[] toFloatArray(List<Double> embeddingDouble) {
  448 + float[] embeddingFloat = new float[embeddingDouble.size()];
  449 + int i = 0;
  450 + for (Double d : embeddingDouble) {
  451 + embeddingFloat[i++] = d.floatValue();
  452 + }
  453 + return embeddingFloat;
  454 + }
  455 +
  456 +}
0 457 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.chat;
  2 +
  3 +import com.azure.ai.openai.OpenAIClient;
  4 +import com.azure.ai.openai.OpenAIClientBuilder;
  5 +import com.azure.core.credential.AzureKeyCredential;
  6 +import com.azure.core.util.ClientOptions;
  7 +import org.junit.jupiter.api.Disabled;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
  10 +import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
  11 +import org.springframework.ai.chat.messages.Message;
  12 +import org.springframework.ai.chat.messages.SystemMessage;
  13 +import org.springframework.ai.chat.messages.UserMessage;
  14 +import org.springframework.ai.chat.model.ChatResponse;
  15 +import org.springframework.ai.chat.prompt.Prompt;
  16 +import reactor.core.publisher.Flux;
  17 +
  18 +import java.util.ArrayList;
  19 +import java.util.List;
  20 +
  21 +import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
  22 +
  23 +/**
  24 + * {@link AzureOpenAiChatModel} 集成测试
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +public class AzureOpenAIChatModelTests {
  29 +
  30 + private final OpenAIClient openAiApi = (new OpenAIClientBuilder())
  31 + .endpoint("https://eastusprejade.openai.azure.com")
  32 + .credential(new AzureKeyCredential("xxx"))
  33 + .clientOptions((new ClientOptions()).setApplicationId("spring-ai"))
  34 + .buildClient();
  35 + private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi,
  36 + AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
  37 +
  38 + @Test
  39 + @Disabled
  40 + public void testCall() {
  41 + // 准备参数
  42 + List<Message> messages = new ArrayList<>();
  43 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  44 + messages.add(new UserMessage("1 + 1 = ?"));
  45 +
  46 + // 调用
  47 + ChatResponse response = chatModel.call(new Prompt(messages));
  48 + // 打印结果
  49 + System.out.println(response);
  50 + System.out.println(response.getResult().getOutput());
  51 + }
  52 +
  53 + @Test
  54 + @Disabled
  55 + public void testStream() {
  56 + // 准备参数
  57 + List<Message> messages = new ArrayList<>();
  58 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  59 + messages.add(new UserMessage("1 + 1 = ?"));
  60 +
  61 + // 调用
  62 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
  63 + // 打印结果
  64 + flux.doOnNext(response -> {
  65 +// System.out.println(response);
  66 + System.out.println(response.getResult().getOutput());
  67 + }).then().block();
  68 + }
  69 +
  70 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java
1 1 package cn.iocoder.yudao.framework.ai.chat;
2 2  
3   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
4 3 import org.junit.jupiter.api.Disabled;
5 4 import org.junit.jupiter.api.Test;
6 5 import org.springframework.ai.chat.messages.Message;
... ... @@ -17,7 +16,7 @@ import java.util.ArrayList;
17 16 import java.util.List;
18 17  
19 18 /**
20   - * {@link XingHuoChatModel} 集成测试
  19 + * {@link OpenAiChatModel} 集成测试
21 20 *
22 21 * @author 芋道源码
23 22 */
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java
1 1 package cn.iocoder.yudao.framework.ai.music;
2 2  
  3 +import cn.hutool.core.collection.ListUtil;
3 4 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
4 5 import org.junit.jupiter.api.Disabled;
5 6 import org.junit.jupiter.api.Test;
... ... @@ -66,7 +67,7 @@ public class SunoApiTests {
66 67 String id = "584729e5-0fe9-4157-86da-1b4803ff42bf";
67 68  
68 69 // 调用方法
69   - List<SunoApi.MusicData> musicList = sunoApi.getMusicList(List.of(id));
  70 + List<SunoApi.MusicData> musicList = sunoApi.getMusicList(ListUtil.of(id));
70 71 // 打印结果
71 72 System.out.println(musicList);
72 73 }
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
... ... @@ -11,6 +11,8 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModel
11 11 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
12 12 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
13 13 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
  14 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  15 +import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
14 16 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
15 17 import org.flowable.common.engine.impl.db.SuspensionState;
16 18 import org.flowable.engine.repository.Deployment;
... ... @@ -52,7 +54,7 @@ public interface BpmModelConvert {
52 54 BpmModelMetaInfoVO metaInfo = buildMetaInfo(model);
53 55 BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null);
54 56 if (ArrayUtil.isNotEmpty(bpmnBytes)) {
55   - modelVO.setBpmnXml(new String(bpmnBytes));
  57 + modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes));
56 58 }
57 59 return modelVO;
58 60 }
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
1 1 package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4   -import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
5 4 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
6 5 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
  6 +import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
7 7 import lombok.Setter;
8 8 import org.flowable.bpmn.model.Activity;
9 9 import org.flowable.engine.delegate.DelegateExecution;
... ... @@ -50,14 +50,18 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
50 50 super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
51 51  
52 52 // 第二步,获取任务的所有处理人
53   - Set<Long> assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
54   - if (CollUtil.isEmpty(assigneeUserIds)) {
55   - // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
56   - // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
57   - // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
58   - assigneeUserIds = SetUtils.asSet((Long) null);
  53 + @SuppressWarnings("unchecked")
  54 + Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
  55 + if (assigneeUserIds == null) {
  56 + assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
  57 + execution.setVariable(super.collectionVariable, assigneeUserIds);
  58 + if (CollUtil.isEmpty(assigneeUserIds)) {
  59 + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
  60 + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
  61 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
  62 + assigneeUserIds = SetUtils.asSet((Long) null);
  63 + }
59 64 }
60   - execution.setVariable(super.collectionVariable, assigneeUserIds);
61 65 return assigneeUserIds.size();
62 66 }
63 67  
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
... ... @@ -43,14 +43,18 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
43 43 super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
44 44  
45 45 // 第二步,获取任务的所有处理人
46   - Set<Long> assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
47   - if (CollUtil.isEmpty(assigneeUserIds)) {
48   - // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
49   - // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
50   - // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
51   - assigneeUserIds = SetUtils.asSet((Long) null);
  46 + @SuppressWarnings("unchecked")
  47 + Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
  48 + if (assigneeUserIds == null) {
  49 + assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
  50 + execution.setVariable(super.collectionVariable, assigneeUserIds);
  51 + if (CollUtil.isEmpty(assigneeUserIds)) {
  52 + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
  53 + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
  54 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时
  55 + assigneeUserIds = SetUtils.asSet((Long) null);
  56 + }
52 57 }
53   - execution.setVariable(super.collectionVariable, assigneeUserIds);
54 58 return assigneeUserIds.size();
55 59 }
56 60  
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
... ... @@ -211,7 +211,14 @@ public class BpmnModelUtils {
211 211 return null;
212 212 }
213 213 BpmnXMLConverter converter = new BpmnXMLConverter();
214   - return new String(converter.convertToXML(model));
  214 + return StrUtil.utf8Str(converter.convertToXML(model));
  215 + }
  216 +
  217 + public static String getBpmnXml(byte[] bpmnBytes) {
  218 + if (ArrayUtil.isEmpty(bpmnBytes)) {
  219 + return null;
  220 + }
  221 + return StrUtil.utf8Str(bpmnBytes);
215 222 }
216 223  
217 224 // ========== 遍历相关的方法 ==========
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
... ... @@ -65,6 +65,7 @@ public class BpmModelServiceImpl implements BpmModelService {
65 65 @Override
66 66 public PageResult<Model> getModelPage(BpmModelPageReqVO pageVO) {
67 67 ModelQuery modelQuery = repositoryService.createModelQuery();
  68 + modelQuery.modelTenantId(FlowableUtils.getTenantId());
68 69 if (StrUtil.isNotBlank(pageVO.getKey())) {
69 70 modelQuery.modelKey(pageVO.getKey());
70 71 }
... ... @@ -80,7 +81,6 @@ public class BpmModelServiceImpl implements BpmModelService {
80 81 return PageResult.empty(count);
81 82 }
82 83 List<Model> models = modelQuery
83   - .modelTenantId(FlowableUtils.getTenantId())
84 84 .orderByCreateTime().desc()
85 85 .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
86 86 return new PageResult<>(models, count);
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
... ... @@ -106,7 +106,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
106 106 }
107 107 if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
108 108 taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
109   - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1]));
  109 + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
110 110 }
111 111 long count = taskQuery.count();
112 112 if (count == 0) {
... ... @@ -128,7 +128,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
128 128 }
129 129 if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
130 130 taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
131   - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1]));
  131 + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
132 132 }
133 133 // 执行查询
134 134 long count = taskQuery.count();
... ... @@ -150,7 +150,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
150 150 }
151 151 if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
152 152 taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
153   - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[1]));
  153 + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
154 154 }
155 155 // 执行查询
156 156 long count = taskQuery.count();
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
... ... @@ -92,7 +92,7 @@ public interface CrmReceivableMapper extends BaseMapperX&lt;CrmReceivableDO&gt; {
92 92 List<Map<String, Object>> result = selectMaps(new QueryWrapper<CrmReceivableDO>()
93 93 .select("contract_id, SUM(price) AS total_price")
94 94 .in("audit_status", CrmAuditStatusEnum.DRAFT.getStatus(), // 草稿 + 审批中 + 审批通过
95   - CrmAuditStatusEnum.PROCESS, CrmAuditStatusEnum.APPROVE.getStatus())
  95 + CrmAuditStatusEnum.PROCESS.getStatus(), CrmAuditStatusEnum.APPROVE.getStatus())
96 96 .groupBy("contract_id")
97 97 .in("contract_id", contractIds));
98 98 // 获得金额
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
1 1 package cn.iocoder.yudao.module.crm.service.statistics;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.collection.ListUtil;
4 5 import cn.hutool.core.util.ObjUtil;
5 6 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
6 7 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
... ... @@ -106,7 +107,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
106 107 private List<Long> getUserIds(CrmStatisticsPerformanceReqVO reqVO) {
107 108 // 情况一:选中某个用户
108 109 if (ObjUtil.isNotNull(reqVO.getUserId())) {
109   - return List.of(reqVO.getUserId());
  110 + return ListUtil.of(reqVO.getUserId());
110 111 }
111 112 // 情况二:选中某个部门
112 113 // 2.1 获得部门列表
... ...
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
3 3 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
4 4  
5 5 import jakarta.validation.Valid;
  6 +import org.springframework.scheduling.annotation.Async;
6 7  
7 8 /**
8 9 * API 访问日志的 API 接口
... ... @@ -18,4 +19,14 @@ public interface ApiAccessLogApi {
18 19 */
19 20 void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO);
20 21  
  22 + /**
  23 + * 【异步】创建 API 访问日志
  24 + *
  25 + * @param createDTO 访问日志 DTO
  26 + */
  27 + @Async
  28 + default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
  29 + createApiAccessLog(createDTO);
  30 + }
  31 +
21 32 }
... ...
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
3 3 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
4 4  
5 5 import jakarta.validation.Valid;
  6 +import org.springframework.scheduling.annotation.Async;
6 7  
7 8 /**
8 9 * API 错误日志的 API 接口
... ... @@ -18,4 +19,14 @@ public interface ApiErrorLogApi {
18 19 */
19 20 void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO);
20 21  
  22 + /**
  23 + * 【异步】创建 API 异常日志
  24 + *
  25 + * @param createDTO 异常日志 DTO
  26 + */
  27 + @Async
  28 + default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
  29 + createApiErrorLog(createDTO);
  30 + }
  31 +
21 32 }
... ...
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
... ... @@ -22,7 +22,7 @@ public interface ErrorCodeConstants {
22 22 ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改");
23 23 ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改");
24 24 ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确");
25   - ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在");
  25 + ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在,注意 Bean 默认首字母小写");
26 26 ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口");
27 27  
28 28 // ========== API 错误日志 1-001-002-000 ==========
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
... ... @@ -14,7 +14,6 @@ public enum CodegenFrontTypeEnum {
14 14  
15 15 VUE2(10), // Vue2 Element UI 标准模版
16 16 VUE3(20), // Vue3 Element Plus 标准模版
17   - VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版
18 17 VUE3_VBEN(30), // Vue3 VBEN 模版
19 18 ;
20 19  
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
... ... @@ -91,7 +91,7 @@ public class S3FileClient extends AbstractFileClient&lt;S3FileClientConfig&gt; {
91 91 * 开启 VirtualStyle 模式
92 92 */
93 93 private void enableVirtualStyleEndpoint() {
94   - if (StrUtil.containsAll(config.getEndpoint(),
  94 + if (StrUtil.containsAny(config.getEndpoint(),
95 95 S3FileClientConfig.ENDPOINT_TENCENT, // 腾讯云 https://cloud.tencent.com/document/product/436/41284
96 96 S3FileClientConfig.ENDPOINT_VOLCES)) { // 火山云 https://www.volcengine.com/docs/6349/1288493
97 97 client.enableVirtualStyleEndpoint();
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
... ... @@ -135,15 +135,6 @@ public class CodegenEngine {
135 135 vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
136 136 .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"),
137 137 vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
138   - // Vue3 Schema 模版
139   - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"),
140   - vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
141   - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"),
142   - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
143   - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"),
144   - vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
145   - .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"),
146   - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
147 138 // Vue3 vben 模版
148 139 .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
149 140 vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
... ... @@ -496,10 +487,6 @@ public class CodegenEngine {
496 487 "src/" + path;
497 488 }
498 489  
499   - private static String vue3SchemaTemplatePath(String path) {
500   - return "codegen/vue3_schema/" + path + ".vm";
501   - }
502   -
503 490 private static String vue3VbenTemplatePath(String path) {
504 491 return "codegen/vue3_vben/" + path + ".vm";
505 492 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java
... ... @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
14 14 import jakarta.annotation.Resource;
15 15 import lombok.extern.slf4j.Slf4j;
16 16 import org.quartz.SchedulerException;
  17 +import org.springframework.beans.factory.NoSuchBeanDefinitionException;
17 18 import org.springframework.stereotype.Service;
18 19 import org.springframework.transaction.annotation.Transactional;
19 20 import org.springframework.validation.annotation.Validated;
... ... @@ -91,13 +92,15 @@ public class JobServiceImpl implements JobService {
91 92 }
92 93  
93 94 private void validateJobHandlerExists(String handlerName) {
94   - Object handler = SpringUtil.getBean(handlerName);
95   - if (handler == null) {
  95 + try {
  96 + Object handler = SpringUtil.getBean(handlerName);
  97 + assert handler != null;
  98 + if (!(handler instanceof JobHandler)) {
  99 + throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
  100 + }
  101 + } catch (NoSuchBeanDefinitionException e) {
96 102 throw exception(JOB_HANDLER_BEAN_NOT_EXISTS);
97 103 }
98   - if (!(handler instanceof JobHandler)) {
99   - throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
100   - }
101 104 }
102 105  
103 106 @Override
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm
... ... @@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
286 286 // 校验存在
287 287 validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
288 288 // 更新
  289 + ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
289 290 ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
290 291 }
291 292  
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
... ... @@ -64,12 +64,11 @@
64 64 <el-checkbox
65 65 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
66 66 :key="dict.value"
67   - :label="dict.value"
68   - >
69   - {{ dict.label }}
70   - </el-checkbox>
  67 + :label="dict.label"
  68 + :value="dict.value"
  69 + />
71 70 #else##没数据字典
72   - <el-checkbox>请选择字典生成</el-checkbox>
  71 + <el-checkbox label="请选择字典生成" />
73 72 #end
74 73 </el-checkbox-group>
75 74 </el-form-item>
... ... @@ -85,7 +84,7 @@
85 84 {{ dict.label }}
86 85 </el-radio>
87 86 #else##没数据字典
88   - <el-radio label="1">请选择字典生成</el-radio>
  87 + <el-radio value="1">请选择字典生成</el-radio>
89 88 #end
90 89 </el-radio-group>
91 90 </el-form-item>
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
... ... @@ -92,12 +92,11 @@
92 92 <el-checkbox
93 93 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
94 94 :key="dict.value"
95   - :label="dict.value"
96   - >
97   - {{ dict.label }}
98   - </el-checkbox>
  95 + :label="dict.label"
  96 + :value="dict.value"
  97 + />
99 98 #else##没数据字典
100   - <el-checkbox>请选择字典生成</el-checkbox>
  99 + <el-checkbox label="请选择字典生成" />
101 100 #end
102 101 </el-checkbox-group>
103 102 </el-form-item>
... ... @@ -117,7 +116,7 @@
117 116 {{ dict.label }}
118 117 </el-radio>
119 118 #else##没数据字典
120   - <el-radio label="1">请选择字典生成</el-radio>
  119 + <el-radio value="1">请选择字典生成</el-radio>
121 120 #end
122 121 </el-radio-group>
123 122 </el-form-item>
... ... @@ -219,12 +218,11 @@
219 218 <el-checkbox
220 219 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
221 220 :key="dict.value"
222   - :label="dict.value"
223   - >
224   - {{ dict.label }}
225   - </el-checkbox>
  221 + :label="dict.label"
  222 + :value="dict.value"
  223 + />
226 224 #else##没数据字典
227   - <el-checkbox>请选择字典生成</el-checkbox>
  225 + <el-checkbox label="请选择字典生成" />
228 226 #end
229 227 </el-checkbox-group>
230 228 </el-form-item>
... ... @@ -240,7 +238,7 @@
240 238 {{ dict.label }}
241 239 </el-radio>
242 240 #else##没数据字典
243   - <el-radio label="1">请选择字典生成</el-radio>
  241 + <el-radio value="1">请选择字典生成</el-radio>
244 242 #end
245 243 </el-radio-group>
246 244 </el-form-item>
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
... ... @@ -75,12 +75,11 @@
75 75 <el-checkbox
76 76 v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
77 77 :key="dict.value"
78   - :label="dict.value"
79   - >
80   - {{ dict.label }}
81   - </el-checkbox>
  78 + :label="dict.label"
  79 + :value="dict.value"
  80 + />
82 81 #else##没数据字典
83   - <el-checkbox>请选择字典生成</el-checkbox>
  82 + <el-checkbox label="请选择字典生成" />
84 83 #end
85 84 </el-checkbox-group>
86 85 </el-form-item>
... ... @@ -96,7 +95,7 @@
96 95 {{ dict.label }}
97 96 </el-radio>
98 97 #else##没数据字典
99   - <el-radio label="1">请选择字典生成</el-radio>
  98 + <el-radio value="1">请选择字典生成</el-radio>
100 99 #end
101 100 </el-radio-group>
102 101 </el-form-item>
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm deleted
1   -import request from '@/config/axios'
2   -#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
3   -
4   -export interface ${simpleClassName}VO {
5   - #foreach ($column in $columns)
6   - #if ($column.createOperation || $column.updateOperation)
7   - #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
8   - ${column.javaField}: number
9   - #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime")
10   - ${column.javaField}: Date
11   - #else
12   - ${column.javaField}: ${column.javaType.toLowerCase()}
13   - #end
14   - #end
15   - #end
16   -}
17   -
18   -// 查询${table.classComment}列表
19   -export const get${simpleClassName}Page = async (params) => {
20   - return await request.get({ url: '${baseURL}/page', params })
21   -}
22   -
23   -// 查询${table.classComment}详情
24   -export const get${simpleClassName} = async (id: number) => {
25   - return await request.get({ url: '${baseURL}/get?id=' + id })
26   -}
27   -
28   -// 新增${table.classComment}
29   -export const create${simpleClassName} = async (data: ${simpleClassName}VO) => {
30   - return await request.post({ url: '${baseURL}/create', data })
31   -}
32   -
33   -// 修改${table.classComment}
34   -export const update${simpleClassName} = async (data: ${simpleClassName}VO) => {
35   - return await request.put({ url: '${baseURL}/update', data })
36   -}
37   -
38   -// 删除${table.classComment}
39   -export const delete${simpleClassName} = async (id: number) => {
40   - return await request.delete({ url: '${baseURL}/delete?id=' + id })
41   -}
42   -
43   -// 导出${table.classComment} Excel
44   -export const export${simpleClassName}Api = async (params) => {
45   - return await request.download({ url: '${baseURL}/export-excel', params })
46   -}
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm deleted
1   -import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
2   -import { dateFormatter } from '@/utils/formatTime'
3   -
4   -// 表单校验
5   -export const rules = reactive({
6   -#foreach ($column in $columns)
7   -#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
8   -#set($comment=$column.columnComment)
9   - $column.javaField: [required],
10   -#end
11   -#end
12   -})
13   -
14   -// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
15   -const crudSchemas = reactive<CrudSchema[]>([
16   -#foreach($column in $columns)
17   -#if ($column.listOperation || $column.listOperationResult || $column.createOperation || $column.updateOperation)
18   -#set ($dictType = $column.dictType)
19   -#set ($javaField = $column.javaField)
20   -#set ($javaType = $column.javaType)
21   - {
22   - label: '${column.columnComment}',
23   - field: '${column.javaField}',
24   -## ========= 字典部分 =========
25   - #if ("" != $dictType)## 有数据字典
26   - dictType: DICT_TYPE.$dictType.toUpperCase(),
27   - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
28   - dictClass: 'number',
29   - #elseif ($javaType == "String")
30   - dictClass: 'string',
31   - #elseif ($javaType == "Boolean")
32   - dictClass: 'boolean',
33   - #end
34   - #end
35   -## ========= Table 表格部分 =========
36   - #if (!$column.listOperationResult)
37   - isTable: false,
38   - #else
39   - #if ($column.htmlType == "datetime")
40   - formatter: dateFormatter,
41   - #end
42   - #end
43   -## ========= Search 表格部分 =========
44   - #if ($column.listOperation)
45   - isSearch: true,
46   - #if ($column.htmlType == "datetime")
47   - search: {
48   - component: 'DatePicker',
49   - componentProps: {
50   - valueFormat: 'YYYY-MM-DD HH:mm:ss',
51   - type: 'daterange',
52   - defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
53   - }
54   - },
55   - #end
56   - #end
57   -## ========= Form 表单部分 =========
58   - #if ((!$column.createOperation && !$column.updateOperation) || $column.primaryKey)
59   - isForm: false,
60   - #else
61   - #if($column.htmlType == "imageUpload")## 图片上传
62   - form: {
63   - component: 'UploadImg'
64   - },
65   - #elseif($column.htmlType == "fileUpload")## 文件上传
66   - form: {
67   - component: 'UploadFile'
68   - },
69   - #elseif($column.htmlType == "editor")## 文本编辑器
70   - form: {
71   - component: 'Editor',
72   - componentProps: {
73   - valueHtml: '',
74   - height: 200
75   - }
76   - },
77   - #elseif($column.htmlType == "select")## 下拉框
78   - form: {
79   - component: 'SelectV2'
80   - },
81   - #elseif($column.htmlType == "checkbox")## 多选框
82   - form: {
83   - component: 'Checkbox'
84   - },
85   - #elseif($column.htmlType == "radio")## 单选框
86   - form: {
87   - component: 'Radio'
88   - },
89   - #elseif($column.htmlType == "datetime")## 时间框
90   - form: {
91   - component: 'DatePicker',
92   - componentProps: {
93   - type: 'datetime',
94   - valueFormat: 'x'
95   - }
96   - },
97   - #elseif($column.htmlType == "textarea")## 文本框
98   - form: {
99   - component: 'Input',
100   - componentProps: {
101   - type: 'textarea',
102   - rows: 4
103   - },
104   - colProps: {
105   - span: 24
106   - }
107   - },
108   - #elseif(${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer")## 文本框
109   - form: {
110   - component: 'InputNumber',
111   - value: 0
112   - },
113   - #end
114   - #end
115   - },
116   -#end
117   -#end
118   - {
119   - label: '操作',
120   - field: 'action',
121   - isForm: false
122   - }
123   -])
124   -export const { allSchemas } = useCrudSchemas(crudSchemas)
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm deleted
1   -<template>
2   - <Dialog :title="dialogTitle" v-model="dialogVisible">
3   - <Form ref="formRef" :schema="allSchemas.formSchema" :rules="rules" v-loading="formLoading" />
4   - <template #footer>
5   - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
6   - <el-button @click="dialogVisible = false">取 消</el-button>
7   - </template>
8   - </Dialog>
9   -</template>
10   -<script setup lang="ts">
11   -import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
12   -import { rules, allSchemas } from './${classNameVar}.data'
13   -const { t } = useI18n() // 国际化
14   -const message = useMessage() // 消息弹窗
15   -
16   -const dialogVisible = ref(false) // 弹窗的是否展示
17   -const dialogTitle = ref('') // 弹窗的标题
18   -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
19   -const formType = ref('') // 表单的类型:create - 新增;update - 修改
20   -const formRef = ref() // 表单 Ref
21   -
22   -/** 打开弹窗 */
23   -const open = async (type: string, id?: number) => {
24   - dialogVisible.value = true
25   - dialogTitle.value = t('action.' + type)
26   - formType.value = type
27   - // 修改时,设置数据
28   - if (id) {
29   - formLoading.value = true
30   - try {
31   - const data = await ${simpleClassName}Api.get${simpleClassName}(id)
32   - formRef.value.setValues(data)
33   - } finally {
34   - formLoading.value = false
35   - }
36   - }
37   -}
38   -defineExpose({ open }) // 提供 open 方法,用于打开弹窗
39   -
40   -/** 提交表单 */
41   -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
42   -const submitForm = async () => {
43   - // 校验表单
44   - if (!formRef) return
45   - const valid = await formRef.value.getElFormRef().validate()
46   - if (!valid) return
47   - // 提交请求
48   - formLoading.value = true
49   - try {
50   - const data = formRef.value.formModel as ${simpleClassName}Api.${simpleClassName}VO
51   - if (formType.value === 'create') {
52   - await ${simpleClassName}Api.create${simpleClassName}(data)
53   - message.success(t('common.createSuccess'))
54   - } else {
55   - await ${simpleClassName}Api.update${simpleClassName}(data)
56   - message.success(t('common.updateSuccess'))
57   - }
58   - dialogVisible.value = false
59   - // 发送操作成功的事件
60   - emit('success')
61   - } finally {
62   - formLoading.value = false
63   - }
64   -}
65   -</script>
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm deleted
1   -<template>
2   - <!-- 搜索工作栏 -->
3   - <ContentWrap>
4   - <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams">
5   - <!-- 新增等操作按钮 -->
6   - <template #actionMore>
7   - <el-button
8   - type="primary"
9   - plain
10   - @click="openForm('create')"
11   - v-hasPermi="['${permissionPrefix}:create']"
12   - >
13   - <Icon icon="ep:plus" class="mr-5px" /> 新增
14   - </el-button>
15   - </template>
16   - </Search>
17   - </ContentWrap>
18   -
19   - <!-- 列表 -->
20   - <ContentWrap>
21   - <Table
22   - :columns="allSchemas.tableColumns"
23   - :data="tableObject.tableList"
24   - :loading="tableObject.loading"
25   - :pagination="{
26   - total: tableObject.total
27   - }"
28   - v-model:pageSize="tableObject.pageSize"
29   - v-model:currentPage="tableObject.currentPage"
30   - >
31   - <template #action="{ row }">
32   - <el-button
33   - link
34   - type="primary"
35   - @click="openForm('update', row.id)"
36   - v-hasPermi="['${permissionPrefix}:update']"
37   - >
38   - 编辑
39   - </el-button>
40   - <el-button
41   - link
42   - type="danger"
43   - v-hasPermi="['${permissionPrefix}:delete']"
44   - @click="handleDelete(row.id)"
45   - >
46   - 删除
47   - </el-button>
48   - </template>
49   - </Table>
50   - </ContentWrap>
51   -
52   - <!-- 表单弹窗:添加/修改 -->
53   - <${simpleClassName}Form ref="formRef" @success="getList" />
54   -</template>
55   -<script setup lang="ts" name="${table.className}">
56   -import { allSchemas } from './${classNameVar}.data'
57   -import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
58   -import ${simpleClassName}Form from './${simpleClassName}Form.vue'
59   -
60   -// tableObject:表格的属性对象,可获得分页大小、条数等属性
61   -// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
62   -// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
63   -const { tableObject, tableMethods } = useTable({
64   - getListApi: ${simpleClassName}Api.get${simpleClassName}Page, // 分页接口
65   - delListApi: ${simpleClassName}Api.delete${simpleClassName} // 删除接口
66   -})
67   -// 获得表格的各种操作
68   -const { getList, setSearchParams } = tableMethods
69   -
70   -/** 添加/修改操作 */
71   -const formRef = ref()
72   -const openForm = (type: string, id?: number) => {
73   - formRef.value.open(type, id)
74   -}
75   -
76   -/** 删除按钮操作 */
77   -const handleDelete = (id: number) => {
78   - tableMethods.delList(id, false)
79   -}
80   -
81   -/** 初始化 **/
82   -onMounted(() => {
83   - getList()
84   -})
85   -</script>
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm
... ... @@ -42,9 +42,17 @@ export const searchFormSchema: FormSchema[] = [
42 42 #foreach($column in $columns)
43 43 #if ($column.listOperation)
44 44 #set ($dictType=$column.dictType)
  45 + #set ($javaType = $column.javaType)
45 46 #set ($javaField = $column.javaField)
46 47 #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
47 48 #set ($comment=$column.columnComment)
  49 + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
  50 + #set ($dictMethod = "number")
  51 + #elseif ($javaType == "String")
  52 + #set ($dictMethod = "string")
  53 + #elseif ($javaType == "Boolean")
  54 + #set ($dictMethod = "boolean")
  55 + #end
48 56 {
49 57 label: '${comment}',
50 58 field: '${javaField}',
... ... @@ -54,16 +62,16 @@ export const searchFormSchema: FormSchema[] = [
54 62 component: 'Select',
55 63 componentProps: {
56 64 #if ("" != $dictType)## 设置了 dictType 数据字典的情况
57   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()),
  65 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
58 66 #else## 未设置 dictType 数据字典的情况
59 67 options: [],
60 68 #end
61 69 },
62 70 #elseif ($column.htmlType == "radio")
63   - component: 'Radio',
  71 + component: 'RadioButtonGroup',
64 72 componentProps: {
65 73 #if ("" != $dictType)## 设置了 dictType 数据字典的情况
66   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()),
  74 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
67 75 #else## 未设置 dictType 数据字典的情况
68 76 options: [],
69 77 #end
... ... @@ -87,9 +95,17 @@ export const createFormSchema: FormSchema[] = [
87 95 #foreach($column in $columns)
88 96 #if ($column.createOperation)
89 97 #set ($dictType = $column.dictType)
  98 + #set ($javaType = $column.javaType)
90 99 #set ($javaField = $column.javaField)
91 100 #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
92 101 #set ($comment = $column.columnComment)
  102 + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
  103 + #set ($dictMethod = "number")
  104 + #elseif ($javaType == "String")
  105 + #set ($dictMethod = "string")
  106 + #elseif ($javaType == "Boolean")
  107 + #set ($dictMethod = "boolean")
  108 + #end
93 109 #if (!$column.primaryKey)## 忽略主键,不用在表单里
94 110 {
95 111 label: '${comment}',
... ... @@ -117,7 +133,7 @@ export const createFormSchema: FormSchema[] = [
117 133 component: 'Select',
118 134 componentProps: {
119 135 #if ("" != $dictType)## 有数据字典
120   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
  136 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
121 137 #else##没数据字典
122 138 options:[],
123 139 #end
... ... @@ -126,7 +142,7 @@ export const createFormSchema: FormSchema[] = [
126 142 component: 'Checkbox',
127 143 componentProps: {
128 144 #if ("" != $dictType)## 有数据字典
129   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
  145 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
130 146 #else##没数据字典
131 147 options:[],
132 148 #end
... ... @@ -135,7 +151,7 @@ export const createFormSchema: FormSchema[] = [
135 151 component: 'RadioButtonGroup',
136 152 componentProps: {
137 153 #if ("" != $dictType)## 有数据字典
138   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
  154 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
139 155 #else##没数据字典
140 156 options:[],
141 157 #end
... ... @@ -166,9 +182,17 @@ export const updateFormSchema: FormSchema[] = [
166 182 #foreach($column in $columns)
167 183 #if ($column.updateOperation)
168 184 #set ($dictType = $column.dictType)
  185 +#set ($javaType = $column.javaType)
169 186 #set ($javaField = $column.javaField)
170 187 #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
171 188 #set ($comment = $column.columnComment)
  189 +#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
  190 + #set ($dictMethod = "number")
  191 +#elseif ($javaType == "String")
  192 + #set ($dictMethod = "string")
  193 +#elseif ($javaType == "Boolean")
  194 + #set ($dictMethod = "boolean")
  195 +#end
172 196 #if (!$column.primaryKey)## 忽略主键,不用在表单里
173 197 {
174 198 label: '${comment}',
... ... @@ -196,7 +220,7 @@ export const updateFormSchema: FormSchema[] = [
196 220 component: 'Select',
197 221 componentProps: {
198 222 #if ("" != $dictType)## 有数据字典
199   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
  223 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
200 224 #else##没数据字典
201 225 options:[],
202 226 #end
... ... @@ -205,7 +229,7 @@ export const updateFormSchema: FormSchema[] = [
205 229 component: 'Checkbox',
206 230 componentProps: {
207 231 #if ("" != $dictType)## 有数据字典
208   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
  232 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
209 233 #else##没数据字典
210 234 options:[],
211 235 #end
... ... @@ -214,7 +238,7 @@ export const updateFormSchema: FormSchema[] = [
214 238 component: 'RadioButtonGroup',
215 239 componentProps: {
216 240 #if ("" != $dictType)## 有数据字典
217   - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
  241 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
218 242 #else##没数据字典
219 243 options:[],
220 244 #end
... ...
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImplTest.java
... ... @@ -114,7 +114,8 @@ public class JobLogServiceImplTest extends BaseDbUnitTest {
114 114 assertEquals(1, count);
115 115 List<JobLogDO> logs = jobLogMapper.selectList();
116 116 assertEquals(1, logs.size());
117   - assertEquals(log02, logs.get(0));
  117 + // TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
  118 + assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
118 119 }
119 120  
120 121 @Test
... ...
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImplTest.java
... ... @@ -91,7 +91,8 @@ public class ApiAccessLogServiceImplTest extends BaseDbUnitTest {
91 91 assertEquals(1, count);
92 92 List<ApiAccessLogDO> logs = apiAccessLogMapper.selectList();
93 93 assertEquals(1, logs.size());
94   - assertEquals(log02, logs.get(0));
  94 + // TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
  95 + assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
95 96 }
96 97  
97 98 @Test
... ...
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImplTest.java
... ... @@ -157,7 +157,8 @@ public class ApiErrorLogServiceImplTest extends BaseDbUnitTest {
157 157 assertEquals(1, count);
158 158 List<ApiErrorLogDO> logs = apiErrorLogMapper.selectList();
159 159 assertEquals(1, logs.size());
160   - assertEquals(log02, logs.get(0));
  160 + // TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
  161 + assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
161 162 }
162 163  
163 164 }
... ...
yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml
... ... @@ -19,6 +19,7 @@ spring:
19 19 sql:
20 20 init:
21 21 schema-locations: classpath:/sql/create_tables.sql
  22 + encoding: UTF-8
22 23  
23 24 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
24 25 data:
... ...
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java
... ... @@ -3,7 +3,8 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
3 3 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
4 4 import lombok.Data;
5 5  
6   -// TODO @LeeYan9: ProductSpuRespDTO
  6 +import java.util.List;
  7 +
7 8 /**
8 9 * 商品 SPU 信息 Response DTO
9 10 *
... ... @@ -70,6 +71,13 @@ public class ProductSpuRespDTO {
70 71 // ========== 物流相关字段 =========
71 72  
72 73 /**
  74 + * 配送方式数组
  75 + *
  76 + * 对应 DeliveryTypeEnum 枚举
  77 + */
  78 + private List<Integer> deliveryTypes;
  79 +
  80 + /**
73 81 * 物流配置模板编号
74 82 *
75 83 * 对应 TradeDeliveryExpressTemplateDO 的 id 编号
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyController.java
... ... @@ -17,7 +17,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
17 17 import org.springframework.validation.annotation.Validated;
18 18 import org.springframework.web.bind.annotation.*;
19 19  
  20 +import java.util.List;
  21 +
20 22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
21 24  
22 25 @Tag(name = "管理后台 - 商品属性项")
23 26 @RestController
... ... @@ -69,4 +72,12 @@ public class ProductPropertyController {
69 72 return success(BeanUtils.toBean(pageResult, ProductPropertyRespVO.class));
70 73 }
71 74  
  75 + @GetMapping("/simple-list")
  76 + @Operation(summary = "获得属性项精简列表")
  77 + public CommonResult<List<ProductPropertyRespVO>> getPropertySimpleList() {
  78 + List<ProductPropertyDO> list = productPropertyService.getPropertyList();
  79 + return success(convertList(list, property -> new ProductPropertyRespVO() // 只返回 id、name 属性
  80 + .setId(property.getId()).setName(property.getName())));
  81 + }
  82 +
72 83 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyValueController.java
... ... @@ -17,7 +17,11 @@ import org.springframework.security.access.prepost.PreAuthorize;
17 17 import org.springframework.validation.annotation.Validated;
18 18 import org.springframework.web.bind.annotation.*;
19 19  
  20 +import java.util.List;
  21 +
20 22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton;
21 25  
22 26 @Tag(name = "管理后台 - 商品属性值")
23 27 @RestController
... ... @@ -69,4 +73,13 @@ public class ProductPropertyValueController {
69 73 return success(BeanUtils.toBean(pageResult, ProductPropertyValueRespVO.class));
70 74 }
71 75  
  76 + @GetMapping("/simple-list")
  77 + @Operation(summary = "获得属性值精简列表")
  78 + @Parameter(name = "propertyId", description = "属性项编号", required = true, example = "1024")
  79 + public CommonResult<List<ProductPropertyValueRespVO>> getPropertyValueSimpleList(@RequestParam("propertyId") Long propertyId) {
  80 + List<ProductPropertyValueDO> list = productPropertyValueService.getPropertyValueListByPropertyId(singleton(propertyId));
  81 + return success(convertList(list, value -> new ProductPropertyValueRespVO() // 只返回 id、name 属性
  82 + .setId(value.getId()).setName(value.getName())));
  83 + }
  84 +
72 85 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
... ... @@ -148,5 +148,4 @@ public class AppProductSpuController {
148 148 return price - newPrice;
149 149 }
150 150  
151   - // TODO 芋艿:商品的浏览记录;
152 151 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java
... ... @@ -48,6 +48,4 @@ public class ProductBrandDO extends BaseDO {
48 48 */
49 49 private Integer status;
50 50  
51   - // TODO 芋艿:firstLetter 首字母
52   -
53 51 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/property/ProductPropertyDO.java
... ... @@ -40,10 +40,6 @@ public class ProductPropertyDO extends BaseDO {
40 40 */
41 41 private String name;
42 42 /**
43   - * 状态
44   - */
45   - private Integer status;
46   - /**
47 43 * 备注
48 44 */
49 45 private String remark;
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java
... ... @@ -130,11 +130,5 @@ public class ProductSkuDO extends BaseDO {
130 130  
131 131 }
132 132  
133   - // TODO 芋艿:integral from y
134   - // TODO 芋艿:pinkPrice from y
135   - // TODO 芋艿:seckillPrice from y
136   - // TODO 芋艿:pinkStock from y
137   - // TODO 芋艿:seckillStock from y
138   -
139 133 }
140 134  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
... ... @@ -67,7 +67,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
67 67 // 校验 SPU
68 68 ProductSpuDO spu = validateSpu(sku.getSpuId());
69 69 // 校验评论
70   - validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderId());
  70 + validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderItemId());
71 71 // 获取用户详细信息
72 72 MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId());
73 73  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyService.java
... ... @@ -62,4 +62,11 @@ public interface ProductPropertyService {
62 62 */
63 63 List<ProductPropertyDO> getPropertyList(Collection<Long> ids);
64 64  
  65 + /**
  66 + * 获得指定状态的属性项列表
  67 + *
  68 + * @return 属性项列表
  69 + */
  70 + List<ProductPropertyDO> getPropertyList();
  71 +
65 72 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyServiceImpl.java
... ... @@ -109,4 +109,9 @@ public class ProductPropertyServiceImpl implements ProductPropertyService {
109 109 return productPropertyMapper.selectBatchIds(ids);
110 110 }
111 111  
  112 + @Override
  113 + public List<ProductPropertyDO> getPropertyList() {
  114 + return productPropertyMapper.selectList();
  115 + }
  116 +
112 117 }
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
... ... @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.promotion.api.combination;
2 2  
3 3 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
4 4 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO;
  5 +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
5 6 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
6   -
7 7 import jakarta.validation.Valid;
8 8  
9 9 /**
... ... @@ -33,13 +33,13 @@ public interface CombinationRecordApi {
33 33 CombinationRecordCreateRespDTO createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
34 34  
35 35 /**
36   - * 查询拼团记录是否成功
  36 + * 基于订单编号,查询拼团记录
37 37 *
38 38 * @param userId 用户编号
39 39 * @param orderId 订单编号
40   - * @return 拼团是否成功
  40 + * @return 拼团记录
41 41 */
42   - boolean isCombinationRecordSuccess(Long userId, Long orderId);
  42 + CombinationRecordRespDTO getCombinationRecordByOrderId(Long userId, Long orderId);
43 43  
44 44 /**
45 45 * 【下单前】校验是否满足拼团活动条件
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.api.combination.dto;
  2 +
  3 +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +/**
  9 + * 拼团记录 Response DTO
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +public class CombinationRecordRespDTO {
  15 +
  16 + /**
  17 + * 编号,主键自增
  18 + */
  19 + private Long id;
  20 +
  21 + /**
  22 + * 拼团活动编号
  23 + *
  24 + * 关联 CombinationActivityDO 的 id 字段
  25 + */
  26 + private Long activityId;
  27 + /**
  28 + * 拼团商品单价
  29 + *
  30 + * 冗余 CombinationProductDO 的 combinationPrice 字段
  31 + */
  32 + private Integer combinationPrice;
  33 + /**
  34 + * SPU 编号
  35 + */
  36 + private Long spuId;
  37 + /**
  38 + * 商品名字
  39 + */
  40 + private String spuName;
  41 + /**
  42 + * 商品图片
  43 + */
  44 + private String picUrl;
  45 + /**
  46 + * SKU 编号
  47 + */
  48 + private Long skuId;
  49 + /**
  50 + * 购买的商品数量
  51 + */
  52 + private Integer count;
  53 +
  54 + /**
  55 + * 用户编号
  56 + */
  57 + private Long userId;
  58 +
  59 + /**
  60 + * 用户昵称
  61 + */
  62 + private String nickname;
  63 + /**
  64 + * 用户头像
  65 + */
  66 + private String avatar;
  67 +
  68 + /**
  69 + * 团长编号
  70 + */
  71 + private Long headId;
  72 + /**
  73 + * 开团状态
  74 + *
  75 + * 关联 {@link CombinationRecordStatusEnum}
  76 + */
  77 + private Integer status;
  78 + /**
  79 + * 订单编号
  80 + */
  81 + private Long orderId;
  82 + /**
  83 + * 开团需要人数
  84 + *
  85 + * 关联 CombinationActivityDO 的 userSize 字段
  86 + */
  87 + private Integer userSize;
  88 + /**
  89 + * 已加入拼团人数
  90 + */
  91 + private Integer userCount;
  92 + /**
  93 + * 是否虚拟成团
  94 + */
  95 + private Boolean virtualGroup;
  96 +
  97 + /**
  98 + * 过期时间
  99 + */
  100 + private LocalDateTime expireTime;
  101 + /**
  102 + * 开始时间 (订单付款后开始的时间)
  103 + */
  104 + private LocalDateTime startTime;
  105 + /**
  106 + * 结束时间(成团时间/失败时间)
  107 + */
  108 + private LocalDateTime endTime;
  109 +
  110 +}
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
... ... @@ -2,10 +2,11 @@ package cn.iocoder.yudao.module.promotion.api.coupon;
2 2  
3 3 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
4 4 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
5   -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
6   -
7 5 import jakarta.validation.Valid;
8 6  
  7 +import java.util.List;
  8 +import java.util.Map;
  9 +
9 10 /**
10 11 * 优惠劵 API 接口
11 12 *
... ... @@ -14,6 +15,15 @@ import jakarta.validation.Valid;
14 15 public interface CouponApi {
15 16  
16 17 /**
  18 + * 获得用户的优惠劵列表
  19 + *
  20 + * @param userId 用户编号
  21 + * @param status 优惠劵状态
  22 + * @return 优惠劵列表
  23 + */
  24 + List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status);
  25 +
  26 + /**
17 27 * 使用优惠劵
18 28 *
19 29 * @param useReqDTO 使用请求
... ... @@ -28,11 +38,20 @@ public interface CouponApi {
28 38 void returnUsedCoupon(Long id);
29 39  
30 40 /**
31   - * 校验优惠劵
  41 + * 【管理员】给指定用户批量发送优惠券
  42 + *
  43 + * @param giveCoupons key: 优惠劵模版编号,value:对应的数量
  44 + * @param userId 用户编号
  45 + * @return 优惠券编号列表
  46 + */
  47 + List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
  48 +
  49 + /**
  50 + * 【管理员】作废指定用户的指定优惠劵
32 51 *
33   - * @param validReqDTO 校验请求
34   - * @return 优惠劵
  52 + * @param giveCouponIds 赠送的优惠券编号
  53 + * @param userId 用户编号
35 54 */
36   - CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO);
  55 + void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
37 56  
38 57 }
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java deleted
1   -package cn.iocoder.yudao.module.promotion.api.coupon.dto;
2   -
3   -import lombok.Data;
4   -
5   -import jakarta.validation.constraints.NotNull;
6   -
7   -/**
8   - * 优惠劵使用 Request DTO
9   - *
10   - * @author 芋道源码
11   - */
12   -@Data
13   -public class CouponValidReqDTO {
14   -
15   - /**
16   - * 优惠劵编号
17   - */
18   - @NotNull(message = "优惠劵编号不能为空")
19   - private Long id;
20   -
21   - /**
22   - * 用户编号
23   - */
24   - @NotNull(message = "用户编号不能为空")
25   - private Long userId;
26   -
27   -}
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java
1 1 package cn.iocoder.yudao.module.promotion.api.reward.dto;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
  5 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
4 6 import lombok.Data;
5 7  
  8 +import java.io.Serializable;
  9 +import java.time.LocalDateTime;
6 10 import java.util.List;
  11 +import java.util.Map;
7 12  
8 13 /**
9 14 * 满减送活动的匹配 Response DTO
... ... @@ -22,27 +27,49 @@ public class RewardActivityMatchRespDTO {
22 27 */
23 28 private String name;
24 29 /**
  30 + * 状态
  31 + *
  32 + * 枚举 {@link CommonStatusEnum}
  33 + */
  34 + private Integer status;
  35 + /**
  36 + * 开始时间
  37 + */
  38 + private LocalDateTime startTime;
  39 + /**
  40 + * 结束时间
  41 + */
  42 + private LocalDateTime endTime;
  43 + /**
  44 + * 备注
  45 + */
  46 + private String remark;
  47 + /**
25 48 * 条件类型
26 49 *
27 50 * 枚举 {@link PromotionConditionTypeEnum}
28 51 */
29 52 private Integer conditionType;
30 53 /**
31   - * 优惠规则的数组
  54 + * 商品范围
  55 + *
  56 + * 枚举 {@link PromotionProductScopeEnum}
32 57 */
33   - private List<Rule> rules;
34   -
  58 + private Integer productScope;
35 59 /**
36 60 * 商品 SPU 编号的数组
37 61 */
38   - private List<Long> spuIds;
  62 + private List<Long> productScopeValues;
  63 + /**
  64 + * 优惠规则的数组
  65 + */
  66 + private List<Rule> rules;
39 67  
40   - // TODO 芋艿:后面 RewardActivityRespDTO 有了之后,Rule 可以放过去
41 68 /**
42 69 * 优惠规则
43 70 */
44 71 @Data
45   - public static class Rule {
  72 + public static class Rule implements Serializable {
46 73  
47 74 /**
48 75 * 优惠门槛
... ... @@ -64,13 +91,14 @@ public class RewardActivityMatchRespDTO {
64 91 */
65 92 private Integer point;
66 93 /**
67   - * 赠送的优惠劵编号的数组
68   - */
69   - private List<Long> couponIds;
70   - /**
71   - * 赠送的优惠券数量的数组
  94 + * 赠送的优惠劵
  95 + *
  96 + * key: 优惠劵模版编号
  97 + * value:对应的优惠券数量
  98 + *
  99 + * 目的:用于订单支付后赠送优惠券
72 100 */
73   - private List<Integer> couponCounts;
  101 + private Map<Long, Integer> giveCouponTemplateCounts;
74 102  
75 103 }
76 104  
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
... ... @@ -20,8 +20,6 @@ public interface ErrorCodeConstants {
20 20 ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1_013_002_000, "Banner 不存在");
21 21  
22 22 // ========== Coupon 相关 1-013-003-000 ============
23   - ErrorCode COUPON_NO_MATCH_SPU = new ErrorCode(1_013_003_000, "优惠劵没有可使用的商品!");
24   - ErrorCode COUPON_NO_MATCH_MIN_PRICE = new ErrorCode(1_013_003_001, "所结算的商品中未满足使用的金额");
25 23  
26 24 // ========== 优惠劵模板 1-013-004-000 ==========
27 25 ErrorCode COUPON_TEMPLATE_NOT_EXISTS = new ErrorCode(1_013_004_000, "优惠劵模板不存在");
... ... @@ -44,7 +42,8 @@ public interface ErrorCodeConstants {
44 42 ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改");
45 43 ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除");
46 44 ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭");
47   - ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1_013_006_005, "满减送活动已结束,不能关闭");
  45 + ErrorCode REWARD_ACTIVITY_SCOPE_ALL_EXISTS = new ErrorCode(1_013_006_005, "已存在商品范围为全场的满减送活动");
  46 + ErrorCode REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS = new ErrorCode(1_013_006_006, "存在商品类型参加了其它满减送活动");
48 47  
49 48 // ========== TODO 空着 1-013-007-000 ============
50 49  
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java
... ... @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
5 5 import lombok.Getter;
6 6  
7 7 import java.util.Arrays;
  8 +import java.util.Objects;
8 9  
9 10 /**
10 11 * 营销的商品范围枚举
... ... @@ -15,10 +16,9 @@ import java.util.Arrays;
15 16 @AllArgsConstructor
16 17 public enum PromotionProductScopeEnum implements IntArrayValuable {
17 18  
18   - ALL(1, "通用券"), // 全部商品
19   - SPU(2, "商品券"), // 指定商品
20   - CATEGORY(3, "品类券"), // 指定品类
21   - ;
  19 + ALL(1, "全部商品"),
  20 + SPU(2, "指定商品"),
  21 + CATEGORY(3, "指定品类");
22 22  
23 23 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray();
24 24  
... ... @@ -36,4 +36,16 @@ public enum PromotionProductScopeEnum implements IntArrayValuable {
36 36 return ARRAYS;
37 37 }
38 38  
  39 + public static boolean isAll(Integer scope) {
  40 + return Objects.equals(scope, ALL.scope);
  41 + }
  42 +
  43 + public static boolean isSpu(Integer scope) {
  44 + return Objects.equals(scope, SPU.scope);
  45 + }
  46 +
  47 + public static boolean isCategory(Integer scope) {
  48 + return Objects.equals(scope, CATEGORY.scope);
  49 + }
  50 +
39 51 }
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java
... ... @@ -17,8 +17,7 @@ public enum CouponStatusEnum implements IntArrayValuable {
17 17  
18 18 UNUSED(1, "未使用"),
19 19 USED(2, "已使用"),
20   - EXPIRE(3, "已过期"),
21   - ;
  20 + EXPIRE(3, "已过期");
22 21  
23 22 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray();
24 23  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
1 1 package cn.iocoder.yudao.module.promotion.api.combination;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
3 4 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
4 5 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO;
  6 +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
5 7 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
6 8 import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
7 9 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
8   -import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
9 10 import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
10 11 import jakarta.annotation.Resource;
11 12 import org.springframework.stereotype.Service;
12 13 import org.springframework.validation.annotation.Validated;
13 14  
14   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
15   -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COMBINATION_RECORD_NOT_EXISTS;
16   -
17 15 /**
18 16 * 拼团活动 API 实现类
19 17 *
... ... @@ -37,12 +35,9 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
37 35 }
38 36  
39 37 @Override
40   - public boolean isCombinationRecordSuccess(Long userId, Long orderId) {
  38 + public CombinationRecordRespDTO getCombinationRecordByOrderId(Long userId, Long orderId) {
41 39 CombinationRecordDO record = combinationRecordService.getCombinationRecord(userId, orderId);
42   - if (record == null) {
43   - throw exception(COMBINATION_RECORD_NOT_EXISTS);
44   - }
45   - return CombinationRecordStatusEnum.isSuccess(record.getStatus());
  40 + return BeanUtils.toBean(record, CombinationRecordRespDTO.class);
46 41 }
47 42  
48 43 @Override
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
1 1 package cn.iocoder.yudao.module.promotion.api.coupon;
2 2  
3 3  
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 5 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
5 6 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
6   -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
7   -import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
8   -import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
9 7 import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
  8 +import jakarta.annotation.Resource;
10 9 import org.springframework.stereotype.Service;
11 10 import org.springframework.validation.annotation.Validated;
12 11  
13   -import jakarta.annotation.Resource;
  12 +import java.util.List;
  13 +import java.util.Map;
14 14  
15 15 /**
16 16 * 优惠劵 API 实现类
... ... @@ -25,6 +25,11 @@ public class CouponApiImpl implements CouponApi {
25 25 private CouponService couponService;
26 26  
27 27 @Override
  28 + public List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status) {
  29 + return BeanUtils.toBean(couponService.getCouponList(userId, status), CouponRespDTO.class);
  30 + }
  31 +
  32 + @Override
28 33 public void useCoupon(CouponUseReqDTO useReqDTO) {
29 34 couponService.useCoupon(useReqDTO.getId(), useReqDTO.getUserId(),
30 35 useReqDTO.getOrderId());
... ... @@ -36,9 +41,13 @@ public class CouponApiImpl implements CouponApi {
36 41 }
37 42  
38 43 @Override
39   - public CouponRespDTO validateCoupon(CouponValidReqDTO validReqDTO) {
40   - CouponDO coupon = couponService.validCoupon(validReqDTO.getId(), validReqDTO.getUserId());
41   - return CouponConvert.INSTANCE.convert(coupon);
  44 + public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
  45 + return couponService.takeCouponsByAdmin(giveCoupons, userId);
  46 + }
  47 +
  48 + @Override
  49 + public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
  50 + couponService.invalidateCouponsByAdmin(giveCouponIds, userId);
42 51 }
43 52  
44 53 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java
1 1 package cn.iocoder.yudao.module.promotion.controller.admin.combination;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 7 import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
... ... @@ -16,18 +17,20 @@ import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordSe
16 17 import io.swagger.v3.oas.annotations.Operation;
17 18 import io.swagger.v3.oas.annotations.Parameter;
18 19 import io.swagger.v3.oas.annotations.tags.Tag;
  20 +import jakarta.annotation.Resource;
  21 +import jakarta.validation.Valid;
19 22 import org.springframework.security.access.prepost.PreAuthorize;
20 23 import org.springframework.validation.annotation.Validated;
21 24 import org.springframework.web.bind.annotation.*;
22 25  
23   -import jakarta.annotation.Resource;
24   -import jakarta.validation.Valid;
  26 +import java.util.Collections;
25 27 import java.util.List;
26 28 import java.util.Map;
27 29 import java.util.Set;
28 30  
29 31 import static cn.hutool.core.collection.CollectionUtil.newArrayList;
30 32 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
31 34 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
32 35  
33 36 @Tag(name = "管理后台 - 拼团活动")
... ... @@ -87,6 +90,23 @@ public class CombinationActivityController {
87 90 return success(CombinationActivityConvert.INSTANCE.convert(activity, products));
88 91 }
89 92  
  93 + @GetMapping("/list-by-ids")
  94 + @Operation(summary = "获得拼团活动列表,基于活动编号数组")
  95 + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
  96 + public CommonResult<List<CombinationActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
  97 + // 1. 获得开启的活动列表
  98 + List<CombinationActivityDO> activityList = combinationActivityService.getCombinationActivityListByIds(ids);
  99 + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
  100 + if (CollUtil.isEmpty(activityList)) {
  101 + return success(Collections.emptyList());
  102 + }
  103 + // 2. 拼接返回
  104 + List<CombinationProductDO> productList = combinationActivityService.getCombinationProductListByActivityIds(
  105 + convertList(activityList, CombinationActivityDO::getId));
  106 + List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId));
  107 + return success(CombinationActivityConvert.INSTANCE.convertList(activityList, productList, spuList));
  108 + }
  109 +
90 110 @GetMapping("/page")
91 111 @Operation(summary = "获得拼团活动分页")
92 112 @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')")
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java
... ... @@ -27,4 +27,14 @@ public class CombinationActivityRespVO extends CombinationActivityBaseVO {
27 27 @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED)
28 28 private List<CombinationProductRespVO> products;
29 29  
  30 + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜")
  31 + private String spuName; // 从 SPU 的 name 读取
  32 + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
  33 + private String picUrl; // 从 SPU 的 picUrl 读取
  34 + @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
  35 + private Integer marketPrice; // 从 SPU 的 marketPrice 读取
  36 +
  37 + @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
  38 + private Integer combinationPrice; // 从 products 获取最小 price 读取
  39 +
30 40 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java
... ... @@ -33,6 +33,9 @@ public class CouponTemplateBaseVO {
33 33 @NotNull(message = "优惠劵名不能为空")
34 34 private String name;
35 35  
  36 + @Schema(description = "优惠券说明", example = "优惠券使用说明")
  37 + private String description;
  38 +
36 39 @Schema(description = "发行总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // -1 - 则表示不限制发放数量
37 40 @NotNull(message = "发行总量不能为空")
38 41 private Integer totalCount;
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java
... ... @@ -2,23 +2,22 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 6 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
6 7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
7 8 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
8 9 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
9   -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;
10 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
11 11 import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
12 12 import io.swagger.v3.oas.annotations.Operation;
13 13 import io.swagger.v3.oas.annotations.Parameter;
14 14 import io.swagger.v3.oas.annotations.tags.Tag;
  15 +import jakarta.annotation.Resource;
  16 +import jakarta.validation.Valid;
15 17 import org.springframework.security.access.prepost.PreAuthorize;
16 18 import org.springframework.validation.annotation.Validated;
17 19 import org.springframework.web.bind.annotation.*;
18 20  
19   -import jakarta.annotation.Resource;
20   -import jakarta.validation.Valid;
21   -
22 21 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
23 22  
24 23 @Tag(name = "管理后台 - 满减送活动")
... ... @@ -69,7 +68,7 @@ public class RewardActivityController {
69 68 @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
70 69 public CommonResult<RewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) {
71 70 RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id);
72   - return success(RewardActivityConvert.INSTANCE.convert(rewardActivity));
  71 + return success(BeanUtils.toBean(rewardActivity, RewardActivityRespVO.class));
73 72 }
74 73  
75 74 @GetMapping("/page")
... ... @@ -77,7 +76,7 @@ public class RewardActivityController {
77 76 @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
78 77 public CommonResult<PageResult<RewardActivityRespVO>> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) {
79 78 PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(pageVO);
80   - return success(RewardActivityConvert.INSTANCE.convertPage(pageResult));
  79 + return success(BeanUtils.toBean(pageResult, RewardActivityRespVO.class));
81 80 }
82 81  
83 82 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java
... ... @@ -6,18 +6,17 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum
6 6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
7 7 import com.fasterxml.jackson.annotation.JsonIgnore;
8 8 import io.swagger.v3.oas.annotations.media.Schema;
9   -import lombok.Data;
10   -import org.springframework.format.annotation.DateTimeFormat;
11   -
12 9 import jakarta.validation.Valid;
13 10 import jakarta.validation.constraints.AssertTrue;
14 11 import jakarta.validation.constraints.Future;
15 12 import jakarta.validation.constraints.Min;
16 13 import jakarta.validation.constraints.NotNull;
  14 +import lombok.Data;
  15 +
17 16 import java.time.LocalDateTime;
18 17 import java.util.List;
19   -
20   -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  18 +import java.util.Map;
  19 +import java.util.Objects;
21 20  
22 21 /**
23 22 * 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用
... ... @@ -32,12 +31,10 @@ public class RewardActivityBaseVO {
32 31  
33 32 @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
34 33 @NotNull(message = "开始时间不能为空")
35   - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
36 34 private LocalDateTime startTime;
37 35  
38 36 @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
39 37 @NotNull(message = "结束时间不能为空")
40   - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
41 38 @Future(message = "结束时间必须大于当前时间")
42 39 private LocalDateTime endTime;
43 40  
... ... @@ -54,8 +51,8 @@ public class RewardActivityBaseVO {
54 51 @InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}")
55 52 private Integer productScope;
56 53  
57   - @Schema(description = "商品 SPU 编号的数组", example = "1,2,3")
58   - private List<Long> productSpuIds;
  54 + @Schema(description = "商品范围编号的数组", example = "[1, 3]")
  55 + private List<Long> productScopeValues;
59 56  
60 57 /**
61 58 * 优惠规则的数组
... ... @@ -76,24 +73,28 @@ public class RewardActivityBaseVO {
76 73 private Integer discountPrice;
77 74  
78 75 @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  76 + @NotNull(message = "规则是否包邮不能为空")
79 77 private Boolean freeDelivery;
80 78  
81 79 @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
82   - @Min(value = 1L, message = "赠送的积分必须大于等于 1")
83 80 private Integer point;
84 81  
85   - @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3")
86   - private List<Long> couponIds;
87   -
88   - @Schema(description = "赠送的优惠券数量的数组", example = "1,2,3")
89   - private List<Integer> couponCounts;
  82 + @Schema(description = "赠送的优惠劵编号的数组")
  83 + private Map<Long, Integer> giveCouponTemplateCounts;
90 84  
91   - @AssertTrue(message = "优惠劵和数量必须一一对应")
  85 + @AssertTrue(message = "赠送的积分不能小于 0")
92 86 @JsonIgnore
93   - public boolean isCouponCountsValid() {
94   - return CollUtil.size(couponCounts) == CollUtil.size(couponCounts);
  87 + public boolean isPointValid() {
  88 + return point == null || point >= 0;
95 89 }
96 90  
97 91 }
98 92  
  93 + @AssertTrue(message = "商品范围编号的数组不能为空")
  94 + @JsonIgnore
  95 + public boolean isProductScopeValuesValid() {
  96 + return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空
  97 + || CollUtil.isNotEmpty(productScopeValues);
  98 + }
  99 +
99 100 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java
1 1 package cn.iocoder.yudao.module.promotion.controller.admin.seckill;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 7 import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
... ... @@ -13,15 +14,17 @@ import cn.iocoder.yudao.module.promotion.service.seckill.SeckillActivityService;
13 14 import io.swagger.v3.oas.annotations.Operation;
14 15 import io.swagger.v3.oas.annotations.Parameter;
15 16 import io.swagger.v3.oas.annotations.tags.Tag;
  17 +import jakarta.annotation.Resource;
  18 +import jakarta.validation.Valid;
16 19 import org.springframework.security.access.prepost.PreAuthorize;
17 20 import org.springframework.validation.annotation.Validated;
18 21 import org.springframework.web.bind.annotation.*;
19 22  
20   -import jakarta.annotation.Resource;
21   -import jakarta.validation.Valid;
  23 +import java.util.Collections;
22 24 import java.util.List;
23 25  
24 26 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
25 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
26 29  
27 30 @Tag(name = "管理后台 - 秒杀活动")
... ... @@ -89,11 +92,28 @@ public class SeckillActivityController {
89 92 }
90 93  
91 94 // 拼接数据
92   - List<SeckillProductDO> products = seckillActivityService.getSeckillProductListByActivityId(
  95 + List<SeckillProductDO> products = seckillActivityService.getSeckillProductListByActivityIds(
93 96 convertSet(pageResult.getList(), SeckillActivityDO::getId));
94 97 List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(
95 98 convertSet(pageResult.getList(), SeckillActivityDO::getSpuId));
96 99 return success(SeckillActivityConvert.INSTANCE.convertPage(pageResult, products, spuList));
97 100 }
98 101  
  102 + @GetMapping("/list-by-ids")
  103 + @Operation(summary = "获得秒杀活动列表,基于活动编号数组")
  104 + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
  105 + public CommonResult<List<SeckillActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
  106 + // 1. 获得开启的活动列表
  107 + List<SeckillActivityDO> activityList = seckillActivityService.getSeckillActivityListByIds(ids);
  108 + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
  109 + if (CollUtil.isEmpty(activityList)) {
  110 + return success(Collections.emptyList());
  111 + }
  112 + // 2. 拼接返回
  113 + List<SeckillProductDO> productList = seckillActivityService.getSeckillProductListByActivityIds(
  114 + convertList(activityList, SeckillActivityDO::getId));
  115 + List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId));
  116 + return success(SeckillActivityConvert.INSTANCE.convertList(activityList, productList, spuList));
  117 + }
  118 +
99 119 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java
... ... @@ -54,4 +54,7 @@ public class SeckillActivityRespVO extends SeckillActivityBaseVO {
54 54 example = "50")
55 55 private Integer marketPrice;
56 56  
  57 + @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
  58 + private Integer seckillPrice; // 从 products 获取最小 price 读取
  59 +
57 60 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
... ... @@ -2,8 +2,11 @@ package cn.iocoder.yudao.module.promotion.controller.app.activity;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.ObjUtil;
5 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
  9 +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
7 10 import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO;
8 11 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
9 12 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
... ... @@ -11,7 +14,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
11 14 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
12 15 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
13 16 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
14   -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
  17 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
15 18 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
16 19 import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService;
17 20 import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
... ... @@ -30,7 +33,6 @@ import org.springframework.web.bind.annotation.RestController;
30 33  
31 34 import java.time.LocalDateTime;
32 35 import java.util.*;
33   -import java.util.stream.Collectors;
34 36  
35 37 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
36 38 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
... ... @@ -52,6 +54,9 @@ public class AppActivityController {
52 54 @Resource
53 55 private RewardActivityService rewardActivityService;
54 56  
  57 + @Resource
  58 + private ProductSpuApi productSpuApi;
  59 +
55 60 @GetMapping("/list-by-spu-id")
56 61 @Operation(summary = "获得单个商品,近期参与的每个活动")
57 62 @Parameter(name = "spuId", description = "商品编号", required = true)
... ... @@ -87,7 +92,7 @@ public class AppActivityController {
87 92 // 4. 限时折扣活动
88 93 getDiscountActivities(spuIds, now, activityList);
89 94 // 5. 满减送活动
90   - getRewardActivities(spuIds, now, activityList);
  95 + getRewardActivityList(spuIds, now, activityList);
91 96 return activityList;
92 97 }
93 98  
... ... @@ -144,28 +149,51 @@ public class AppActivityController {
144 149 item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime())));
145 150 }
146 151  
147   - private void getRewardActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
148   - // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部
149   - List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(
150   - spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now);
  152 + private void getRewardActivityList(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
  153 + // 1.1 获得所有的活动
  154 + List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(
  155 + CommonStatusEnum.ENABLE.getStatus(), now);
151 156 if (CollUtil.isEmpty(rewardActivityList)) {
152 157 return;
153 158 }
  159 + // 1.2 获得所有的商品信息
  160 + List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
  161 + if (CollUtil.isEmpty(spuList)) {
  162 + return;
  163 + }
154 164  
155   - Map<Long, Optional<RewardActivityDO>> spuIdAndActivityMap = spuIds.stream()
156   - .collect(Collectors.toMap(
157   - spuId -> spuId,
158   - spuId -> rewardActivityList.stream()
159   - .filter(activity -> activity.getProductSpuIds().contains(spuId))
160   - .max(Comparator.comparing(RewardActivityDO::getCreateTime))));
161   - for (Long supId : spuIdAndActivityMap.keySet()) {
162   - if (spuIdAndActivityMap.get(supId).isEmpty()) {
163   - continue;
  165 + // 2. 构建活动
  166 + for (RewardActivityDO rewardActivity : rewardActivityList) {
  167 + // 情况一:所有商品都能参加
  168 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
  169 + buildAppActivityRespVO(rewardActivity, spuIds, activityList);
  170 + }
  171 + // 情况二:指定商品参加
  172 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
  173 + List<Long> fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id ->
  174 + rewardActivity.getProductScopeValues().contains(id)).toList();
  175 + buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
164 176 }
  177 + // 情况三:指定商品类型参加
  178 + if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
  179 + List<Long> fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues()
  180 + .contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList();
  181 + buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
  182 + }
  183 + }
  184 + }
165 185  
166   - RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get();
167   - activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
168   - rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime()));
  186 + private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection<Long> spuIds,
  187 + List<AppActivityRespVO> activityList) {
  188 + for (Long spuId : spuIds) {
  189 + // 校验商品是否已经加入过活动
  190 + if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) &&
  191 + ObjUtil.equal(appActivity.getSpuId(), spuId))) {
  192 + continue;
  193 + }
  194 + activityList.add(new AppActivityRespVO(rewardActivity.getId(),
  195 + PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId,
  196 + rewardActivity.getStartTime(), rewardActivity.getEndTime()));
169 197 }
170 198 }
171 199  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java
... ... @@ -14,24 +14,20 @@ import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivity
14 14 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
15 15 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
16 16 import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
17   -import com.google.common.cache.CacheLoader;
18   -import com.google.common.cache.LoadingCache;
19 17 import io.swagger.v3.oas.annotations.Operation;
20 18 import io.swagger.v3.oas.annotations.Parameter;
21 19 import io.swagger.v3.oas.annotations.tags.Tag;
  20 +import jakarta.annotation.Resource;
22 21 import org.springframework.validation.annotation.Validated;
23 22 import org.springframework.web.bind.annotation.GetMapping;
24 23 import org.springframework.web.bind.annotation.RequestMapping;
25 24 import org.springframework.web.bind.annotation.RequestParam;
26 25 import org.springframework.web.bind.annotation.RestController;
27 26  
28   -import jakarta.annotation.Resource;
29   -import java.time.Duration;
30 27 import java.util.Collections;
31 28 import java.util.List;
32 29  
33 30 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
34   -import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
35 31 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
36 32  
37 33 @Tag(name = "用户 APP - 拼团活动")
... ... @@ -40,45 +36,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
40 36 @Validated
41 37 public class AppCombinationActivityController {
42 38  
43   - /**
44   - * {@link AppCombinationActivityRespVO} 缓存,通过它异步刷新 {@link #getCombinationActivityList0(Integer)} 所要的首页数据
45   - */
46   - private final LoadingCache<Integer, List<AppCombinationActivityRespVO>> combinationActivityListCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
47   - new CacheLoader<Integer, List<AppCombinationActivityRespVO>>() {
48   -
49   - @Override
50   - public List<AppCombinationActivityRespVO> load(Integer count) {
51   - return getCombinationActivityList0(count);
52   - }
53   -
54   - });
55   -
56 39 @Resource
57 40 private CombinationActivityService activityService;
58 41  
59 42 @Resource
60 43 private ProductSpuApi spuApi;
61 44  
62   - @GetMapping("/list")
63   - @Operation(summary = "获得拼团活动列表", description = "用于小程序首页")
64   - @Parameter(name = "count", description = "需要展示的数量", example = "6")
65   - public CommonResult<List<AppCombinationActivityRespVO>> getCombinationActivityList(
66   - @RequestParam(name = "count", defaultValue = "6") Integer count) {
67   - return success(combinationActivityListCache.getUnchecked(count));
68   - }
69   -
70   - private List<AppCombinationActivityRespVO> getCombinationActivityList0(Integer count) {
71   - List<CombinationActivityDO> activityList = activityService.getCombinationActivityListByCount(count);
72   - if (CollUtil.isEmpty(activityList)) {
73   - return Collections.emptyList();
74   - }
75   - // 拼接返回
76   - List<CombinationProductDO> productList = activityService.getCombinationProductListByActivityIds(
77   - convertList(activityList, CombinationActivityDO::getId));
78   - List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId));
79   - return CombinationActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList);
80   - }
81   -
82 45 @GetMapping("/page")
83 46 @Operation(summary = "获得拼团活动分页")
84 47 public CommonResult<PageResult<AppCombinationActivityRespVO>> getCombinationActivityPage(PageParam pageParam) {
... ... @@ -93,6 +56,23 @@ public class AppCombinationActivityController {
93 56 return success(CombinationActivityConvert.INSTANCE.convertAppPage(pageResult, productList, spuList));
94 57 }
95 58  
  59 + @GetMapping("/list-by-ids")
  60 + @Operation(summary = "获得拼团活动列表,基于活动编号数组")
  61 + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
  62 + public CommonResult<List<AppCombinationActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
  63 + // 1. 获得开启的活动列表
  64 + List<CombinationActivityDO> activityList = activityService.getCombinationActivityListByIds(ids);
  65 + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
  66 + if (CollUtil.isEmpty(activityList)) {
  67 + return success(Collections.emptyList());
  68 + }
  69 + // 2. 拼接返回
  70 + List<CombinationProductDO> productList = activityService.getCombinationProductListByActivityIds(
  71 + convertList(activityList, CombinationActivityDO::getId));
  72 + List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId));
  73 + return success(CombinationActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList));
  74 + }
  75 +
96 76 @GetMapping("/get-detail")
97 77 @Operation(summary = "获得拼团活动明细")
98 78 @Parameter(name = "id", description = "活动编号", required = true, example = "1024")
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java
... ... @@ -10,9 +10,7 @@ import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.Ap
10 10 import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordSummaryRespVO;
11 11 import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
12 12 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
13   -import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
14 13 import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
15   -import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
16 14 import io.swagger.v3.oas.annotations.Operation;
17 15 import io.swagger.v3.oas.annotations.Parameter;
18 16 import io.swagger.v3.oas.annotations.Parameters;
... ... @@ -20,7 +18,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
20 18 import jakarta.annotation.Resource;
21 19 import jakarta.validation.Valid;
22 20 import jakarta.validation.constraints.Max;
23   -import org.springframework.context.annotation.Lazy;
24 21 import org.springframework.validation.annotation.Validated;
25 22 import org.springframework.web.bind.annotation.GetMapping;
26 23 import org.springframework.web.bind.annotation.RequestMapping;
... ... @@ -43,9 +40,6 @@ public class AppCombinationRecordController {
43 40  
44 41 @Resource
45 42 private CombinationRecordService combinationRecordService;
46   - @Resource
47   - @Lazy
48   - private TradeOrderApi tradeOrderApi;
49 43  
50 44 @GetMapping("/get-summary")
51 45 @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
... ... @@ -117,26 +111,4 @@ public class AppCombinationRecordController {
117 111 return success(CombinationActivityConvert.INSTANCE.convert(getLoginUserId(), headRecord, memberRecords));
118 112 }
119 113  
120   - @GetMapping("/cancel")
121   - @Operation(summary = "取消拼团")
122   - @Parameter(name = "id", description = "拼团记录编号", required = true, example = "1024")
123   - public CommonResult<Boolean> cancelCombinationRecord(@RequestParam("id") Long id) {
124   - Long userId = getLoginUserId();
125   - // 1、查找这条拼团记录
126   - CombinationRecordDO record = combinationRecordService.getCombinationRecordByIdAndUser(userId, id);
127   - if (record == null) {
128   - return success(Boolean.FALSE);
129   - }
130   - // 1.1、需要先校验拼团记录未完成;
131   - if (!CombinationRecordStatusEnum.isInProgress(record.getStatus())) {
132   - return success(Boolean.FALSE);
133   - }
134   -
135   - // 2. 取消已支付的订单
136   - tradeOrderApi.cancelPaidOrder(userId, record.getOrderId());
137   - // 3. 取消拼团记录
138   - combinationRecordService.cancelCombinationRecord(userId, record.getId(), record.getHeadId());
139   - return success(Boolean.TRUE);
140   - }
141   -
142 114 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java
... ... @@ -19,15 +19,14 @@ public class AppCombinationActivityRespVO {
19 19 @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
20 20 private Long spuId;
21 21  
  22 + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜")
  23 + private String spuName; // 从 SPU 的 name 读取
22 24 @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
23   - // 从 SPU 的 picUrl 读取
24   - private String picUrl;
25   -
  25 + private String picUrl; // 从 SPU 的 picUrl 读取
26 26 @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
27   - // 从 SPU 的 marketPrice 读取
28   - private Integer marketPrice;
  27 + private Integer marketPrice; // 从 SPU 的 marketPrice 读取
29 28  
30 29 @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
31   - private Integer combinationPrice;
  30 + private Integer combinationPrice; // 从 products 获取最小 price 读取
32 31  
33 32 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java
... ... @@ -5,7 +5,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 7 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
8   -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.*;
  8 +import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO;
  9 +import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO;
  10 +import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponTakeReqVO;
9 11 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
10 12 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
11 13 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
... ... @@ -15,13 +17,12 @@ import cn.iocoder.yudao.module.promotion.service.coupon.CouponTemplateService;
15 17 import io.swagger.v3.oas.annotations.Operation;
16 18 import io.swagger.v3.oas.annotations.Parameter;
17 19 import io.swagger.v3.oas.annotations.tags.Tag;
  20 +import jakarta.annotation.Resource;
  21 +import jakarta.validation.Valid;
18 22 import org.springframework.validation.annotation.Validated;
19 23 import org.springframework.web.bind.annotation.*;
20 24  
21   -import jakarta.annotation.Resource;
22   -import jakarta.validation.Valid;
23 25 import java.util.Collections;
24   -import java.util.List;
25 26  
26 27 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
27 28 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
... ... @@ -56,14 +57,6 @@ public class AppCouponController {
56 57 return success(canTakeAgain);
57 58 }
58 59  
59   - @GetMapping("/match-list")
60   - @Operation(summary = "获得匹配指定商品的优惠劵列表", description = "用于下单页,展示优惠劵列表")
61   - public CommonResult<List<AppCouponMatchRespVO>> getMatchCouponList(AppCouponMatchReqVO matchReqVO) {
62   - // todo: 优化:优惠金额倒序
63   - List<CouponDO> list = couponService.getMatchCouponList(getLoginUserId(), matchReqVO);
64   - return success(BeanUtils.toBean(list, AppCouponMatchRespVO.class));
65   - }
66   -
67 60 @GetMapping("/page")
68 61 @Operation(summary = "我的优惠劵列表")
69 62 @PreAuthenticated
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java deleted
1   -package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
2   -
3   -import io.swagger.v3.oas.annotations.media.Schema;
4   -import lombok.Data;
5   -
6   -import jakarta.validation.constraints.NotEmpty;
7   -import jakarta.validation.constraints.NotNull;
8   -import java.util.List;
9   -
10   -@Schema(description = "用户 App - 优惠劵的匹配 Request VO")
11   -@Data
12   -public class AppCouponMatchReqVO {
13   -
14   - @Schema(description = "商品金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
15   - @NotNull(message = "商品金额不能为空")
16   - private Integer price;
17   -
18   - @Schema(description = "商品 SPU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
19   - @NotEmpty(message = "商品 SPU 编号不能为空")
20   - private List<Long> spuIds;
21   -
22   - @Schema(description = "商品 SKU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
23   - @NotEmpty(message = "商品 SKU 编号不能为空")
24   - private List<Long> skuIds;
25   -
26   - @Schema(description = "分类编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[10, 20]")
27   - @NotEmpty(message = "分类编号不能为空")
28   - private List<Long> categoryIds;
29   -
30   -}
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java deleted
1   -package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
2   -
3   -import io.swagger.v3.oas.annotations.media.Schema;
4   -import lombok.Data;
5   -
6   -@Schema(description = "用户 App - 优惠劵 Response VO")
7   -@Data
8   -public class AppCouponMatchRespVO extends AppCouponRespVO {
9   -
10   - @Schema(description = "是否匹配", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
11   - private Boolean match;
12   -
13   - @Schema(description = "匹配条件的提示", example = "所结算商品没有符合条件的商品")
14   - private String description;
15   -
16   -}
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.Data;
5 5  
6   -import jakarta.validation.constraints.Min;
7 6 import java.time.LocalDateTime;
8 7 import java.util.List;
9 8  
... ... @@ -42,7 +41,6 @@ public class AppCouponRespVO {
42 41 private Integer discountPercent;
43 42  
44 43 @Schema(description = "优惠金额", example = "10")
45   - @Min(value = 0, message = "优惠金额需要大于等于 0")
46 44 private Integer discountPrice;
47 45  
48 46 @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java
1 1 package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.template;
2 2  
3   -import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
4   -import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
5   -import com.baomidou.mybatisplus.annotation.TableField;
6 3 import io.swagger.v3.oas.annotations.media.Schema;
7 4 import lombok.Data;
8 5  
... ... @@ -20,6 +17,9 @@ public class AppCouponTemplateRespVO {
20 17 @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送")
21 18 private String name;
22 19  
  20 + @Schema(description = "优惠券说明", example = "优惠券使用说明")
  21 + private String description;
  22 +
23 23 @Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制
24 24 private Integer takeLimitCount;
25 25  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java
... ... @@ -23,6 +23,7 @@ import com.google.common.cache.LoadingCache;
23 23 import io.swagger.v3.oas.annotations.Operation;
24 24 import io.swagger.v3.oas.annotations.Parameter;
25 25 import io.swagger.v3.oas.annotations.tags.Tag;
  26 +import jakarta.annotation.Resource;
26 27 import org.springframework.context.annotation.Lazy;
27 28 import org.springframework.validation.annotation.Validated;
28 29 import org.springframework.web.bind.annotation.GetMapping;
... ... @@ -30,11 +31,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
30 31 import org.springframework.web.bind.annotation.RequestParam;
31 32 import org.springframework.web.bind.annotation.RestController;
32 33  
33   -import jakarta.annotation.Resource;
34 34 import java.time.Duration;
35 35 import java.time.LocalDate;
36 36 import java.time.LocalDateTime;
37 37 import java.time.LocalTime;
  38 +import java.util.Collections;
38 39 import java.util.List;
39 40  
40 41 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
... ... @@ -86,7 +87,7 @@ public class AppSeckillActivityController {
86 87  
87 88 // 2.1 查询满足当前阶段的活动
88 89 List<SeckillActivityDO> activityList = activityService.getSeckillActivityListByConfigIdAndStatus(config.getId(), CommonStatusEnum.ENABLE.getStatus());
89   - List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityId(
  90 + List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityIds(
90 91 convertList(activityList, SeckillActivityDO::getId));
91 92 // 2.2 获取 spu 信息
92 93 List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId));
... ... @@ -101,7 +102,7 @@ public class AppSeckillActivityController {
101 102 if (CollUtil.isEmpty(pageResult.getList())) {
102 103 return success(PageResult.empty(pageResult.getTotal()));
103 104 }
104   - List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityId(
  105 + List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityIds(
105 106 convertList(pageResult.getList(), SeckillActivityDO::getId));
106 107  
107 108 // 2. 拼接数据
... ... @@ -149,4 +150,21 @@ public class AppSeckillActivityController {
149 150 return success(SeckillActivityConvert.INSTANCE.convert3(activity, productList, startTime, endTime));
150 151 }
151 152  
  153 + @GetMapping("/list-by-ids")
  154 + @Operation(summary = "获得拼团活动列表,基于活动编号数组")
  155 + @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
  156 + public CommonResult<List<AppSeckillActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
  157 + // 1. 获得开启的活动列表
  158 + List<SeckillActivityDO> activityList = activityService.getSeckillActivityListByIds(ids);
  159 + activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
  160 + if (CollUtil.isEmpty(activityList)) {
  161 + return success(Collections.emptyList());
  162 + }
  163 + // 2. 拼接返回
  164 + List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityIds(
  165 + convertList(activityList, SeckillActivityDO::getId));
  166 + List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId));
  167 + return success(SeckillActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList));
  168 + }
  169 +
152 170 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java
... ... @@ -16,6 +16,9 @@ public class AppSeckillActivityRespVO {
16 16 @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
17 17 private Long spuId;
18 18  
  19 + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜")
  20 + private String spuName; // 从 SPU 的 name 读取
  21 +
19 22 @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 picUrl 读取
20 23 example = "https://www.iocoder.cn/xx.png")
21 24 private String picUrl;
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
... ... @@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
6 6 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 8 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
8 9 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
9 10 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
... ... @@ -127,40 +128,42 @@ public interface CombinationActivityConvert {
127 128 .setSpuName(spu.getName()).setPicUrl(sku.getPicUrl());
128 129 }
129 130  
130   - List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list);
131   -
132   - default List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list,
133   - List<CombinationProductDO> productList,
134   - List<ProductSpuRespDTO> spuList) {
135   - List<AppCombinationActivityRespVO> activityList = convertAppList(list);
  131 + default List<CombinationActivityRespVO> convertList(List<CombinationActivityDO> list,
  132 + List<CombinationProductDO> productList,
  133 + List<ProductSpuRespDTO> spuList) {
  134 + List<CombinationActivityRespVO> activityList = BeanUtils.toBean(list, CombinationActivityRespVO.class);
136 135 Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
137 136 Map<Long, List<CombinationProductDO>> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId);
138 137 return CollectionUtils.convertList(activityList, item -> {
139 138 // 设置 product 信息
140 139 item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice));
141 140 // 设置 SPU 信息
142   - findAndThen(spuMap, item.getSpuId(), spu -> item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
  141 + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
  142 + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
143 143 return item;
144 144 });
145 145 }
146 146  
147   - PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result);
148   -
149   - default PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result,
150   - List<CombinationProductDO> productList,
151   - List<ProductSpuRespDTO> spuList) {
152   - PageResult<AppCombinationActivityRespVO> appPage = convertAppPage(result);
  147 + default List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list,
  148 + List<CombinationProductDO> productList,
  149 + List<ProductSpuRespDTO> spuList) {
  150 + List<AppCombinationActivityRespVO> activityList = BeanUtils.toBean(list, AppCombinationActivityRespVO.class);
153 151 Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
154 152 Map<Long, List<CombinationProductDO>> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId);
155   - List<AppCombinationActivityRespVO> list = CollectionUtils.convertList(appPage.getList(), item -> {
  153 + return CollectionUtils.convertList(activityList, item -> {
156 154 // 设置 product 信息
157 155 item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice));
158 156 // 设置 SPU 信息
159   - findAndThen(spuMap, item.getSpuId(), spu -> item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
  157 + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
  158 + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
160 159 return item;
161 160 });
162   - appPage.setList(list);
163   - return appPage;
  161 + }
  162 +
  163 + default PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result,
  164 + List<CombinationProductDO> productList,
  165 + List<ProductSpuRespDTO> spuList) {
  166 + return new PageResult<>(convertAppList(result.getList(), productList, spuList), result.getTotal());
164 167 }
165 168  
166 169 AppCombinationActivityDetailRespVO convert2(CombinationActivityDO combinationActivity);
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java
... ... @@ -4,9 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
5 5 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO;
6 6 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
7   -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchRespVO;
8 7 import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO;
9   -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO;
10 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
11 9 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
12 10 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
... ... @@ -16,7 +14,6 @@ import org.mapstruct.factory.Mappers;
16 14  
17 15 import java.time.LocalDateTime;
18 16 import java.util.Collection;
19   -import java.util.List;
20 17  
21 18 /**
22 19 * 优惠劵 Convert
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java deleted
1   -package cn.iocoder.yudao.module.promotion.convert.reward;
2   -
3   -import cn.iocoder.yudao.framework.common.pojo.PageResult;
4   -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
5   -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
6   -import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
7   -import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
8   -import org.mapstruct.Mapper;
9   -import org.mapstruct.factory.Mappers;
10   -
11   -/**
12   - * 满减送活动 Convert
13   - *
14   - * @author 芋道源码
15   - */
16   -@Mapper
17   -public interface RewardActivityConvert {
18   -
19   - RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class);
20   -
21   - RewardActivityDO convert(RewardActivityCreateReqVO bean);
22   -
23   - RewardActivityDO convert(RewardActivityUpdateReqVO bean);
24   -
25   - RewardActivityRespVO convert(RewardActivityDO bean);
26   -
27   - PageResult<RewardActivityRespVO> convertPage(PageResult<RewardActivityDO> page);
28   -
29   -}
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.convert.seckill;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 5 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 7 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
7 8 import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinRespDTO;
8 9 import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO;
... ... @@ -87,6 +88,38 @@ public interface SeckillActivityConvert {
87 88 return CollectionUtils.convertList(products, item -> convert(activity, item).setActivityStatus(activity.getStatus()));
88 89 }
89 90  
  91 + default List<SeckillActivityRespVO> convertList(List<SeckillActivityDO> list,
  92 + List<SeckillProductDO> productList,
  93 + List<ProductSpuRespDTO> spuList) {
  94 + List<SeckillActivityRespVO> activityList = BeanUtils.toBean(list, SeckillActivityRespVO.class);
  95 + Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
  96 + Map<Long, List<SeckillProductDO>> productMap = convertMultiMap(productList, SeckillProductDO::getActivityId);
  97 + return CollectionUtils.convertList(activityList, item -> {
  98 + // 设置 product 信息
  99 + item.setSeckillPrice(getMinValue(productMap.get(item.getId()), SeckillProductDO::getSeckillPrice));
  100 + // 设置 SPU 信息
  101 + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
  102 + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
  103 + return item;
  104 + });
  105 + }
  106 +
  107 + default List<AppSeckillActivityRespVO> convertAppList(List<SeckillActivityDO> list,
  108 + List<SeckillProductDO> productList,
  109 + List<ProductSpuRespDTO> spuList) {
  110 + List<AppSeckillActivityRespVO> activityList = BeanUtils.toBean(list, AppSeckillActivityRespVO.class);
  111 + Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
  112 + Map<Long, List<SeckillProductDO>> productMap = convertMultiMap(productList, SeckillProductDO::getActivityId);
  113 + return CollectionUtils.convertList(activityList, item -> {
  114 + // 设置 product 信息
  115 + item.setSeckillPrice(getMinValue(productMap.get(item.getId()), SeckillProductDO::getSeckillPrice));
  116 + // 设置 SPU 信息
  117 + findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
  118 + .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
  119 + return item;
  120 + });
  121 + }
  122 +
90 123 List<SeckillProductRespVO> convertList2(List<SeckillProductDO> list);
91 124  
92 125 List<AppSeckillActivityRespVO> convertList3(List<SeckillActivityDO> activityList);
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java
... ... @@ -50,7 +50,6 @@ public class CouponDO extends BaseDO {
50 50 *
51 51 * 枚举 {@link CouponStatusEnum}
52 52 */
53   - // TODO 芋艿:已作废?
54 53 private Integer status;
55 54  
56 55 // TODO 芋艿:发放 adminid?
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java
... ... @@ -41,6 +41,10 @@ public class CouponTemplateDO extends BaseDO {
41 41 */
42 42 private String name;
43 43 /**
  44 + * 优惠券说明
  45 + */
  46 + private String description;
  47 + /**
44 48 * 状态
45 49 *
46 50 * 枚举 {@link CommonStatusEnum}
... ... @@ -158,9 +162,9 @@ public class CouponTemplateDO extends BaseDO {
158 162 * 使用优惠券的次数
159 163 */
160 164 private Integer useCount;
  165 +
161 166 // ========== 统计信息 END ==========
162 167  
163 168 // TODO 芋艿:领取开始时间、领取结束时间
164 169  
165   - // TODO 芋艿:要不要加描述
166 170 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java
1 1 package cn.iocoder.yudao.module.promotion.dal.dataobject.reward;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5   -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
6 6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
7 7 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
8 8 import com.baomidou.mybatisplus.annotation.KeySequence;
... ... @@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode;
16 16 import java.io.Serializable;
17 17 import java.time.LocalDateTime;
18 18 import java.util.List;
  19 +import java.util.Map;
19 20  
20 21 /**
21 22 * 满减送活动 DO
... ... @@ -40,7 +41,7 @@ public class RewardActivityDO extends BaseDO {
40 41 /**
41 42 * 状态
42 43 *
43   - * 枚举 {@link PromotionActivityStatusEnum}
  44 + * 枚举 {@link CommonStatusEnum}
44 45 */
45 46 private Integer status;
46 47 /**
... ... @@ -71,7 +72,7 @@ public class RewardActivityDO extends BaseDO {
71 72 * 商品 SPU 编号的数组
72 73 */
73 74 @TableField(typeHandler = LongListTypeHandler.class)
74   - private List<Long> productSpuIds;
  75 + private List<Long> productScopeValues;
75 76 /**
76 77 * 优惠规则的数组
77 78 */
... ... @@ -104,13 +105,14 @@ public class RewardActivityDO extends BaseDO {
104 105 */
105 106 private Integer point;
106 107 /**
107   - * 赠送的优惠劵编号的数组
108   - */
109   - private List<Long> couponIds;
110   - /**
111   - * 赠送的优惠券数量的数组
  108 + * 赠送的优惠劵
  109 + *
  110 + * key: 优惠劵模版编号
  111 + * value:对应的优惠券数量
  112 + *
  113 + * 目的:用于订单支付后赠送优惠券
112 114 */
113   - private List<Integer> couponCounts;
  115 + private Map<Long, Integer> giveCouponTemplateCounts;
114 116  
115 117 }
116 118  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java
1 1 package cn.iocoder.yudao.module.promotion.dal.mysql.coupon;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.StrUtil;
5 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
7 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
8 7 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
9 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
10   -import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
11 9 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
12 10 import com.github.yulichang.toolkit.MPJWrappers;
13 11 import org.apache.ibatis.annotations.Mapper;
... ... @@ -16,8 +14,6 @@ import java.time.LocalDateTime;
16 14 import java.util.Collection;
17 15 import java.util.List;
18 16 import java.util.Map;
19   -import java.util.function.Function;
20   -import java.util.stream.Collectors;
21 17  
22 18 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
23 19  
... ... @@ -84,22 +80,6 @@ public interface CouponMapper extends BaseMapperX&lt;CouponDO&gt; {
84 80 return convertMap(list, map -> MapUtil.getLong(map, templateIdAlias), map -> MapUtil.getInt(map, countAlias));
85 81 }
86 82  
87   - default List<CouponDO> selectListByUserIdAndStatusAndUsePriceLeAndProductScope(
88   - Long userId, Integer status, Integer usePrice, List<Long> spuIds, List<Long> categoryIds) {
89   - Function<List<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
90   - .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
91   - .collect(Collectors.joining(" OR "));
92   - return selectList(new LambdaQueryWrapperX<CouponDO>()
93   - .eq(CouponDO::getUserId, userId)
94   - .eq(CouponDO::getStatus, status)
95   - .le(CouponDO::getUsePrice, usePrice) // 价格小于等于,满足价格使用条件
96   - .and(w -> w.eq(CouponDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()) // 商品范围一:全部
97   - .or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.SPU.getScope()) // 商品范围二:满足指定商品
98   - .apply(productScopeValuesFindInSetFunc.apply(spuIds)))
99   - .or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope()) // 商品范围三:满足指定分类
100   - .apply(productScopeValuesFindInSetFunc.apply(categoryIds)))));
101   - }
102   -
103 83 default List<CouponDO> selectListByStatusAndValidEndTimeLe(Integer status, LocalDateTime validEndTime) {
104 84 return selectList(new LambdaQueryWrapperX<CouponDO>()
105 85 .eq(CouponDO::getStatus, status)
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
... ... @@ -30,19 +30,9 @@ public interface RewardActivityMapper extends BaseMapperX&lt;RewardActivityDO&gt; {
30 30 .orderByDesc(RewardActivityDO::getId));
31 31 }
32 32  
33   - default List<RewardActivityDO> selectListByStatus(Collection<Integer> statuses) {
34   - return selectList(RewardActivityDO::getStatus, statuses);
35   - }
36   -
37   - default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) {
38   - return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
39   - .eq(RewardActivityDO::getProductScope, productScope)
40   - .eq(RewardActivityDO::getStatus, status));
41   - }
42   -
43 33 default List<RewardActivityDO> selectListBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
44 34 Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
45   - .map(id -> StrUtil.format("FIND_IN_SET({}, product_spu_ids) ", id))
  35 + .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
46 36 .collect(Collectors.joining(" OR "));
47 37 return selectList(new QueryWrapper<RewardActivityDO>()
48 38 .eq("status", status)
... ... @@ -53,16 +43,16 @@ public interface RewardActivityMapper extends BaseMapperX&lt;RewardActivityDO&gt; {
53 43 * 获取指定活动编号的活动列表且
54 44 * 开始时间和结束时间小于给定时间 dateTime 的活动列表
55 45 *
56   - * @param ids 活动编号
  46 + * @param status 状态
57 47 * @param dateTime 指定日期
58 48 * @return 活动列表
59 49 */
60   - default List<RewardActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
  50 + default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
61 51 return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
62   - .in(RewardActivityDO::getId, ids)
  52 + .eq(RewardActivityDO::getStatus, status)
63 53 .lt(RewardActivityDO::getStartTime, dateTime)
64 54 .gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
65   - .orderByDesc(RewardActivityDO::getCreateTime)
  55 + .orderByAsc(RewardActivityDO::getStartTime)
66 56 );
67 57 }
68 58  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
... ... @@ -72,7 +72,6 @@ public interface SeckillActivityMapper extends BaseMapperX&lt;SeckillActivityDO&gt; {
72 72 default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
73 73 return selectPage(pageReqVO, new LambdaQueryWrapperX<SeckillActivityDO>()
74 74 .eqIfPresent(SeckillActivityDO::getStatus, status)
75   - // TODO 芋艿:对 find in set 的想法;
76 75 .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
77 76 }
78 77  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/coupon/CouponExpireJob.java
... ... @@ -8,7 +8,6 @@ import org.springframework.stereotype.Component;
8 8  
9 9 import jakarta.annotation.Resource;
10 10  
11   -// TODO 芋艿:配置一个 Job
12 11 /**
13 12 * 优惠券过期 Job
14 13 *
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java
... ... @@ -103,14 +103,6 @@ public interface CombinationActivityService {
103 103 /**
104 104 * 获取正在进行的活动分页数据
105 105 *
106   - * @param count 需要的数量
107   - * @return 拼团活动分页
108   - */
109   - List<CombinationActivityDO> getCombinationActivityListByCount(Integer count);
110   -
111   - /**
112   - * 获取正在进行的活动分页数据
113   - *
114 106 * @param pageParam 分页请求
115 107 * @return 拼团活动分页
116 108 */
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
... ... @@ -226,11 +226,6 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
226 226 }
227 227  
228 228 @Override
229   - public List<CombinationActivityDO> getCombinationActivityListByCount(Integer count) {
230   - return combinationActivityMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus(), count);
231   - }
232   -
233   - @Override
234 229 public PageResult<CombinationActivityDO> getCombinationActivityPage(PageParam pageParam) {
235 230 return combinationActivityMapper.selectPage(pageParam, CommonStatusEnum.ENABLE.getStatus());
236 231 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java
... ... @@ -140,24 +140,6 @@ public interface CombinationRecordService {
140 140 @Nullable Long headId);
141 141  
142 142 /**
143   - * 获取拼团记录
144   - *
145   - * @param userId 用户编号
146   - * @param id 拼团记录编号
147   - * @return 拼团记录
148   - */
149   - CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id);
150   -
151   - /**
152   - * 取消拼团
153   - *
154   - * @param userId 用户编号
155   - * @param id 拼团记录编号
156   - * @param headId 团长编号
157   - */
158   - void cancelCombinationRecord(Long userId, Long id, Long headId);
159   -
160   - /**
161 143 * 处理过期拼团
162 144 *
163 145 * @return key 过期拼团数量, value 虚拟成团数量
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
... ... @@ -27,6 +27,7 @@ import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStat
27 27 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
28 28 import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
29 29 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
  30 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum;
30 31 import jakarta.annotation.Nullable;
31 32 import jakarta.annotation.Resource;
32 33 import lombok.extern.slf4j.Slf4j;
... ... @@ -37,7 +38,10 @@ import org.springframework.transaction.annotation.Transactional;
37 38 import org.springframework.validation.annotation.Validated;
38 39  
39 40 import java.time.LocalDateTime;
40   -import java.util.*;
  41 +import java.util.ArrayList;
  42 +import java.util.Collection;
  43 +import java.util.List;
  44 +import java.util.Map;
41 45  
42 46 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
43 47 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
... ... @@ -69,7 +73,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
69 73 private ProductSpuApi productSpuApi;
70 74 @Resource
71 75 private ProductSkuApi productSkuApi;
72   -
73 76 @Resource
74 77 @Lazy // 延迟加载,避免循环依赖
75 78 private TradeOrderApi tradeOrderApi;
... ... @@ -290,61 +293,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
290 293 }
291 294  
292 295 @Override
293   - public CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id) {
294   - return combinationRecordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
295   - }
296   -
297   - @Override
298   - @Transactional(rollbackFor = Exception.class)
299   - public void cancelCombinationRecord(Long userId, Long id, Long headId) {
300   - // 删除记录
301   - combinationRecordMapper.deleteById(id);
302   -
303   - // 需要更新的记录
304   - List<CombinationRecordDO> updateRecords = new ArrayList<>();
305   - // 如果它是团长,则顺序(下单时间)继承
306   - if (Objects.equals(headId, CombinationRecordDO.HEAD_ID_GROUP)) { // 情况一:团长
307   - // 团员
308   - List<CombinationRecordDO> list = getCombinationRecordListByHeadId(id);
309   - if (CollUtil.isEmpty(list)) {
310   - return;
311   - }
312   - // 按照创建时间升序排序
313   - list.sort(Comparator.comparing(CombinationRecordDO::getCreateTime)); // 影响原 list
314   - CombinationRecordDO newHead = list.get(0); // 新团长继位
315   - list.forEach(item -> {
316   - CombinationRecordDO recordDO = new CombinationRecordDO();
317   - recordDO.setId(item.getId());
318   - if (ObjUtil.equal(item.getId(), newHead.getId())) { // 新团长
319   - recordDO.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
320   - } else {
321   - recordDO.setHeadId(newHead.getId());
322   - }
323   - recordDO.setUserCount(list.size());
324   - updateRecords.add(recordDO);
325   - });
326   - } else { // 情况二:团员
327   - // 团长
328   - CombinationRecordDO recordHead = combinationRecordMapper.selectById(headId);
329   - // 团员
330   - List<CombinationRecordDO> records = getCombinationRecordListByHeadId(headId);
331   - if (CollUtil.isEmpty(records)) {
332   - return;
333   - }
334   - records.add(recordHead); // 加入团长,团长数据也需要更新
335   - records.forEach(item -> {
336   - CombinationRecordDO recordDO = new CombinationRecordDO();
337   - recordDO.setId(item.getId());
338   - recordDO.setUserCount(records.size());
339   - updateRecords.add(recordDO);
340   - });
341   - }
342   -
343   - // 更新拼团记录
344   - combinationRecordMapper.updateBatch(updateRecords);
345   - }
346   -
347   - @Override
348 296 public KeyValue<Integer, Integer> expireCombinationRecord() {
349 297 // 1. 获取所有正在进行中的过期的父拼团
350 298 List<CombinationRecordDO> headExpireRecords = combinationRecordMapper.selectListByHeadIdAndStatusAndExpireTimeLt(
... ... @@ -391,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
391 339 List<CombinationRecordDO> headAndRecords = updateBatchCombinationRecords(headRecord,
392 340 CombinationRecordStatusEnum.FAILED);
393 341 // 2. 订单取消
394   - headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId()));
  342 + headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(),
  343 + TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()));
395 344 }
396 345  
397 346 /**
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
... ... @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.map.MapUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
7   -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO;
8 7 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
9 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
10 9 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
... ... @@ -19,34 +18,6 @@ import java.util.*;
19 18 public interface CouponService {
20 19  
21 20 /**
22   - * 校验优惠劵,包括状态、有限期
23   - * <p>
24   - * 1. 如果校验通过,则返回优惠劵信息
25   - * 2. 如果校验不通过,则直接抛出业务异常
26   - *
27   - * @param id 优惠劵编号
28   - * @param userId 用户编号
29   - * @return 优惠劵信息
30   - */
31   - CouponDO validCoupon(Long id, Long userId);
32   -
33   - /**
34   - * 校验优惠劵,包括状态、有限期
35   - *
36   - * @param coupon 优惠劵
37   - * @see #validCoupon(Long, Long) 逻辑相同,只是入参不同
38   - */
39   - void validCoupon(CouponDO coupon);
40   -
41   - /**
42   - * 获得优惠劵分页
43   - *
44   - * @param pageReqVO 分页查询
45   - * @return 优惠劵分页
46   - */
47   - PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
48   -
49   - /**
50 21 * 使用优惠劵
51 22 *
52 23 * @param id 优惠劵编号
... ... @@ -70,42 +41,44 @@ public interface CouponService {
70 41 void deleteCoupon(Long id);
71 42  
72 43 /**
73   - * 获得用户的优惠劵列表
74   - *
75   - * @param userId 用户编号
76   - * @param status 优惠劵状态
77   - * @return 优惠劵列表
78   - */
79   - List<CouponDO> getCouponList(Long userId, Integer status);
80   -
81   - /**
82   - * 获得未使用的优惠劵数量
83   - *
84   - * @param userId 用户编号
85   - * @return 未使用的优惠劵数量
86   - */
87   - Long getUnusedCouponCount(Long userId);
88   -
89   - /**
90 44 * 领取优惠券
91 45 *
92 46 * @param templateId 优惠券模板编号
93 47 * @param userIds 用户编号列表
94 48 * @param takeType 领取方式
  49 + * @return key: userId, value: 优惠券编号列表
95 50 */
96   - void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
  51 + Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
97 52  
98 53 /**
99 54 * 【管理员】给用户发送优惠券
100 55 *
101 56 * @param templateId 优惠券模板编号
102 57 * @param userIds 用户编号列表
  58 + * @return key: userId, value: 优惠券编号列表
103 59 */
104   - default void takeCouponByAdmin(Long templateId, Set<Long> userIds) {
105   - takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
  60 + default Map<Long, List<Long>> takeCouponByAdmin(Long templateId, Set<Long> userIds) {
  61 + return takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
106 62 }
107 63  
108 64 /**
  65 + * 【管理员】给指定用户批量发送优惠券
  66 + *
  67 + * @param giveCoupons key: 优惠劵模版编号,value:对应的数量
  68 + * @param userId 用户编号
  69 + * @return 优惠券编号列表
  70 + */
  71 + List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
  72 +
  73 + /**
  74 + * 【管理员】作废指定用户的指定优惠劵
  75 + *
  76 + * @param giveCouponIds 赠送的优惠券编号
  77 + * @param userId 用户编号
  78 + */
  79 + void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
  80 +
  81 + /**
109 82 * 【会员】领取优惠券
110 83 *
111 84 * @param templateId 优惠券模板编号
... ... @@ -123,16 +96,38 @@ public interface CouponService {
123 96 void takeCouponByRegister(Long userId);
124 97  
125 98 /**
126   - * 获取会员领取指定优惠券的数量
  99 + * 过期优惠券
127 100 *
128   - * @param templateId 优惠券模板编号
129   - * @param userId 用户编号
130   - * @return 领取优惠券的数量
  101 + * @return 过期数量
131 102 */
132   - default Integer getTakeCount(Long templateId, Long userId) {
133   - Map<Long, Integer> map = getTakeCountMapByTemplateIds(Collections.singleton(templateId), userId);
134   - return MapUtil.getInt(map, templateId, 0);
135   - }
  103 + int expireCoupon();
  104 +
  105 + // ======================= 查询相关 =======================
  106 +
  107 + /**
  108 + * 获得未使用的优惠劵数量
  109 + *
  110 + * @param userId 用户编号
  111 + * @return 未使用的优惠劵数量
  112 + */
  113 + Long getUnusedCouponCount(Long userId);
  114 +
  115 + /**
  116 + * 获得优惠劵分页
  117 + *
  118 + * @param pageReqVO 分页查询
  119 + * @return 优惠劵分页
  120 + */
  121 + PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
  122 +
  123 + /**
  124 + * 获得用户的优惠劵列表
  125 + *
  126 + * @param userId 用户编号
  127 + * @param status 优惠劵状态
  128 + * @return 优惠劵列表
  129 + */
  130 + List<CouponDO> getCouponList(Long userId, Integer status);
136 131  
137 132 /**
138 133 * 统计会员领取优惠券的数量
... ... @@ -144,20 +139,16 @@ public interface CouponService {
144 139 Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId);
145 140  
146 141 /**
147   - * 获取用户匹配的优惠券列表
  142 + * 获取会员领取指定优惠券的数量
148 143 *
  144 + * @param templateId 优惠券模板编号
149 145 * @param userId 用户编号
150   - * @param matchReqVO 匹配参数
151   - * @return 优惠券列表
152   - */
153   - List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO);
154   -
155   - /**
156   - * 过期优惠券
157   - *
158   - * @return 过期数量
  146 + * @return 领取优惠券的数量
159 147 */
160   - int expireCoupon();
  148 + default Integer getTakeCount(Long templateId, Long userId) {
  149 + Map<Long, Integer> map = getTakeCountMapByTemplateIds(Collections.singleton(templateId), userId);
  150 + return MapUtil.getInt(map, templateId, 0);
  151 + }
161 152  
162 153 /**
163 154 * 获取用户是否可以领取优惠券
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.service.coupon;
3 3 import cn.hutool.core.collection.CollStreamUtil;
4 4 import cn.hutool.core.collection.CollUtil;
5 5 import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.util.ObjUtil;
6 7 import cn.hutool.core.util.ObjectUtil;
7 8 import cn.hutool.core.util.StrUtil;
8 9 import cn.hutool.extra.spring.SpringUtil;
... ... @@ -11,7 +12,6 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
11 12 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
12 13 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
13 14 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
14   -import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO;
15 15 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
16 16 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
17 17 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
... ... @@ -19,19 +19,19 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper;
19 19 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
20 20 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
21 21 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
  22 +import jakarta.annotation.Resource;
22 23 import lombok.extern.slf4j.Slf4j;
23 24 import org.springframework.stereotype.Service;
24 25 import org.springframework.transaction.annotation.Transactional;
25 26 import org.springframework.validation.annotation.Validated;
26 27  
27   -import jakarta.annotation.Resource;
28   -
29 28 import java.time.LocalDateTime;
30 29 import java.util.*;
31 30 import java.util.stream.Collectors;
32 31  
33 32 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
34 33 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
  34 +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
35 35 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
36 36 import static java.util.Arrays.asList;
37 37  
... ... @@ -55,18 +55,9 @@ public class CouponServiceImpl implements CouponService {
55 55 private MemberUserApi memberUserApi;
56 56  
57 57 @Override
58   - public CouponDO validCoupon(Long id, Long userId) {
59   - CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId);
60   - if (coupon == null) {
61   - throw exception(COUPON_NOT_EXISTS);
62   - }
63   - validCoupon(coupon);
64   - return coupon;
65   - }
66   -
67   - @Override
68   - public void validCoupon(CouponDO coupon) {
  58 + public void useCoupon(Long id, Long userId, Long orderId) {
69 59 // 校验状态
  60 + CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId);
70 61 if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) {
71 62 throw exception(COUPON_STATUS_NOT_UNUSED);
72 63 }
... ... @@ -74,26 +65,6 @@ public class CouponServiceImpl implements CouponService {
74 65 if (!LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) {
75 66 throw exception(COUPON_VALID_TIME_NOT_NOW);
76 67 }
77   - }
78   -
79   - @Override
80   - public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {
81   - // 获得用户编号
82   - if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
83   - List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
84   - if (CollUtil.isEmpty(users)) {
85   - return PageResult.empty();
86   - }
87   - pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
88   - }
89   - // 分页查询
90   - return couponMapper.selectPage(pageReqVO);
91   - }
92   -
93   - @Override
94   - public void useCoupon(Long id, Long userId, Long orderId) {
95   - // 校验优惠劵
96   - validCoupon(id, userId);
97 68  
98 69 // 更新状态
99 70 int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
... ... @@ -147,25 +118,8 @@ public class CouponServiceImpl implements CouponService {
147 118 }
148 119  
149 120 @Override
150   - public List<CouponDO> getCouponList(Long userId, Integer status) {
151   - return couponMapper.selectListByUserIdAndStatus(userId, status);
152   - }
153   -
154   - private CouponDO validateCouponExists(Long id) {
155   - CouponDO coupon = couponMapper.selectById(id);
156   - if (coupon == null) {
157   - throw exception(COUPON_NOT_EXISTS);
158   - }
159   - return coupon;
160   - }
161   -
162   - @Override
163   - public Long getUnusedCouponCount(Long userId) {
164   - return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
165   - }
166   -
167   - @Override
168   - public void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
  121 + @Transactional(rollbackFor = Exception.class)
  122 + public Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
169 123 CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
170 124 // 1. 过滤掉达到领取限制的用户
171 125 removeTakeLimitUser(userIds, template);
... ... @@ -173,37 +127,86 @@ public class CouponServiceImpl implements CouponService {
173 127 validateCouponTemplateCanTake(template, userIds, takeType);
174 128  
175 129 // 3. 批量保存优惠劵
176   - couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)));
  130 + List<CouponDO> couponList = convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId));
  131 + couponMapper.insertBatch(couponList);
177 132  
178   - // 3. 增加优惠劵模板的领取数量
  133 + // 4. 增加优惠劵模板的领取数量
179 134 couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
  135 +
  136 + return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
180 137 }
181 138  
182 139 @Override
183   - @Transactional(rollbackFor = Exception.class)
184   - public void takeCouponByRegister(Long userId) {
185   - List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER);
186   - for (CouponTemplateDO template : templates) {
187   - takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER);
  140 + public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
  141 + if (CollUtil.isEmpty(giveCoupons)) {
  142 + return Collections.emptyList();
188 143 }
  144 +
  145 + List<Long> couponIds = new ArrayList<>();
  146 + // 循环发放
  147 + for (Map.Entry<Long, Integer> entry : giveCoupons.entrySet()) {
  148 + try {
  149 + for (int i = 0; i < entry.getValue(); i++) {
  150 + Map<Long, List<Long>> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId),
  151 + CouponTakeTypeEnum.ADMIN);
  152 + findAndThen(userCouponIdsMap, userId, couponIds::addAll);
  153 + }
  154 + } catch (Exception e) {
  155 + log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败]", entry, e);
  156 + }
  157 + }
  158 + return couponIds;
189 159 }
190 160  
191 161 @Override
192   - public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
193   - if (CollUtil.isEmpty(templateIds)) {
194   - return Collections.emptyMap();
  162 + public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
  163 + // 循环收回
  164 + for (Long couponId : giveCouponIds) {
  165 + try {
  166 + getSelf().invalidateCoupon(couponId, userId);
  167 + } catch (Exception e) {
  168 + log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e);
  169 + }
195 170 }
196   - return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
  171 + }
  172 +
  173 + /**
  174 + * 【管理员】收回优惠券
  175 + *
  176 + * @param couponId 模版编号
  177 + * @param userId 用户编号
  178 + */
  179 + @Transactional(rollbackFor = Exception.class)
  180 + public void invalidateCoupon(Long couponId, Long userId) {
  181 + // 1.1 校验优惠券
  182 + CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId);
  183 + if (coupon == null) {
  184 + throw exception(COUPON_NOT_EXISTS);
  185 + }
  186 + // 1.2 校验模板
  187 + CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId());
  188 + if (couponTemplate == null) {
  189 + throw exception(COUPON_TEMPLATE_NOT_EXISTS);
  190 + }
  191 + // 1.3 校验优惠券是否已经使用,如若使用则先不管
  192 + if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) {
  193 + log.info("[invalidateCoupon][coupon({}) 已经使用,无法作废]", couponId);
  194 + return;
  195 + }
  196 +
  197 + // 2.1 减少优惠劵模板的领取数量
  198 + couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1);
  199 + // 2.2 作废优惠劵
  200 + couponMapper.deleteById(couponId);
197 201 }
198 202  
199 203 @Override
200   - public List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
201   - List<CouponDO> list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
202   - CouponStatusEnum.UNUSED.getStatus(),
203   - matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
204   - // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤
205   - list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
206   - return list;
  204 + @Transactional(rollbackFor = Exception.class)
  205 + public void takeCouponByRegister(Long userId) {
  206 + List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER);
  207 + for (CouponTemplateDO template : templates) {
  208 + takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER);
  209 + }
207 210 }
208 211  
209 212 @Override
... ... @@ -230,27 +233,6 @@ public class CouponServiceImpl implements CouponService {
230 233 return count;
231 234 }
232 235  
233   - @Override
234   - public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
235   - // 1. 未登录时,都显示可以领取
236   - Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
237   - if (userId == null) {
238   - return userCanTakeMap;
239   - }
240   -
241   - // 2.1 过滤领取数量无限制的
242   - Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
243   - // 2.2 检查用户领取的数量是否超过限制
244   - if (CollUtil.isNotEmpty(templateIds)) {
245   - Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
246   - for (CouponTemplateDO template : templates) {
247   - Integer takeCount = couponTakeCountMap.get(template.getId());
248   - userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
249   - }
250   - }
251   - return userCanTakeMap;
252   - }
253   -
254 236 /**
255 237 * 过期单个优惠劵
256 238 *
... ... @@ -322,11 +304,74 @@ public class CouponServiceImpl implements CouponService {
322 304 userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
323 305 }
324 306  
  307 + //======================= 查询相关 =======================
  308 +
  309 + @Override
  310 + public Long getUnusedCouponCount(Long userId) {
  311 + return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
  312 + }
  313 +
  314 + @Override
  315 + public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {
  316 + // 获得用户编号
  317 + if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
  318 + List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
  319 + if (CollUtil.isEmpty(users)) {
  320 + return PageResult.empty();
  321 + }
  322 + pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
  323 + }
  324 + // 分页查询
  325 + return couponMapper.selectPage(pageReqVO);
  326 + }
  327 +
  328 + @Override
  329 + public List<CouponDO> getCouponList(Long userId, Integer status) {
  330 + return couponMapper.selectListByUserIdAndStatus(userId, status);
  331 + }
  332 +
  333 + @Override
  334 + public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
  335 + if (CollUtil.isEmpty(templateIds)) {
  336 + return Collections.emptyMap();
  337 + }
  338 + return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
  339 + }
  340 +
  341 + @Override
  342 + public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
  343 + // 1. 未登录时,都显示可以领取
  344 + Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
  345 + if (userId == null) {
  346 + return userCanTakeMap;
  347 + }
  348 +
  349 + // 2.1 过滤领取数量无限制的
  350 + Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
  351 + // 2.2 检查用户领取的数量是否超过限制
  352 + if (CollUtil.isNotEmpty(templateIds)) {
  353 + Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
  354 + for (CouponTemplateDO template : templates) {
  355 + Integer takeCount = couponTakeCountMap.get(template.getId());
  356 + userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
  357 + }
  358 + }
  359 + return userCanTakeMap;
  360 + }
  361 +
325 362 @Override
326 363 public CouponDO getCoupon(Long userId, Long id) {
327 364 return couponMapper.selectByIdAndUserId(id, userId);
328 365 }
329 366  
  367 + private CouponDO validateCouponExists(Long id) {
  368 + CouponDO coupon = couponMapper.selectById(id);
  369 + if (coupon == null) {
  370 + throw exception(COUPON_NOT_EXISTS);
  371 + }
  372 + return coupon;
  373 + }
  374 +
330 375 /**
331 376 * 获得自身的代理对象,解决 AOP 生效问题
332 377 *
... ... @@ -335,4 +380,5 @@ public class CouponServiceImpl implements CouponService {
335 380 private CouponServiceImpl getSelf() {
336 381 return SpringUtil.getBean(getClass());
337 382 }
  383 +
338 384 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java
... ... @@ -104,7 +104,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
104 104 }
105 105 // 计算新增的记录
106 106 List<DiscountProductDO> newDiscountProducts = convertList(updateReqVO.getProducts(),
107   - product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(updateReqVO.getId()));
  107 + product -> DiscountActivityConvert.INSTANCE.convert(product)
  108 + .setActivityId(updateReqVO.getId())
  109 + .setActivityStartTime(updateReqVO.getStartTime())
  110 + .setActivityEndTime(updateReqVO.getEndTime()));
108 111 newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch(
109 112 dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的
110 113 if (CollectionUtil.isNotEmpty(newDiscountProducts)) {
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java
... ... @@ -75,11 +75,10 @@ public interface RewardActivityService {
75 75 /**
76 76 * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
77 77 *
78   - * @param spuIds spu 编号
79 78 * @param status 状态
80 79 * @param dateTime 当前日期时间
81 80 * @return 满减送活动列表
82 81 */
83   - List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
  82 + List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
84 83  
85 84 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
1 1 package cn.iocoder.yudao.module.promotion.service.reward;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
  7 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
5 8 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
  9 +import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO;
6 10 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
7 11 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
8 12 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
9   -import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;
10 13 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
11 14 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
12   -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
  15 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
13 16 import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
14 17 import jakarta.annotation.Resource;
15 18 import org.springframework.stereotype.Service;
... ... @@ -17,13 +20,13 @@ import org.springframework.validation.annotation.Validated;
17 20  
18 21 import java.time.LocalDateTime;
19 22 import java.util.Collection;
20   -import java.util.Collections;
21 23 import java.util.List;
  24 +import java.util.Objects;
22 25  
  26 +import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
23 27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
24   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
25 29 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
26   -import static java.util.Arrays.asList;
27 30  
28 31 /**
29 32 * 满减送活动 Service 实现类
... ... @@ -37,13 +40,20 @@ public class RewardActivityServiceImpl implements RewardActivityService {
37 40 @Resource
38 41 private RewardActivityMapper rewardActivityMapper;
39 42  
  43 + @Resource
  44 + private ProductCategoryApi productCategoryApi;
  45 + @Resource
  46 + private ProductSpuApi productSpuApi;
  47 +
40 48 @Override
41 49 public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) {
42   - // 校验商品是否冲突
43   - validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds());
  50 + // 1.1 校验商品范围
  51 + validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues());
  52 + // 1.2 校验商品是否冲突
  53 + validateRewardActivitySpuConflicts(null, createReqVO);
44 54  
45   - // 插入
46   - RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO)
  55 + // 2. 插入
  56 + RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class)
47 57 .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
48 58 rewardActivityMapper.insert(rewardActivity);
49 59 // 返回
... ... @@ -52,16 +62,18 @@ public class RewardActivityServiceImpl implements RewardActivityService {
52 62  
53 63 @Override
54 64 public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) {
55   - // 校验存在
  65 + // 1.1 校验存在
56 66 RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId());
57   - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢
  67 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能修改噢
58 68 throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED);
59 69 }
60   - // 校验商品是否冲突
61   - validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds());
  70 + // 1.2 校验商品范围
  71 + validateProductScope(updateReqVO.getProductScope(), updateReqVO.getProductScopeValues());
  72 + // 1.3 校验商品是否冲突
  73 + validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO);
62 74  
63   - // 更新
64   - RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO)
  75 + // 2. 更新
  76 + RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class)
65 77 .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
66 78 rewardActivityMapper.updateById(updateObj);
67 79 }
... ... @@ -70,15 +82,12 @@ public class RewardActivityServiceImpl implements RewardActivityService {
70 82 public void closeRewardActivity(Long id) {
71 83 // 校验存在
72 84 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
73   - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢
  85 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能关闭噢
74 86 throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
75 87 }
76   - if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动,不能关闭噢
77   - throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END);
78   - }
79 88  
80 89 // 更新
81   - RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
  90 + RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus());
82 91 rewardActivityMapper.updateById(updateObj);
83 92 }
84 93  
... ... @@ -86,7 +95,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
86 95 public void deleteRewardActivity(Long id) {
87 96 // 校验存在
88 97 RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
89   - if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢
  98 + if (dbRewardActivity.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())) { // 未关闭的活动,不能删除噢
90 99 throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED);
91 100 }
92 101  
... ... @@ -102,41 +111,39 @@ public class RewardActivityServiceImpl implements RewardActivityService {
102 111 return activity;
103 112 }
104 113  
105   - // TODO @芋艿:逻辑有问题,需要优化;要分成全场、和指定来校验;
106   -
107 114 /**
108 115 * 校验商品参加的活动是否冲突
109 116 *
110   - * @param id 活动编号
111   - * @param spuIds 商品 SPU 编号数组
  117 + * @param id 活动编号
  118 + * @param rewardActivity 请求
112 119 */
113   - private void validateRewardActivitySpuConflicts(Long id, Collection<Long> spuIds) {
114   - if (CollUtil.isEmpty(spuIds)) {
115   - return;
116   - }
117   - // 查询商品参加的活动
118   - List<RewardActivityDO> rewardActivityList = getRewardActivityListBySpuIds(spuIds,
119   - asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus()));
  120 + private void validateRewardActivitySpuConflicts(Long id, RewardActivityBaseVO rewardActivity) {
  121 + List<RewardActivityDO> list = rewardActivityMapper.selectList(RewardActivityDO::getProductScope,
  122 + rewardActivity.getProductScope(), RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
120 123 if (id != null) { // 排除自己这个活动
121   - rewardActivityList.removeIf(activity -> id.equals(activity.getId()));
  124 + list.removeIf(activity -> id.equals(activity.getId()));
122 125 }
123   - // 如果非空,则说明冲突
124   - if (CollUtil.isNotEmpty(rewardActivityList)) {
125   - throw exception(REWARD_ACTIVITY_SPU_CONFLICTS);
  126 +
  127 + // 情况一:全部商品参加
  128 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope()) && !list.isEmpty()) {
  129 + throw exception(REWARD_ACTIVITY_SCOPE_ALL_EXISTS);
  130 + }
  131 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) || // 情况二:指定商品参加
  132 + PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { // 情况三:指定商品类型参加
  133 + if (anyMatch(list, item -> !intersectionDistinct(item.getProductScopeValues(),
  134 + rewardActivity.getProductScopeValues()).isEmpty())) {
  135 + throw exception(PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ?
  136 + REWARD_ACTIVITY_SPU_CONFLICTS : REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS);
  137 + }
126 138 }
127 139 }
128 140  
129   - /**
130   - * 获得商品参加的满减送活动的数组
131   - *
132   - * @param spuIds 商品 SPU 编号数组
133   - * @param statuses 活动状态数组
134   - * @return 商品参加的满减送活动的数组
135   - */
136   - private List<RewardActivityDO> getRewardActivityListBySpuIds(Collection<Long> spuIds,
137   - Collection<Integer> statuses) {
138   - List<RewardActivityDO> list = rewardActivityMapper.selectListByStatus(statuses);
139   - return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds));
  141 + private void validateProductScope(Integer productScope, List<Long> productScopeValues) {
  142 + if (Objects.equals(PromotionProductScopeEnum.SPU.getScope(), productScope)) {
  143 + productSpuApi.validateSpuList(productScopeValues);
  144 + } else if (Objects.equals(PromotionProductScopeEnum.CATEGORY.getScope(), productScope)) {
  145 + productCategoryApi.validateCategoryList(productScopeValues);
  146 + }
140 147 }
141 148  
142 149 @Override
... ... @@ -151,32 +158,13 @@ public class RewardActivityServiceImpl implements RewardActivityService {
151 158  
152 159 @Override
153 160 public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) {
154   - // TODO 芋艿:待实现;先指定,然后再全局的;
155   -// // 如果有全局活动,则直接选择它
156   -// List<RewardActivityDO> allActivities = rewardActivityMapper.selectListByProductScopeAndStatus(
157   -// PromotionProductScopeEnum.ALL.getScope(), PromotionActivityStatusEnum.RUN.getStatus());
158   -// if (CollUtil.isNotEmpty(allActivities)) {
159   -// return MapUtil.builder(allActivities.get(0), spuIds).build();
160   -// }
161   -//
162   -// // 查询某个活动参加的活动
163   -// List<RewardActivityDO> productActivityList = getRewardActivityListBySpuIds(spuIds,
164   -// singleton(PromotionActivityStatusEnum.RUN.getStatus()));
165   -// return convertMap(productActivityList, activity -> activity,
166   -// rewardActivityDO -> intersectionDistinct(rewardActivityDO.getProductSpuIds(), spuIds)); // 求交集返回
167   - return null;
  161 + List<RewardActivityDO> list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus());
  162 + return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
168 163 }
169 164  
170 165 @Override
171   - public List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
172   - // 1. 查询出指定 spuId 的 spu 参加的活动
173   - List<RewardActivityDO> rewardActivityList = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, status);
174   - if (CollUtil.isEmpty(rewardActivityList)) {
175   - return Collections.emptyList();
176   - }
177   -
178   - // 2. 查询活动详情
179   - return rewardActivityMapper.selectListByIdsAndDateTimeLt(convertSet(rewardActivityList, RewardActivityDO::getId), dateTime);
  166 + public List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
  167 + return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
180 168 }
181 169  
182 170 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java
... ... @@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.Se
8 8 import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
9 9 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
10 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
11   -
12 11 import jakarta.validation.Valid;
  12 +
13 13 import java.time.LocalDateTime;
14 14 import java.util.Collection;
15 15 import java.util.List;
... ... @@ -98,7 +98,7 @@ public interface SeckillActivityService {
98 98 * @param activityIds 活动编号
99 99 * @return 活动商品列表
100 100 */
101   - List<SeckillProductDO> getSeckillProductListByActivityId(Collection<Long> activityIds);
  101 + List<SeckillProductDO> getSeckillProductListByActivityIds(Collection<Long> activityIds);
102 102  
103 103 /**
104 104 * 通过活动时段编号获取指定 status 的秒杀活动
... ... @@ -139,4 +139,12 @@ public interface SeckillActivityService {
139 139 */
140 140 List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
141 141  
  142 + /**
  143 + * 获得拼团活动列表
  144 + *
  145 + * @param ids 拼团活动编号数组
  146 + * @return 拼团活动的列表
  147 + */
  148 + List<SeckillActivityDO> getSeckillActivityListByIds(Collection<Long> ids);
  149 +
142 150 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
... ... @@ -23,11 +23,11 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillConfigDO;
23 23 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
24 24 import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillActivityMapper;
25 25 import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillProductMapper;
  26 +import jakarta.annotation.Resource;
26 27 import org.springframework.stereotype.Service;
27 28 import org.springframework.transaction.annotation.Transactional;
28 29 import org.springframework.validation.annotation.Validated;
29 30  
30   -import jakarta.annotation.Resource;
31 31 import java.time.LocalDateTime;
32 32 import java.util.Collection;
33 33 import java.util.Collections;
... ... @@ -276,7 +276,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
276 276 }
277 277  
278 278 @Override
279   - public List<SeckillProductDO> getSeckillProductListByActivityId(Collection<Long> activityIds) {
  279 + public List<SeckillProductDO> getSeckillProductListByActivityIds(Collection<Long> activityIds) {
280 280 return seckillProductMapper.selectListByActivityId(activityIds);
281 281 }
282 282  
... ... @@ -336,4 +336,9 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
336 336 convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
337 337 }
338 338  
  339 + @Override
  340 + public List<SeckillActivityDO> getSeckillActivityListByIds(Collection<Long> ids) {
  341 + return seckillActivityMapper.selectList(SeckillActivityDO::getId, ids);
  342 + }
  343 +
339 344 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
1 1 package cn.iocoder.yudao.module.promotion.service.reward;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  6 +import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
5 7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
6 8 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
7 9 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
8 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
9 11 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
10   -import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
11 12 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
12 13 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
  14 +import jakarta.annotation.Resource;
13 15 import org.junit.jupiter.api.Disabled;
14 16 import org.junit.jupiter.api.Test;
15 17 import org.springframework.context.annotation.Import;
16 18  
17   -import jakarta.annotation.Resource;
18 19 import java.time.Duration;
  20 +import java.util.List;
19 21 import java.util.Set;
20 22  
21 23 import static cn.hutool.core.util.RandomUtil.randomEle;
... ... @@ -27,15 +29,15 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
27 29 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
28 30 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
29 31 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS;
30   -import static java.util.Arrays.asList;
  32 +import static com.google.common.primitives.Longs.asList;
31 33 import static java.util.Collections.singletonList;
32 34 import static org.junit.jupiter.api.Assertions.*;
33 35  
34 36 /**
35   -* {@link RewardActivityServiceImpl} 的单元测试类
36   -*
37   -* @author 芋道源码
38   -*/
  37 + * {@link RewardActivityServiceImpl} 的单元测试类
  38 + *
  39 + * @author 芋道源码
  40 + */
39 41 @Disabled // TODO 芋艿:后续 fix 补充的单测
40 42 @Import(RewardActivityServiceImpl.class)
41 43 public class RewardActivityServiceImplTest extends BaseDbUnitTest {
... ... @@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
63 65 // 校验记录的属性是否正确
64 66 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId);
65 67 assertPojoEquals(reqVO, rewardActivity, "rules");
66   - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus());
  68 + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
67 69 for (int i = 0; i < reqVO.getRules().size(); i++) {
68 70 assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
69 71 }
... ... @@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
72 74 @Test
73 75 public void testUpdateRewardActivity_success() {
74 76 // mock 数据
75   - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()));
  77 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
76 78 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
77 79 // 准备参数
78 80 RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> {
... ... @@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
88 90 // 校验是否更新正确
89 91 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的
90 92 assertPojoEquals(reqVO, rewardActivity, "rules");
91   - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus());
  93 + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
92 94 for (int i = 0; i < reqVO.getRules().size(); i++) {
93 95 assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
94 96 }
... ... @@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
97 99 @Test
98 100 public void testCloseRewardActivity() {
99 101 // mock 数据
100   - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()));
  102 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
101 103 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
102 104 // 准备参数
103 105 Long id = dbRewardActivity.getId();
... ... @@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
106 108 rewardActivityService.closeRewardActivity(id);
107 109 // 校验状态
108 110 RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id);
109   - assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus());
  111 + assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
110 112 }
111 113  
112 114 @Test
... ... @@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
121 123 @Test
122 124 public void testDeleteRewardActivity_success() {
123 125 // mock 数据
124   - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()));
  126 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
125 127 rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
126 128 // 准备参数
127 129 Long id = dbRewardActivity.getId();
128 130  
129 131 // 调用
130 132 rewardActivityService.deleteRewardActivity(id);
131   - // 校验数据不存在了
132   - assertNull(rewardActivityMapper.selectById(id));
  133 + // 校验数据不存在了
  134 + assertNull(rewardActivityMapper.selectById(id));
133 135 }
134 136  
135 137 @Test
... ... @@ -143,77 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
143 145  
144 146 @Test
145 147 public void testGetRewardActivityPage() {
146   - // mock 数据
147   - RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
148   - o.setName("芋艿");
149   - o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
150   - });
151   - rewardActivityMapper.insert(dbRewardActivity);
152   - // 测试 name 不匹配
153   - rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
154   - // 测试 status 不匹配
155   - rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())));
156   - // 准备参数
157   - RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
158   - reqVO.setName("芋艿");
159   - reqVO.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
160   -
161   - // 调用
162   - PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);
163   - // 断言
164   - assertEquals(1, pageResult.getTotal());
165   - assertEquals(1, pageResult.getList().size());
166   - assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
  148 + // mock 数据
  149 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
  150 + o.setName("芋艿");
  151 + o.setStatus(CommonStatusEnum.DISABLE.getStatus());
  152 + });
  153 + rewardActivityMapper.insert(dbRewardActivity);
  154 + // 测试 name 不匹配
  155 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
  156 + // 测试 status 不匹配
  157 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
  158 + // 准备参数
  159 + RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
  160 + reqVO.setName("芋艿");
  161 + reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus());
  162 +
  163 + // 调用
  164 + PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);
  165 + // 断言
  166 + assertEquals(1, pageResult.getTotal());
  167 + assertEquals(1, pageResult.getList().size());
  168 + assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
167 169 }
168 170  
169 171 @Test
170 172 public void testGetRewardActivities_all() {
171 173 // mock 数据
172   - RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
  174 + RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
173 175 .setProductScope(PromotionProductScopeEnum.ALL.getScope()));
174 176 rewardActivityMapper.insert(allActivity);
175   - RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
176   - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L)));
  177 + RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
  178 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
177 179 rewardActivityMapper.insert(productActivity);
178 180 // 准备参数
179 181 Set<Long> spuIds = asSet(1L, 2L);
180 182  
181 183 // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
182   - //Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds);
  184 + List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
183 185 // 断言
184   - //assertEquals(matchRewardActivities.size(), 1);
185   - //Map.Entry<RewardActivityDO, Set<Long>> next = matchRewardActivities.entrySet().iterator().next();
186   - //assertPojoEquals(next.getKey(), allActivity);
187   - //assertEquals(next.getValue(), spuIds);
  186 + assertEquals(matchRewardActivityList.size(), 1);
  187 + matchRewardActivityList.forEach((activity) -> {
  188 + if (activity.getId().equals(productActivity.getId())) {
  189 + assertPojoEquals(activity, productActivity);
  190 + assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
  191 + } else {
  192 + fail();
  193 + }
  194 + });
188 195 }
189 196  
190 197 @Test
191 198 public void testGetRewardActivities_product() {
192 199 // mock 数据
193   - RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
194   - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L)));
  200 + RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
  201 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
195 202 rewardActivityMapper.insert(productActivity01);
196   - RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
197   - .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(singletonList(3L)));
  203 + RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
  204 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)));
198 205 rewardActivityMapper.insert(productActivity02);
199 206 // 准备参数
200 207 Set<Long> spuIds = asSet(1L, 2L, 3L);
201 208  
202 209 // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
203   - //Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds);
  210 + List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
204 211 // 断言
205   - //assertEquals(matchRewardActivities.size(), 2);
206   - //matchRewardActivities.forEach((activity, activitySpuIds) -> {
207   - // if (activity.getId().equals(productActivity01.getId())) {
208   - // assertPojoEquals(activity, productActivity01);
209   - // assertEquals(activitySpuIds, asSet(1L, 2L));
210   - // } else if (activity.getId().equals(productActivity02.getId())) {
211   - // assertPojoEquals(activity, productActivity02);
212   - // assertEquals(activitySpuIds, asSet(3L));
213   - // } else {
214   - // fail();
215   - // }
216   - //});
  212 + assertEquals(matchRewardActivityList.size(), 2);
  213 + matchRewardActivityList.forEach((activity) -> {
  214 + if (activity.getId().equals(productActivity01.getId())) {
  215 + assertPojoEquals(activity, productActivity01);
  216 + assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
  217 + } else if (activity.getId().equals(productActivity02.getId())) {
  218 + assertPojoEquals(activity, productActivity02);
  219 + assertEquals(activity.getProductScopeValues(), singletonList(3L));
  220 + } else {
  221 + fail();
  222 + }
  223 + });
217 224 }
218 225  
219 226 }
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java
... ... @@ -10,8 +10,6 @@ import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsServi
10 10 import jakarta.annotation.Resource;
11 11 import org.springframework.stereotype.Component;
12 12  
13   -// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
14   -
15 13 /**
16 14 * 商品统计 Job
17 15 *
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java
... ... @@ -11,7 +11,6 @@ import org.springframework.stereotype.Component;
11 11  
12 12 import jakarta.annotation.Resource;
13 13  
14   -// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
15 14 /**
16 15 * 交易统计 Job
17 16 *
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/member/MemberStatisticsServiceImpl.java
1 1 package cn.iocoder.yudao.module.statistics.service.member;
2 2  
3 3 import cn.hutool.core.date.LocalDateTimeUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
4 5 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
5 6 import cn.iocoder.yudao.framework.ip.core.Area;
6 7 import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
... ... @@ -15,10 +16,10 @@ import cn.iocoder.yudao.module.statistics.service.pay.PayWalletStatisticsService
15 16 import cn.iocoder.yudao.module.statistics.service.pay.bo.RechargeSummaryRespBO;
16 17 import cn.iocoder.yudao.module.statistics.service.trade.TradeOrderStatisticsService;
17 18 import cn.iocoder.yudao.module.statistics.service.trade.TradeStatisticsService;
  19 +import jakarta.annotation.Resource;
18 20 import org.springframework.stereotype.Service;
19 21 import org.springframework.validation.annotation.Validated;
20 22  
21   -import jakarta.annotation.Resource;
22 23 import java.time.Duration;
23 24 import java.time.LocalDateTime;
24 25 import java.util.List;
... ... @@ -69,9 +70,12 @@ public class MemberStatisticsServiceImpl implements MemberStatisticsService {
69 70 bo -> AreaUtils.getParentIdByType(bo.getAreaId(), AreaTypeEnum.PROVINCE),
70 71 bo -> bo,
71 72 (a, b) -> new MemberAreaStatisticsRespBO()
72   - .setOrderCreateUserCount(a.getOrderCreateUserCount() + b.getOrderCreateUserCount())
73   - .setOrderPayUserCount(a.getOrderPayUserCount() + b.getOrderPayUserCount())
74   - .setOrderPayPrice(a.getOrderPayPrice() + b.getOrderPayPrice()));
  73 + .setOrderCreateUserCount(ObjectUtil.defaultIfNull(a.getOrderCreateUserCount(), 0)
  74 + + ObjectUtil.defaultIfNull(b.getOrderCreateUserCount(), 0))
  75 + .setOrderPayUserCount(ObjectUtil.defaultIfNull(a.getOrderPayUserCount(), 0)
  76 + + ObjectUtil.defaultIfNull(b.getOrderPayUserCount(), 0))
  77 + .setOrderPayPrice(ObjectUtil.defaultIfNull(a.getOrderPayPrice(), 0)
  78 + + ObjectUtil.defaultIfNull(b.getOrderPayPrice(), 0)));
75 79 // 拼接数据
76 80 List<Area> areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
77 81 areaList.add(new Area().setId(null).setName("未知"));
... ...
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java
... ... @@ -28,13 +28,13 @@ public interface TradeOrderApi {
28 28 */
29 29 TradeOrderRespDTO getOrder(Long id);
30 30  
31   - // TODO 芋艿:需要优化下;
32 31 /**
33 32 * 取消支付订单
34 33 *
35   - * @param userId 用户编号
  34 + * @param userId 用户编号
36 35 * @param orderId 订单编号
  36 + * @param cancelType 取消类型
37 37 */
38   - void cancelPaidOrder(Long userId, Long orderId);
  38 + void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
39 39  
40 40 }
... ...
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
... ... @@ -35,6 +35,7 @@ public interface ErrorCodeConstants {
35 35 ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
36 36 ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
37 37 ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
  38 + ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
38 39  
39 40 // ========== After Sale 模块 1-011-000-100 ==========
40 41 ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
... ... @@ -50,6 +51,7 @@ public interface ErrorCodeConstants {
50 51 ErrorCode AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND = new ErrorCode(1_011_000_110, "退款失败,售后单状态不是【待退款】");
51 52 ErrorCode AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE_OR_BUYER_DELIVERY =
52 53 new ErrorCode(1_011_000_111, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】或【商家待收货】");
  54 + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS = new ErrorCode(1_011_000_112, "订单拼团中,无法申请售后");
53 55  
54 56 // ========== Cart 模块 1-011-002-000 ==========
55 57 ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1_011_002_000, "购物车项不存在");
... ... @@ -59,6 +61,8 @@ public interface ErrorCodeConstants {
59 61 ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
60 62 ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
61 63 ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
  64 + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
  65 + ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:{}」");
62 66  
63 67 // ========== 物流 Express 模块 1-011-004-000 ==========
64 68 ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");
... ...
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java
... ... @@ -17,7 +17,8 @@ public enum TradeOrderCancelTypeEnum implements IntArrayValuable {
17 17  
18 18 PAY_TIMEOUT(10, "超时未支付"),
19 19 AFTER_SALE_CLOSE(20, "退款关闭"),
20   - MEMBER_CANCEL(30, "买家取消");
  20 + MEMBER_CANCEL(30, "买家取消"),
  21 + COMBINATION_CLOSE(40, "拼团关闭");
21 22  
22 23 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray();
23 24  
... ...
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java
... ... @@ -17,7 +17,7 @@ public enum TradeOrderOperateTypeEnum {
17 17 ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,调整价格 {adjustPrice},实际支付金额为 {newPayPrice} 元"),
18 18 MEMBER_PAY(10, "用户付款成功"),
19 19 ADMIN_UPDATE_ADDRESS(11, "收货地址修改"),
20   - ADMIN_DELIVERY(20, "已发货,快递公司:{deliveryName},快递单号:{logisticsNo}"),
  20 + ADMIN_DELIVERY(20, "已发货,快递公司:{expressName},快递单号:{logisticsNo}"),
21 21 MEMBER_RECEIVE(30, "用户已收货"),
22 22 SYSTEM_RECEIVE(31, "到期未收货,系统自动确认收货"),
23 23 ADMIN_PICK_UP_RECEIVE(32, "管理员自提收货"),
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
... ... @@ -4,10 +4,10 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
4 4 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
5 5 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
6 6 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
  7 +import jakarta.annotation.Resource;
7 8 import org.springframework.stereotype.Service;
8 9 import org.springframework.validation.annotation.Validated;
9 10  
10   -import jakarta.annotation.Resource;
11 11 import java.util.Collection;
12 12 import java.util.List;
13 13  
... ... @@ -36,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi {
36 36 }
37 37  
38 38 @Override
39   - public void cancelPaidOrder(Long userId, Long orderId) {
40   - tradeOrderUpdateService.cancelPaidOrder(userId, orderId);
  39 + public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
  40 + tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelType);
41 41 }
42 42  
43 43 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java
... ... @@ -21,6 +21,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
21 21 @ToString(callSuper = true)
22 22 public class AfterSalePageReqVO extends PageParam {
23 23  
  24 + @Schema(description = "用户编号", example = "1024")
  25 + private Long userId;
  26 +
24 27 @Schema(description = "售后流水号", example = "202211190847450020500077")
25 28 private String no;
26 29  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.trade.controller.app.aftersale;
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6   -import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.log.AfterSaleLogRespVO;
  6 +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
7 7 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
8 8 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
9 9 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO;
... ... @@ -12,14 +12,11 @@ import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
12 12 import io.swagger.v3.oas.annotations.Operation;
13 13 import io.swagger.v3.oas.annotations.Parameter;
14 14 import io.swagger.v3.oas.annotations.tags.Tag;
  15 +import jakarta.annotation.Resource;
15 16 import lombok.extern.slf4j.Slf4j;
16 17 import org.springframework.validation.annotation.Validated;
17 18 import org.springframework.web.bind.annotation.*;
18 19  
19   -import jakarta.annotation.Resource;
20   -
21   -import java.util.List;
22   -
23 20 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
24 21 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
25 22  
... ... @@ -35,6 +32,7 @@ public class AppAfterSaleController {
35 32  
36 33 @GetMapping(value = "/page")
37 34 @Operation(summary = "获得售后分页")
  35 + @PreAuthenticated
38 36 public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(PageParam pageParam) {
39 37 return success(AfterSaleConvert.INSTANCE.convertPage02(
40 38 afterSaleService.getAfterSalePage(getLoginUserId(), pageParam)));
... ... @@ -43,18 +41,21 @@ public class AppAfterSaleController {
43 41 @GetMapping(value = "/get")
44 42 @Operation(summary = "获得售后订单")
45 43 @Parameter(name = "id", description = "售后编号", required = true, example = "1")
  44 + @PreAuthenticated
46 45 public CommonResult<AppAfterSaleRespVO> getAfterSale(@RequestParam("id") Long id) {
47 46 return success(AfterSaleConvert.INSTANCE.convert(afterSaleService.getAfterSale(getLoginUserId(), id)));
48 47 }
49 48  
50 49 @PostMapping(value = "/create")
51 50 @Operation(summary = "申请售后")
  51 + @PreAuthenticated
52 52 public CommonResult<Long> createAfterSale(@RequestBody AppAfterSaleCreateReqVO createReqVO) {
53 53 return success(afterSaleService.createAfterSale(getLoginUserId(), createReqVO));
54 54 }
55 55  
56 56 @PutMapping(value = "/delivery")
57 57 @Operation(summary = "退回货物")
  58 + @PreAuthenticated
58 59 public CommonResult<Boolean> deliveryAfterSale(@RequestBody AppAfterSaleDeliveryReqVO deliveryReqVO) {
59 60 afterSaleService.deliveryAfterSale(getLoginUserId(), deliveryReqVO);
60 61 return success(true);
... ... @@ -63,6 +64,7 @@ public class AppAfterSaleController {
63 64 @DeleteMapping(value = "/cancel")
64 65 @Operation(summary = "取消售后")
65 66 @Parameter(name = "id", description = "售后编号", required = true, example = "1")
  67 + @PreAuthenticated
66 68 public CommonResult<Boolean> cancelAfterSale(@RequestParam("id") Long id) {
67 69 afterSaleService.cancelAfterSale(getLoginUserId(), id);
68 70 return success(true);
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleLogController.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.controller.app.aftersale;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
5 6 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.log.AppAfterSaleLogRespVO;
6 7 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO;
7 8 import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleLogService;
... ... @@ -33,6 +34,7 @@ public class AppAfterSaleLogController {
33 34 @GetMapping("/list")
34 35 @Operation(summary = "获得售后日志列表")
35 36 @Parameter(name = "afterSaleId", description = "售后编号", required = true, example = "1")
  37 + @PreAuthenticated
36 38 public CommonResult<List<AppAfterSaleLogRespVO>> getAfterSaleLogList(
37 39 @RequestParam("afterSaleId") Long afterSaleId) {
38 40 List<AfterSaleLogDO> logs = afterSaleLogService.getAfterSaleLogList(afterSaleId);
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageRecordController.java
... ... @@ -45,6 +45,7 @@ public class AppBrokerageRecordController {
45 45  
46 46 @GetMapping("/get-product-brokerage-price")
47 47 @Operation(summary = "获得商品的分销金额")
  48 + @PreAuthenticated
48 49 public CommonResult<AppBrokerageProductPriceRespVO> getProductBrokeragePrice(@RequestParam("spuId") Long spuId) {
49 50 return success(brokerageRecordService.calculateProductBrokeragePrice(getLoginUserId(), spuId));
50 51 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java
... ... @@ -133,6 +133,7 @@ public class AppBrokerageUserController {
133 133 @GetMapping("/get-rank-by-price")
134 134 @Operation(summary = "获得分销用户排行(基于佣金)")
135 135 @Parameter(name = "times", description = "时间段", required = true)
  136 + @PreAuthenticated
136 137 public CommonResult<Integer> getRankByPrice(
137 138 @RequestParam("times") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime[] times) {
138 139 return success(brokerageRecordService.getUserRankByPrice(getLoginUserId(), times));
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java
... ... @@ -44,7 +44,7 @@ public class AppBrokerageWithdrawCreateReqVO {
44 44 private String name;
45 45 @Schema(description = "提现银行", example = "1")
46 46 @NotNull(message = "提现银行不能为空", groups = {Bank.class})
47   - private Integer bankName;
  47 + private String bankName;
48 48 @Schema(description = "开户地址", example = "海淀支行")
49 49 private String bankAddress;
50 50  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverConfigController.java deleted
1   -package cn.iocoder.yudao.module.trade.controller.app.delivery;
2   -
3   -import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4   -import cn.iocoder.yudao.module.trade.controller.app.delivery.vo.config.AppDeliveryConfigRespVO;
5   -import io.swagger.v3.oas.annotations.Operation;
6   -import io.swagger.v3.oas.annotations.tags.Tag;
7   -import org.springframework.validation.annotation.Validated;
8   -import org.springframework.web.bind.annotation.GetMapping;
9   -import org.springframework.web.bind.annotation.RequestMapping;
10   -import org.springframework.web.bind.annotation.RestController;
11   -
12   -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
13   -
14   -@Tag(name = "用户 App - 配送配置")
15   -@RestController
16   -@RequestMapping("/trade/delivery/config")
17   -@Validated
18   -public class AppDeliverConfigController {
19   -
20   - // TODO @芋艿:这里后面干掉,合并到 AppTradeConfigController 中
21   - @GetMapping("/get")
22   - @Operation(summary = "获得配送配置")
23   - public CommonResult<AppDeliveryConfigRespVO> getDeliveryConfig() {
24   - return success(new AppDeliveryConfigRespVO().setPickUpEnable(true).setTencentLbsKey("123456"));
25   - }
26   -
27   -}
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java
... ... @@ -80,6 +80,7 @@ public class AppTradeOrderController {
80 80 @GetMapping("/get-detail")
81 81 @Operation(summary = "获得交易订单")
82 82 @Parameter(name = "id", description = "交易订单编号")
  83 + @PreAuthenticated
83 84 public CommonResult<AppTradeOrderDetailRespVO> getOrder(@RequestParam("id") Long id) {
84 85 // 查询订单
85 86 TradeOrderDO order = tradeOrderQueryService.getOrder(getLoginUserId(), id);
... ... @@ -99,6 +100,7 @@ public class AppTradeOrderController {
99 100 @GetMapping("/get-express-track-list")
100 101 @Operation(summary = "获得交易订单的物流轨迹")
101 102 @Parameter(name = "id", description = "交易订单编号")
  103 + @PreAuthenticated
102 104 public CommonResult<List<AppOrderExpressTrackRespDTO>> getOrderExpressTrackList(@RequestParam("id") Long id) {
103 105 return success(TradeOrderConvert.INSTANCE.convertList02(
104 106 tradeOrderQueryService.getExpressTrackList(id, getLoginUserId())));
... ... @@ -106,6 +108,7 @@ public class AppTradeOrderController {
106 108  
107 109 @GetMapping("/page")
108 110 @Operation(summary = "获得交易订单分页")
  111 + @PreAuthenticated
109 112 public CommonResult<PageResult<AppTradeOrderPageItemRespVO>> getOrderPage(AppTradeOrderPageReqVO reqVO) {
110 113 // 查询订单
111 114 PageResult<TradeOrderDO> pageResult = tradeOrderQueryService.getOrderPage(getLoginUserId(), reqVO);
... ... @@ -118,6 +121,7 @@ public class AppTradeOrderController {
118 121  
119 122 @GetMapping("/get-count")
120 123 @Operation(summary = "获得交易订单数量")
  124 + @PreAuthenticated
121 125 public CommonResult<Map<String, Long>> getOrderCount() {
122 126 Map<String, Long> orderCount = Maps.newLinkedHashMapWithExpectedSize(5);
123 127 // 全部
... ... @@ -142,6 +146,7 @@ public class AppTradeOrderController {
142 146 @PutMapping("/receive")
143 147 @Operation(summary = "确认交易订单收货")
144 148 @Parameter(name = "id", description = "交易订单编号")
  149 + @PreAuthenticated
145 150 public CommonResult<Boolean> receiveOrder(@RequestParam("id") Long id) {
146 151 tradeOrderUpdateService.receiveOrderByMember(getLoginUserId(), id);
147 152 return success(true);
... ... @@ -150,6 +155,7 @@ public class AppTradeOrderController {
150 155 @DeleteMapping("/cancel")
151 156 @Operation(summary = "取消交易订单")
152 157 @Parameter(name = "id", description = "交易订单编号")
  158 + @PreAuthenticated
153 159 public CommonResult<Boolean> cancelOrder(@RequestParam("id") Long id) {
154 160 tradeOrderUpdateService.cancelOrderByMember(getLoginUserId(), id);
155 161 return success(true);
... ... @@ -158,6 +164,7 @@ public class AppTradeOrderController {
158 164 @DeleteMapping("/delete")
159 165 @Operation(summary = "删除交易订单")
160 166 @Parameter(name = "id", description = "交易订单编号")
  167 + @PreAuthenticated
161 168 public CommonResult<Boolean> deleteOrder(@RequestParam("id") Long id) {
162 169 tradeOrderUpdateService.deleteOrder(getLoginUserId(), id);
163 170 return success(true);
... ... @@ -168,6 +175,7 @@ public class AppTradeOrderController {
168 175 @GetMapping("/item/get")
169 176 @Operation(summary = "获得交易订单项")
170 177 @Parameter(name = "id", description = "交易订单项编号")
  178 + @PreAuthenticated
171 179 public CommonResult<AppTradeOrderItemRespVO> getOrderItem(@RequestParam("id") Long id) {
172 180 TradeOrderItemDO item = tradeOrderQueryService.getOrderItem(getLoginUserId(), id);
173 181 return success(TradeOrderConvert.INSTANCE.convert03(item));
... ... @@ -175,6 +183,7 @@ public class AppTradeOrderController {
175 183  
176 184 @PostMapping("/item/create-comment")
177 185 @Operation(summary = "创建交易订单项的评价")
  186 + @PreAuthenticated
178 187 public CommonResult<Long> createOrderItemComment(@RequestBody AppTradeOrderItemCommentCreateReqVO createReqVO) {
179 188 return success(tradeOrderUpdateService.createOrderItemCommentByMember(getLoginUserId(), createReqVO));
180 189 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java
... ... @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.trade.controller.app.order.vo;
2 2  
3 3 import cn.iocoder.yudao.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO;
4 4 import io.swagger.v3.oas.annotations.media.Schema;
5   -import jakarta.validation.constraints.NotNull;
6 5 import lombok.AllArgsConstructor;
7 6 import lombok.Data;
8 7 import lombok.NoArgsConstructor;
9 8  
  9 +import java.time.LocalDateTime;
10 10 import java.util.List;
11 11  
12 12 @Schema(description = "用户 App - 交易订单结算信息 Response VO")
... ... @@ -19,6 +19,9 @@ public class AppTradeOrderSettlementRespVO {
19 19 @Schema(description = "购物项数组", requiredMode = Schema.RequiredMode.REQUIRED)
20 20 private List<Item> items;
21 21  
  22 + @Schema(description = "优惠劵数组", requiredMode = Schema.RequiredMode.REQUIRED)
  23 + private List<Coupon> coupons; // 可用 + 不可用
  24 +
22 25 @Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED)
23 26 private Price price;
24 27  
... ... @@ -109,7 +112,6 @@ public class AppTradeOrderSettlementRespVO {
109 112 private String mobile;
110 113  
111 114 @Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED)
112   - @NotNull(message = "地区编号不能为空")
113 115 private Long areaId;
114 116 @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区")
115 117 private String areaName;
... ... @@ -122,4 +124,43 @@ public class AppTradeOrderSettlementRespVO {
122 124  
123 125 }
124 126  
  127 + @Schema(description = "优惠劵信息")
  128 + @Data
  129 + public static class Coupon {
  130 +
  131 + @Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  132 + private Long id;
  133 +
  134 + @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送")
  135 + private String name;
  136 +
  137 + @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位:分;0 - 不限制
  138 + private Integer usePrice;
  139 +
  140 + @Schema(description = "固定日期 - 生效开始时间")
  141 + private LocalDateTime validStartTime;
  142 +
  143 + @Schema(description = "固定日期 - 生效结束时间")
  144 + private LocalDateTime validEndTime;
  145 +
  146 + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  147 + private Integer discountType;
  148 +
  149 + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80
  150 + private Integer discountPercent;
  151 +
  152 + @Schema(description = "优惠金额", example = "10")
  153 + private Integer discountPrice;
  154 +
  155 + @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用
  156 + private Integer discountLimitPrice;
  157 +
  158 + @Schema(description = "是否可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  159 + private Boolean match;
  160 +
  161 + @Schema(description = "不可用原因", example = "优惠劵已过期")
  162 + private String mismatchReason;
  163 +
  164 + }
  165 +
125 166 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java
... ... @@ -43,7 +43,8 @@ public interface AfterSaleConvert {
43 43 @Mapping(source = "afterSale.orderId", target = "merchantOrderId"),
44 44 @Mapping(source = "afterSale.id", target = "merchantRefundId"),
45 45 @Mapping(source = "afterSale.applyReason", target = "reason"),
46   - @Mapping(source = "afterSale.refundPrice", target = "price")
  46 + @Mapping(source = "afterSale.refundPrice", target = "price"),
  47 + @Mapping(source = "orderProperties.payAppKey", target = "appKey")
47 48 })
48 49 PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale,
49 50 TradeOrderProperties orderProperties);
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
... ... @@ -101,7 +101,7 @@ public interface TradeOrderConvert {
101 101 default PayOrderCreateReqDTO convert(TradeOrderDO order, List<TradeOrderItemDO> orderItems,
102 102 TradeOrderProperties orderProperties) {
103 103 PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO()
104   - .setAppId(orderProperties.getAppId()).setUserIp(order.getUserIp());
  104 + .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp());
105 105 // 商户相关字段
106 106 createReqDTO.setMerchantOrderId(String.valueOf(order.getId()));
107 107 String subject = orderItems.get(0).getSpuName();
... ... @@ -261,7 +261,7 @@ public interface TradeOrderConvert {
261 261 default BrokerageAddReqBO convert(MemberUserRespDTO user, TradeOrderItemDO item,
262 262 ProductSpuRespDTO spu, ProductSkuRespDTO sku) {
263 263 BrokerageAddReqBO bo = new BrokerageAddReqBO().setBizId(String.valueOf(item.getId())).setSourceUserId(item.getUserId())
264   - .setBasePrice(item.getPayPrice() * item.getCount())
  264 + .setBasePrice(item.getPayPrice())
265 265 .setTitle(StrUtil.format("{}成功购买{}", user.getNickname(), item.getSpuName()))
266 266 .setFirstFixedPrice(0).setSecondFixedPrice(0);
267 267 if (BooleanUtil.isTrue(spu.getSubCommissionType())) {
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.order;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 6 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
6 7 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
7 8 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
... ... @@ -12,10 +13,14 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
12 13 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
13 14 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
14 15 import com.baomidou.mybatisplus.annotation.KeySequence;
  16 +import com.baomidou.mybatisplus.annotation.TableField;
15 17 import com.baomidou.mybatisplus.annotation.TableName;
  18 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
16 19 import lombok.*;
17 20  
18 21 import java.time.LocalDateTime;
  22 +import java.util.List;
  23 +import java.util.Map;
19 24  
20 25 /**
21 26 * 交易订单 DO
... ... @@ -292,6 +297,24 @@ public class TradeOrderDO extends BaseDO {
292 297 private Integer vipPrice;
293 298  
294 299 /**
  300 + * 赠送的优惠劵
  301 + *
  302 + * key: 优惠劵模版编号
  303 + * value:对应的优惠券数量
  304 + *
  305 + * 目的:用于订单支付后赠送优惠券
  306 + */
  307 + @TableField(typeHandler = JacksonTypeHandler.class)
  308 + private Map<Long, Integer> giveCouponTemplateCounts;
  309 + /**
  310 + * 赠送的优惠劵编号
  311 + *
  312 + * 目的:用于后续取消或者售后订单时,需要扣减赠送
  313 + */
  314 + @TableField(typeHandler = LongListTypeHandler.class)
  315 + private List<Long> giveCouponIds;
  316 +
  317 + /**
295 318 * 秒杀活动编号
296 319 *
297 320 * 关联 SeckillActivityDO 的 id 字段
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleLogMapper.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.mysql.aftersale;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO;
  5 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
5 6 import org.apache.ibatis.annotations.Mapper;
6 7  
7 8 import java.util.List;
... ... @@ -10,7 +11,9 @@ import java.util.List;
10 11 public interface AfterSaleLogMapper extends BaseMapperX<AfterSaleLogDO> {
11 12  
12 13 default List<AfterSaleLogDO> selectListByAfterSaleId(Long afterSaleId) {
13   - return selectList(AfterSaleLogDO::getAfterSaleId, afterSaleId);
  14 + return selectList(new LambdaQueryWrapper<AfterSaleLogDO>()
  15 + .eq(AfterSaleLogDO::getAfterSaleId, afterSaleId)
  16 + .orderByDesc(AfterSaleLogDO::getCreateTime));
14 17 }
15 18  
16 19 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java
... ... @@ -16,6 +16,7 @@ public interface AfterSaleMapper extends BaseMapperX&lt;AfterSaleDO&gt; {
16 16  
17 17 default PageResult<AfterSaleDO> selectPage(AfterSalePageReqVO reqVO) {
18 18 return selectPage(reqVO, new LambdaQueryWrapperX<AfterSaleDO>()
  19 + .eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId())
19 20 .likeIfPresent(AfterSaleDO::getNo, reqVO.getNo())
20 21 .eqIfPresent(AfterSaleDO::getStatus, reqVO.getStatus())
21 22 .eqIfPresent(AfterSaleDO::getType, reqVO.getType())
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderLogMapper.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.mysql.order;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderLogDO;
  5 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
5 6 import org.apache.ibatis.annotations.Mapper;
6 7  
7 8 import java.util.List;
... ... @@ -10,7 +11,9 @@ import java.util.List;
10 11 public interface TradeOrderLogMapper extends BaseMapperX<TradeOrderLogDO> {
11 12  
12 13 default List<TradeOrderLogDO> selectListByOrderId(Long orderId) {
13   - return selectList(TradeOrderLogDO::getOrderId, orderId);
  14 + return selectList(new LambdaQueryWrapper<TradeOrderLogDO>()
  15 + .eq(TradeOrderLogDO::getOrderId, orderId)
  16 + .orderByDesc(TradeOrderLogDO::getCreateTime));
14 17 }
15 18  
16 19 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/package-info.java deleted
1   -/**
2   - * TODO 占位
3   - */
4   -package cn.iocoder.yudao.module.trade.dal.mysql;
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java
... ... @@ -13,7 +13,7 @@ public class ExpressTrackQueryReqDTO {
13 13  
14 14 /**
15 15 * 快递公司编码
16   - *
  16 + * <p>
17 17 * 对应 {@link DeliveryExpressDO#getCode()}
18 18 */
19 19 private String expressCode;
... ... @@ -28,4 +28,9 @@ public class ExpressTrackQueryReqDTO {
28 28 */
29 29 private String phone;
30 30  
  31 + /**
  32 + * 自定义名称(顺丰专用)
  33 + */
  34 + private String customerName;
  35 +
31 36 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java
... ... @@ -29,4 +29,10 @@ public class KdNiaoExpressQueryReqDTO {
29 29 @JsonProperty("OrderCode")
30 30 private String orderNo;
31 31  
  32 + /**
  33 + * 自定义名称(顺丰专用)
  34 + */
  35 + @JsonProperty("CustomerName")
  36 + private String customerName;
  37 +
32 38 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java
... ... @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.trade.framework.delivery.core.client.impl.kdniao
3 3 import cn.hutool.core.codec.Base64;
4 4 import cn.hutool.core.collection.CollUtil;
5 5 import cn.hutool.core.net.URLEncodeUtil;
  6 +import cn.hutool.core.util.ObjUtil;
  7 +import cn.hutool.core.util.StrUtil;
6 8 import cn.hutool.crypto.digest.DigestUtil;
7 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 10 import cn.iocoder.yudao.module.trade.framework.delivery.config.TradeExpressProperties;
... ... @@ -60,6 +62,11 @@ public class KdNiaoExpressClient implements ExpressClient {
60 62 // 发起请求
61 63 KdNiaoExpressQueryReqDTO requestDTO = INSTANCE.convert(reqDTO)
62 64 .setExpressCode(reqDTO.getExpressCode().toUpperCase());
  65 + if (ObjUtil.equal(requestDTO.getExpressCode(), "SF")
  66 + && StrUtil.isBlank(reqDTO.getCustomerName())
  67 + && StrUtil.length(reqDTO.getPhone()) >= 4) {
  68 + requestDTO.setCustomerName(StrUtil.subSufByLength(reqDTO.getPhone(), 4));
  69 + }
63 70 KdNiaoExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, REAL_TIME_FREE_REQ_TYPE,
64 71 requestDTO, KdNiaoExpressQueryRespDTO.class);
65 72  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderConfig.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.trade.framework.order.config;
3 3 import org.springframework.boot.context.properties.EnableConfigurationProperties;
4 4 import org.springframework.context.annotation.Configuration;
5 5  
6   -// TODO @LeeYan9: 可以直接给 TradeOrderProperties 一个 @Component生效哈
7 6 /**
8 7 * @author LeeYan9
9 8 * @since 2022-09-15
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java
1 1 package cn.iocoder.yudao.module.trade.framework.order.config;
2 2  
  3 +import jakarta.validation.constraints.NotEmpty;
3 4 import lombok.Data;
4 5 import org.springframework.boot.context.properties.ConfigurationProperties;
5 6 import org.springframework.validation.annotation.Validated;
6 7  
7 8 import jakarta.validation.constraints.NotNull;
  9 +
8 10 import java.time.Duration;
9 11  
10 12 /**
... ... @@ -18,11 +20,15 @@ import java.time.Duration;
18 20 @Validated
19 21 public class TradeOrderProperties {
20 22  
  23 + private static final String PAY_APP_KEY_DEFAULT = "mall";
  24 +
21 25 /**
22   - * 应用编号
  26 + * 支付应用标识
  27 + *
  28 + * 在 pay 模块的 [支付管理 -> 应用信息] 里添加
23 29 */
24   - @NotNull(message = "应用编号不能为空")
25   - private Long appId;
  30 + @NotEmpty(message = "Pay 应用标识不能为空")
  31 + private String payAppKey = PAY_APP_KEY_DEFAULT;
26 32  
27 33 /**
28 34 * 支付超时时间
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java
... ... @@ -8,6 +8,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 8 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
9 9 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
10 10 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
  11 +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
  12 +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
  13 +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
11 14 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO;
12 15 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
13 16 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO;
... ... @@ -26,6 +29,7 @@ import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleTypeEnum;
26 29 import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleWayEnum;
27 30 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum;
28 31 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
  32 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
29 33 import cn.iocoder.yudao.module.trade.framework.aftersale.core.annotations.AfterSaleLog;
30 34 import cn.iocoder.yudao.module.trade.framework.aftersale.core.utils.AfterSaleLogUtils;
31 35 import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
... ... @@ -71,6 +75,8 @@ public class AfterSaleServiceImpl implements AfterSaleService {
71 75  
72 76 @Resource
73 77 private PayRefundApi payRefundApi;
  78 + @Resource
  79 + private CombinationRecordApi combinationRecordApi;
74 80  
75 81 @Resource
76 82 private TradeOrderProperties tradeOrderProperties;
... ... @@ -148,6 +154,14 @@ public class AfterSaleServiceImpl implements AfterSaleService {
148 154 && !TradeOrderStatusEnum.haveDelivered(order.getStatus())) {
149 155 throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_DELIVERED);
150 156 }
  157 + // 如果是拼团订单,则进行中不允许售后
  158 + if (TradeOrderTypeEnum.isCombination(order.getType())) {
  159 + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(
  160 + order.getUserId(), order.getId());
  161 + if (combinationRecord != null && CombinationRecordStatusEnum.isInProgress(combinationRecord.getStatus())) {
  162 + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS);
  163 + }
  164 + }
151 165 return orderItem;
152 166 }
153 167  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java
... ... @@ -125,7 +125,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
125 125  
126 126 @Override
127 127 public BrokerageUserDO getOrCreateBrokerageUser(Long id) {
128   - // TODO @芋艿:这块优化下;统一到注册时处理;
129 128 BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(id);
130 129 // 特殊:人人分销的情况下,如果分销人为空则创建分销人
131 130 if (brokerageUser == null && ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(),
... ... @@ -193,6 +192,8 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
193 192 Integer enabledCondition = tradeConfigService.getTradeConfig().getBrokerageEnabledCondition();
194 193 if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格
195 194 brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
  195 + } else {
  196 + brokerageUser.setBrokerageEnabled(false).setBrokerageTime(LocalDateTime.now());
196 197 }
197 198 brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser));
198 199 } else {
... ... @@ -267,11 +268,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
267 268 return false;
268 269 }
269 270  
270   - // 校验分佣模式:仅可后台手动设置推广员
271   - if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) {
272   - throw exception(BROKERAGE_BIND_CONDITION_ADMIN);
273   - }
274   -
275 271 // 校验分销关系绑定模式
276 272 if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) {
277 273 // 判断是否为新用户:注册时间在 30 秒内的,都算新用户
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
... ... @@ -96,7 +96,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
96 96 Map<String, Object> templateParams = MapUtil.<String, Object>builder()
97 97 .put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime()))
98 98 .put("price", MoneyUtils.fenToYuanStr(withdraw.getPrice()))
99   - .put("reason", withdraw.getAuditReason())
  99 + .put("reason", auditReason)
100 100 .build();
101 101 notifyMessageSendApi.sendSingleMessageToMember(new NotifySendSingleToUserReqDTO()
102 102 .setUserId(withdraw.getUserId()).setTemplateCode(templateCode).setTemplateParams(templateParams));
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java
... ... @@ -206,7 +206,7 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
206 206  
207 207 /**
208 208 * 查询物流轨迹
209   - *
  209 + * <p>
210 210 * 缓存的目的:考虑及时性要求不高,但是每次调用需要钱
211 211 *
212 212 * @param code 快递公司编码
... ... @@ -217,9 +217,8 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
217 217 @Cacheable(cacheNames = RedisKeyConstants.EXPRESS_TRACK, key = "#code + '-' + #logisticsNo + '-' + #receiverMobile",
218 218 condition = "#result != null && #result.length() > 0")
219 219 public List<ExpressTrackRespDTO> getExpressTrackList(String code, String logisticsNo, String receiverMobile) {
220   - return expressClientFactory.getDefaultExpressClient().getExpressTrackList(
221   - new ExpressTrackQueryReqDTO().setExpressCode(code).setLogisticsNo(logisticsNo)
222   - .setPhone(receiverMobile));
  220 + return expressClientFactory.getDefaultExpressClient().getExpressTrackList(new ExpressTrackQueryReqDTO()
  221 + .setExpressCode(code).setLogisticsNo(logisticsNo).setPhone(receiverMobile));
223 222 }
224 223  
225 224 // =================== Order Item ===================
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
1 1 package cn.iocoder.yudao.module.trade.service.order;
2 2  
3   -import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
4 3 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
5 4 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO;
6 5 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO;
... ... @@ -10,9 +9,10 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle
10 9 import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO;
11 10 import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
12 11 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
13   -
14 12 import jakarta.validation.constraints.NotNull;
15 13  
  14 +import java.util.List;
  15 +
16 16 /**
17 17 * 交易订单【写】Service 接口
18 18 *
... ... @@ -187,13 +187,22 @@ public interface TradeOrderUpdateService {
187 187 */
188 188 void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId);
189 189  
190   - // TODO 芋艿:拼团取消,不调这个接口哈;
191 190 /**
192 191 * 取消支付订单
193 192 *
194   - * @param userId 用户编号
195   - * @param orderId 订单编号
  193 + * @param userId 用户编号
  194 + * @param orderId 订单编号
  195 + * @param cancelType 取消类型
  196 + */
  197 + void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
  198 +
  199 + /**
  200 + * 更新下单赠送的优惠券编号到订单
  201 + *
  202 + * @param userId 用户编号
  203 + * @param orderId 订单编号
  204 + * @param giveCouponIds 赠送的优惠券编号列表
196 205 */
197   - void cancelPaidOrder(Long userId, Long orderId);
  206 + void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds);
198 207  
199 208 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
... ... @@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
18 18 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
19 19 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
20 20 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
  21 +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
  22 +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
21 23 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
22 24 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
23 25 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
... ... @@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
111 113 private ProductCommentApi productCommentApi;
112 114 @Resource
113 115 public SocialClientApi socialClientApi;
  116 + @Resource
  117 + public PayRefundApi payRefundApi;
114 118  
115 119 @Resource
116 120 private TradeOrderProperties tradeOrderProperties;
... ... @@ -197,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
197 201 order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
198 202 order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum));
199 203 order.setUserIp(getClientIP()).setTerminal(getTerminal());
  204 + // 使用 + 赠送优惠券
  205 + order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts());
200 206 // 支付 + 退款信息
201 207 order.setAdjustPrice(0).setPayStatus(false);
202 208 order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0);
... ... @@ -369,7 +375,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
369 375  
370 376 // 3. 记录订单日志
371 377 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.DELIVERED.getStatus(),
372   - MapUtil.<String, Object>builder().put("expressName", express != null ? express.getName() : "")
  378 + MapUtil.<String, Object>builder().put("deliveryName", express != null ? express.getName() : "")
373 379 .put("logisticsNo", express != null ? deliveryReqVO.getLogisticsNo() : "").build());
374 380  
375 381 // 4.1 发送站内信
... ... @@ -854,13 +860,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
854 860  
855 861 @Override
856 862 @Transactional(rollbackFor = Exception.class)
857   - public void cancelPaidOrder(Long userId, Long orderId) {
858   - // TODO 芋艿:这里实现要优化下;
  863 + public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
  864 + // 1.1 这里校验下 cancelType 只允许拼团关闭;
  865 + if (ObjUtil.notEqual(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType(), cancelType)) {
  866 + return;
  867 + }
  868 + // 1.2 检验订单存在
859 869 TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
860 870 if (order == null) {
861 871 throw exception(ORDER_NOT_FOUND);
862 872 }
863   - cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
  873 +
  874 + // 1.3 校验订单是否支付
  875 + if (!order.getPayStatus()) {
  876 + throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
  877 + }
  878 + // 1.3 校验订单是否已退款
  879 + if (ObjUtil.equal(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
  880 + throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
  881 + }
  882 +
  883 + // 2.1 取消订单
  884 + cancelOrder0(order, TradeOrderCancelTypeEnum.COMBINATION_CLOSE);
  885 + // 2.2 创建退款单
  886 + payRefundApi.createRefund(new PayRefundCreateReqDTO()
  887 + .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用
  888 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
  889 + .setMerchantRefundId(String.valueOf(order.getId()))
  890 + .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice())); // 价格信息
  891 + }
  892 +
  893 + @Override
  894 + public void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds) {
  895 + // 1. 检验订单存在
  896 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
  897 + if (order == null) {
  898 + throw exception(ORDER_NOT_FOUND);
  899 + }
  900 +
  901 + // 2. 更新订单赠送的优惠券编号列表
  902 + tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds));
864 903 }
865 904  
866 905 /**
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java
... ... @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.trade.service.order.handler;
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
5 5 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO;
  6 +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
  7 +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
6 8 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
7 9 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
8 10 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
... ... @@ -84,7 +86,9 @@ public class TradeCombinationOrderHandler implements TradeOrderHandler {
84 86 return;
85 87 }
86 88 // 校验订单拼团是否成功
87   - if (!combinationRecordApi.isCombinationRecordSuccess(order.getUserId(), order.getId())) {
  89 + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(order.getUserId(), order.getId());
  90 + Assert.notNull(combinationRecord, "订单({})对应的拼团记录不存在", order.getId());
  91 + if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) {
88 92 throw exception(ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS);
89 93 }
90 94 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java
1 1 package cn.iocoder.yudao.module.trade.service.order.handler;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
4 5 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
5 6 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
6 7 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  8 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
  9 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
  10 +import jakarta.annotation.Resource;
  11 +import org.springframework.context.annotation.Lazy;
7 12 import org.springframework.stereotype.Component;
8 13  
9   -import jakarta.annotation.Resource;
10 14 import java.util.List;
11 15  
12 16 /**
... ... @@ -18,6 +22,12 @@ import java.util.List;
18 22 public class TradeCouponOrderHandler implements TradeOrderHandler {
19 23  
20 24 @Resource
  25 + @Lazy // 延迟加载,避免循环依赖
  26 + private TradeOrderUpdateService orderUpdateService;
  27 + @Resource
  28 + private TradeOrderQueryService orderQueryService;
  29 +
  30 + @Resource
21 31 private CouponApi couponApi;
22 32  
23 33 @Override
... ... @@ -31,12 +41,30 @@ public class TradeCouponOrderHandler implements TradeOrderHandler {
31 41 }
32 42  
33 43 @Override
  44 + public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  45 + if (CollUtil.isEmpty(order.getGiveCouponTemplateCounts())) {
  46 + return;
  47 + }
  48 + // 赠送优惠券
  49 + List<Long> couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponTemplateCounts(), order.getUserId());
  50 + if (CollUtil.isEmpty(couponIds)) {
  51 + return;
  52 + }
  53 + orderUpdateService.updateOrderGiveCouponIds(order.getUserId(), order.getId(), couponIds);
  54 + }
  55 +
  56 + @Override
34 57 public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
35   - if (order.getCouponId() == null || order.getCouponId() <= 0) {
  58 + // 情况一:退还订单使用的优惠券
  59 + if (order.getCouponId() != null && order.getCouponId() > 0) {
  60 + // 退回优惠劵
  61 + couponApi.returnUsedCoupon(order.getCouponId());
  62 + }
  63 + // 情况二:收回赠送的优惠券
  64 + if (CollUtil.isEmpty(order.getGiveCouponIds())) {
36 65 return;
37 66 }
38   - // 退回优惠劵
39   - couponApi.returnUsedCoupon(order.getCouponId());
  67 + couponApi.invalidateCouponsByAdmin(order.getGiveCouponIds(), order.getUserId());
40 68 }
41 69  
42 70 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java
... ... @@ -78,12 +78,10 @@ public class TradeMemberPointOrderHandler implements TradeOrderHandler {
78 78  
79 79 @Override
80 80 public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
81   - // 扣减(回滚)积分(订单赠送)
82   - reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM,
83   - orderItem.getId());
84 81 // 增加(回滚)积分(订单抵扣)
85   - addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM,
86   - orderItem.getId());
  82 + addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM, orderItem.getId());
  83 + // 扣减(回滚)积分(订单赠送)
  84 + reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM, orderItem.getId());
87 85  
88 86 // 扣减(回滚)用户经验
89 87 AfterSaleDO afterSale = afterSaleService.getAfterSale(orderItem.getAfterSaleId());
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java
... ... @@ -5,7 +5,9 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
5 5 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
6 6 import lombok.Data;
7 7  
  8 +import java.time.LocalDateTime;
8 9 import java.util.List;
  10 +import java.util.Map;
9 11  
10 12 /**
11 13 * 价格计算 Response BO
... ... @@ -44,9 +46,13 @@ public class TradePriceCalculateRespBO {
44 46 private List<Promotion> promotions;
45 47  
46 48 /**
47   - * 优惠劵编号
  49 + * 使用的优惠劵编号
48 50 */
49 51 private Long couponId;
  52 + /**
  53 + * 用户的优惠劵列表(可用 + 不可用)
  54 + */
  55 + private List<Coupon> coupons;
50 56  
51 57 /**
52 58 * 会员剩余积分
... ... @@ -68,6 +74,21 @@ public class TradePriceCalculateRespBO {
68 74 private Long bargainActivityId;
69 75  
70 76 /**
  77 + * 是否包邮
  78 + */
  79 + private Boolean freeDelivery;
  80 +
  81 + /**
  82 + * 赠送的优惠劵
  83 + *
  84 + * key: 优惠劵模版编号
  85 + * value:对应的优惠券数量
  86 + *
  87 + * 目的:用于订单支付后赠送优惠券
  88 + */
  89 + private Map<Long, Integer> giveCouponTemplateCounts;
  90 +
  91 + /**
71 92 * 订单价格
72 93 */
73 94 @Data
... ... @@ -213,8 +234,19 @@ public class TradePriceCalculateRespBO {
213 234 */
214 235 private Long categoryId;
215 236  
  237 + // ========== 物流相关字段 =========
  238 +
  239 + /**
  240 + * 配送方式数组
  241 + *
  242 + * 对应 DeliveryTypeEnum 枚举
  243 + */
  244 + private List<Integer> deliveryTypes;
  245 +
216 246 /**
217   - * 运费模板 Id
  247 + * 物流配置模板编号
  248 + *
  249 + * 对应 TradeDeliveryExpressTemplateDO 的 id 编号
218 250 */
219 251 private Long deliveryTemplateId;
220 252  
... ... @@ -234,7 +266,7 @@ public class TradePriceCalculateRespBO {
234 266 private List<ProductPropertyValueDetailRespDTO> properties;
235 267  
236 268 /**
237   - * 使用的积分
  269 + * 赠送的积分
238 270 */
239 271 private Integer givePoint;
240 272  
... ... @@ -312,4 +344,62 @@ public class TradePriceCalculateRespBO {
312 344  
313 345 }
314 346  
  347 + /**
  348 + * 优惠劵信息
  349 + */
  350 + @Data
  351 + public static class Coupon {
  352 +
  353 + /**
  354 + * 优惠劵编号
  355 + */
  356 + private Long id;
  357 + /**
  358 + * 优惠劵名
  359 + */
  360 + private String name;
  361 +
  362 + /**
  363 + * 是否设置满多少金额可用,单位:分
  364 + */
  365 + private Integer usePrice;
  366 +
  367 + /**
  368 + * 生效开始时间
  369 + */
  370 + private LocalDateTime validStartTime;
  371 + /**
  372 + * 生效结束时间
  373 + */
  374 + private LocalDateTime validEndTime;
  375 +
  376 + /**
  377 + * 优惠类型
  378 + */
  379 + private Integer discountType;
  380 + /**
  381 + * 折扣百分比
  382 + */
  383 + private Integer discountPercent;
  384 + /**
  385 + * 优惠金额,单位:分
  386 + */
  387 + private Integer discountPrice;
  388 + /**
  389 + * 折扣上限,单位:分
  390 + */
  391 + private Integer discountLimitPrice;
  392 +
  393 + /**
  394 + * 是否匹配
  395 + */
  396 + private Boolean match;
  397 + /**
  398 + * 不匹配的原因
  399 + */
  400 + private String mismatchReason;
  401 +
  402 + }
  403 +
  404 +
315 405 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java
1 1 package cn.iocoder.yudao.module.trade.service.price.calculator;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.lang.Assert;
5 4 import cn.hutool.core.util.ObjectUtil;
6 5 import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 8 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
8 9 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
9   -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
10 10 import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
11 11 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
12 12 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
  13 +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
13 14 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
14 15 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
15 16 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
  17 +import jakarta.annotation.Resource;
16 18 import org.springframework.core.annotation.Order;
17 19 import org.springframework.stereotype.Component;
18 20  
19   -import jakarta.annotation.Resource;
20 21 import java.util.List;
21 22 import java.util.function.Predicate;
22 23  
23 24 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
24 26 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
25   -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
26   -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
  27 +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_CAN_NOT_USE;
27 28 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER;
28 29  
29 30 /**
... ... @@ -40,33 +41,37 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
40 41  
41 42 @Override
42 43 public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
43   - // 1.1 校验优惠劵
44   - if (param.getCouponId() == null) {
45   - return;
46   - }
47   - CouponRespDTO coupon = couponApi.validateCoupon(new CouponValidReqDTO()
48   - .setId(param.getCouponId()).setUserId(param.getUserId()));
49   - Assert.notNull(coupon, "校验通过的优惠劵({}),不能为空", param.getCouponId());
50   - // 1.2 只有【普通】订单,才允许使用优惠劵
  44 + // 只有【普通】订单,才允许使用优惠劵
51 45 if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
52   - throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
  46 + if (param.getCouponId() != null) {
  47 + throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
  48 + }
  49 + return;
53 50 }
54 51  
55   - // 2.1 获得匹配的商品 SKU 数组
56   - List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
57   - if (CollUtil.isEmpty(orderItems)) {
58   - throw exception(COUPON_NO_MATCH_SPU);
  52 + // 1.1 加载用户的优惠劵列表
  53 + List<CouponRespDTO> coupons = couponApi.getCouponListByUserId(param.getUserId(), CouponStatusEnum.UNUSED.getStatus());
  54 + coupons.removeIf(coupon -> LocalDateTimeUtils.beforeNow(coupon.getValidEndTime()));
  55 + // 1.2 计算优惠劵的使用条件
  56 + result.setCoupons(calculateCoupons(coupons, result));
  57 +
  58 + // 2. 校验优惠劵是否可用
  59 + if (param.getCouponId() == null) {
  60 + return;
59 61 }
60   - // 2.2 计算是否满足优惠劵的使用金额
61   - Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
62   - if (totalPayPrice < coupon.getUsePrice()) {
63   - throw exception(COUPON_NO_MATCH_MIN_PRICE);
  62 + TradePriceCalculateRespBO.Coupon couponBO = CollUtil.findOne(result.getCoupons(), item -> item.getId().equals(param.getCouponId()));
  63 + CouponRespDTO coupon = CollUtil.findOne(coupons, item -> item.getId().equals(param.getCouponId()));
  64 + if (couponBO == null || coupon == null) {
  65 + throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, "优惠劵不存在");
  66 + }
  67 + if (Boolean.FALSE.equals(couponBO.getMatch())) {
  68 + throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, couponBO.getMismatchReason());
64 69 }
65 70  
66 71 // 3.1 计算可以优惠的金额
  72 + List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
  73 + Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
67 74 Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
68   - Assert.isTrue(couponPrice < totalPayPrice,
69   - "优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice);
70 75 // 3.2 计算分摊的优惠金额
71 76 List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);
72 77  
... ... @@ -74,7 +79,7 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
74 79 result.setCouponId(param.getCouponId());
75 80 // 4.2 记录优惠明细
76 81 TradePriceCalculatorHelper.addPromotion(result, orderItems,
77   - param.getCouponId(), coupon.getName(), PromotionTypeEnum.COUPON.getType(),
  82 + param.getCouponId(), couponBO.getName(), PromotionTypeEnum.COUPON.getType(),
78 83 StrUtil.format("优惠劵:省 {} 元", TradePriceCalculatorHelper.formatPrice(couponPrice)),
79 84 divideCouponPrices);
80 85 // 4.3 更新 SKU 优惠金额
... ... @@ -86,6 +91,43 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
86 91 TradePriceCalculatorHelper.recountAllPrice(result);
87 92 }
88 93  
  94 + /**
  95 + * 计算用户的优惠劵列表(可用 + 不可用)
  96 + *
  97 + * @param coupons 优惠劵
  98 + * @param result 计算结果
  99 + * @return 优惠劵列表
  100 + */
  101 + private List<TradePriceCalculateRespBO.Coupon> calculateCoupons(List<CouponRespDTO> coupons,
  102 + TradePriceCalculateRespBO result) {
  103 + return convertList(coupons, coupon -> {
  104 + TradePriceCalculateRespBO.Coupon matchCoupon = BeanUtils.toBean(coupon, TradePriceCalculateRespBO.Coupon.class);
  105 + // 1.1 优惠劵未到使用时间
  106 + if (LocalDateTimeUtils.afterNow(coupon.getValidStartTime())) {
  107 + return matchCoupon.setMatch(false).setMismatchReason("优惠劵未到使用时间");
  108 + }
  109 + // 1.2 优惠劵没有匹配的商品
  110 + List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
  111 + if (CollUtil.isEmpty(orderItems)) {
  112 + return matchCoupon.setMatch(false).setMismatchReason("优惠劵没有匹配的商品");
  113 + }
  114 + // 1.3 差 %1$,.2f 元可用优惠劵
  115 + Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
  116 + if (totalPayPrice < coupon.getUsePrice()) {
  117 + return matchCoupon.setMatch(false)
  118 + .setMismatchReason(String.format("差 %1$,.2f 元可用优惠劵", (coupon.getUsePrice() - totalPayPrice) / 100D));
  119 + }
  120 + // 1.4 优惠金额超过订单金额
  121 + Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
  122 + if (couponPrice >= totalPayPrice) {
  123 + return matchCoupon.setMatch(false).setMismatchReason("优惠金额超过订单金额");
  124 + }
  125 +
  126 + // 2. 满足条件
  127 + return matchCoupon.setMatch(true);
  128 + });
  129 + }
  130 +
89 131 private Integer getCouponPrice(CouponRespDTO coupon, Integer totalPayPrice) {
90 132 if (PromotionDiscountTypeEnum.PRICE.getType().equals(coupon.getDiscountType())) { // 减价
91 133 return coupon.getDiscountPrice();
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
... ... @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.lang.Assert;
5 5 import cn.hutool.core.util.StrUtil;
6 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
7 8 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
8 9 import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
9 10 import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
... ... @@ -17,11 +18,11 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate
17 18 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
18 19 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
19 20 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem;
  21 +import jakarta.annotation.Resource;
20 22 import lombok.extern.slf4j.Slf4j;
21 23 import org.springframework.core.annotation.Order;
22 24 import org.springframework.stereotype.Component;
23 25  
24   -import jakarta.annotation.Resource;
25 26 import java.util.List;
26 27 import java.util.Map;
27 28 import java.util.Set;
... ... @@ -55,7 +56,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
55 56 if (param.getDeliveryType() == null) {
56 57 return;
57 58 }
58   - // TODO @puhui999:需要校验,是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈
  59 + // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈
  60 + if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) {
  61 + throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL);
  62 + }
  63 +
59 64 if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) {
60 65 calculateByPickUp(param);
61 66 } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) {
... ... @@ -90,7 +95,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
90 95 return;
91 96 }
92 97  
93   - // 情况二:快递模版
  98 + // 情况二:活动包邮
  99 + if (Boolean.TRUE.equals(result.getFreeDelivery())) {
  100 + return;
  101 + }
  102 +
  103 + // 情况三:快递模版
94 104 // 2.1 过滤出已选中的商品 SKU
95 105 List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
96 106 Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
... ... @@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
124 134 Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
125 135 // 依次计算快递运费
126 136 for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) {
127   - Long templateId = entry.getKey();
  137 + Long templateId = entry.getKey();
128 138 List<OrderItem> orderItems = entry.getValue();
129 139 DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId);
130 140 if (templateBO == null) {
... ... @@ -144,8 +154,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
144 154 /**
145 155 * 按配送方式来计算运费
146 156 *
147   - * @param orderItems SKU 商品项目
148   - * @param chargeMode 配送计费方式
  157 + * @param orderItems SKU 商品项目
  158 + * @param chargeMode 配送计费方式
149 159 * @param templateCharge 快递运费配置
150 160 */
151 161 private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode,
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
1 1 package cn.iocoder.yudao.module.trade.service.price.calculator;
2 2  
3 3 import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.StrUtil;
4 5 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 6 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
6 7 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
... ... @@ -10,6 +11,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
10 11 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
11 12  
12 13 import java.util.ArrayList;
  14 +import java.util.LinkedHashMap;
13 15 import java.util.List;
14 16 import java.util.Map;
15 17  
... ... @@ -30,8 +32,7 @@ public class TradePriceCalculatorHelper {
30 32 List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) {
31 33 // 创建 PriceCalculateRespDTO 对象
32 34 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO();
33   - result.setType(getOrderType(param));
34   - result.setPromotions(new ArrayList<>());
  35 + result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>());
35 36  
36 37 // 创建它的 OrderItem 属性
37 38 result.setItems(new ArrayList<>(param.getItems().size()));
... ... @@ -59,9 +60,9 @@ public class TradePriceCalculatorHelper {
59 60 .setWeight(sku.getWeight()).setVolume(sku.getVolume());
60 61 // spu 信息
61 62 orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
62   - .setDeliveryTemplateId(spu.getDeliveryTemplateId())
  63 + .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId())
63 64 .setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
64   - if (orderItem.getPicUrl() == null) {
  65 + if (StrUtil.isBlank(orderItem.getPicUrl())) {
65 66 orderItem.setPicUrl(spu.getPicUrl());
66 67 }
67 68 });
... ... @@ -240,7 +241,7 @@ public class TradePriceCalculatorHelper {
240 241 *
241 242 * 和 {@link #dividePrice(List, Integer)} 逻辑一致,只是传入的是 TradeOrderItemDO 对象
242 243 *
243   - * @param items 订单项
  244 + * @param items 订单项
244 245 * @param price 订单支付金额
245 246 * @return 分摊金额数组,和传入的 orderItems 一一对应
246 247 */
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java
1 1 package cn.iocoder.yudao.module.trade.service.price.calculator;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.collection.ListUtil;
4 5 import cn.hutool.core.util.ObjectUtil;
5 6 import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
6 8 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
7 9 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
8 10 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
  11 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
9 12 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
10 13 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
11 14 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
12 15 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
  16 +import jakarta.annotation.Resource;
13 17 import org.springframework.core.annotation.Order;
14 18 import org.springframework.stereotype.Component;
15 19  
16   -import jakarta.annotation.Resource;
  20 +import java.util.ArrayList;
  21 +import java.util.Comparator;
17 22 import java.util.List;
  23 +import java.util.Map;
18 24  
19 25 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
20 26 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
21 27 import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
22 28  
  29 +// TODO @puhui999:相关的单测,建议改一改
  30 +
23 31 /**
24 32 * 满减送活动的 {@link TradePriceCalculator} 实现类
25 33 *
... ... @@ -52,7 +60,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
52 60 private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result,
53 61 RewardActivityMatchRespDTO rewardActivity) {
54 62 // 1.1 获得满减送的订单项(商品)列表
55   - List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, rewardActivity);
  63 + List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchActivityOrderItems(result, rewardActivity);
56 64 if (CollUtil.isEmpty(orderItems)) {
57 65 return;
58 66 }
... ... @@ -61,7 +69,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
61 69 if (rule == null) {
62 70 TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems,
63 71 rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
64   - getRewardActivityNotMeetTip(rewardActivity));
  72 + getRewardActivityNotMeetTip(rewardActivity, orderItems));
65 73 return;
66 74 }
67 75  
... ... @@ -84,6 +92,36 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
84 92 TradePriceCalculatorHelper.recountPayPrice(orderItem);
85 93 }
86 94 TradePriceCalculatorHelper.recountAllPrice(result);
  95 +
  96 + // 4.1 记录赠送的积分
  97 + if (rule.getPoint() != null && rule.getPoint() > 0) {
  98 + List<Integer> dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint());
  99 + for (int i = 0; i < orderItems.size(); i++) {
  100 + // 商品可能赠送了积分,所以这里要加上
  101 + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i);
  102 + orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i));
  103 + }
  104 + }
  105 + // 4.2 记录订单是否包邮
  106 + if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
  107 + // 只要满足一个活动包邮那么这单就包邮
  108 + result.setFreeDelivery(true);
  109 + }
  110 + // 4.3 记录赠送的优惠券
  111 + if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
  112 + for (Map.Entry<Long, Integer> entry : rule.getGiveCouponTemplateCounts().entrySet()) {
  113 + Map<Long, Integer> giveCouponTemplateCounts = result.getGiveCouponTemplateCounts();
  114 + // TODO @puhui999:是不是有一种可能性,这个 key 没有,别的 key 有哈。
  115 + // TODO 这里还有一种简化的写法。就是下面,大概两行就可以啦
  116 +// result.getGiveCouponTemplateCounts().put(entry.getKey(),
  117 +// result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue());
  118 + if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券
  119 + result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts());
  120 + } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量
  121 + giveCouponTemplateCounts.put(entry.getKey(), giveCouponTemplateCounts.get(entry.getKey()) + entry.getValue());
  122 + }
  123 + }
  124 + }
87 125 }
88 126  
89 127 /**
... ... @@ -93,10 +131,23 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
93 131 * @param rewardActivity 满减送活动
94 132 * @return 订单项(商品)列表
95 133 */
96   - private List<TradePriceCalculateRespBO.OrderItem> filterMatchCouponOrderItems(TradePriceCalculateRespBO result,
97   - RewardActivityMatchRespDTO rewardActivity) {
98   - return filterList(result.getItems(),
99   - orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId()));
  134 + private List<TradePriceCalculateRespBO.OrderItem> filterMatchActivityOrderItems(TradePriceCalculateRespBO result,
  135 + RewardActivityMatchRespDTO rewardActivity) {
  136 + // 情况一:全部商品都可以参与
  137 + if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
  138 + return result.getItems();
  139 + }
  140 + // 情况二:指定商品参与
  141 + if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
  142 + return filterList(result.getItems(),
  143 + orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId()));
  144 + }
  145 + // 情况三:指定商品类型参与
  146 + if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
  147 + return filterList(result.getItems(),
  148 + orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId()));
  149 + }
  150 + return ListUtil.of();
100 151 }
101 152  
102 153 /**
... ... @@ -129,14 +180,30 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
129 180 }
130 181  
131 182 /**
132   - * 获得满减送活动部匹配时的提示
  183 + * 获得满减送活动不匹配时的提示
133 184 *
134 185 * @param rewardActivity 满减送活动
135 186 * @return 提示
136 187 */
137   - private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity) {
138   - // TODO 芋艿:后面再想想;应该找第一个规则,算下还差多少即可。
139   - return "TODO";
  188 + private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity,
  189 + List<TradePriceCalculateRespBO.OrderItem> orderItems) {
  190 + // 1. 计算数量和价格
  191 + Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems);
  192 + Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
  193 + assert count != null && price != null;
  194 +
  195 + // 2. 构建不满足时的提示信息:按最低档规则算
  196 + String meetTip = "满减送:购满 {} {},可以减 {} 元";
  197 + List<RewardActivityMatchRespDTO.Rule> rules = new ArrayList<>(rewardActivity.getRules());
  198 + rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛升序
  199 + RewardActivityMatchRespDTO.Rule rule = rules.get(0);
  200 + if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) {
  201 + return StrUtil.format(meetTip, rule.getLimit(), "元", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
  202 + }
  203 + if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())) {
  204 + return StrUtil.format(meetTip, rule.getLimit(), "件", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
  205 + }
  206 + return StrUtil.EMPTY;
140 207 }
141 208  
142 209 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java
... ... @@ -99,7 +99,7 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
99 99  
100 100 @BeforeEach
101 101 public void setUp() {
102   - when(tradeOrderProperties.getAppId()).thenReturn(888L);
  102 + when(tradeOrderProperties.getPayAppKey()).thenReturn("mall");
103 103 when(tradeOrderProperties.getPayExpireTime()).thenReturn(Duration.ofDays(1));
104 104 when(tradeNoRedisDAO.generate(anyString())).thenReturn(IdUtil.randomUUID());
105 105 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java
1 1 package cn.iocoder.yudao.module.trade.service.price.calculator;
2 2  
  3 +import cn.hutool.core.collection.ListUtil;
3 4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
4 5 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
5 6 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
6   -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
7 7 import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
8 8 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
9 9 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
  10 +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
10 11 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
11 12 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
12 13 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
... ... @@ -14,8 +15,10 @@ import org.junit.jupiter.api.Test;
14 15 import org.mockito.InjectMocks;
15 16 import org.mockito.Mock;
16 17  
  18 +import java.time.Duration;
17 19 import java.util.ArrayList;
18 20  
  21 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
19 22 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
20 23 import static java.util.Arrays.asList;
21 24 import static org.junit.jupiter.api.Assertions.assertEquals;
... ... @@ -69,8 +72,10 @@ public class TradeCouponPriceCalculatorTest extends BaseMockitoUnitTest {
69 72 CouponRespDTO coupon = randomPojo(CouponRespDTO.class, o -> o.setId(1024L).setName("程序员节")
70 73 .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
71 74 .setUsePrice(350).setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType())
72   - .setDiscountPercent(50).setDiscountLimitPrice(70));
73   - when(couponApi.validateCoupon(eq(new CouponValidReqDTO().setId(1024L).setUserId(233L)))).thenReturn(coupon);
  75 + .setDiscountPercent(50).setDiscountLimitPrice(70))
  76 + .setValidStartTime(addTime(Duration.ofDays(1))).setValidEndTime(addTime(Duration.ofDays(2)));
  77 + when(couponApi.getCouponListByUserId(eq(233L), eq(CouponStatusEnum.UNUSED.getStatus())))
  78 + .thenReturn(ListUtil.toList(coupon));
74 79  
75 80 // 调用
76 81 tradeCouponPriceCalculator.calculate(param, result);
... ...
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
4 4 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
5 5 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
6 6 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
7 8 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
8 9 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
9 10 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
... ... @@ -13,6 +14,7 @@ import org.mockito.InjectMocks;
13 14 import org.mockito.Mock;
14 15  
15 16 import java.util.ArrayList;
  17 +import java.util.LinkedHashMap;
16 18  
17 19 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
18 20 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
... ... @@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
47 49 TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
48 50 .setType(TradeOrderTypeEnum.NORMAL.getType())
49 51 .setPrice(new TradePriceCalculateRespBO.Price())
50   - .setPromotions(new ArrayList<>())
  52 + .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>())
51 53 .setItems(asList(
52 54 new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
53 55 .setPrice(100).setSpuId(1L),
... ... @@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
60 62 TradePriceCalculatorHelper.recountPayPrice(result.getItems());
61 63 TradePriceCalculatorHelper.recountAllPrice(result);
62 64  
63   - // mock 方法(限时折扣 DiscountActivity 信息)
  65 + // mock 方法(满减送 RewardActivity 信息)
64 66 when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList(
65 67 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
66   - .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
67   - .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))),
  68 + .setConditionType(PromotionConditionTypeEnum.PRICE.getType())
  69 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
  70 + .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70)
  71 + .setFreeDelivery(false)))),
68 72 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号")
69   - .setSpuIds(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType())
70   - .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10),
71   - new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个
72   - new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100))))
  73 + .setConditionType(PromotionConditionTypeEnum.COUNT.getType())
  74 + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
  75 + .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10)
  76 + .setPoint(50).setFreeDelivery(false),
  77 + new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60)
  78 + .setPoint(100).setFreeDelivery(false), // 最大可满足,因为是 4 个
  79 + new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)
  80 + .setFreeDelivery(false))))
73 81 ));
74 82  
75 83 // 调用
... ... @@ -94,6 +102,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
94 102 assertEquals(orderItem01.getCouponPrice(), 0);
95 103 assertEquals(orderItem01.getPointPrice(), 0);
96 104 assertEquals(orderItem01.getPayPrice(), 160);
  105 + assertEquals(orderItem01.getGivePoint(), 0);
97 106 // 断言:SKU 2
98 107 TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
99 108 assertEquals(orderItem02.getSkuId(), 20L);
... ... @@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
104 113 assertEquals(orderItem02.getCouponPrice(), 0);
105 114 assertEquals(orderItem02.getPointPrice(), 0);
106 115 assertEquals(orderItem02.getPayPrice(), 120);
  116 + assertEquals(orderItem02.getGivePoint(), 0);
107 117 // 断言:SKU 3
108 118 TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2);
109 119 assertEquals(orderItem03.getSkuId(), 30L);
... ... @@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
114 124 assertEquals(orderItem03.getCouponPrice(), 0);
115 125 assertEquals(orderItem03.getPointPrice(), 0);
116 126 assertEquals(orderItem03.getPayPrice(), 60);
  127 + assertEquals(orderItem03.getGivePoint(), 100);
117 128 // 断言:Promotion 部分(第一个)
118 129 assertEquals(result.getPromotions().size(), 2);
119 130 TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
... ... @@ -175,7 +186,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
175 186 // mock 方法(限时折扣 DiscountActivity 信息)
176 187 when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList(
177 188 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
178   - .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
  189 + .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
179 190 .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70))))
180 191 ));
181 192  
... ...
yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql
... ... @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS &quot;trade_order&quot;
48 48 "give_point" int NULL,
49 49 "refund_point" int NULL,
50 50 "vip_price" int NULL,
  51 + "give_coupons_map" varchar NULL,
51 52 "seckill_activity_id" long NULL,
52 53 "bargain_activity_id" long NULL,
53 54 "bargain_record_id" long NULL,
... ...
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
... ... @@ -18,12 +18,12 @@ import cn.iocoder.yudao.module.member.service.user.MemberUserService;
18 18 import io.swagger.v3.oas.annotations.Operation;
19 19 import io.swagger.v3.oas.annotations.Parameter;
20 20 import io.swagger.v3.oas.annotations.tags.Tag;
  21 +import jakarta.annotation.Resource;
  22 +import jakarta.validation.Valid;
21 23 import org.springframework.security.access.prepost.PreAuthorize;
22 24 import org.springframework.validation.annotation.Validated;
23 25 import org.springframework.web.bind.annotation.*;
24 26  
25   -import jakarta.annotation.Resource;
26   -import jakarta.validation.Valid;
27 27 import java.util.Collection;
28 28 import java.util.List;
29 29 import java.util.Objects;
... ... @@ -76,14 +76,6 @@ public class MemberUserController {
76 76 return success(true);
77 77 }
78 78  
79   - @PutMapping("/update-balance")
80   - @Operation(summary = "更新会员用户余额")
81   - @PreAuthorize("@ss.hasPermission('member:user:update-balance')")
82   - public CommonResult<Boolean> updateUserBalance(@Valid @RequestBody Long id) {
83   - // todo @jason:增加一个【修改余额】
84   - return success(true);
85   - }
86   -
87 79 @GetMapping("/get")
88 80 @Operation(summary = "获得会员用户")
89 81 @Parameter(name = "id", description = "编号", required = true, example = "1024")
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java
1 1 package cn.iocoder.yudao.module.pay.api.order.dto;
2 2  
3   -import lombok.Data;
4   -import org.hibernate.validator.constraints.Length;
5   -
6 3 import jakarta.validation.constraints.DecimalMin;
7 4 import jakarta.validation.constraints.NotEmpty;
8 5 import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import org.hibernate.validator.constraints.Length;
  8 +
9 9 import java.io.Serializable;
10 10 import java.time.LocalDateTime;
11 11  
... ... @@ -18,10 +18,10 @@ public class PayOrderCreateReqDTO implements Serializable {
18 18 public static final int SUBJECT_MAX_LENGTH = 32;
19 19  
20 20 /**
21   - * 应用编号
  21 + * 应用标识
22 22 */
23   - @NotNull(message = "应用编号不能为空")
24   - private Long appId;
  23 + @NotNull(message = "应用标识不能为空")
  24 + private String appKey;
25 25 /**
26 26 * 用户 IP
27 27 */
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java
1 1 package cn.iocoder.yudao.module.pay.api.refund.dto;
2 2  
3   -import lombok.Data;
4   -import org.hibernate.validator.constraints.Length;
5   -
6 3 import jakarta.validation.constraints.Min;
7 4 import jakarta.validation.constraints.NotEmpty;
8 5 import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import org.hibernate.validator.constraints.Length;
9 8  
10 9 /**
11 10 * 退款单创建 Request DTO
... ... @@ -16,10 +15,10 @@ import jakarta.validation.constraints.NotNull;
16 15 public class PayRefundCreateReqDTO {
17 16  
18 17 /**
19   - * 应用编号
  18 + * 应用标识
20 19 */
21   - @NotNull(message = "应用编号不能为空")
22   - private Long appId;
  20 + @NotNull(message = "应用标识不能为空")
  21 + private String appKey;
23 22 /**
24 23 * 用户 IP
25 24 */
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
... ... @@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.pay.api.transfer.dto;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
4 4 import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
5   -import lombok.Data;
6   -
7 5 import jakarta.validation.constraints.Min;
8 6 import jakarta.validation.constraints.NotBlank;
9 7 import jakarta.validation.constraints.NotEmpty;
10 8 import jakarta.validation.constraints.NotNull;
  9 +import lombok.Data;
  10 +
11 11 import java.util.Map;
12 12  
13 13 /**
... ... @@ -19,10 +19,10 @@ import java.util.Map;
19 19 public class PayTransferCreateReqDTO {
20 20  
21 21 /**
22   - * 应用编号
  22 + * 应用标识
23 23 */
24   - @NotNull(message = "应用编号不能为空")
25   - private Long appId;
  24 + @NotNull(message = "应用标识不能为空")
  25 + private String appKey;
26 26  
27 27 @NotEmpty(message = "转账渠道不能为空")
28 28 private String channelCode;
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
... ... @@ -14,6 +14,7 @@ public interface ErrorCodeConstants {
14 14 ErrorCode APP_IS_DISABLE = new ErrorCode(1_007_000_002, "App 已经被禁用");
15 15 ErrorCode APP_EXIST_ORDER_CANT_DELETE = new ErrorCode(1_007_000_003, "支付应用存在支付订单,无法删除");
16 16 ErrorCode APP_EXIST_REFUND_CANT_DELETE = new ErrorCode(1_007_000_004, "支付应用存在退款订单,无法删除");
  17 + ErrorCode APP_KEY_EXISTS = new ErrorCode(1_007_000_005, "支付应用标识已经存在");
17 18  
18 19 // ========== CHANNEL 模块 1-007-001-000 ==========
19 20 ErrorCode CHANNEL_NOT_FOUND = new ErrorCode(1_007_001_000, "支付渠道的配置不存在");
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
... ... @@ -18,9 +18,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
18 18 RECHARGE(1, "充值"),
19 19 RECHARGE_REFUND(2, "充值退款"),
20 20 PAYMENT(3, "支付"),
21   - PAYMENT_REFUND(4, "支付退款");
22   -
23   - // TODO 后续增加
  21 + PAYMENT_REFUND(4, "支付退款"),
  22 + UPDATE_BALANCE(5, "更新余额");
24 23  
25 24 /**
26 25 * 业务分类
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java
... ... @@ -14,6 +14,10 @@ import jakarta.validation.constraints.*;
14 14 @Data
15 15 public class PayAppBaseVO {
16 16  
  17 + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
  18 + @NotEmpty(message = "应用标识不能为空")
  19 + private String appKey;
  20 +
17 21 @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆")
18 22 @NotNull(message = "应用名不能为空")
19 23 private String name;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java
1 1 package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
  2 +
2 3 import io.swagger.v3.oas.annotations.media.Schema;
3   -import lombok.*;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +import lombok.ToString;
4 7  
5 8 @Schema(description = "管理后台 - 支付应用信息创建 Request VO")
6 9 @Data
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java
... ... @@ -20,6 +20,9 @@ public class PayAppPageReqVO extends PageParam {
20 20 @Schema(description = "应用名", example = "小豆")
21 21 private String name;
22 22  
  23 + @Schema(description = "应用标识", example = "yudao")
  24 + private String appKey;
  25 +
23 26 @Schema(description = "开启状态", example = "0")
24 27 private Integer status;
25 28  
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java
1 1 package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
  2 +
2 3 import io.swagger.v3.oas.annotations.media.Schema;
3   -import lombok.*;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +import lombok.ToString;
4 7  
5 8 import java.time.LocalDateTime;
6 9  
... ... @@ -13,6 +16,9 @@ public class PayAppRespVO extends PayAppBaseVO {
13 16 @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
14 17 private Long id;
15 18  
  19 + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
  20 + private String appKey;
  21 +
16 22 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
17 23 private LocalDateTime createTime;
18 24  
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java
1 1 package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
  2 +
2 3 import io.swagger.v3.oas.annotations.media.Schema;
3 4 import lombok.*;
4 5 import jakarta.validation.constraints.*;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java
... ... @@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
6 6 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO;
  7 +import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO;
7 8 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO;
8 9 import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert;
9 10 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
  11 +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
10 12 import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
11 13 import io.swagger.v3.oas.annotations.Operation;
12 14 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -15,12 +17,12 @@ import jakarta.validation.Valid;
15 17 import lombok.extern.slf4j.Slf4j;
16 18 import org.springframework.security.access.prepost.PreAuthorize;
17 19 import org.springframework.validation.annotation.Validated;
18   -import org.springframework.web.bind.annotation.GetMapping;
19   -import org.springframework.web.bind.annotation.RequestMapping;
20   -import org.springframework.web.bind.annotation.RestController;
  20 +import org.springframework.web.bind.annotation.*;
21 21  
22 22 import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER;
  23 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
23 24 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  25 +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND;
24 26  
25 27 @Tag(name = "管理后台 - 用户钱包")
26 28 @RestController
... ... @@ -48,4 +50,21 @@ public class PayWalletController {
48 50 return success(PayWalletConvert.INSTANCE.convertPage(pageResult));
49 51 }
50 52  
  53 + @PutMapping("/update-balance")
  54 + @Operation(summary = "更新会员用户余额")
  55 + @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')")
  56 + public CommonResult<Boolean> updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) {
  57 + // 获得用户钱包
  58 + PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue());
  59 + if (wallet == null) {
  60 + log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO);
  61 + throw exception(WALLET_NOT_FOUND);
  62 + }
  63 +
  64 + // 更新钱包余额
  65 + payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()),
  66 + PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance());
  67 + return success(true);
  68 + }
  69 +
51 70 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java
1 1 package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
4 6 import io.swagger.v3.oas.annotations.media.Schema;
5 7 import lombok.Data;
6 8  
... ... @@ -8,7 +10,14 @@ import lombok.Data;
8 10 @Data
9 11 public class PayWalletTransactionPageReqVO extends PageParam {
10 12  
11   - @Schema(description = "钱包编号", example = "1")
  13 + @Schema(description = "钱包编号", example = "888")
12 14 private Long walletId;
13 15  
  16 + @Schema(description = "用户编号", example = "1024")
  17 + private Long userId;
  18 +
  19 + @Schema(description = "用户类型", example = "1")
  20 + @InEnum(UserTypeEnum.class)
  21 + private Integer userType;
  22 +
14 23 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - 修改钱包余额 Request VO")
  8 +@Data
  9 +public class PayWalletUpdateBalanceReqVO {
  10 +
  11 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
  12 + @NotNull(message = "用户编号不能为空")
  13 + private Long userId;
  14 +
  15 + @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
  16 + @NotNull(message = "变动余额不能为空")
  17 + private Integer balance;
  18 +
  19 +}
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
... ... @@ -32,6 +32,10 @@ public class PayAppDO extends BaseDO {
32 32 @TableId
33 33 private Long id;
34 34 /**
  35 + * 应用标识
  36 + */
  37 + private String appKey;
  38 + /**
35 39 * 应用名
36 40 */
37 41 private String name;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.pay.dal.mysql.app;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6   -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
7 6 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
8 7 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
9 8 import org.apache.ibatis.annotations.Mapper;
... ... @@ -14,9 +13,14 @@ public interface PayAppMapper extends BaseMapperX&lt;PayAppDO&gt; {
14 13 default PageResult<PayAppDO> selectPage(PayAppPageReqVO reqVO) {
15 14 return selectPage(reqVO, new LambdaQueryWrapperX<PayAppDO>()
16 15 .likeIfPresent(PayAppDO::getName, reqVO.getName())
  16 + .likeIfPresent(PayAppDO::getAppKey, reqVO.getAppKey())
17 17 .eqIfPresent(PayAppDO::getStatus, reqVO.getStatus())
18 18 .betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime())
19 19 .orderByDesc(PayAppDO::getId));
20 20 }
21 21  
  22 + default PayAppDO selectByAppKey(String appKey) {
  23 + return selectOne(PayAppDO::getAppKey, appKey);
  24 + }
  25 +
22 26 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java
... ... @@ -15,6 +15,8 @@ public class PayProperties {
15 15 private static final String ORDER_NO_PREFIX = "P";
16 16 private static final String REFUND_NO_PREFIX = "R";
17 17  
  18 + private static final String WALLET_PAY_APP_KEY_DEFAULT = "wallet";
  19 +
18 20 /**
19 21 * 支付回调地址
20 22 *
... ... @@ -49,4 +51,10 @@ public class PayProperties {
49 51 @NotEmpty(message = "退款订单 no 的前缀不能为空")
50 52 private String refundNoPrefix = REFUND_NO_PREFIX;
51 53  
  54 + /**
  55 + * 钱包支付应用 AppKey
  56 + */
  57 + @NotEmpty(message = "钱包支付应用 AppKey 不能为空")
  58 + private String walletPayAppKey = WALLET_PAY_APP_KEY_DEFAULT;
  59 +
52 60 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java
... ... @@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
7 7 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
8 8 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
9 9 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
10   -
11 10 import jakarta.validation.Valid;
  11 +
12 12 import java.util.Collection;
13 13 import java.util.List;
14 14 import java.util.Map;
... ... @@ -88,13 +88,13 @@ public interface PayAppService {
88 88 * @return 商户 Map
89 89 */
90 90 default Map<Long, PayAppDO> getAppMap(Collection<Long> ids) {
91   - List<PayAppDO> list = getAppList(ids);
  91 + List<PayAppDO> list = getAppList(ids);
92 92 return CollectionUtils.convertMap(list, PayAppDO::getId);
93 93 }
94 94  
95 95 /**
96 96 * 支付应用的合法性
97   - *
  97 + * <p>
98 98 * 如果不合法,抛出 {@link ServiceException} 业务异常
99 99 *
100 100 * @param id 应用编号
... ... @@ -102,4 +102,14 @@ public interface PayAppService {
102 102 */
103 103 PayAppDO validPayApp(Long id);
104 104  
  105 + /**
  106 + * 支付应用的合法性
  107 + * <p>
  108 + * 如果不合法,抛出 {@link ServiceException} 业务异常
  109 + *
  110 + * @param appKey 应用标识
  111 + * @return 应用
  112 + */
  113 + PayAppDO validPayApp(String appKey);
  114 +
105 115 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java
... ... @@ -11,11 +11,11 @@ import cn.iocoder.yudao.module.pay.dal.mysql.app.PayAppMapper;
11 11 import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
12 12 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
13 13 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
  14 +import jakarta.annotation.Resource;
14 15 import org.springframework.context.annotation.Lazy;
15 16 import org.springframework.stereotype.Service;
16 17 import org.springframework.validation.annotation.Validated;
17 18  
18   -import jakarta.annotation.Resource;
19 19 import java.util.Collection;
20 20 import java.util.List;
21 21  
... ... @@ -43,6 +43,9 @@ public class PayAppServiceImpl implements PayAppService {
43 43  
44 44 @Override
45 45 public Long createApp(PayAppCreateReqVO createReqVO) {
  46 + // 验证 appKey 是否重复
  47 + validateAppKeyUnique(null, createReqVO.getAppKey());
  48 +
46 49 // 插入
47 50 PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
48 51 appMapper.insert(app);
... ... @@ -54,11 +57,28 @@ public class PayAppServiceImpl implements PayAppService {
54 57 public void updateApp(PayAppUpdateReqVO updateReqVO) {
55 58 // 校验存在
56 59 validateAppExists(updateReqVO.getId());
  60 + // 验证 appKey 是否重复
  61 + validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey());
  62 +
57 63 // 更新
58 64 PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
59 65 appMapper.updateById(updateObj);
60 66 }
61 67  
  68 + void validateAppKeyUnique(Long id, String appKey) {
  69 + PayAppDO app = appMapper.selectByAppKey(appKey);
  70 + if (app == null) {
  71 + return;
  72 + }
  73 + // 如果 id 为空,说明不用比较是否为相同 appKey 的应用
  74 + if (id == null) {
  75 + throw exception(APP_KEY_EXISTS);
  76 + }
  77 + if (!app.getId().equals(id)) {
  78 + throw exception(APP_KEY_EXISTS);
  79 + }
  80 + }
  81 +
62 82 @Override
63 83 public void updateAppStatus(Long id, Integer status) {
64 84 // 校验商户存在
... ... @@ -101,7 +121,7 @@ public class PayAppServiceImpl implements PayAppService {
101 121  
102 122 @Override
103 123 public List<PayAppDO> getAppList() {
104   - return appMapper.selectList();
  124 + return appMapper.selectList();
105 125 }
106 126  
107 127 @Override
... ... @@ -110,14 +130,30 @@ public class PayAppServiceImpl implements PayAppService {
110 130 }
111 131  
112 132 @Override
113   - public PayAppDO validPayApp(Long id) {
114   - PayAppDO app = appMapper.selectById(id);
  133 + public PayAppDO validPayApp(Long appId) {
  134 + PayAppDO app = appMapper.selectById(appId);
  135 + return validatePayApp(app);
  136 + }
  137 +
  138 + @Override
  139 + public PayAppDO validPayApp(String appKey) {
  140 + PayAppDO app = appMapper.selectByAppKey(appKey);
  141 + return validatePayApp(app);
  142 + }
  143 +
  144 + /**
  145 + * 校验支付应用实体的有效性:存在 + 开启
  146 + *
  147 + * @param app 待校验的支付应用实体
  148 + * @return 校验通过的支付应用实体
  149 + */
  150 + private PayAppDO validatePayApp(PayAppDO app) {
115 151 // 校验是否存在
116 152 if (app == null) {
117 153 throw exception(ErrorCodeConstants.APP_NOT_FOUND);
118 154 }
119 155 // 校验是否禁用
120   - if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
  156 + if (CommonStatusEnum.isDisable(app.getStatus())) {
121 157 throw exception(ErrorCodeConstants.APP_IS_DISABLE);
122 158 }
123 159 return app;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
... ... @@ -43,11 +43,11 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
43 43 public class PayDemoOrderServiceImpl implements PayDemoOrderService {
44 44  
45 45 /**
46   - * 接入的实力应用编号
  46 + * 接入的支付应用标识
47 47 *
48 48 * 从 [支付管理 -> 应用信息] 里添加
49 49 */
50   - private static final Long PAY_APP_ID = 7L;
  50 + private static final String PAY_APP_KEY = "demo";
51 51  
52 52 /**
53 53 * 商品信息 Map
... ... @@ -88,7 +88,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
88 88  
89 89 // 2.1 创建支付单
90 90 Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
91   - .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
  91 + .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
92 92 .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号
93 93 .setSubject(spuName).setBody("").setPrice(price) // 价格信息
94 94 .setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间
... ... @@ -190,7 +190,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
190 190 String refundId = order.getId() + "-refund";
191 191 // 2.2 创建退款单
192 192 Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
193   - .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
  193 + .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
194 194 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
195 195 .setMerchantRefundId(refundId)
196 196 .setReason("想退钱").setPrice(order.getPrice()));// 价格信息
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
... ... @@ -111,11 +111,11 @@ public class PayOrderServiceImpl implements PayOrderService {
111 111 @Override
112 112 public Long createOrder(PayOrderCreateReqDTO reqDTO) {
113 113 // 校验 App
114   - PayAppDO app = appService.validPayApp(reqDTO.getAppId());
  114 + PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
115 115  
116 116 // 查询对应的支付交易单是否已经存在。如果是,则直接返回
117 117 PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
118   - reqDTO.getAppId(), reqDTO.getMerchantOrderId());
  118 + app.getId(), reqDTO.getMerchantOrderId());
119 119 if (order != null) {
120 120 log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
121 121 order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况
... ... @@ -431,9 +431,7 @@ public class PayOrderServiceImpl implements PayOrderService {
431 431 return;
432 432 }
433 433  
434   - // TODO 芋艿:应该 new 出来更新
435   - order.setPrice(payPrice);
436   - orderMapper.updateById(order);
  434 + orderMapper.updateById(new PayOrderDO().setId(order.getId()).setPrice(payPrice));
437 435 }
438 436  
439 437 @Override
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
... ... @@ -26,12 +26,12 @@ import cn.iocoder.yudao.module.pay.service.app.PayAppService;
26 26 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
27 27 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
28 28 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
  29 +import jakarta.annotation.Resource;
29 30 import lombok.extern.slf4j.Slf4j;
30 31 import org.springframework.stereotype.Service;
31 32 import org.springframework.transaction.annotation.Transactional;
32 33 import org.springframework.validation.annotation.Validated;
33 34  
34   -import jakarta.annotation.Resource;
35 35 import java.util.List;
36 36  
37 37 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -93,9 +93,9 @@ public class PayRefundServiceImpl implements PayRefundService {
93 93 @Override
94 94 public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
95 95 // 1.1 校验 App
96   - PayAppDO app = appService.validPayApp(reqDTO.getAppId());
  96 + PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
97 97 // 1.2 校验支付订单
98   - PayOrderDO order = validatePayOrderCanRefund(reqDTO);
  98 + PayOrderDO order = validatePayOrderCanRefund(reqDTO, app.getId());
99 99 // 1.3 校验支付渠道是否有效
100 100 PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
101 101 PayClient client = channelService.getPayClient(channel.getId());
... ... @@ -113,7 +113,7 @@ public class PayRefundServiceImpl implements PayRefundService {
113 113 // 2.1 插入退款单
114 114 String no = noRedisDAO.generate(payProperties.getRefundNoPrefix());
115 115 refund = PayRefundConvert.INSTANCE.convert(reqDTO)
116   - .setNo(no).setOrderId(order.getId()).setOrderNo(order.getNo())
  116 + .setNo(no).setAppId(app.getId()).setOrderId(order.getId()).setOrderNo(order.getNo())
117 117 .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
118 118 // 商户相关的字段
119 119 .setNotifyUrl(app.getRefundNotifyUrl())
... ... @@ -153,8 +153,8 @@ public class PayRefundServiceImpl implements PayRefundService {
153 153 * @param reqDTO 退款申请信息
154 154 * @return 支付订单
155 155 */
156   - private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
157   - PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
  156 + private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO, Long appId) {
  157 + PayOrderDO order = orderService.getOrder(appId, reqDTO.getMerchantOrderId());
158 158 if (order == null) {
159 159 throw exception(PAY_ORDER_NOT_FOUND);
160 160 }
... ... @@ -164,11 +164,11 @@ public class PayRefundServiceImpl implements PayRefundService {
164 164 }
165 165  
166 166 // 校验金额,退款金额不能大于原定的金额
167   - if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
  167 + if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()) {
168 168 throw exception(REFUND_PRICE_EXCEED);
169 169 }
170 170 // 是否有退款中的订单
171   - if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
  171 + if (refundMapper.selectCountByAppIdAndOrderId(appId, order.getId(),
172 172 PayRefundStatusEnum.WAITING.getStatus()) > 0) {
173 173 throw exception(REFUND_HAS_REFUNDING);
174 174 }
... ... @@ -197,9 +197,10 @@ public class PayRefundServiceImpl implements PayRefundService {
197 197 * 通知并更新订单的退款结果
198 198 *
199 199 * @param channel 支付渠道
200   - * @param notify 通知
  200 + * @param notify 通知
201 201 */
202   - @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
  202 + // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
  203 + @Transactional(rollbackFor = Exception.class)
203 204 public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
204 205 // 情况一:退款成功
205 206 if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
... ... @@ -24,12 +24,12 @@ import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
24 24 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
25 25 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
26 26 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
  27 +import jakarta.annotation.Resource;
  28 +import jakarta.validation.Validator;
27 29 import lombok.extern.slf4j.Slf4j;
28 30 import org.springframework.stereotype.Service;
29 31 import org.springframework.transaction.annotation.Transactional;
30 32  
31   -import jakarta.annotation.Resource;
32   -import jakarta.validation.Validator;
33 33 import java.util.List;
34 34  
35 35 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -79,16 +79,16 @@ public class PayTransferServiceImpl implements PayTransferService {
79 79 @Override
80 80 public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
81 81 // 1.1 校验 App
82   - PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
  82 + PayAppDO payApp = appService.validPayApp(reqDTO.getAppKey());
83 83 // 1.2 校验支付渠道是否有效
84   - PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
  84 + PayChannelDO channel = channelService.validPayChannel(payApp.getId(), reqDTO.getChannelCode());
85 85 PayClient client = channelService.getPayClient(channel.getId());
86 86 if (client == null) {
87 87 log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
88 88 throw exception(CHANNEL_NOT_FOUND);
89 89 }
90 90 // 1.3 校验转账单已经发起过转账。
91   - PayTransferDO transfer = validateTransferCanCreate(reqDTO);
  91 + PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId());
92 92  
93 93 if (transfer == null) {
94 94 // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
... ... @@ -116,8 +116,8 @@ public class PayTransferServiceImpl implements PayTransferService {
116 116 return transfer.getId();
117 117 }
118 118  
119   - private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
120   - PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
  119 + private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
  120 + PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
121 121 if (transfer != null) {
122 122 // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
123 123 if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
... ... @@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletRechargeMapper;
18 18 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
19 19 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
20 20 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
  21 +import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
21 22 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
22 23 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
23 24 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
... ... @@ -51,11 +52,6 @@ import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
51 52 @Slf4j
52 53 public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
53 54  
54   - /**
55   - * TODO 芋艿:放到 payconfig
56   - */
57   - private static final Long WALLET_PAY_APP_ID = 8L;
58   -
59 55 private static final String WALLET_RECHARGE_ORDER_SUBJECT = "钱包余额充值";
60 56  
61 57 @Resource
... ... @@ -68,9 +64,13 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
68 64 private PayRefundService payRefundService;
69 65 @Resource
70 66 private PayWalletRechargePackageService payWalletRechargePackageService;
  67 +
71 68 @Resource
72 69 public SocialClientApi socialClientApi;
73 70  
  71 + @Resource
  72 + private PayProperties payProperties;
  73 +
74 74 @Override
75 75 @Transactional(rollbackFor = Exception.class)
76 76 public PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp,
... ... @@ -92,7 +92,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
92 92  
93 93 // 2.1 创建支付单
94 94 Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO()
95   - .setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
  95 + .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
96 96 .setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号
97 97 .setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("")
98 98 .setPrice(recharge.getPayPrice())
... ... @@ -174,7 +174,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
174 174 String walletRechargeId = String.valueOf(id);
175 175 String refundId = walletRechargeId + "-refund";
176 176 Long payRefundId = payRefundService.createPayRefund(new PayRefundCreateReqDTO()
177   - .setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
  177 + .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
178 178 .setMerchantOrderId(walletRechargeId)
179 179 .setMerchantRefundId(refundId)
180 180 .setReason("想退钱").setPrice(walletRecharge.getPayPrice()));
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
... ... @@ -12,12 +12,12 @@ import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
12 12 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
13 13 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
14 14 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
  15 +import jakarta.annotation.Resource;
15 16 import lombok.extern.slf4j.Slf4j;
16 17 import org.springframework.context.annotation.Lazy;
17 18 import org.springframework.stereotype.Service;
18 19 import org.springframework.transaction.annotation.Transactional;
19 20  
20   -import jakarta.annotation.Resource;
21 21 import java.time.LocalDateTime;
22 22  
23 23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements PayWalletService {
176 176 walletMapper.updateWhenRecharge(payWallet.getId(), price);
177 177 break;
178 178 }
  179 + case UPDATE_BALANCE: // 更新余额
  180 + walletMapper.updateWhenRecharge(payWallet.getId(), price);
  181 + break;
179 182 default: {
180 183 // TODO 其它类型待实现
181 184 throw new UnsupportedOperationException("待实现");
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java
1 1 package cn.iocoder.yudao.module.pay.service.wallet;
2 2  
  3 +import cn.hutool.core.util.ObjectUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO;
5 6 import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO;
... ... @@ -11,12 +12,11 @@ import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletTransactionMapper;
11 12 import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
12 13 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
13 14 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
  15 +import jakarta.annotation.Resource;
14 16 import lombok.extern.slf4j.Slf4j;
15 17 import org.springframework.stereotype.Service;
16 18 import org.springframework.validation.annotation.Validated;
17 19  
18   -import jakarta.annotation.Resource;
19   -
20 20 import java.time.LocalDateTime;
21 21  
22 22 import static cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_EXPENSE;
... ... @@ -53,6 +53,16 @@ public class PayWalletTransactionServiceImpl implements PayWalletTransactionServ
53 53  
54 54 @Override
55 55 public PageResult<PayWalletTransactionDO> getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO) {
  56 + // 基于 userId + userType 查询钱包
  57 + if (pageVO.getWalletId() == null
  58 + && ObjectUtil.isAllNotEmpty(pageVO.getUserId(), pageVO.getUserType())) {
  59 + PayWalletDO wallet = payWalletService.getOrCreateWallet(pageVO.getUserId(), pageVO.getUserType());
  60 + if (wallet != null) {
  61 + pageVO.setWalletId(wallet.getId());
  62 + }
  63 + }
  64 +
  65 + // 查询分页
56 66 return payWalletTransactionMapper.selectPage(pageVO.getWalletId(), null, pageVO, null);
57 67 }
58 68  
... ...
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
... ... @@ -218,11 +218,11 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
218 218 public void testCreateOrder_success() {
219 219 // mock 参数
220 220 PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class,
221   - o -> o.setAppId(1L).setMerchantOrderId("10")
  221 + o -> o.setAppKey("demo").setMerchantOrderId("10")
222 222 .setSubject(randomString()).setBody(randomString()));
223 223 // mock 方法
224 224 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1"));
225   - when(appService.validPayApp(eq(reqDTO.getAppId()))).thenReturn(app);
  225 + when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app);
226 226  
227 227 // 调用
228 228 Long orderId = orderService.createOrder(reqDTO);
... ... @@ -239,10 +239,13 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
239 239 public void testCreateOrder_exists() {
240 240 // mock 参数
241 241 PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class,
242   - o -> o.setAppId(1L).setMerchantOrderId("10"));
  242 + o -> o.setAppKey("demo").setMerchantOrderId("10"));
243 243 // mock 数据
244 244 PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> o.setAppId(1L).setMerchantOrderId("10"));
245 245 orderMapper.insert(dbOrder);
  246 + // mock 方法
  247 + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1"));
  248 + when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app);
246 249  
247 250 // 调用
248 251 Long orderId = orderService.createOrder(reqDTO);
... ...
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java
... ... @@ -209,10 +209,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
209 209 @Test
210 210 public void testCreateRefund_orderNotFound() {
211 211 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
212   - o -> o.setAppId(1L));
  212 + o -> o.setAppKey("demo"));
213 213 // mock 方法(app)
214 214 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
215   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  215 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
216 216  
217 217 // 调用,并断言异常
218 218 assertServiceException(() -> refundService.createPayRefund(reqDTO),
... ... @@ -232,10 +232,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
232 232 private void testCreateRefund_orderWaitingOrClosed(Integer status) {
233 233 // 准备参数
234 234 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
235   - o -> o.setAppId(1L).setMerchantOrderId("100"));
  235 + o -> o.setAppKey("demo").setMerchantOrderId("100"));
236 236 // mock 方法(app)
237 237 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
238   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  238 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
239 239 // mock 数据(order)
240 240 PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status));
241 241 when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
... ... @@ -249,10 +249,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
249 249 public void testCreateRefund_refundPriceExceed() {
250 250 // 准备参数
251 251 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
252   - o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
  252 + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10));
253 253 // mock 方法(app)
254 254 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
255   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  255 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
256 256 // mock 数据(order)
257 257 PayOrderDO order = randomPojo(PayOrderDO.class, o ->
258 258 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
... ... @@ -268,10 +268,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
268 268 public void testCreateRefund_orderHasRefunding() {
269 269 // 准备参数
270 270 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
271   - o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
  271 + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10));
272 272 // mock 方法(app)
273 273 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
274   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  274 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
275 275 // mock 数据(order)
276 276 PayOrderDO order = randomPojo(PayOrderDO.class, o ->
277 277 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
... ... @@ -291,10 +291,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
291 291 public void testCreateRefund_channelNotFound() {
292 292 // 准备参数
293 293 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
294   - o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9));
  294 + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9));
295 295 // mock 方法(app)
296 296 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
297   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  297 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
298 298 // mock 数据(order)
299 299 PayOrderDO order = randomPojo(PayOrderDO.class, o ->
300 300 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
... ... @@ -315,11 +315,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
315 315 public void testCreateRefund_refundExists() {
316 316 // 准备参数
317 317 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
318   - o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
  318 + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
319 319 .setMerchantRefundId("200").setReason("测试退款"));
320 320 // mock 方法(app)
321 321 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
322   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  322 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
323 323 // mock 数据(order)
324 324 PayOrderDO order = randomPojo(PayOrderDO.class, o ->
325 325 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
... ... @@ -347,11 +347,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
347 347 public void testCreateRefund_invokeException() {
348 348 // 准备参数
349 349 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
350   - o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
  350 + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
351 351 .setMerchantRefundId("200").setReason("测试退款"));
352 352 // mock 方法(app)
353 353 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
354   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  354 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
355 355 // mock 数据(order)
356 356 PayOrderDO order = randomPojo(PayOrderDO.class, o ->
357 357 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
... ... @@ -391,11 +391,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
391 391  
392 392 // 准备参数
393 393 PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
394   - o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
  394 + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
395 395 .setMerchantRefundId("200").setReason("测试退款"));
396 396 // mock 方法(app)
397 397 PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
398   - when(appService.validPayApp(eq(1L))).thenReturn(app);
  398 + when(appService.validPayApp(eq("demo"))).thenReturn(app);
399 399 // mock 数据(order)
400 400 PayOrderDO order = randomPojo(PayOrderDO.class, o ->
401 401 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
... ...
yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql
1 1 CREATE TABLE IF NOT EXISTS "pay_app" (
2 2 "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  3 + "app_key" varchar(64) NOT NULL,
3 4 "name" varchar(64) NOT NULL,
4 5 "status" tinyint NOT NULL,
5 6 "remark" varchar(255) DEFAULT NULL,
... ...
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java
... ... @@ -27,6 +27,11 @@ public class AlipayPayClientConfig implements PayClientConfig {
27 27 public static final Integer MODE_CERTIFICATE = 2;
28 28  
29 29 /**
  30 + * 接口内容加密方式 - AES 加密
  31 + */
  32 + public static final String ENC_TYPE_AES = "AES";
  33 +
  34 + /**
30 35 * 签名算法类型 - RSA
31 36 */
32 37 public static final String SIGN_TYPE_DEFAULT = "RSA2";
... ... @@ -92,6 +97,22 @@ public class AlipayPayClientConfig implements PayClientConfig {
92 97 @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
93 98 private String rootCertContent;
94 99  
  100 + /**
  101 + * 接口内容加密方式
  102 + *
  103 + * 1. 如果为空,将使用无加密方式
  104 + * 2. 如果要加密,目前支付宝只有 AES 一种加密方式
  105 + *
  106 + * @see <a href="https://opendocs.alipay.com/common/02mse3">支付宝开放平台</a>
  107 + * @see AlipayPayClientConfig#ENC_TYPE_AES
  108 + */
  109 + private String encryptType;
  110 +
  111 + /**
  112 + * 接口内容加密的私钥
  113 + */
  114 + private String encryptKey;
  115 +
95 116 public interface ModePublicKey {
96 117 }
97 118  
... ...
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
... ... @@ -266,7 +266,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
266 266 String errorCode = getErrorCode(e);
267 267 String errorMessage = getErrorMessage(e);
268 268 return PayRefundRespDTO.failureOf(errorCode, errorMessage,
269   - reqDTO.getOutTradeNo(), e.getXmlString());
  269 + reqDTO.getOutRefundNo(), e.getXmlString());
270 270 }
271 271 }
272 272  
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogCreateReqDTO;
5 5 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogPageReqDTO;
6 6 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogRespDTO;
7 7 import jakarta.validation.Valid;
  8 +import org.springframework.scheduling.annotation.Async;
8 9  
9 10 /**
10 11 * 操作日志 API 接口
... ... @@ -21,6 +22,16 @@ public interface OperateLogApi {
21 22 void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO);
22 23  
23 24 /**
  25 + * 【异步】创建操作日志
  26 + *
  27 + * @param createReqDTO 请求
  28 + */
  29 + @Async
  30 + default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) {
  31 + createOperateLog(createReqDTO);
  32 + }
  33 +
  34 + /**
24 35 * 获取指定模块的指定数据的操作日志分页
25 36 *
26 37 * @param pageReqDTO 请求
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/DictTypeConstants.java
... ... @@ -13,12 +13,11 @@ public interface DictTypeConstants {
13 13 // ========== SYSTEM 模块 ==========
14 14  
15 15 String USER_SEX = "system_user_sex"; // 用户性别
  16 + String DATA_SCOPE = "system_data_scope"; // 数据范围
16 17  
17 18 String LOGIN_TYPE = "system_login_type"; // 登录日志的类型
18 19 String LOGIN_RESULT = "system_login_result"; // 登录结果
19 20  
20   - String ERROR_CODE_TYPE = "system_error_code_type"; // 错误码的类型枚举
21   -
22 21 String SMS_CHANNEL_CODE = "system_sms_channel_code"; // 短信渠道编码
23 22 String SMS_TEMPLATE_TYPE = "system_sms_template_type"; // 短信模板类型
24 23 String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
... ... @@ -27,10 +27,10 @@ public interface ErrorCodeConstants {
27 27 // ========== 角色模块 1-002-002-000 ==========
28 28 ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在");
29 29 ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1_002_002_001, "已经存在名为【{}】的角色");
30   - ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1_002_002_002, "已经存在编码为【{}】的角色");
  30 + ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1_002_002_002, "已经存在标识为【{}】的角色");
31 31 ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能操作类型为系统内置的角色");
32 32 ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用");
33   - ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "编码【{}】不能使用");
  33 + ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用");
34 34  
35 35 // ========== 用户模块 1-002-003-000 ==========
36 36 ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在");
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java
... ... @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO;
9 9 import cn.iocoder.yudao.module.system.service.logger.OperateLogService;
10 10 import com.fhs.core.trans.anno.TransMethodResult;
11 11 import jakarta.annotation.Resource;
12   -import org.springframework.scheduling.annotation.Async;
13 12 import org.springframework.stereotype.Service;
14 13 import org.springframework.validation.annotation.Validated;
15 14  
... ... @@ -26,7 +25,6 @@ public class OperateLogApiImpl implements OperateLogApi {
26 25 private OperateLogService operateLogService;
27 26  
28 27 @Override
29   - @Async
30 28 public void createOperateLog(OperateLogCreateReqDTO createReqDTO) {
31 29 operateLogService.createOperateLog(createReqDTO);
32 30 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java
... ... @@ -6,9 +6,9 @@ import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6 6 import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
7 7 import com.alibaba.excel.annotation.ExcelProperty;
8 8 import io.swagger.v3.oas.annotations.media.Schema;
  9 +import jakarta.validation.constraints.NotBlank;
9 10 import lombok.Data;
10 11  
11   -import jakarta.validation.constraints.NotBlank;
12 12 import java.time.LocalDateTime;
13 13 import java.util.Set;
14 14  
... ... @@ -46,7 +46,8 @@ public class RoleRespVO {
46 46 private String remark;
47 47  
48 48 @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
49   - @ExcelProperty("数据范围")
  49 + @ExcelProperty(value = "数据范围", converter = DictConvert.class)
  50 + @DictFormat(DictTypeConstants.DATA_SCOPE)
50 51 private Integer dataScope;
51 52  
52 53 @Schema(description = "数据范围(指定部门数组)", example = "1")
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
3 5 import com.mzt.logapi.starter.annotation.DiffLogField;
4 6 import io.swagger.v3.oas.annotations.media.Schema;
5   -import lombok.Data;
6   -
7 7 import jakarta.validation.constraints.NotBlank;
8 8 import jakarta.validation.constraints.NotNull;
9 9 import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
10 11  
11 12 @Schema(description = "管理后台 - 角色创建/更新 Request VO")
12 13 @Data
... ... @@ -23,7 +24,7 @@ public class RoleSaveReqVO {
23 24  
24 25 @NotBlank(message = "角色标志不能为空")
25 26 @Size(max = 100, message = "角色标志长度不能超过 100 个字符")
26   - @Schema(description = "角色编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ADMIN")
  27 + @Schema(description = "角色标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "ADMIN")
27 28 @DiffLogField(name = "角色标志")
28 29 private String code;
29 30  
... ... @@ -32,7 +33,14 @@ public class RoleSaveReqVO {
32 33 @DiffLogField(name = "显示顺序")
33 34 private Integer sort;
34 35  
  36 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  37 + @DiffLogField(name = "状态")
  38 + @NotNull(message = "状态不能为空")
  39 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
  40 + private Integer status;
  41 +
35 42 @Schema(description = "备注", example = "我是一个角色")
  43 + @Size(max = 500, message = "备注长度不能超过 500 个字符")
36 44 @DiffLogField(name = "备注")
37 45 private String remark;
38 46  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
... ... @@ -6,9 +6,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
6 6 import cn.iocoder.yudao.module.system.service.sms.SmsSendService;
7 7 import io.swagger.v3.oas.annotations.Operation;
8 8 import io.swagger.v3.oas.annotations.tags.Tag;
9   -import org.springframework.web.bind.annotation.PostMapping;
10   -import org.springframework.web.bind.annotation.RequestMapping;
11   -import org.springframework.web.bind.annotation.RestController;
  9 +import org.springframework.web.bind.annotation.*;
12 10  
13 11 import jakarta.annotation.Resource;
14 12 import jakarta.annotation.security.PermitAll;
... ... @@ -26,7 +24,7 @@ public class SmsCallbackController {
26 24  
27 25 @PostMapping("/aliyun")
28 26 @PermitAll
29   - @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/zh/sms/developer-reference/configure-delivery-receipts-1 文档")
  27 + @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
30 28 public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
31 29 String text = ServletUtils.getBody(request);
32 30 smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
... ... @@ -35,7 +33,7 @@ public class SmsCallbackController {
35 33  
36 34 @PostMapping("/tencent")
37 35 @PermitAll
38   - @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/59178 文档")
  36 + @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档")
39 37 public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable {
40 38 String text = ServletUtils.getBody(request);
41 39 smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text);
... ... @@ -46,10 +44,17 @@ public class SmsCallbackController {
46 44 @PostMapping("/huawei")
47 45 @PermitAll
48 46 @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
49   - public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable {
50   - String text = ServletUtils.getBody(request);
51   - smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text);
  47 + public CommonResult<Boolean> receiveHuaweiSmsStatus(@RequestBody String requestBody) throws Throwable {
  48 + smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), requestBody);
  49 + return success(true);
  50 + }
  51 +
  52 + @PostMapping("/qiniu")
  53 + @PermitAll
  54 + @Operation(summary = "七牛云短信的回调", description = "参见 https://developer.qiniu.com/sms/5910/message-push 文档")
  55 + public CommonResult<Boolean> receiveQiniuSmsStatus(@RequestBody String requestBody) throws Throwable {
  56 + smsSendService.receiveSmsStatus(SmsChannelEnum.QINIU.getCode(), requestBody);
52 57 return success(true);
53 58 }
54 59  
55 60 -}
  61 +}
56 62 \ No newline at end of file
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java
... ... @@ -30,7 +30,8 @@ public interface SmsClientFactory {
30 30 * 创建短信 Client
31 31 *
32 32 * @param properties 配置对象
  33 + * @return 短信 Client
33 34 */
34   - void createOrUpdateSmsClient(SmsChannelProperties properties);
  35 + SmsClient createOrUpdateSmsClient(SmsChannelProperties properties);
35 36  
36 37 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java
... ... @@ -26,15 +26,9 @@ public abstract class AbstractSmsClient implements SmsClient {
26 26 * 初始化
27 27 */
28 28 public final void init() {
29   - doInit();
30 29 log.debug("[init][配置({}) 初始化完成]", properties);
31 30 }
32 31  
33   - /**
34   - * 自定义初始化
35   - */
36   - protected abstract void doInit();
37   -
38 32 public final void refresh(SmsChannelProperties properties) {
39 33 // 判断是否更新
40 34 if (properties.equals(this.properties)) {
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
... ... @@ -51,10 +51,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
51 51 }
52 52  
53 53 @Override
54   - protected void doInit() {
55   - }
56   -
57   - @Override
58 54 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
59 55 List<KeyValue<String, Object>> templateParams) throws Throwable {
60 56 Assert.notBlank(properties.getSignature(), "短信签名不能为空");
... ... @@ -80,7 +76,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
80 76 @Override
81 77 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
82 78 JSONArray statuses = JSONUtil.parseArray(text);
83   - // 字段参考
  79 + // 字段参考 https://help.aliyun.com/zh/sms/developer-reference/smsreport-2
84 80 return convertList(statuses, status -> {
85 81 JSONObject statusObj = (JSONObject) status;
86 82 return new SmsReceiveRespDTO()
... ... @@ -102,8 +98,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
102 98 queryParam.put("TemplateCode", apiTemplateId);
103 99 JSONObject response = request("QuerySmsTemplate", queryParam);
104 100  
105   - System.out.println("getSmsTemplate response is =====" + response.toString());
106   -
107 101 // 2.1 请求失败
108 102 String code = response.getStr("Code");
109 103 if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
... ... @@ -168,9 +162,9 @@ public class AliyunSmsClient extends AbstractSmsClient {
168 162 String hashedRequestBody = DigestUtil.sha256Hex(requestBody);
169 163  
170 164 // 4. 构建 Authorization 签名
171   - String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
  165 + String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n"
  166 + + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
172 167 String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
173   -
174 168 String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
175 169 String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
176 170 headers.put("Authorization", "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey()
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java
... ... @@ -37,10 +37,6 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
37 37 }
38 38  
39 39 @Override
40   - protected void doInit() {
41   - }
42   -
43   - @Override
44 40 public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
45 41 String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
46 42 // 构建请求
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
1 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
3   -
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.hutool.core.date.format.FastDateFormat;
4 5 import cn.hutool.core.lang.Assert;
  6 +import cn.hutool.core.util.CharsetUtil;
5 7 import cn.hutool.core.util.StrUtil;
6   -
7 8 import cn.hutool.crypto.SecureUtil;
8   -import cn.hutool.http.HttpRequest;
9   -import cn.hutool.http.HttpResponse;
  9 +import cn.hutool.http.HttpUtil;
10 10 import cn.hutool.json.JSONObject;
11 11 import cn.hutool.json.JSONUtil;
12 12 import cn.iocoder.yudao.framework.common.core.KeyValue;
13   -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  13 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
14 14 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
15 15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
16 16 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
17 17 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
18 18 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
19 19 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
20   -
21   -import com.fasterxml.jackson.annotation.JsonFormat;
22   -import com.fasterxml.jackson.annotation.JsonProperty;
23   -import lombok.Data;
24 20 import lombok.extern.slf4j.Slf4j;
25 21  
26 22 import java.io.UnsupportedEncodingException;
27 23 import java.net.URLEncoder;
28   -import java.text.SimpleDateFormat;
29   -import java.util.*;
30   -
31   -
  24 +import java.nio.charset.StandardCharsets;
  25 +import java.time.Instant;
32 26 import java.time.LocalDateTime;
33   -
  27 +import java.time.ZoneId;
  28 +import java.util.*;
34 29  
35 30 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
36 31 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
37   -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
38   -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
39   -
40 32  
41 33 /**
42 34 * 华为短信客户端的实现类
... ... @@ -47,182 +39,128 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
47 39 @Slf4j
48 40 public class HuaweiSmsClient extends AbstractSmsClient {
49 41  
50   - /**
51   - * 调用成功 code
52   - */
53   - public static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
54   - public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
55   - public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
56   -
57   - @Override
58   - protected void doInit() {
  42 + private static final String URL = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";//APP接入地址+接口访问URI
  43 + private static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
  44 + private static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
59 45  
60   - }
  46 + private static final String RESPONSE_CODE_SUCCESS = "000000";
61 47  
62 48 public HuaweiSmsClient(SmsChannelProperties properties) {
63 49 super(properties);
64 50 Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
65 51 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
  52 + validateSender(properties);
66 53 }
67 54  
68   - @Override
69   - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
70   - List<KeyValue<String, Object>> templateParams) throws Throwable {
71   - // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
72   - // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
73   - String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
74   - String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID
75   -
76   - //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
77   - String statusCallBack = properties.getCallbackUrl();
78   -
79   - List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));
80   -
81   - JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack);
82   - SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
83   -
84   - return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
  55 + /**
  56 + * 参数校验华为云的 sender 通道号
  57 + *
  58 + * 原因是:验华为云发放短信的时候,需要额外的参数 sender
  59 + *
  60 + * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
  61 + *
  62 + * @param properties 配置
  63 + */
  64 + private static void validateSender(SmsChannelProperties properties) {
  65 + String combineKey = properties.getApiKey();
  66 + Assert.notEmpty(combineKey, "apiKey 不能为空");
  67 + String[] keys = combineKey.trim().split(" ");
  68 + Assert.isTrue(keys.length == 2, "华为云短信 apiKey 配置格式错误,请配置 为[accessKeyId sender]");
85 69 }
86 70  
87   - JSONObject sendSmsRequest(String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {
88   -
89   - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
90   - sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
91   - String sdkDate = sdf.format(new Date());
92   -
93   - // ************* 步骤 1:拼接规范请求串 *************
94   - String httpRequestMethod = "POST";
95   - String canonicalUri = "/sms/batchSendSms/v1/";
96   - String canonicalQueryString = "";//查询参数为空
97   - String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
98   - + "host:"+ HOST +"\n"
99   - + "x-sdk-date:" + sdkDate + "\n";
100   - //请求Body,不携带签名名称时,signature请填null
101   - String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);
102   - if (null == body || body.isEmpty()) {
103   - return null;
104   - }
105   - String hashedRequestBody = sha256Hex(body);
106   - String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
107   - + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + hashedRequestBody;
108   -
109   - // ************* 步骤 2:拼接待签名字符串 *************
110   - String hashedCanonicalRequest = sha256Hex(canonicalRequest);
111   - String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + hashedCanonicalRequest;
112   -
113   - // ************* 步骤 3:计算签名 *************
114   - String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign);
115   -
116   - // ************* 步骤 4:拼接 Authorization *************
117   - String authorization = "SDK-HMAC-SHA256" + " " + "Access=" + properties.getApiKey() + ", "
118   - + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature;
119   -
120   - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
121   - HttpResponse response = HttpRequest.post(URL)
122   - .header("Content-Type", "application/x-www-form-urlencoded")
123   - .header("X-Sdk-Date", sdkDate)
124   - .header("host",HOST)
125   - .header("Authorization", authorization)
126   - .body(body)
127   - .execute();
128   -
129   - return JSONUtil.parseObj(response.body());
  71 + private String getAccessKey() {
  72 + return StrUtil.subBefore(properties.getApiKey(), " ", true);
130 73 }
131 74  
132   - private SmsResponse getSmsSendResponse(JSONObject resJson) {
133   - SmsResponse smsResponse = new SmsResponse();
134   - smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
135   - smsResponse.setData(resJson);
136   - return smsResponse;
  75 + private String getSender() {
  76 + return StrUtil.subAfter(properties.getApiKey(), " ", true);
137 77 }
138 78  
139   - static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
140   - String statusCallBack, String signature) throws UnsupportedEncodingException {
141   - if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
142   - || templateId.isEmpty()) {
143   - System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
144   - return null;
  79 + @Override
  80 + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
  81 + List<KeyValue<String, Object>> templateParams) throws Throwable {
  82 + StringBuilder requestBody = new StringBuilder();
  83 + appendToBody(requestBody, "from=", getSender());
  84 + appendToBody(requestBody, "&to=", mobile);
  85 + appendToBody(requestBody, "&templateId=", apiTemplateId);
  86 + appendToBody(requestBody, "&templateParas=", JsonUtils.toJsonString(
  87 + convertList(templateParams, kv -> String.valueOf(kv.getValue()))));
  88 + appendToBody(requestBody, "&statusCallback=", properties.getCallbackUrl());
  89 + appendToBody(requestBody, "&extend=", String.valueOf(sendLogId));
  90 + JSONObject response = request("/sms/batchSendSms/v1/", "POST", requestBody.toString());
  91 +
  92 + // 2. 解析请求
  93 + if (!response.containsKey("result")) { // 例如说:密钥不正确
  94 + return new SmsSendRespDTO().setSuccess(false)
  95 + .setApiCode(response.getStr("code"))
  96 + .setApiMsg(response.getStr("description"));
145 97 }
146   -
147   - StringBuilder body = new StringBuilder();
148   - appendToBody(body, "from=", sender);
149   - appendToBody(body, "&to=", receiver);
150   - appendToBody(body, "&templateId=", templateId);
151   - appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
152   - appendToBody(body, "&statusCallback=", statusCallBack);
153   - appendToBody(body, "&signature=", signature);
154   - return body.toString();
  98 + JSONObject sendResult = response.getJSONArray("result").getJSONObject(0);
  99 + return new SmsSendRespDTO().setSuccess(RESPONSE_CODE_SUCCESS.equals(response.getStr("code")))
  100 + .setSerialNo(sendResult.getStr("smsMsgId")).setApiCode(sendResult.getStr("status"));
155 101 }
156 102  
157   - private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {
158   - if (null != val && !val.isEmpty()) {
159   - body.append(key).append(URLEncoder.encode(val, "UTF-8"));
160   - }
  103 + /**
  104 + * 请求华为云短信
  105 + *
  106 + * @see <a href="认证鉴权">https://support.huaweicloud.com/api-msgsms/sms_05_0046.html</a>
  107 + * @param uri 请求 URI
  108 + * @param method 请求 Method
  109 + * @param requestBody 请求 Body
  110 + * @return 请求结果
  111 + */
  112 + private JSONObject request(String uri, String method, String requestBody) {
  113 + // 1.1 请求 Header
  114 + TreeMap<String, String> headers = new TreeMap<>();
  115 + headers.put("Content-Type", "application/x-www-form-urlencoded");
  116 + String sdkDate = FastDateFormat.getInstance("yyyyMMdd'T'HHmmss'Z'", TimeZone.getTimeZone("UTC")).format(new Date());
  117 + headers.put("X-Sdk-Date", sdkDate);
  118 + headers.put("host", HOST);
  119 +
  120 + // 1.2 构建签名 Header
  121 + String canonicalQueryString = ""; // 查询参数为空
  122 + String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
  123 + + "host:"+ HOST +"\n" + "x-sdk-date:" + sdkDate + "\n";
  124 + String canonicalRequest = method + "\n" + uri + "\n" + canonicalQueryString + "\n"
  125 + + canonicalHeaders + "\n" + SIGNEDHEADERS + "\n" + sha256Hex(requestBody);
  126 + String stringToSign = "SDK-HMAC-SHA256" + "\n" + sdkDate + "\n" + sha256Hex(canonicalRequest);
  127 + String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
  128 + headers.put("Authorization", "SDK-HMAC-SHA256" + " " + "Access=" + getAccessKey()
  129 + + ", " + "SignedHeaders=" + SIGNEDHEADERS + ", " + "Signature=" + signature);
  130 +
  131 + // 2. 发起请求
  132 + String responseBody = HttpUtils.post(URL, headers, requestBody);
  133 + return JSONUtil.parseObj(responseBody);
161 134 }
  135 +
162 136 @Override
163   - public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
164   - List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
165   - return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD"))
166   - .setErrorCode(status.getStatus()).setErrorMsg(status.getStatus())
167   - .setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime())
168   - .setSerialNo(status.getSmsMsgId()));
  137 + public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String requestBody) {
  138 + Map<String, String> params = HttpUtil.decodeParamMap(requestBody, StandardCharsets.UTF_8);
  139 + // 字段参考 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html
  140 + return ListUtil.of(new SmsReceiveRespDTO()
  141 + .setSuccess("DELIVRD".equals(params.get("status"))) // 是否接收成功
  142 + .setErrorCode(params.get("status")) // 状态报告编码
  143 + .setErrorMsg(params.get("statusDesc"))
  144 + .setMobile(params.get("to")) // 手机号
  145 + .setReceiveTime(LocalDateTime.ofInstant(Instant.parse(params.get("updateTime")), ZoneId.of("UTC"))) // 状态报告时间
  146 + .setSerialNo(params.get("smsMsgId")) // 发送序列号
  147 + .setLogId(Long.valueOf(params.get("extend")))); // 用户序列号
169 148 }
170 149  
171 150 @Override
172 151 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
173   - //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。
174   - return new SmsTemplateRespDTO().setId(null).setContent(null)
  152 + // 华为短信模板查询和发送短信,是不同的两套 key 和 secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现
  153 + String[] strs = apiTemplateId.split(" ");
  154 + Assert.isTrue(strs.length == 2, "格式不正确,需要满足:apiTemplateId sender");
  155 + return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(null)
175 156 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
176   -
177   - }
178   -
179   - @Data
180   - public static class SmsResponse {
181   -
182   - /**
183   - * 是否成功
184   - */
185   - private boolean success;
186   -
187   - /**
188   - * 厂商原返回体
189   - */
190   - private Object data;
191   -
192 157 }
193 158  
194   -
195   - /**
196   - * 短信接收状态
197   - *
198   - * 参见 <a href="https://support.huaweicloud.com/api-msgsms/sms_05_0003.html">文档</a>
199   - *
200   - * @author scholar
201   - */
202   - @Data
203   - public static class SmsReceiveStatus {
204   -
205   - /**
206   - * 本条状态报告对应的短信的接收方号码,仅当状态报告中携带了extend参数时才会同时携带该参数
207   - */
208   - @JsonProperty("to")
209   - private String phoneNumber;
210   -
211   - /**
212   - * 短信资源的更新时间,通常为短信平台接收短信状态报告的时间
213   - */
214   - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
215   - private LocalDateTime updateTime;
216   -
217   - /**
218   - * 短信状态报告枚举值
219   - */
220   - private String status;
221   -
222   - /**
223   - * 发送短信成功时返回的短信唯一标识。
224   - */
225   - private String smsMsgId;
  159 + @SuppressWarnings("CharsetObjectCanBeUsed")
  160 + private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException {
  161 + if (StrUtil.isNotEmpty(value)) {
  162 + body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name()));
  163 + }
226 164 }
227 165  
228   -}
  166 +}
229 167 \ No newline at end of file
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.hutool.core.collection.CollStreamUtil;
  4 +import cn.hutool.core.date.DateUtil;
  5 +import cn.hutool.core.date.LocalDateTimeUtil;
  6 +import cn.hutool.core.lang.Assert;
  7 +import cn.hutool.core.util.ObjectUtil;
  8 +import cn.hutool.crypto.SecureUtil;
  9 +import cn.hutool.crypto.digest.HmacAlgorithm;
  10 +import cn.hutool.json.JSONObject;
  11 +import cn.hutool.json.JSONUtil;
  12 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  13 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  14 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  15 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  16 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
  17 +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  18 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  19 +import com.google.common.annotations.VisibleForTesting;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +
  22 +import java.util.*;
  23 +import java.util.function.Function;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  26 +
  27 +/**
  28 + * 七牛云短信客户端的实现类
  29 + *
  30 + * @author scholar
  31 + * @since 2024/08/26 15:35
  32 + */
  33 +@Slf4j
  34 +public class QiniuSmsClient extends AbstractSmsClient {
  35 +
  36 + private static final String HOST = "sms.qiniuapi.com";
  37 +
  38 + public QiniuSmsClient(SmsChannelProperties properties) {
  39 + super(properties);
  40 + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
  41 + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
  42 + }
  43 +
  44 + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
  45 + List<KeyValue<String, Object>> templateParams) throws Throwable {
  46 + // 1. 执行请求
  47 + // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages
  48 + LinkedHashMap<String, Object> body = new LinkedHashMap<>();
  49 + body.put("template_id", apiTemplateId);
  50 + body.put("mobile", mobile);
  51 + body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue));
  52 + body.put("seq", Long.toString(sendLogId));
  53 + JSONObject response = request("POST", body, "/v1/message/single");
  54 +
  55 + // 2. 解析请求
  56 + if (ObjectUtil.isNotEmpty(response.getStr("error"))) {
  57 + // 短信请求失败
  58 + return new SmsSendRespDTO().setSuccess(false)
  59 + .setApiCode(response.getStr("error"))
  60 + .setApiRequestId(response.getStr("request_id"))
  61 + .setApiMsg(response.getStr("message"));
  62 + }
  63 + return new SmsSendRespDTO().setSuccess(response.containsKey("message_id"))
  64 + .setSerialNo(response.getStr("message_id"));
  65 + }
  66 +
  67 + /**
  68 + * 请求七牛云短信
  69 + *
  70 + * @see <a href="https://developer.qiniu.com/sms/5842/sms-api-authentication"</>
  71 + * @param httpMethod http请求方法
  72 + * @param body http请求消息体
  73 + * @param path URL path
  74 + * @return 请求结果
  75 + */
  76 + private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, String path) {
  77 + String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'");
  78 + // 1. 请求头
  79 + Map<String, String> header = new HashMap<>(4);
  80 + header.put("HOST", HOST);
  81 + header.put("Authorization", getSignature(httpMethod, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate));
  82 + header.put("Content-Type", "application/json");
  83 + header.put("X-Qiniu-Date", signDate);
  84 +
  85 + // 2. 发起请求
  86 + String responseBody;
  87 + if (Objects.equals(httpMethod, "POST")){
  88 + responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body));
  89 + } else {
  90 + responseBody = HttpUtils.get("https://" + HOST + path, header);
  91 + }
  92 + return JSONUtil.parseObj(responseBody);
  93 + }
  94 +
  95 + private String getSignature(String method, String path, String body, String signDate) {
  96 + StringBuilder dataToSign = new StringBuilder();
  97 + dataToSign.append(method.toUpperCase()).append(" ").append(path)
  98 + .append("\nHost: ").append(HOST)
  99 + .append("\n").append("Content-Type").append(": ").append("application/json")
  100 + .append("\n").append("X-Qiniu-Date").append(": ").append(signDate)
  101 + .append("\n\n");
  102 + if (ObjectUtil.isNotEmpty(body)) {
  103 + dataToSign.append(body);
  104 + }
  105 + String signature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret())
  106 + .digestBase64(dataToSign.toString(), true);
  107 + return "Qiniu " + properties.getApiKey() + ":" + signature;
  108 + }
  109 +
  110 + @Override
  111 + public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
  112 + JSONObject status = JSONUtil.parseObj(text);
  113 + // 字段参考 https://developer.qiniu.com/sms/5910/message-push
  114 + return convertList(status.getJSONArray("items"), new Function<Object, SmsReceiveRespDTO>() {
  115 +
  116 + @Override
  117 + public SmsReceiveRespDTO apply(Object item) {
  118 + JSONObject statusObj = (JSONObject) item;
  119 + return new SmsReceiveRespDTO()
  120 + .setSuccess("DELIVRD".equals(statusObj.getStr("status"))) // 是否接收成功
  121 + .setErrorMsg(statusObj.getStr("status")) // 状态报告编码
  122 + .setMobile(statusObj.getStr("mobile")) // 手机号
  123 + .setReceiveTime(LocalDateTimeUtil.of(statusObj.getLong("delivrd_at") * 1000L)) // 状态报告时间
  124 + .setSerialNo(statusObj.getStr("message_id")) // 发送序列号
  125 + .setLogId(statusObj.getLong("seq")); // 用户序列号
  126 + }
  127 +
  128 + });
  129 + }
  130 +
  131 + @Override
  132 + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
  133 + // 1. 执行请求
  134 + // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template
  135 + JSONObject response = request("GET", null, "/v1/template/" + apiTemplateId);
  136 +
  137 + // 2.2 解析请求
  138 + return new SmsTemplateRespDTO()
  139 + .setId(response.getStr("id"))
  140 + .setContent(response.getStr("template"))
  141 + .setAuditStatus(convertSmsTemplateAuditStatus(response.getStr("audit_status")))
  142 + .setAuditReason(response.getStr("reject_reason"));
  143 + }
  144 +
  145 + @VisibleForTesting
  146 + Integer convertSmsTemplateAuditStatus(String templateStatus) {
  147 + switch (templateStatus) {
  148 + case "passed": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
  149 + case "reviewing": return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
  150 + case "rejected": return SmsTemplateAuditStatusEnum.FAIL.getStatus();
  151 + default:
  152 + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus));
  153 + }
  154 + }
  155 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java
... ... @@ -59,7 +59,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
59 59 }
60 60  
61 61 @Override
62   - public void createOrUpdateSmsClient(SmsChannelProperties properties) {
  62 + public SmsClient createOrUpdateSmsClient(SmsChannelProperties properties) {
63 63 AbstractSmsClient client = channelIdClients.get(properties.getId());
64 64 if (client == null) {
65 65 client = this.createSmsClient(properties);
... ... @@ -68,6 +68,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
68 68 } else {
69 69 client.refresh(properties);
70 70 }
  71 + return client;
71 72 }
72 73  
73 74 private AbstractSmsClient createSmsClient(SmsChannelProperties properties) {
... ... @@ -79,6 +80,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
79 80 case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
80 81 case TENCENT: return new TencentSmsClient(properties);
81 82 case HUAWEI: return new HuaweiSmsClient(properties);
  83 + case QINIU: return new QiniuSmsClient(properties);
82 84 }
83 85 // 创建失败,错误日志 + 抛出异常
84 86 log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java
1 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
  3 +import cn.hutool.core.date.format.FastDateFormat;
3 4 import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.HexUtil;
4 6 import cn.hutool.core.util.StrUtil;
5   -import cn.hutool.http.HttpRequest;
6   -import cn.hutool.http.HttpResponse;
  7 +import cn.hutool.crypto.digest.DigestUtil;
  8 +import cn.hutool.crypto.digest.HmacAlgorithm;
7 9 import cn.hutool.json.JSONArray;
8 10 import cn.hutool.json.JSONObject;
9 11 import cn.hutool.json.JSONUtil;
10 12 import cn.iocoder.yudao.framework.common.core.KeyValue;
11 13 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
12   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  14 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
13 15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
14 16 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
15 17 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
16 18 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
17 19 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
18   -import com.fasterxml.jackson.annotation.JsonFormat;
19   -import com.fasterxml.jackson.annotation.JsonProperty;
20 20 import com.google.common.annotations.VisibleForTesting;
21   -import jakarta.xml.bind.DatatypeConverter;
22   -import lombok.Data;
23 21  
24   -import javax.crypto.Mac;
25   -import javax.crypto.spec.SecretKeySpec;
26 22 import java.nio.charset.StandardCharsets;
27   -import java.text.SimpleDateFormat;
28   -import java.time.LocalDateTime;
29 23 import java.util.*;
30 24  
31 25 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
32 26 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
33   -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
34   -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
35 27  
36   -// TODO @scholar 建议参考 AliyunSmsClient 优化下
37 28 /**
38 29 * 腾讯云短信功能实现
39 30 *
... ... @@ -43,6 +34,10 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
43 34 */
44 35 public class TencentSmsClient extends AbstractSmsClient {
45 36  
  37 + private static final String HOST = "sms.tencentcloudapi.com";
  38 + private static final String VERSION = "2021-01-11";
  39 + private static final String REGION = "ap-guangzhou";
  40 +
46 41 /**
47 42 * 调用成功 code
48 43 */
... ... @@ -56,18 +51,12 @@ public class TencentSmsClient extends AbstractSmsClient {
56 51 */
57 52 private static final long INTERNATIONAL_CHINA = 0L;
58 53  
59   -
60 54 public TencentSmsClient(SmsChannelProperties properties) {
61 55 super(properties);
62 56 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
63 57 validateSdkAppId(properties);
64 58 }
65 59  
66   - @Override
67   - protected void doInit() {
68   -
69   - }
70   -
71 60 /**
72 61 * 参数校验腾讯云的 SDK AppId
73 62 *
... ... @@ -95,145 +84,63 @@ public class TencentSmsClient extends AbstractSmsClient {
95 84 @Override
96 85 public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
97 86 String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
98   - // 构建请求
  87 + // 1. 执行请求
  88 + // 参考链接 https://cloud.tencent.com/document/product/382/55981
99 89 TreeMap<String, Object> body = new TreeMap<>();
100   - String[] phones = {mobile};
101   - body.put("PhoneNumberSet",phones);
102   - body.put("SmsSdkAppId",getSdkAppId());
103   - body.put("SignName",properties.getSignature());
104   - body.put("TemplateId",apiTemplateId);
105   - body.put("TemplateParamSet",ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
106   -
107   - JSONObject JsonResponse = sendSmsRequest(body,"SendSms","2021-01-11","ap-guangzhou");
108   - SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
109   -
110   - return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
111   -
112   - }
113   -
114   - JSONObject sendSmsRequest(TreeMap<String, Object> body,String action,String version,String region) throws Exception {
115   -
116   - String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
117   - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
118   - // 注意时区,否则容易出错
119   - sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
120   - String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
121   -
122   - // ************* 步骤 1:拼接规范请求串 *************
123   - String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
124   - String httpMethod = "POST"; // 请求方式
125   - String canonicalUri = "/";
126   - String canonicalQueryString = "";
127   -
128   - String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
129   - + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
130   - String signedHeaders = "content-type;host;x-tc-action";
131   - String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
132   - String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
133   -
134   - // ************* 步骤 2:拼接待签名字符串 *************
135   - String credentialScope = date + "/" + "sms" + "/" + "tc3_request";
136   - String hashedCanonicalRequest = sha256Hex(canonicalRequest);
137   - String stringToSign = "TC3-HMAC-SHA256" + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
138   -
139   - // ************* 步骤 3:计算签名 *************
140   - byte[] secretDate = hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), date);
141   - byte[] secretService = hmac256(secretDate, "sms");
142   - byte[] secretSigning = hmac256(secretService, "tc3_request");
143   - String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
144   -
145   - // ************* 步骤 4:拼接 Authorization *************
146   - String authorization = "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "
147   - + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
148   -
149   - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
150   - Map<String, String> headers = new HashMap<>();
151   - headers.put("Authorization", authorization);
152   - headers.put("Content-Type", "application/json; charset=utf-8");
153   - headers.put("Host", host);
154   - headers.put("X-TC-Action", action);
155   - headers.put("X-TC-Timestamp", timestamp);
156   - headers.put("X-TC-Version", version);
157   - headers.put("X-TC-Region", region);
158   -
159   - HttpResponse response = HttpRequest.post("https://"+host)
160   - .addHeaders(headers)
161   - .body(JSONUtil.toJsonStr(body))
162   - .execute();
163   -
164   - return JSONUtil.parseObj(response.body());
165   - }
166   -
167   - public static byte[] hmac256(byte[] key, String msg) throws Exception {
168   - Mac mac = Mac.getInstance("HmacSHA256");
169   - SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
170   - mac.init(secretKeySpec);
171   - return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
172   - }
173   -
174   - private SmsResponse getSmsSendResponse(JSONObject resJson) {
175   - SmsResponse smsResponse = new SmsResponse();
176   - JSONArray statusJson =resJson.getJSONObject("Response").getJSONArray("SendStatusSet");
177   - smsResponse.setSuccess("Ok".equals(statusJson.getJSONObject(0).getStr("Code")));
178   - smsResponse.setData(resJson);
179   - return smsResponse;
  90 + body.put("PhoneNumberSet", new String[]{mobile});
  91 + body.put("SmsSdkAppId", getSdkAppId());
  92 + body.put("SignName", properties.getSignature());
  93 + body.put("TemplateId", apiTemplateId);
  94 + body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
  95 + JSONObject response = request("SendSms", body);
  96 +
  97 + // 2. 解析请求
  98 + JSONObject responseResult = response.getJSONObject("Response");
  99 + JSONObject error = responseResult.getJSONObject("Error");
  100 + if (error != null) {
  101 + return new SmsSendRespDTO().setSuccess(false)
  102 + .setApiRequestId(responseResult.getStr("RequestId"))
  103 + .setApiCode(error.getStr("Code"))
  104 + .setApiMsg(error.getStr("Message"));
  105 + }
  106 + JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
  107 + return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code")))
  108 + .setApiRequestId(responseResult.getStr("RequestId"))
  109 + .setSerialNo(sendResult.getStr("SerialNo"))
  110 + .setApiMsg(sendResult.getStr("Message"));
180 111 }
181 112  
182 113 @Override
183 114 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
184   - List<SmsReceiveStatus> callback = JsonUtils.parseArray(text, SmsReceiveStatus.class);
185   - return convertList(callback, status -> new SmsReceiveRespDTO()
186   - .setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()))
187   - .setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription())
188   - .setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime())
189   - .setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId()));
  115 + JSONArray statuses = JSONUtil.parseArray(text);
  116 + // 字段参考
  117 + return convertList(statuses, status -> {
  118 + JSONObject statusObj = (JSONObject) status;
  119 + return new SmsReceiveRespDTO()
  120 + .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
  121 + .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
  122 + .setMobile(statusObj.getStr("mobile")) // 手机号
  123 + .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
  124 + .setSerialNo(statusObj.getStr("sid")); // 发送序列号
  125 + });
190 126 }
191 127  
192 128 @Override
193 129 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
194   -
195   - // 构建请求
  130 + // 1. 构建请求
  131 + // 参考链接 https://cloud.tencent.com/document/product/382/52067
196 132 TreeMap<String, Object> body = new TreeMap<>();
197   - body.put("International",0);
198   - Integer[] templateIds = {Integer.valueOf(apiTemplateId)};
199   - body.put("TemplateIdSet",templateIds);
  133 + body.put("International", INTERNATIONAL_CHINA);
  134 + body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
  135 + JSONObject response = request("DescribeSmsTemplateList", body);
200 136  
201   - JSONObject JsonResponse = sendSmsRequest(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou");
202   - QuerySmsTemplateResponse smsTemplateResponse = getSmsTemplateResponse(JsonResponse);
203   - String templateId = Integer.toString(smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateId());
204   - String content = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateContent();
205   - Integer templateStatus = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getStatusCode();
206   - String auditReason = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getReviewReply();
207   -
208   - return new SmsTemplateRespDTO().setId(templateId).setContent(content)
209   - .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
210   - }
211   -
212   - private QuerySmsTemplateResponse getSmsTemplateResponse(JSONObject resJson) {
213   -
214   - QuerySmsTemplateResponse smsTemplateResponse = new QuerySmsTemplateResponse();
215   -
216   - smsTemplateResponse.setRequestId(resJson.getJSONObject("Response").getStr("RequestId"));
217   -
218   - smsTemplateResponse.setDescribeTemplateStatusSet(new ArrayList<>());
219   -
220   - QuerySmsTemplateResponse.TemplateInfo templateInfo = new QuerySmsTemplateResponse.TemplateInfo();
221   -
222   - Object statusObject = resJson.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").get(0);
223   -
224   - JSONObject statusJSON = new JSONObject(statusObject);
225   -
226   - templateInfo.setTemplateContent(statusJSON.get("TemplateContent").toString());
227   -
228   - templateInfo.setStatusCode(Integer.parseInt(statusJSON.get("StatusCode").toString()));
229   -
230   - templateInfo.setReviewReply(statusJSON.get("ReviewReply").toString());
231   -
232   - templateInfo.setTemplateId(Integer.parseInt(statusJSON.get("TemplateId").toString()));
233   -
234   - smsTemplateResponse.getDescribeTemplateStatusSet().add(templateInfo);
235   -
236   - return smsTemplateResponse;
  137 + // 2. 解析请求
  138 + JSONObject statusResult = response.getJSONObject("Response")
  139 + .getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
  140 + return new SmsTemplateRespDTO().setId(apiTemplateId)
  141 + .setContent(statusResult.get("TemplateContent").toString())
  142 + .setAuditStatus(convertSmsTemplateAuditStatus(statusResult.getInt("StatusCode")))
  143 + .setAuditReason(statusResult.get("ReviewReply").toString());
237 144 }
238 145  
239 146 @VisibleForTesting
... ... @@ -246,112 +153,49 @@ public class TencentSmsClient extends AbstractSmsClient {
246 153 }
247 154 }
248 155  
249   - @Data
250   - public static class SmsResponse {
251   -
252   - /**
253   - * 是否成功
254   - */
255   - private boolean success;
256   -
257   - /**
258   - * 厂商原返回体
259   - */
260   - private Object data;
261   -
262   - }
263   -
264   -
265 156 /**
266   - * <p>类名: QuerySmsTemplateResponse
267   - * <p>说明: sms模板查询返回信息
  157 + * 请求腾讯云短信
268 158 *
269   - * @author :scholar
270   - * 2024/07/17 0:25
271   - **/
272   - @Data
273   - public static class QuerySmsTemplateResponse {
274   - private List<TemplateInfo> DescribeTemplateStatusSet;
275   - private String RequestId;
276   - @Data
277   - static class TemplateInfo {
278   - private String TemplateName;
279   - private Integer TemplateId;
280   - private Integer International;
281   - private String ReviewReply;
282   - private long CreateTime;
283   - private String TemplateContent;
284   - private Integer StatusCode;
285   - }
286   - }
287   -
288   - @Data
289   - private static class SmsReceiveStatus {
290   -
291   - /**
292   - * 短信接受成功 code
293   - */
294   - public static final String SUCCESS_CODE = "SUCCESS";
295   -
296   - /**
297   - * 用户实际接收到短信的时间
298   - */
299   - @JsonProperty("user_receive_time")
300   - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
301   - private LocalDateTime receiveTime;
302   -
303   - /**
304   - * 国家(或地区)码
305   - */
306   - @JsonProperty("nationcode")
307   - private String nationCode;
308   -
309   - /**
310   - * 手机号码
311   - */
312   - private String mobile;
313   -
314   - /**
315   - * 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败)
316   - */
317   - @JsonProperty("report_status")
318   - private String status;
319   -
320   - /**
321   - * 用户接收短信状态码错误信息
322   - */
323   - @JsonProperty("errmsg")
324   - private String errCode;
325   -
326   - /**
327   - * 用户接收短信状态描述
328   - */
329   - @JsonProperty("description")
330   - private String description;
331   -
332   - /**
333   - * 本次发送标识 ID(与发送接口返回的SerialNo对应)
334   - */
335   - @JsonProperty("sid")
336   - private String serialNo;
  159 + * @see <a href="https://cloud.tencent.com/document/product/382/52072">签名方法 v3</a>
  160 + *
  161 + * @param action 请求的 API 名称
  162 + * @param body 请求参数
  163 + * @return 请求结果
  164 + */
  165 + private JSONObject request(String action, TreeMap<String, Object> body) {
  166 + // 1.1 请求 Header
  167 + Map<String, String> headers = new HashMap<>();
  168 + headers.put("Content-Type", "application/json; charset=utf-8");
  169 + headers.put("Host", HOST);
  170 + headers.put("X-TC-Action", action);
  171 + Date now = new Date();
  172 + String nowStr = FastDateFormat.getInstance("yyyy-MM-dd", TimeZone.getTimeZone("UTC")).format(now);
  173 + headers.put("X-TC-Timestamp", String.valueOf(now.getTime() / 1000));
  174 + headers.put("X-TC-Version", VERSION);
  175 + headers.put("X-TC-Region", REGION);
337 176  
338   - /**
339   - * 用户的 session 内容(与发送接口的请求参数 SessionContext 一致)
340   - */
341   - @JsonProperty("ext")
342   - private SessionContext sessionContext;
  177 + // 1.2 构建签名 Header
  178 + String canonicalQueryString = "";
  179 + String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
  180 + + "host:" + HOST + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
  181 + String signedHeaders = "content-type;host;x-tc-action";
  182 + String canonicalRequest = "POST" + "\n" + "/" + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n"
  183 + + signedHeaders + "\n" + sha256Hex(JSONUtil.toJsonStr(body));
  184 + String credentialScope = nowStr + "/" + "sms" + "/" + "tc3_request";
  185 + String stringToSign = "TC3-HMAC-SHA256" + "\n" + now.getTime() / 1000 + "\n" + credentialScope + "\n" +
  186 + sha256Hex(canonicalRequest);
  187 + byte[] secretService = hmac256(hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), nowStr), "sms");
  188 + String signature = HexUtil.encodeHexStr(hmac256(hmac256(secretService, "tc3_request"), stringToSign));
  189 + headers.put("Authorization", "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "
  190 + + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature);
343 191  
  192 + // 2. 发起请求
  193 + String responseBody = HttpUtils.post("https://" + HOST, headers, JSONUtil.toJsonStr(body));
  194 + return JSONUtil.parseObj(responseBody);
344 195 }
345 196  
346   - @VisibleForTesting
347   - @Data
348   - static class SessionContext {
349   -
350   - /**
351   - * 发送短信记录id
352   - */
353   - private Long logId;
354   -
  197 + private static byte[] hmac256(byte[] key, String msg) {
  198 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg);
355 199 }
356 200  
357 201 -}
  202 +}
358 203 \ No newline at end of file
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java
... ... @@ -18,6 +18,7 @@ public enum SmsChannelEnum {
18 18 ALIYUN("ALIYUN", "阿里云"),
19 19 TENCENT("TENCENT", "腾讯云"),
20 20 HUAWEI("HUAWEI", "华为云"),
  21 + QINIU("QINIU", "七牛云"),
21 22 ;
22 23  
23 24 /**
... ... @@ -34,3 +35,4 @@ public enum SmsChannelEnum {
34 35 }
35 36  
36 37 }
  38 +
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java
... ... @@ -56,7 +56,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
56 56 private AdminUserService adminUserService;
57 57  
58 58 @Override
59   - @Transactional
  59 + @Transactional(rollbackFor = Exception.class)
60 60 public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
61 61 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
62 62 // 创建刷新令牌
... ... @@ -66,6 +66,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
66 66 }
67 67  
68 68 @Override
  69 + @Transactional(rollbackFor = Exception.class)
69 70 public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
70 71 // 查询访问令牌
71 72 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
... ... @@ -82,7 +83,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
82 83 // 移除相关的访问令牌
83 84 List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
84 85 if (CollUtil.isNotEmpty(accessTokenDOs)) {
85   - oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
  86 + oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
86 87 oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken));
87 88 }
88 89  
... ... @@ -126,6 +127,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
126 127 }
127 128  
128 129 @Override
  130 + @Transactional(rollbackFor = Exception.class)
129 131 public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
130 132 // 删除访问令牌
131 133 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java
... ... @@ -140,17 +140,19 @@ public class MenuServiceImpl implements MenuService {
140 140 return true;
141 141 }
142 142  
143   - // 1. 遍历到 parentId 为根节点,则无需判断
  143 + // 1. 先判断自身是否禁用
  144 + if (CommonStatusEnum.isDisable(node.getStatus())) {
  145 + disabledMenuCache.add(node.getId());
  146 + return true;
  147 + }
  148 +
  149 + // 2. 遍历到 parentId 为根节点,则无需判断
144 150 Long parentId = node.getParentId();
145 151 if (ObjUtil.equal(parentId, ID_ROOT)) {
146   - if (CommonStatusEnum.isDisable(node.getStatus())) {
147   - disabledMenuCache.add(node.getId());
148   - return true;
149   - }
150 152 return false;
151 153 }
152 154  
153   - // 2. 继续遍历 parent 节点
  155 + // 3. 继续遍历 parent 节点
154 156 MenuDO parent = menuMap.get(parentId);
155 157 if (parent == null || isMenuDisabled(parent, menuMap, disabledMenuCache)) {
156 158 disabledMenuCache.add(node.getId());
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.service.permission;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.collection.CollectionUtil;
  5 +import cn.hutool.core.util.ObjUtil;
5 6 import cn.hutool.core.util.ObjectUtil;
6 7 import cn.hutool.extra.spring.SpringUtil;
7 8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
... ... @@ -61,7 +62,7 @@ public class RoleServiceImpl implements RoleService {
61 62 // 2. 插入到数据库
62 63 RoleDO role = BeanUtils.toBean(createReqVO, RoleDO.class)
63 64 .setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType()))
64   - .setStatus(CommonStatusEnum.ENABLE.getStatus())
  65 + .setStatus(ObjUtil.defaultIfNull(createReqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus()))
65 66 .setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据。原因是,可能一些项目不需要项目权限
66 67 roleMapper.insert(role);
67 68  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java
1 1 package cn.iocoder.yudao.module.system.service.sms;
2 2  
3   -import cn.hutool.core.util.StrUtil;
4 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6   -import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
7   -import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory;
8   -import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
9 5 import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
10 6 import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
11 7 import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO;
12 8 import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper;
13   -import com.google.common.cache.CacheLoader;
14   -import com.google.common.cache.LoadingCache;
15   -import lombok.Getter;
  9 +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
  10 +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory;
  11 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  12 +import jakarta.annotation.Resource;
16 13 import lombok.extern.slf4j.Slf4j;
17 14 import org.springframework.stereotype.Service;
18 15  
19   -import jakarta.annotation.Resource;
20   -import java.time.Duration;
21 16 import java.util.List;
22 17  
23 18 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
24   -import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
25 19 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
26 20 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
27 21  
... ... @@ -34,46 +28,6 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SMS_CHANNE
34 28 @Slf4j
35 29 public class SmsChannelServiceImpl implements SmsChannelService {
36 30  
37   - /**
38   - * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory
39   - */
40   - @Getter
41   - private final LoadingCache<Long, SmsClient> idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
42   - new CacheLoader<Long, SmsClient>() {
43   -
44   - @Override
45   - public SmsClient load(Long id) {
46   - // 查询,然后尝试刷新
47   - SmsChannelDO channel = smsChannelMapper.selectById(id);
48   - if (channel != null) {
49   - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
50   - smsClientFactory.createOrUpdateSmsClient(properties);
51   - }
52   - return smsClientFactory.getSmsClient(id);
53   - }
54   -
55   - });
56   -
57   - /**
58   - * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory
59   - */
60   - @Getter
61   - private final LoadingCache<String, SmsClient> codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L),
62   - new CacheLoader<String, SmsClient>() {
63   -
64   - @Override
65   - public SmsClient load(String code) {
66   - // 查询,然后尝试刷新
67   - SmsChannelDO channel = smsChannelMapper.selectByCode(code);
68   - if (channel != null) {
69   - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
70   - smsClientFactory.createOrUpdateSmsClient(properties);
71   - }
72   - return smsClientFactory.getSmsClient(code);
73   - }
74   -
75   - });
76   -
77 31 @Resource
78 32 private SmsClientFactory smsClientFactory;
79 33  
... ... @@ -93,41 +47,22 @@ public class SmsChannelServiceImpl implements SmsChannelService {
93 47 @Override
94 48 public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) {
95 49 // 校验存在
96   - SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId());
  50 + validateSmsChannelExists(updateReqVO.getId());
97 51 // 更新
98 52 SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class);
99 53 smsChannelMapper.updateById(updateObj);
100   -
101   - // 清空缓存
102   - clearCache(updateReqVO.getId(), channel.getCode());
103 54 }
104 55  
105 56 @Override
106 57 public void deleteSmsChannel(Long id) {
107 58 // 校验存在
108   - SmsChannelDO channel = validateSmsChannelExists(id);
  59 + validateSmsChannelExists(id);
109 60 // 校验是否有在使用该账号的模版
110 61 if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) {
111 62 throw exception(SMS_CHANNEL_HAS_CHILDREN);
112 63 }
113 64 // 删除
114 65 smsChannelMapper.deleteById(id);
115   -
116   - // 清空缓存
117   - clearCache(id, channel.getCode());
118   - }
119   -
120   - /**
121   - * 清空指定渠道编号的缓存
122   - *
123   - * @param id 渠道编号
124   - * @param code 渠道编码
125   - */
126   - private void clearCache(Long id, String code) {
127   - idClientCache.invalidate(id);
128   - if (StrUtil.isNotEmpty(code)) {
129   - codeClientCache.invalidate(code);
130   - }
131 66 }
132 67  
133 68 private SmsChannelDO validateSmsChannelExists(Long id) {
... ... @@ -155,12 +90,14 @@ public class SmsChannelServiceImpl implements SmsChannelService {
155 90  
156 91 @Override
157 92 public SmsClient getSmsClient(Long id) {
158   - return idClientCache.getUnchecked(id);
  93 + SmsChannelDO channel = smsChannelMapper.selectById(id);
  94 + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
  95 + return smsClientFactory.createOrUpdateSmsClient(properties);
159 96 }
160 97  
161 98 @Override
162 99 public SmsClient getSmsClient(String code) {
163   - return codeClientCache.getUnchecked(code);
  100 + return smsClientFactory.getSmsClient(code);
164 101 }
165 102  
166 103 }
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java
... ... @@ -39,15 +39,6 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
39 39 private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
40 40  
41 41 @Test
42   - public void testDoInit() {
43   - // 准备参数
44   - // mock 方法
45   -
46   - // 调用
47   - smsClient.doInit();
48   - }
49   -
50   - @Test
51 42 public void tesSendSms_success() throws Throwable {
52 43 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
53 44 // 准备参数
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  7 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  8 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  9 +import com.google.common.collect.Lists;
  10 +
  11 +import org.junit.jupiter.api.Test;
  12 +import org.mockito.InjectMocks;
  13 +import org.mockito.MockedStatic;
  14 +
  15 +import java.time.LocalDateTime;
  16 +import java.util.List;
  17 +
  18 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  19 +import static org.junit.jupiter.api.Assertions.*;
  20 +import static org.mockito.ArgumentMatchers.*;
  21 +import static org.mockito.ArgumentMatchers.anyString;
  22 +import static org.mockito.Mockito.mockStatic;
  23 +
  24 +/**
  25 + * {@link HuaweiSmsClient} 的单元测试
  26 + *
  27 + * @author scholar
  28 + */
  29 +public class HuaweiSmsClientTest extends BaseMockitoUnitTest {
  30 +
  31 + private final SmsChannelProperties properties = new SmsChannelProperties()
  32 + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错
  33 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
  34 + .setSignature("芋道源码");
  35 +
  36 + @InjectMocks
  37 + private HuaweiSmsClient smsClient = new HuaweiSmsClient(properties);
  38 +
  39 + @Test
  40 + public void testDoSendSms_success() throws Throwable {
  41 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  42 + // 准备参数
  43 + Long sendLogId = randomLongId();
  44 + String mobile = randomString();
  45 + String apiTemplateId = randomString() + " " + randomString();
  46 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  47 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  48 +
  49 + // mock 方法
  50 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  51 + .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");
  52 +
  53 + // 调用
  54 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  55 + apiTemplateId, templateParams);
  56 + // 断言
  57 + assertTrue(result.getSuccess());
  58 + assertEquals("d6e3cdd0-522b-4692-8304-a07553cdf591_8539659", result.getSerialNo());
  59 + assertEquals("000000", result.getApiCode());
  60 + }
  61 + }
  62 +
  63 + @Test
  64 + public void testDoSendSms_fail_01() throws Throwable {
  65 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  66 + // 准备参数
  67 + Long sendLogId = randomLongId();
  68 + String mobile = randomString();
  69 + String apiTemplateId = randomString() + " " + randomString();
  70 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  71 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  72 +
  73 + // mock 方法
  74 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  75 + .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.\"}");
  76 +
  77 + // 调用
  78 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  79 + apiTemplateId, templateParams);
  80 + // 断言
  81 + assertFalse(result.getSuccess());
  82 + assertEquals("06e4b966-ad87-479f-8b74-f57fb7aafb60_304613461", result.getSerialNo());
  83 + assertEquals("E200033", result.getApiCode());
  84 + }
  85 + }
  86 +
  87 + @Test
  88 + public void testDoSendSms_fail_02() throws Throwable {
  89 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  90 + // 准备参数
  91 + Long sendLogId = randomLongId();
  92 + String mobile = randomString();
  93 + String apiTemplateId = randomString() + " " + randomString();
  94 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  95 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  96 +
  97 + // mock 方法
  98 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  99 + .thenReturn("{\"code\":\"E000102\",\"description\":\"Invalid app_key.\"}");
  100 +
  101 + // 调用
  102 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  103 + apiTemplateId, templateParams);
  104 + // 断言
  105 + assertFalse(result.getSuccess());
  106 + assertEquals("E000102", result.getApiCode());
  107 + assertEquals("Invalid app_key.", result.getApiMsg());
  108 + }
  109 + }
  110 +
  111 + @Test
  112 + public void testParseSmsReceiveStatus() {
  113 + // 准备参数
  114 + String text = "sequence=1&total=1&statusDesc=%E7%94%A8%E6%88%B7%E5%B7%B2%E6%88%90%E5%8A%9F%E6%94%B6%E5%88%B0%E7%9F%AD%E4%BF%A1&updateTime=2024-08-15T03%3A00%3A34Z&source=2&smsMsgId=70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459&status=DELIVRD&extend=176";
  115 +
  116 + // 调用
  117 + List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
  118 + // 断言
  119 + assertEquals(1, statuses.size());
  120 + SmsReceiveRespDTO status = statuses.get(0);
  121 + assertTrue(status.getSuccess());
  122 + assertEquals("DELIVRD", status.getErrorCode());
  123 + assertEquals(LocalDateTime.of(2024, 8, 15, 3, 0, 34), status.getReceiveTime());
  124 + assertEquals("70207ed7-1d02-41b0-8537-bb25fd1c2364_143684459", status.getSerialNo());
  125 + }
  126 +
  127 +}
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  7 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  8 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
  9 +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  10 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  11 +import com.google.common.collect.Lists;
  12 +
  13 +import org.junit.jupiter.api.Test;
  14 +import org.mockito.InjectMocks;
  15 +import org.mockito.MockedStatic;
  16 +
  17 +import java.time.LocalDateTime;
  18 +import java.util.List;
  19 +
  20 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  21 +import static org.junit.jupiter.api.Assertions.*;
  22 +import static org.mockito.ArgumentMatchers.*;
  23 +import static org.mockito.ArgumentMatchers.anyString;
  24 +import static org.mockito.Mockito.mockStatic;
  25 +
  26 +/**
  27 + * {@link QiniuSmsClient} 的单元测试
  28 + *
  29 + * @author scholar
  30 + */
  31 +public class QiniuSmsClientTest extends BaseMockitoUnitTest {
  32 +
  33 + private final SmsChannelProperties properties = new SmsChannelProperties()
  34 + .setApiKey(randomString())// 随机一个 apiKey,避免构建报错
  35 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
  36 + .setSignature("芋道源码");
  37 +
  38 + @InjectMocks
  39 + private QiniuSmsClient smsClient = new QiniuSmsClient(properties);
  40 +
  41 + @Test
  42 + public void testDoSendSms_success() throws Throwable {
  43 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  44 + // 准备参数
  45 + Long sendLogId = randomLongId();
  46 + String mobile = randomString();
  47 + String apiTemplateId = randomString() + " " + randomString();
  48 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  49 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  50 + // mock 方法
  51 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  52 + .thenReturn("{\"message_id\":\"17245678901\"}");
  53 + // 调用
  54 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  55 + apiTemplateId, templateParams);
  56 + // 断言
  57 + assertTrue(result.getSuccess());
  58 + assertEquals("17245678901", result.getSerialNo());
  59 + }
  60 + }
  61 +
  62 + @Test
  63 + public void testDoSendSms_fail() throws Throwable {
  64 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  65 + // 准备参数
  66 + Long sendLogId = randomLongId();
  67 + String mobile = randomString();
  68 + String apiTemplateId = randomString() + " " + randomString();
  69 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  70 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  71 + // mock 方法
  72 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  73 + .thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}");
  74 + // 调用
  75 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  76 + apiTemplateId, templateParams);
  77 + // 断言
  78 + assertFalse(result.getSuccess());
  79 + assertEquals("BadToken", result.getApiCode());
  80 + assertEquals("Your authorization token is invalid", result.getApiMsg());
  81 + assertEquals("etziWcJFo1C8Ne8X", result.getApiRequestId());
  82 + }
  83 + }
  84 +
  85 + @Test
  86 + public void testGetSmsTemplate() throws Throwable {
  87 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  88 + // 准备参数
  89 + String apiTemplateId = randomString();
  90 + // mock 方法
  91 + httpUtilsMockedStatic.when(() -> HttpUtils.get(anyString(), anyMap()))
  92 + .thenReturn("{\"audit_status\":\"passed\",\"created_at\":1724231187,\"description\":\"\",\"disable_broadcast\":false,\"disable_broadcast_reason\":\"\",\"disable_reason\":\"\",\"disabled\":false,\"id\":\"1826184073773596672\",\"is_oversea\":false,\"name\":\"dd\",\"parameters\":[\"code\"],\"reject_reason\":\"\",\"signature_id\":\"1826099896017498112\",\"signature_text\":\"yudao\",\"template\":\"您的验证码为:${code}\",\"type\":\"verification\",\"uid\":1383022432,\"updated_at\":1724288561,\"variable_count\":0}");
  93 + // 调用
  94 + SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
  95 + // 断言
  96 + assertEquals("1826184073773596672", result.getId());
  97 + assertEquals("您的验证码为:${code}", result.getContent());
  98 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
  99 + assertEquals("", result.getAuditReason());
  100 + }
  101 + }
  102 +
  103 + @Test
  104 + public void testParseSmsReceiveStatus() {
  105 + // 准备参数
  106 + String text = "{\"items\":[{\"mobile\":\"18881234567\",\"message_id\":\"10135515063508004167\",\"status\":\"DELIVRD\",\"delivrd_at\":1724591666,\"error\":\"DELIVRD\",\"seq\":\"123\"}]}";
  107 + // 调用
  108 + List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
  109 + // 断言
  110 + assertEquals(1, statuses.size());
  111 + SmsReceiveRespDTO status = statuses.get(0);
  112 + assertTrue(status.getSuccess());
  113 + assertEquals("DELIVRD", status.getErrorMsg());
  114 + assertEquals(LocalDateTime.of(2024, 8, 25, 21, 14, 26), status.getReceiveTime());
  115 + assertEquals("18881234567", status.getMobile());
  116 + assertEquals("10135515063508004167", status.getSerialNo());
  117 + assertEquals(123, status.getLogId());
  118 + }
  119 +
  120 + @Test
  121 + public void testConvertSmsTemplateAuditStatus() {
  122 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
  123 + smsClient.convertSmsTemplateAuditStatus("passed"));
  124 + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
  125 + smsClient.convertSmsTemplateAuditStatus("reviewing"));
  126 + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
  127 + smsClient.convertSmsTemplateAuditStatus("rejected"));
  128 + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("unknown"),
  129 + "未知审核状态(3)");
  130 + }
  131 +}
0 132 \ No newline at end of file
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
1 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
  3 +import cn.hutool.core.collection.ListUtil;
3 4 import cn.iocoder.yudao.framework.common.core.KeyValue;
4   -import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  5 +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
5 6 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
6 7 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
7 8 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
... ... @@ -11,38 +12,20 @@ import org.junit.jupiter.api.Test;
11 12 import java.util.List;
12 13  
13 14 /**
14   - * 各种 {@link SmsClientTests 集成测试
  15 + * 各种 {@link SmsClient} 的集成测试
15 16 *
16 17 * @author 芋道源码
17 18 */
18 19 public class SmsClientTests {
19 20  
20   - @Test
21   - @Disabled
22   - public void testHuaweiSmsClient_sendSms() throws Throwable {
23   - SmsChannelProperties properties = new SmsChannelProperties()
24   - .setApiKey("123")
25   - .setApiSecret("456");
26   - HuaweiSmsClient client = new HuaweiSmsClient(properties);
27   - // 准备参数
28   - Long sendLogId = System.currentTimeMillis();
29   - String mobile = "15601691323";
30   - String apiTemplateId = "xx test01";
31   - List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
32   - // 调用
33   - SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
34   - // 打印结果
35   - System.out.println(smsSendRespDTO);
36   - }
37   -
38 21 // ========== 阿里云 ==========
39 22  
40 23 @Test
41 24 @Disabled
42 25 public void testAliyunSmsClient_getSmsTemplate() throws Throwable {
43 26 SmsChannelProperties properties = new SmsChannelProperties()
44   - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
45   - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");
  27 + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
  28 + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"));
46 29 AliyunSmsClient client = new AliyunSmsClient(properties);
47 30 // 准备参数
48 31 String apiTemplateId = "SMS_207945135";
... ... @@ -56,47 +39,113 @@ public class SmsClientTests {
56 39 @Disabled
57 40 public void testAliyunSmsClient_sendSms() throws Throwable {
58 41 SmsChannelProperties properties = new SmsChannelProperties()
59   - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
60   - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
  42 + .setApiKey(System.getenv("SMS_ALIYUN_ACCESS_KEY"))
  43 + .setApiSecret(System.getenv("SMS_ALIYUN_SECRET_KEY"))
61 44 .setSignature("Ballcat");
62 45 AliyunSmsClient client = new AliyunSmsClient(properties);
63 46 // 准备参数
64 47 Long sendLogId = System.currentTimeMillis();
65   - String mobile = "173213154791";
  48 + String mobile = "15601691323";
66 49 String apiTemplateId = "SMS_207945135";
67 50 // 调用
68   - SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
  51 + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
69 52 // 打印结果
70 53 System.out.println(sendRespDTO);
71 54 }
72 55  
  56 + // ========== 腾讯云 ==========
  57 +
73 58 @Test
74 59 @Disabled
75   - public void testAliyunSmsClient_parseSmsReceiveStatus() {
  60 + public void testTencentSmsClient_sendSms() throws Throwable {
  61 + String sdkAppId = "1400500458";
76 62 SmsChannelProperties properties = new SmsChannelProperties()
77   - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
78   - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz");
79   - AliyunSmsClient client = new AliyunSmsClient(properties);
  63 + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
  64 + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
  65 + .setSignature("芋道源码");
  66 + TencentSmsClient client = new TencentSmsClient(properties);
80 67 // 准备参数
81   - String text = "[\n" +
82   - " {\n" +
83   - " \"phone_number\" : \"13900000001\",\n" +
84   - " \"send_time\" : \"2017-01-01 11:12:13\",\n" +
85   - " \"report_time\" : \"2017-02-02 22:23:24\",\n" +
86   - " \"success\" : true,\n" +
87   - " \"err_code\" : \"DELIVERED\",\n" +
88   - " \"err_msg\" : \"用户接收成功\",\n" +
89   - " \"sms_size\" : \"1\",\n" +
90   - " \"biz_id\" : \"12345\",\n" +
91   - " \"out_id\" : \"67890\"\n" +
92   - " }\n" +
93   - "]";
94   - // mock 方法
  68 + Long sendLogId = System.currentTimeMillis();
  69 + String mobile = "15601691323";
  70 + String apiTemplateId = "358212";
  71 + // 调用
  72 + SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
  73 + // 打印结果
  74 + System.out.println(sendRespDTO);
  75 + }
95 76  
  77 + @Test
  78 + @Disabled
  79 + public void testTencentSmsClient_getSmsTemplate() throws Throwable {
  80 + String sdkAppId = "1400500458";
  81 + SmsChannelProperties properties = new SmsChannelProperties()
  82 + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
  83 + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
  84 + .setSignature("芋道源码");
  85 + TencentSmsClient client = new TencentSmsClient(properties);
  86 + // 准备参数
  87 + String apiTemplateId = "358212";
96 88 // 调用
97   - List<SmsReceiveRespDTO> statuses = client.parseSmsReceiveStatus(text);
  89 + SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
98 90 // 打印结果
99   - System.out.println(statuses);
  91 + System.out.println(template);
100 92 }
101 93  
  94 + // ========== 华为云 ==========
  95 +
  96 + @Test
  97 + @Disabled
  98 + public void testHuaweiSmsClient_sendSms() throws Throwable {
  99 + String sender = "x8824060312575";
  100 + SmsChannelProperties properties = new SmsChannelProperties()
  101 + .setApiKey(System.getenv("SMS_HUAWEI_ACCESS_KEY") + " " + sender)
  102 + .setApiSecret(System.getenv("SMS_HUAWEI_SECRET_KEY"))
  103 + .setSignature("runpu");
  104 + HuaweiSmsClient client = new HuaweiSmsClient(properties);
  105 + // 准备参数
  106 + Long sendLogId = System.currentTimeMillis();
  107 + String mobile = "17321315478";
  108 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
  109 + List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024"));
  110 + // 调用
  111 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
  112 + // 打印结果
  113 + System.out.println(smsSendRespDTO);
  114 + }
  115 +
  116 + // ========== 七牛云 ==========
  117 +
  118 + @Test
  119 + @Disabled
  120 + public void testQiniuSmsClient_sendSms() throws Throwable {
  121 + SmsChannelProperties properties = new SmsChannelProperties()
  122 + .setApiKey("SMS_QINIU_ACCESS_KEY")
  123 + .setApiSecret("SMS_QINIU_SECRET_KEY");
  124 + QiniuSmsClient client = new QiniuSmsClient(properties);
  125 + // 准备参数
  126 + Long sendLogId = System.currentTimeMillis();
  127 + String mobile = "17321315478";
  128 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
  129 + List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1122"));
  130 + // 调用
  131 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
  132 + // 打印结果
  133 + System.out.println(smsSendRespDTO);
  134 + }
  135 +
  136 + @Test
  137 + @Disabled
  138 + public void testQiniuSmsClient_getSmsTemplate() throws Throwable {
  139 + SmsChannelProperties properties = new SmsChannelProperties()
  140 + .setApiKey("SMS_QINIU_ACCESS_KEY")
  141 + .setApiSecret("SMS_QINIU_SECRET_KEY");
  142 + QiniuSmsClient client = new QiniuSmsClient(properties);
  143 + // 准备参数
  144 + String apiTemplateId = "3644cdab863546a3b718d488659a99ef";
  145 + // 调用
  146 + SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
  147 + // 打印结果
  148 + System.out.println(template);
  149 + }
102 150 }
  151 +
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java
1 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
3   -import cn.hutool.core.util.ReflectUtil;
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
4 5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
5   -import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
6 6 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
  7 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  8 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
7 9 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
8 10 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  11 +import com.google.common.collect.Lists;
  12 +
9 13 import org.junit.jupiter.api.Test;
10 14 import org.mockito.InjectMocks;
11   -import org.mockito.Mock;
  15 +import org.mockito.MockedStatic;
12 16  
13 17 import java.time.LocalDateTime;
14 18 import java.util.List;
15 19  
16   -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
  20 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
17 21 import static org.junit.jupiter.api.Assertions.*;
  22 +import static org.mockito.ArgumentMatchers.*;
  23 +import static org.mockito.ArgumentMatchers.anyString;
  24 +import static org.mockito.Mockito.mockStatic;
18 25  
19   -// TODO @芋艿:补全单测
20 26 /**
21 27 * {@link TencentSmsClient} 的单元测试
22 28 *
... ... @@ -32,114 +38,109 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
32 38 @InjectMocks
33 39 private TencentSmsClient smsClient = new TencentSmsClient(properties);
34 40  
35   - @Mock
36   - private SmsClient client;
37   -
38 41 @Test
39   - public void testDoInit() {
40   - // 准备参数
41   - // mock 方法
42   -
43   - // 调用
44   - smsClient.doInit();
45   - // 断言
46   - assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
  42 + public void testDoSendSms_success() throws Throwable {
  43 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  44 + // 准备参数
  45 + Long sendLogId = randomLongId();
  46 + String mobile = randomString();
  47 + String apiTemplateId = randomString();
  48 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  49 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  50 + // mock 方法
  51 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  52 + .thenReturn("{\n" +
  53 + " \"Response\": {\n" +
  54 + " \"SendStatusSet\": [\n" +
  55 + " {\n" +
  56 + " \"SerialNo\": \"5000:1045710669157053657849499619\",\n" +
  57 + " \"PhoneNumber\": \"+8618511122233\",\n" +
  58 + " \"Fee\": 1,\n" +
  59 + " \"SessionContext\": \"test\",\n" +
  60 + " \"Code\": \"Ok\",\n" +
  61 + " \"Message\": \"send success\",\n" +
  62 + " \"IsoCode\": \"CN\"\n" +
  63 + " },\n" +
  64 + " ],\n" +
  65 + " \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
  66 + " }\n" +
  67 + "}");
  68 +
  69 + // 调用
  70 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  71 + apiTemplateId, templateParams);
  72 + // 断言
  73 + assertTrue(result.getSuccess());
  74 + assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
  75 + assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
  76 + assertEquals("send success", result.getApiMsg());
  77 + }
47 78 }
48 79  
49 80 @Test
50   - public void testRefresh() {
51   - // 准备参数
52   - SmsChannelProperties p = new SmsChannelProperties()
53   - .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错
54   - .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
55   - .setSignature("芋道源码");
56   - // 调用
57   - smsClient.refresh(p);
58   - // 断言
59   - assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
  81 + public void testDoSendSms_fail_01() throws Throwable {
  82 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  83 + // 准备参数
  84 + Long sendLogId = randomLongId();
  85 + String mobile = randomString();
  86 + String apiTemplateId = randomString();
  87 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  88 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  89 +
  90 + // mock 方法
  91 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  92 + .thenReturn("{\n" +
  93 + " \"Response\": {\n" +
  94 + " \"SendStatusSet\": [\n" +
  95 + " {\n" +
  96 + " \"SerialNo\": \"5000:1045710669157053657849499619\",\n" +
  97 + " \"PhoneNumber\": \"+8618511122233\",\n" +
  98 + " \"Fee\": 1,\n" +
  99 + " \"SessionContext\": \"test\",\n" +
  100 + " \"Code\": \"ERROR\",\n" +
  101 + " \"Message\": \"send success\",\n" +
  102 + " \"IsoCode\": \"CN\"\n" +
  103 + " },\n" +
  104 + " ],\n" +
  105 + " \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
  106 + " }\n" +
  107 + "}");
  108 +
  109 + // 调用
  110 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  111 + apiTemplateId, templateParams);
  112 + // 断言
  113 + assertFalse(result.getSuccess());
  114 + assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
  115 + assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
  116 + assertEquals("send success", result.getApiMsg());
  117 + }
60 118 }
61 119  
62   -// @Test
63   -// public void testDoSendSms_success() throws Throwable {
64   -// // 准备参数
65   -// Long sendLogId = randomLongId();
66   -// String mobile = randomString();
67   -// String apiTemplateId = randomString();
68   -// List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
69   -// new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
70   -// String requestId = randomString();
71   -// String serialNo = randomString();
72   -// // mock 方法
73   -// SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
74   -// o.setRequestId(requestId);
75   -// SendStatus[] sendStatuses = new SendStatus[1];
76   -// o.setSendStatusSet(sendStatuses);
77   -// SendStatus sendStatus = new SendStatus();
78   -// sendStatuses[0] = sendStatus;
79   -// sendStatus.setCode(TencentSmsClient.API_CODE_SUCCESS);
80   -// sendStatus.setMessage("send success");
81   -// sendStatus.setSerialNo(serialNo);
82   -// });
83   -// when(client.SendSms(argThat(request -> {
84   -// assertEquals(mobile, request.getPhoneNumberSet()[0]);
85   -// assertEquals(properties.getSignature(), request.getSignName());
86   -// assertEquals(apiTemplateId, request.getTemplateId());
87   -// assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
88   -// toJsonString(request.getTemplateParamSet()));
89   -// assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
90   -// return true;
91   -// }))).thenReturn(response);
92   -//
93   -// // 调用
94   -// SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
95   -// // 断言
96   -// assertTrue(result.getSuccess());
97   -// assertEquals(response.getRequestId(), result.getApiRequestId());
98   -// assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
99   -// assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
100   -// assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
101   -// }
102   -
103   -// @Test
104   -// public void testDoSendSms_fail() throws Throwable {
105   -// // 准备参数
106   -// Long sendLogId = randomLongId();
107   -// String mobile = randomString();
108   -// String apiTemplateId = randomString();
109   -// List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
110   -// new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
111   -// String requestId = randomString();
112   -// String serialNo = randomString();
113   -// // mock 方法
114   -// SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
115   -// o.setRequestId(requestId);
116   -// SendStatus[] sendStatuses = new SendStatus[1];
117   -// o.setSendStatusSet(sendStatuses);
118   -// SendStatus sendStatus = new SendStatus();
119   -// sendStatuses[0] = sendStatus;
120   -// sendStatus.setCode("ERROR");
121   -// sendStatus.setMessage("send success");
122   -// sendStatus.setSerialNo(serialNo);
123   -// });
124   -// when(client.SendSms(argThat(request -> {
125   -// assertEquals(mobile, request.getPhoneNumberSet()[0]);
126   -// assertEquals(properties.getSignature(), request.getSignName());
127   -// assertEquals(apiTemplateId, request.getTemplateId());
128   -// assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
129   -// toJsonString(request.getTemplateParamSet()));
130   -// assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
131   -// return true;
132   -// }))).thenReturn(response);
133   -//
134   -// // 调用
135   -// SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
136   -// // 断言
137   -// assertFalse(result.getSuccess());
138   -// assertEquals(response.getRequestId(), result.getApiRequestId());
139   -// assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
140   -// assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
141   -// assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
142   -// }
  120 + @Test
  121 + public void testDoSendSms_fail_02() throws Throwable {
  122 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  123 + // 准备参数
  124 + Long sendLogId = randomLongId();
  125 + String mobile = randomString();
  126 + String apiTemplateId = randomString();
  127 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  128 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  129 +
  130 + // mock 方法
  131 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  132 + .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\"}}");
  133 +
  134 + // 调用
  135 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  136 + apiTemplateId, templateParams);
  137 + // 断言
  138 + assertFalse(result.getSuccess());
  139 + assertEquals("2a88f82a-261c-4ac6-9fa9-c7d01aaa486a", result.getApiRequestId());
  140 + assertEquals("AuthFailure.SecretIdNotFound", result.getApiCode());
  141 + assertEquals("The SecretId is not found, please ensure that your SecretId is correct.", result.getApiMsg());
  142 + }
  143 + }
143 144  
144 145 @Test
145 146 public void testParseSmsReceiveStatus() {
... ... @@ -156,7 +157,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
156 157 " \"ext\": {\"logId\":\"67890\"}\n" +
157 158 " }\n" +
158 159 "]";
159   - // mock 方法
160 160  
161 161 // 调用
162 162 List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
... ... @@ -164,42 +164,44 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
164 164 assertEquals(1, statuses.size());
165 165 assertTrue(statuses.get(0).getSuccess());
166 166 assertEquals("DELIVRD", statuses.get(0).getErrorCode());
167   - assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg());
168 167 assertEquals("13900000001", statuses.get(0).getMobile());
169 168 assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime());
170 169 assertEquals("12345", statuses.get(0).getSerialNo());
171   - assertEquals(67890L, statuses.get(0).getLogId());
172 170 }
173 171  
174   -// @Test
175   -// public void testGetSmsTemplate() throws Throwable {
176   -// // 准备参数
177   -// Long apiTemplateId = randomLongId();
178   -// String requestId = randomString();
179   -//
180   -// // mock 方法
181   -// DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> {
182   -// DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1];
183   -// DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
184   -// templateStatus.setTemplateId(apiTemplateId);
185   -// templateStatus.setStatusCode(0L);// 设置模板通过
186   -// describeTemplateListStatuses[0] = templateStatus;
187   -// o.setDescribeTemplateStatusSet(describeTemplateListStatuses);
188   -// o.setRequestId(requestId);
189   -// });
190   -// when(client.DescribeSmsTemplateList(argThat(request -> {
191   -// assertEquals(apiTemplateId, request.getTemplateIdSet()[0]);
192   -// return true;
193   -// }))).thenReturn(response);
194   -//
195   -// // 调用
196   -// SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString());
197   -// // 断言
198   -// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId());
199   -// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent());
200   -// assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
201   -// assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason());
202   -// }
  172 + @Test
  173 + public void testGetSmsTemplate() throws Throwable {
  174 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  175 + // 准备参数
  176 + String apiTemplateId = "1122";
  177 +
  178 + // mock 方法
  179 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  180 + .thenReturn("{ \"Response\": {\n" +
  181 + " \"DescribeTemplateStatusSet\": [\n" +
  182 + " {\n" +
  183 + " \"TemplateName\": \"验证码\",\n" +
  184 + " \"TemplateId\": 1122,\n" +
  185 + " \"International\": 0,\n" +
  186 + " \"ReviewReply\": \"审批备注\",\n" +
  187 + " \"CreateTime\": 1617379200,\n" +
  188 + " \"TemplateContent\": \"您的验证码是{1}\",\n" +
  189 + " \"StatusCode\": 0\n" +
  190 + " },\n" +
  191 + " \n" +
  192 + " ],\n" +
  193 + " \"RequestId\": \"f36e4f00-605e-49b1-ad0d-bfaba81c7325\"\n" +
  194 + " }}");
  195 +
  196 + // 调用
  197 + SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
  198 + // 断言
  199 + assertEquals("1122", result.getId());
  200 + assertEquals("您的验证码是{1}", result.getContent());
  201 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
  202 + assertEquals("审批备注", result.getAuditReason());
  203 + }
  204 + }
203 205  
204 206 @Test
205 207 public void testConvertSmsTemplateAuditStatus() {
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java
... ... @@ -209,7 +209,8 @@ public class OAuth2ApproveServiceImplTest extends BaseDbUnitTest {
209 209 List<OAuth2ApproveDO> result = oauth2ApproveService.getApproveList(userId, userType, clientId);
210 210 // 断言
211 211 assertEquals(1, result.size());
212   - assertPojoEquals(approve, result.get(0));
  212 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  213 + assertPojoEquals(approve, result.get(0), "expiresTime");
213 214 }
214 215  
215 216 @Test
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java
... ... @@ -50,7 +50,8 @@ class OAuth2CodeServiceImplTest extends BaseDbUnitTest {
50 50 scopes, redirectUri, state);
51 51 // 断言
52 52 OAuth2CodeDO dbCodeDO = oauth2CodeMapper.selectByCode(codeDO.getCode());
53   - assertPojoEquals(codeDO, dbCodeDO, "createTime", "updateTime", "deleted");
  53 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  54 + assertPojoEquals(codeDO, dbCodeDO, "expiresTime", "createTime", "updateTime", "deleted");
54 55 assertEquals(userId, codeDO.getUserId());
55 56 assertEquals(userType, codeDO.getUserType());
56 57 assertEquals(clientId, codeDO.getClientId());
... ... @@ -92,7 +93,8 @@ class OAuth2CodeServiceImplTest extends BaseDbUnitTest {
92 93  
93 94 // 调用
94 95 OAuth2CodeDO result = oauth2CodeService.consumeAuthorizationCode(code);
95   - assertPojoEquals(codeDO, result);
  96 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  97 + assertPojoEquals(codeDO, result, "expiresTime");
96 98 assertNull(oauth2CodeMapper.selectByCode(code));
97 99 }
98 100  
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java
... ... @@ -77,7 +77,8 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
77 77 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, userType, clientId, scopes);
78 78 // 断言访问令牌
79 79 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken());
80   - assertPojoEquals(accessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted");
  80 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  81 + assertPojoEquals(accessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted");
81 82 assertEquals(userId, accessTokenDO.getUserId());
82 83 assertEquals(userType, accessTokenDO.getUserType());
83 84 assertEquals(2, accessTokenDO.getUserInfo().size());
... ... @@ -88,7 +89,8 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
88 89 assertFalse(DateUtils.isExpired(accessTokenDO.getExpiresTime()));
89 90 // 断言访问令牌的缓存
90 91 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken());
91   - assertPojoEquals(accessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted");
  92 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  93 + assertPojoEquals(accessTokenDO, redisAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted");
92 94 // 断言刷新令牌
93 95 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0);
94 96 assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted");
... ... @@ -142,7 +144,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
142 144 // 调用,并断言
143 145 assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId),
144 146 new ErrorCode(401, "刷新令牌已过期"));
145   - assertEquals(0, oauth2RefreshTokenMapper.selectCount());
  147 + assertEquals(0, oauth2AccessTokenMapper.selectCount());
146 148 }
147 149  
148 150 @Test
... ... @@ -177,13 +179,15 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
177 179 assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()));
178 180 // 断言,新的访问令牌
179 181 OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken());
180   - assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted");
  182 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  183 + assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted");
181 184 assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted",
182 185 "creator", "updater");
183 186 assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime()));
184 187 // 断言,新的访问令牌的缓存
185 188 OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(newAccessTokenDO.getAccessToken());
186   - assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted");
  189 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  190 + assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted");
187 191 }
188 192  
189 193 @Test
... ... @@ -198,9 +202,11 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
198 202 // 调用
199 203 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken);
200 204 // 断言
201   - assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted",
  205 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  206 + assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted",
202 207 "creator", "updater");
203   - assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "createTime", "updateTime", "deleted",
  208 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  209 + assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "expiresTime", "createTime", "updateTime", "deleted",
204 210 "creator", "updater");
205 211 }
206 212  
... ... @@ -237,7 +243,8 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
237 243 // 调研,并断言
238 244 OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken);
239 245 // 断言
240   - assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted",
  246 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  247 + assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted",
241 248 "creator", "updater");
242 249 }
243 250  
... ... @@ -259,7 +266,8 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
259 266 oauth2RefreshTokenMapper.insert(refreshTokenDO);
260 267 // 调用
261 268 OAuth2AccessTokenDO result = oauth2TokenService.removeAccessToken(accessTokenDO.getAccessToken());
262   - assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted",
  269 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  270 + assertPojoEquals(accessTokenDO, result, "expiresTime", "createTime", "updateTime", "deleted",
263 271 "creator", "updater");
264 272 // 断言数据
265 273 assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()));
... ... @@ -297,7 +305,8 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
297 305 // 断言
298 306 assertEquals(1, pageResult.getTotal());
299 307 assertEquals(1, pageResult.getList().size());
300   - assertPojoEquals(dbAccessToken, pageResult.getList().get(0));
  308 + // TODO @芋艿:expiresTime 被屏蔽,仅 win11 会复现,建议后续修复。
  309 + assertPojoEquals(dbAccessToken, pageResult.getList().get(0), "expiresTime");
301 310 }
302 311  
303 312 }
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java
... ... @@ -51,7 +51,8 @@ public class RoleServiceImplTest extends BaseDbUnitTest {
51 51 public void testCreateRole() {
52 52 // 准备参数
53 53 RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class)
54   - .setId(null); // 防止 id 被赋值
  54 + .setId(null) // 防止 id 被赋值
  55 + .setStatus(randomCommonStatus());
55 56  
56 57 // 调用
57 58 Long roleId = roleService.createRole(reqVO, null);
... ... @@ -59,7 +60,6 @@ public class RoleServiceImplTest extends BaseDbUnitTest {
59 60 RoleDO roleDO = roleMapper.selectById(roleId);
60 61 assertPojoEquals(reqVO, roleDO, "id");
61 62 assertEquals(RoleTypeEnum.CUSTOM.getType(), roleDO.getType());
62   - assertEquals(CommonStatusEnum.ENABLE.getStatus(), roleDO.getStatus());
63 63 assertEquals(DataScopeEnum.ALL.getScope(), roleDO.getDataScope());
64 64 }
65 65  
... ... @@ -70,7 +70,8 @@ public class RoleServiceImplTest extends BaseDbUnitTest {
70 70 roleMapper.insert(roleDO);
71 71 // 准备参数
72 72 Long id = roleDO.getId();
73   - RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class, o -> o.setId(id));
  73 + RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class, o -> o.setId(id)
  74 + .setStatus(randomCommonStatus()));
74 75  
75 76 // 调用
76 77 roleService.updateRole(reqVO);
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java
... ... @@ -57,9 +57,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
57 57 // 校验记录的属性是否正确
58 58 SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId);
59 59 assertPojoEquals(reqVO, smsChannel, "id");
60   - // 断言 cache
61   - assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));
62   - assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));
63 60 }
64 61  
65 62 @Test
... ... @@ -79,9 +76,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
79 76 // 校验是否更新正确
80 77 SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的
81 78 assertPojoEquals(reqVO, smsChannel);
82   - // 断言 cache
83   - assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));
84   - assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));
85 79 }
86 80  
87 81 @Test
... ... @@ -105,9 +99,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
105 99 smsChannelService.deleteSmsChannel(id);
106 100 // 校验数据不存在了
107 101 assertNull(smsChannelMapper.selectById(id));
108   - // 断言 cache
109   - assertNull(smsChannelService.getIdClientCache().getIfPresent(dbSmsChannel.getId()));
110   - assertNull(smsChannelService.getCodeClientCache().getIfPresent(dbSmsChannel.getCode()));
111 102 }
112 103  
113 104 @Test
... ... @@ -196,29 +187,23 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
196 187 // mock 数据
197 188 SmsChannelDO channel = randomPojo(SmsChannelDO.class);
198 189 smsChannelMapper.insert(channel);
199   - // mock 参数
  190 + // 准备参数
200 191 Long id = channel.getId();
201 192 // mock 方法
202 193 SmsClient mockClient = mock(SmsClient.class);
203   - when(smsClientFactory.getSmsClient(eq(id))).thenReturn(mockClient);
  194 + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
  195 + when(smsClientFactory.createOrUpdateSmsClient(eq(properties))).thenReturn(mockClient);
204 196  
205 197 // 调用
206 198 SmsClient client = smsChannelService.getSmsClient(id);
207 199 // 断言
208 200 assertSame(client, mockClient);
209   - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {
210   - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
211   - return properties.equals(arg);
212   - }));
213 201 }
214 202  
215 203 @Test
216 204 public void testGetSmsClient_code() {
217   - // mock 数据
218   - SmsChannelDO channel = randomPojo(SmsChannelDO.class);
219   - smsChannelMapper.insert(channel);
220   - // mock 参数
221   - String code = channel.getCode();
  205 + // 准备参数
  206 + String code = randomString();
222 207 // mock 方法
223 208 SmsClient mockClient = mock(SmsClient.class);
224 209 when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient);
... ... @@ -227,10 +212,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
227 212 SmsClient client = smsChannelService.getSmsClient(code);
228 213 // 断言
229 214 assertSame(client, mockClient);
230   - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {
231   - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
232   - return properties.equals(arg);
233   - }));
234 215 }
235 216  
236 217 }
... ...
yudao-server/src/main/resources/application-dev.yaml
... ... @@ -40,12 +40,12 @@ spring:
40 40 primary: master
41 41 datasource:
42 42 master:
43   - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
  43 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
44 44 username: root
45 45 password: 123456
46 46 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
47 47 lazy: true # 开启懒加载,保证启动速度
48   - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
  48 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
49 49 username: root
50 50 password: 123456
51 51  
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -45,8 +45,8 @@ spring:
45 45 primary: master
46 46 datasource:
47 47 master:
48   - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
49   - # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例
  48 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
  49 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
50 50 # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
51 51 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
52 52 # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例
... ... @@ -63,7 +63,7 @@ spring:
63 63 # password: Yudao@2024 # OpenGauss 连接的示例
64 64 slave: # 模拟从库,可根据自己需要修改
65 65 lazy: true # 开启懒加载,保证启动速度
66   - url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
  66 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
67 67 username: root
68 68 password: 123456
69 69  
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -147,14 +147,28 @@ spring:
147 147  
148 148 spring:
149 149 ai:
  150 + vectorstore: # 向量存储
  151 + redis:
  152 + index: default-index
  153 + prefix: "default:"
  154 + embedding:
  155 + transformer:
  156 + onnx:
  157 + model-uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/model.onnx
  158 + tokenizer:
  159 + uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/tokenizer.json
150 160 qianfan: # 文心一言
151 161 api-key: x0cuLZ7XsaTCU08vuJWO87Lg
152 162 secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
153 163 zhipuai: # 智谱 AI
154 164 api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs
155   - openai:
  165 + openai: # OpenAI 官方
156 166 api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z
157 167 base-url: https://api.gptsapi.net
  168 + azure: # OpenAI 微软
  169 + openai:
  170 + endpoint: https://eastusprejade.openai.azure.com
  171 + api-key: xxx
158 172 ollama:
159 173 base-url: http://127.0.0.1:11434
160 174 chat:
... ... @@ -231,7 +245,7 @@ yudao:
231 245 codegen:
232 246 base-package: ${yudao.info.base-package}
233 247 db-schemas: ${spring.datasource.dynamic.datasource.master.name}
234   - front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类
  248 + front-type: 20 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类
235 249 tenant: # 多租户相关配置项
236 250 enable: true
237 251 ignore-urls:
... ... @@ -287,6 +301,7 @@ yudao:
287 301 - tmp_report_data_1
288 302 - tmp_report_data_income
289 303 ignore-caches:
  304 + - user_role_ids
290 305 - permission_menu_ids
291 306 - oauth_client
292 307 - notify_template
... ... @@ -301,7 +316,6 @@ yudao:
301 316 end-code: 9999 # 这里配置 9999 的原因是,测试方便。
302 317 trade:
303 318 order:
304   - app-id: 1 # 商户编号
305 319 pay-expire-time: 2h # 支付的过期时间
306 320 receive-expire-time: 14d # 收货的过期时间
307 321 comment-expire-time: 7d # 评论的过期时间
... ...