Commit 4e8e491035180f3c86e4a9f0f6233d6bd43dbe08
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
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
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java
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<AiImageDO> { |
| 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<CrmReceivableDO> { |
| 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
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<S3FileClientConfig> { |
| 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
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
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/property/ProductPropertyDO.java
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
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
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<CouponDO> { |
| 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<RewardActivityDO> { |
| 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<RewardActivityDO> { |
| 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<SeckillActivityDO> { |
| 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
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
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java
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<AfterSaleDO> { |
| 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
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
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 "trade_order" |
| 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
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
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<PayAppDO> { |
| 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
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<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
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 # 评论的过期时间 | ... | ... |