diff --git a/pom.xml b/pom.xml index 96e0e08ce..949978260 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ 2.2.7 2.2 1.0.5 + @@ -271,6 +272,27 @@ ${screw.version} + + + + + com.yunpian.sdk + yunpian-java-sdk + 1.2.7 + + + + com.aliyun + aliyun-java-sdk-core + 4.5.18 + + + com.aliyun + aliyun-java-sdk-dysmsapi + 2.1.0 + + + diff --git a/ruoyi-ui/.env.demo1024 b/ruoyi-ui/.env.demo1024 new file mode 100644 index 000000000..ffeadbf8c --- /dev/null +++ b/ruoyi-ui/.env.demo1024 @@ -0,0 +1,7 @@ +NODE_ENV = production + +# 测试环境配置 +ENV = 'staging' + +# 芋道管理系统/测试环境 +VUE_APP_BASE_API = 'http://127.0.0.1:48080' diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json index 46718e1b0..1249d16b3 100644 --- a/ruoyi-ui/package.json +++ b/ruoyi-ui/package.json @@ -8,6 +8,7 @@ "dev": "vue-cli-service serve", "build:prod": "vue-cli-service build", "build:stage": "vue-cli-service build --mode staging", + "build:demo1024": "vue-cli-service build --mode demo1024", "preview": "node build/index.js --preview", "lint": "eslint --ext .js,.vue src" }, diff --git a/ruoyi-ui/src/api/system/sms/smsChannel.js b/ruoyi-ui/src/api/system/sms/smsChannel.js new file mode 100644 index 000000000..4e38de05f --- /dev/null +++ b/ruoyi-ui/src/api/system/sms/smsChannel.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 创建短信渠道 +export function createSmsChannel(data) { + return request({ + url: '/system/sms-channel/create', + method: 'post', + data: data + }) +} + +// 更新短信渠道 +export function updateSmsChannel(data) { + return request({ + url: '/system/sms-channel/update', + method: 'put', + data: data + }) +} + +// 删除短信渠道 +export function deleteSmsChannel(id) { + return request({ + url: '/system/sms-channel/delete?id=' + id, + method: 'delete' + }) +} + +// 获得短信渠道 +export function getSmsChannel(id) { + return request({ + url: '/system/sms-channel/get?id=' + id, + method: 'get' + }) +} + +// 获得短信渠道分页 +export function getSmsChannelPage(query) { + return request({ + url: '/system/sms-channel/page', + method: 'get', + params: query + }) +} + +// 获得短信渠道精简列表 +export function getSimpleSmsChannels() { + return request({ + url: '/system/sms-channel/list-all-simple', + method: 'get', + }) +} diff --git a/ruoyi-ui/src/api/system/sms/smsLog.js b/ruoyi-ui/src/api/system/sms/smsLog.js new file mode 100644 index 000000000..8a9083b71 --- /dev/null +++ b/ruoyi-ui/src/api/system/sms/smsLog.js @@ -0,0 +1,20 @@ +import request from '@/utils/request' + +// 获得短信日志分页 +export function getSmsLogPage(query) { + return request({ + url: '/system/sms-log/page', + method: 'get', + params: query + }) +} + +// 导出短信日志 Excel +export function exportSmsLogExcel(query) { + return request({ + url: '/system/sms-log/export-excel', + method: 'get', + params: query, + responseType: 'blob' + }) +} diff --git a/ruoyi-ui/src/api/system/sms/smsTemplate.js b/ruoyi-ui/src/api/system/sms/smsTemplate.js new file mode 100644 index 000000000..d6d933044 --- /dev/null +++ b/ruoyi-ui/src/api/system/sms/smsTemplate.js @@ -0,0 +1,64 @@ +import request from '@/utils/request' + +// 创建短信模板 +export function createSmsTemplate(data) { + return request({ + url: '/system/sms-template/create', + method: 'post', + data: data + }) +} + +// 更新短信模板 +export function updateSmsTemplate(data) { + return request({ + url: '/system/sms-template/update', + method: 'put', + data: data + }) +} + +// 删除短信模板 +export function deleteSmsTemplate(id) { + return request({ + url: '/system/sms-template/delete?id=' + id, + method: 'delete' + }) +} + +// 获得短信模板 +export function getSmsTemplate(id) { + return request({ + url: '/system/sms-template/get?id=' + id, + method: 'get' + }) +} + +// 获得短信模板分页 +export function getSmsTemplatePage(query) { + return request({ + url: '/system/sms-template/page', + method: 'get', + params: query + }) +} + +// 创建短信模板 +export function sendSms(data) { + return request({ + url: '/system/sms-template/send-sms', + method: 'post', + data: data + }) +} + +// 导出短信模板 Excel +export function exportSmsTemplateExcel(query) { + return request({ + url: '/system/sms-template/export-excel', + method: 'get', + params: query, + responseType: 'blob' + }) +} + diff --git a/ruoyi-ui/src/utils/dict.js b/ruoyi-ui/src/utils/dict.js index 3817a86fe..e3dc92d5b 100644 --- a/ruoyi-ui/src/utils/dict.js +++ b/ruoyi-ui/src/utils/dict.js @@ -17,6 +17,10 @@ export const DICT_TYPE = { SYS_OPERATE_TYPE: 'sys_operate_type', SYS_LOGIN_RESULT: 'sys_login_result', SYS_CONFIG_TYPE: 'sys_config_type', + SYS_SMS_CHANNEL_CODE: 'sys_sms_channel_code', + SYS_SMS_TEMPLATE_TYPE: 'sys_sms_template_type', + SYS_SMS_SEND_STATUS: 'sys_sms_send_status', + SYS_SMS_RECEIVE_STATUS: 'sys_sms_receive_status', INF_REDIS_TIMEOUT_TYPE: 'inf_redis_timeout_type', INF_JOB_STATUS: 'inf_job_status', diff --git a/ruoyi-ui/src/views/system/operatelog/index.vue b/ruoyi-ui/src/views/system/operatelog/index.vue index 8833a749e..7131654bf 100644 --- a/ruoyi-ui/src/views/system/operatelog/index.vue +++ b/ruoyi-ui/src/views/system/operatelog/index.vue @@ -163,7 +163,6 @@ export default { businessType: undefined, status: undefined }, - }; }, created() { diff --git a/ruoyi-ui/src/views/system/sms/smsChannel.vue b/ruoyi-ui/src/views/system/sms/smsChannel.vue new file mode 100644 index 000000000..ae4b9d7f2 --- /dev/null +++ b/ruoyi-ui/src/views/system/sms/smsChannel.vue @@ -0,0 +1,542 @@ + + + + + diff --git a/ruoyi-ui/src/views/system/sms/smsLog.vue b/ruoyi-ui/src/views/system/sms/smsLog.vue new file mode 100644 index 000000000..137f04850 --- /dev/null +++ b/ruoyi-ui/src/views/system/sms/smsLog.vue @@ -0,0 +1,297 @@ + + + diff --git a/ruoyi-ui/src/views/system/sms/smsTemplate.vue b/ruoyi-ui/src/views/system/sms/smsTemplate.vue new file mode 100644 index 000000000..c65bf8838 --- /dev/null +++ b/ruoyi-ui/src/views/system/sms/smsTemplate.vue @@ -0,0 +1,405 @@ + + + diff --git a/sql/ruoyi-vue-pro.sql b/sql/ruoyi-vue-pro.sql index 8a95afdca..e636149fa 100644 --- a/sql/ruoyi-vue-pro.sql +++ b/sql/ruoyi-vue-pro.sql @@ -1,7 +1,7 @@ /* Navicat Premium Data Transfer - Source Server : 127.0.0.1 + Source Server : local-mysql001 Source Server Type : MySQL Source Server Version : 50718 Source Host : localhost:3306 @@ -11,7 +11,7 @@ Target Server Version : 50718 File Encoding : 65001 - Date: 21/03/2021 18:53:24 + Date: 05/04/2021 23:51:38 */ SET NAMES utf8mb4; @@ -43,7 +43,7 @@ CREATE TABLE `inf_api_access_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=1318 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表'; +) ENGINE=InnoDB AUTO_INCREMENT=1909 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表'; -- ---------------------------- -- Records of inf_api_access_log @@ -84,7 +84,7 @@ CREATE TABLE `inf_api_error_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='系统异常日志'; +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='系统异常日志'; -- ---------------------------- -- Records of inf_api_error_log @@ -201,7 +201,7 @@ CREATE TABLE `inf_job_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=627 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表'; +) ENGINE=InnoDB AUTO_INCREMENT=1035 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表'; -- ---------------------------- -- Records of inf_job_log @@ -264,7 +264,7 @@ CREATE TABLE `sys_dict_data` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=65 DEFAULT CHARSET=utf8mb4 COMMENT='字典数据表'; +) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8mb4 COMMENT='字典数据表'; -- ---------------------------- -- Records of sys_dict_data @@ -324,6 +324,11 @@ INSERT INTO `sys_dict_data` VALUES (61, 2, '管理员', '2', 'user_type', 0, NUL INSERT INTO `sys_dict_data` VALUES (62, 0, '未处理', '0', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:19', '', '2021-02-26 08:11:23', b'0'); INSERT INTO `sys_dict_data` VALUES (63, 1, '已处理', '1', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:26', '', '2021-02-26 08:11:29', b'0'); INSERT INTO `sys_dict_data` VALUES (64, 2, '已忽略', '2', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:34', '', '2021-02-26 08:11:34', b'0'); +INSERT INTO `sys_dict_data` VALUES (65, 0, '云片', 'YUN_PIAN', 'sys_sms_channel_code', 0, NULL, '1', '2021-04-05 01:05:14', '1', '2021-04-05 01:05:14', b'0'); +INSERT INTO `sys_dict_data` VALUES (66, 0, '阿里云', 'ALIYUN', 'sys_sms_channel_code', 0, NULL, '1', '2021-04-05 01:05:26', '1', '2021-04-05 01:05:26', b'0'); +INSERT INTO `sys_dict_data` VALUES (67, 1, '验证码', '1', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:50:57', '1', '2021-04-05 21:50:57', b'0'); +INSERT INTO `sys_dict_data` VALUES (68, 2, '通知', '2', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:51:08', '1', '2021-04-05 21:51:08', b'0'); +INSERT INTO `sys_dict_data` VALUES (69, 0, '营销', '3', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:51:15', '1', '2021-04-05 21:51:15', b'0'); COMMIT; -- ---------------------------- @@ -343,7 +348,7 @@ CREATE TABLE `sys_dict_type` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `dict_type` (`type`) -) ENGINE=InnoDB AUTO_INCREMENT=111 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表'; +) ENGINE=InnoDB AUTO_INCREMENT=113 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表'; -- ---------------------------- -- Records of sys_dict_type @@ -366,6 +371,8 @@ INSERT INTO `sys_dict_type` VALUES (107, '定时任务状态', 'inf_job_status', INSERT INTO `sys_dict_type` VALUES (108, '定时任务日志状态', 'inf_job_log_status', 0, NULL, '', '2021-02-08 10:03:51', '', '2021-02-08 10:03:51', b'0'); INSERT INTO `sys_dict_type` VALUES (109, '用户类型', 'user_type', 0, NULL, '', '2021-02-26 00:15:51', '', '2021-02-26 00:15:51', b'0'); INSERT INTO `sys_dict_type` VALUES (110, 'API 异常数据的处理状态', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:01', '', '2021-02-26 07:07:01', b'0'); +INSERT INTO `sys_dict_type` VALUES (111, '短信渠道编码', 'sys_sms_channel_code', 0, NULL, '1', '2021-04-05 01:04:50', '1', '2021-04-05 01:04:50', b'0'); +INSERT INTO `sys_dict_type` VALUES (112, '短信模板的类型', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:50:43', '1', '2021-04-05 21:50:43', b'0'); COMMIT; -- ---------------------------- @@ -386,7 +393,7 @@ CREATE TABLE `sys_login_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COMMENT='系统访问记录'; +) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COMMENT='系统访问记录'; -- ---------------------------- -- Records of sys_login_log @@ -415,7 +422,7 @@ CREATE TABLE `sys_menu` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=1093 DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表'; +) ENGINE=InnoDB AUTO_INCREMENT=1100 DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表'; -- ---------------------------- -- Records of sys_menu @@ -530,6 +537,12 @@ INSERT INTO `sys_menu` VALUES (1089, '日志查询', 'infra:api-error-log:query' INSERT INTO `sys_menu` VALUES (1090, '文件管理', '', 2, 0, 2, 'file', 'upload', 'infra/file/index', 0, '', '2021-03-12 20:16:20', '1', '2021-03-13 11:07:05', b'0'); INSERT INTO `sys_menu` VALUES (1091, '文件查询', 'infra:file:query', 3, 1, 1090, '', '', '', 0, '', '2021-03-12 20:16:20', '', '2021-03-12 20:16:20', b'0'); INSERT INTO `sys_menu` VALUES (1092, '文件删除', 'infra:file:delete', 3, 4, 1090, '', '', '', 0, '', '2021-03-12 20:16:20', '', '2021-03-12 20:16:20', b'0'); +INSERT INTO `sys_menu` VALUES (1093, '短信管理', '', 1, 11, 1, 'sms', 'validCode', NULL, 0, '1', '2021-04-05 01:10:16', '1', '2021-04-05 01:11:38', b'0'); +INSERT INTO `sys_menu` VALUES (1094, '短信渠道', '', 2, 0, 1093, 'sms-channel', '', 'system/sms/smsChannel', 0, '', '2021-04-01 11:07:15', '1', '2021-04-05 20:32:53', b'0'); +INSERT INTO `sys_menu` VALUES (1095, '短信渠道查询', 'system:sms-channel:query', 3, 1, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0'); +INSERT INTO `sys_menu` VALUES (1096, '短信渠道创建', 'system:sms-channel:create', 3, 2, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0'); +INSERT INTO `sys_menu` VALUES (1097, '短信渠道更新', 'system:sms-channel:update', 3, 3, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0'); +INSERT INTO `sys_menu` VALUES (1098, '短信渠道删除', 'system:sms-channel:delete', 3, 4, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0'); COMMIT; -- ---------------------------- @@ -589,7 +602,7 @@ CREATE TABLE `sys_operate_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=65 DEFAULT CHARSET=utf8mb4 COMMENT='操作日志记录'; +) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4 COMMENT='操作日志记录'; -- ---------------------------- -- Records of sys_operate_log @@ -834,6 +847,118 @@ INSERT INTO `sys_role_menu` VALUES (237, 101, 1064, '', '2021-01-21 03:23:27', ' INSERT INTO `sys_role_menu` VALUES (238, 101, 1065, '', '2021-01-21 03:23:27', '', '2021-01-21 03:23:27', b'0'); COMMIT; +-- ---------------------------- +-- Table structure for sys_sms_channel +-- ---------------------------- +DROP TABLE IF EXISTS `sys_sms_channel`; +CREATE TABLE `sys_sms_channel` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号', + `signature` varchar(10) NOT NULL COMMENT '短信签名', + `code` varchar(63) NOT NULL COMMENT '渠道编码', + `status` tinyint(4) NOT NULL COMMENT '开启状态', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `api_key` varchar(63) NOT NULL COMMENT '短信 API 的账号', + `api_secret` varchar(63) DEFAULT NULL COMMENT '短信 API 的秘钥', + `callback_url` varchar(255) DEFAULT NULL COMMENT '短信发送回调 URL', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='短信渠道'; + +-- ---------------------------- +-- Records of sys_sms_channel +-- ---------------------------- +BEGIN; +INSERT INTO `sys_sms_channel` VALUES (1, '芋道', 'YUN_PIAN', 0, '呵呵呵哒', '1555a14277cb8a608cf45a9e6a80d510', NULL, 'http://java.nat300.top/api/system/sms/callback/sms/yunpian', '', '2021-03-31 06:12:20', '1', '2021-04-05 21:02:38', b'0'); +INSERT INTO `sys_sms_channel` VALUES (2, 'Ballcat', 'ALIYUN', 0, '啦啦啦', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2021-04-05 21:02:44', b'0'); +INSERT INTO `sys_sms_channel` VALUES (3, '测试', 'YUN_PIAN', 0, '哈哈哈', '23132', NULL, 'http://www.baidu.com', '1', '2021-04-05 21:10:34', '1', '2021-04-05 21:10:34', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_sms_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_sms_log`; +CREATE TABLE `sys_sms_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号', + `channel_id` bigint(20) NOT NULL COMMENT '短信渠道编号', + `channel_code` varchar(63) NOT NULL COMMENT '短信渠道编码', + `template_id` bigint(20) NOT NULL COMMENT '模板编号', + `template_code` varchar(63) NOT NULL COMMENT '模板编码', + `template_type` tinyint(4) NOT NULL COMMENT '短信类型', + `template_content` varchar(255) NOT NULL COMMENT '短信内容', + `template_params` varchar(255) NOT NULL COMMENT '短信参数', + `api_template_id` varchar(63) NOT NULL COMMENT '短信 API 的模板编号', + `mobile` varchar(11) NOT NULL COMMENT '手机号', + `user_id` bigint(20) DEFAULT '0' COMMENT '用户编号', + `user_type` tinyint(4) DEFAULT '0' COMMENT '用户类型', + `send_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '发送状态', + `send_time` datetime DEFAULT NULL COMMENT '发送时间', + `send_code` int(11) DEFAULT NULL COMMENT '发送结果的编码', + `send_msg` varchar(255) DEFAULT NULL COMMENT '发送结果的提示', + `api_send_code` varchar(63) DEFAULT NULL COMMENT '短信 API 发送结果的编码', + `api_send_msg` varchar(255) DEFAULT NULL COMMENT '短信 API 发送失败的提示', + `api_request_id` varchar(255) DEFAULT NULL COMMENT '短信 API 发送返回的唯一请求 ID', + `api_serial_no` varchar(255) DEFAULT NULL COMMENT '短信 API 发送返回的序号', + `receive_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '接收状态', + `receive_time` datetime DEFAULT NULL COMMENT '接收时间', + `api_receive_code` varchar(63) DEFAULT NULL COMMENT 'API 接收结果的编码', + `api_receive_msg` varchar(255) DEFAULT NULL COMMENT 'API 接收结果的说明', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COMMENT='短信moan'; + +-- ---------------------------- +-- Records of sys_sms_log +-- ---------------------------- +BEGIN; +INSERT INTO `sys_sms_log` VALUES (15, 1, 'YUN_PIAN', 2, 'test_01', 1, '正在进行登录操作登陆,您的验证码是1234', '{\"code\":\"1234\",\"operation\":\"登陆\"}', '4383920', '15601691399', 1, 2, 10, '2021-04-04 23:24:13', 0, '成功', '0', '发送成功', NULL, '62922707786', 10, '2021-04-04 23:24:26', 'DELIVRD', 'DELIVRD', NULL, '2021-04-04 23:23:29', NULL, '2021-04-04 23:25:17', b'0'); +INSERT INTO `sys_sms_log` VALUES (16, 2, 'ALIYUN', 3, 'test_02', 1, '您的验证码1234,该验证码5分钟内有效,请勿泄漏于他人!', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:08:39', 999, '未知错误,需要解析', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 2EA9C6A5-579F-4D21-B7D3-0AD3BA4F7741', '2EA9C6A5-579F-4D21-B7D3-0AD3BA4F7741', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:08:39', NULL, '2021-04-05 00:08:39', b'0'); +INSERT INTO `sys_sms_log` VALUES (17, 2, 'ALIYUN', 3, 'test_02', 1, '您的验证码1234,该验证码5分钟内有效,请勿泄漏于他人!', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:09:43', 999, '未知错误,需要解析', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : BF766164-9C03-44FD-B6D3-ADA74118E432', 'BF766164-9C03-44FD-B6D3-ADA74118E432', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:09:43', NULL, '2021-04-05 00:09:43', b'0'); +INSERT INTO `sys_sms_log` VALUES (18, 2, 'ALIYUN', 3, 'test_02', 1, '您的验证码1234,该验证码5分钟内有效,请勿泄漏于他人!', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:11:13', 999, '未知错误,需要解析', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 2D7C0ABC-7538-45B4-BFEF-B610D591CE3D', '2D7C0ABC-7538-45B4-BFEF-B610D591CE3D', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:11:12', NULL, '2021-04-05 00:11:13', b'0'); +INSERT INTO `sys_sms_log` VALUES (19, 2, 'ALIYUN', 3, 'test_02', 1, '您的验证码1234,该验证码5分钟内有效,请勿泄漏于他人!', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:12:21', 999, '未知错误,需要解析', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 0A86DC5C-2985-474F-B076-748C9F2C5D3F', '0A86DC5C-2985-474F-B076-748C9F2C5D3F', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:12:01', NULL, '2021-04-05 00:12:21', b'0'); +INSERT INTO `sys_sms_log` VALUES (20, 1, 'YUN_PIAN', 2, 'test_01', 1, '正在进行登录操作登陆,您的验证码是1234', '{\"code\":\"1234\",\"operation\":\"登陆\"}', '4383920', '15601691399', 1, 2, 10, '2021-04-05 00:14:36', 0, '成功', '0', '发送成功', NULL, '62923244790', 0, NULL, NULL, NULL, NULL, '2021-04-05 00:13:42', NULL, '2021-04-05 00:14:36', b'0'); +INSERT INTO `sys_sms_log` VALUES (21, 2, 'ALIYUN', 3, 'test_02', 1, '您的验证码1234,该验证码5分钟内有效,请勿泄漏于他人!', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:19:43', 999, '未知错误,需要解析', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 3837C6D3-B96F-428C-BBB2-86135D4B5B99', '3837C6D3-B96F-428C-BBB2-86135D4B5B99', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:15:06', NULL, '2021-04-05 00:19:44', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for sys_sms_template +-- ---------------------------- +DROP TABLE IF EXISTS `sys_sms_template`; +CREATE TABLE `sys_sms_template` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号', + `type` tinyint(4) NOT NULL COMMENT '短信签名', + `status` tinyint(4) NOT NULL COMMENT '开启状态', + `code` varchar(63) NOT NULL COMMENT '模板编码', + `name` varchar(63) NOT NULL COMMENT '模板名称', + `content` varchar(255) NOT NULL COMMENT '模板内容', + `params` varchar(255) NOT NULL COMMENT '参数数组', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `api_template_id` varchar(63) NOT NULL COMMENT '短信 API 的模板编号', + `channel_id` bigint(20) NOT NULL COMMENT '短信渠道编号', + `channel_code` varchar(63) NOT NULL COMMENT '短信渠道编码', + `creator` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='短信模板'; + +-- ---------------------------- +-- Records of sys_sms_template +-- ---------------------------- +BEGIN; +INSERT INTO `sys_sms_template` VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', NULL, '4383920', 1, 'YUN_PIAN', '', '2021-03-31 10:49:38', '', '2021-03-31 12:01:38', b'0'); +INSERT INTO `sys_sms_template` VALUES (3, 1, 0, 'test_02', '公告通知', '您的验证码{code},该验证码5分钟内有效,请勿泄漏于他人!', '[\"code\"]', NULL, 'SMS_207945135', 2, 'ALIYUN', '', '2021-03-31 11:56:30', '', '2021-03-31 11:56:30', b'0'); +COMMIT; + -- ---------------------------- -- Table structure for sys_user -- ---------------------------- @@ -865,8 +990,8 @@ CREATE TABLE `sys_user` ( -- Records of sys_user -- ---------------------------- BEGIN; -INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:8080/api/system/file/get/add5ec1891a7d97d2cc1d60847e16294.jpg', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '1', '2021-03-21 18:16:16', b'0'); -INSERT INTO `sys_user` VALUES (2, 'ry', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '若依', '测试员', 105, '[2]', 'ry@qq.com', '15666666666', 1, '', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '', '2021-01-05 17:03:47', b'0'); +INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://api-dashboard.yudao.iocoder.cn/api/infra/file/get/5e8609290e915c4fa8b08e67.jpg', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '1', '2021-04-05 02:16:10', b'0'); +INSERT INTO `sys_user` VALUES (2, 'ry', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '若依', '测试员', 105, '[2]', 'ry@qq.com', '15666666666', 1, '', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '', '2021-04-01 04:50:36', b'1'); INSERT INTO `sys_user` VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 100, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '', NULL, '', '2021-01-07 09:07:17', '1', '2021-03-14 22:35:17', b'0'); INSERT INTO `sys_user` VALUES (103, 'yuanma', '', '源码', NULL, 100, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '', NULL, '', '2021-01-13 23:50:35', '', '2021-01-13 23:50:35', b'0'); INSERT INTO `sys_user` VALUES (104, 'test', '$2a$10$.TOFpaIiI3PzEwkGrNq0Eu6Cc3rOqJMxTb1DqeSEM8StxaGPBRKoi', '测试号', NULL, 100, '[]', '', '15601691200', 1, '', 0, '', NULL, '', '2021-01-21 02:13:53', '1', '2021-03-14 22:36:38', b'0'); @@ -924,11 +1049,17 @@ CREATE TABLE `sys_user_session` ( -- Records of sys_user_session -- ---------------------------- BEGIN; +INSERT INTO `sys_user_session` VALUES ('04c6624c7bf14b1ba1a01cb976a9d876', 1, '2021-04-05 21:40:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 20:21:09', NULL, '2021-04-01 12:25:35', b'1'); +INSERT INTO `sys_user_session` VALUES ('0e235ce5ae7342a09b372a00bd7d1b41', 1, '2021-04-05 01:43:22', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 00:51:03', NULL, '2021-04-01 04:18:06', b'1'); +INSERT INTO `sys_user_session` VALUES ('0e6943f8ca9b4215a014843eb489ccc7', 1, '2021-04-05 22:53:22', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 21:43:59', NULL, '2021-04-05 22:23:22', b'0'); +INSERT INTO `sys_user_session` VALUES ('40d532d8900c43b791266429a7911751', 1, '2021-04-05 22:11:34', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 21:41:34', NULL, '2021-04-01 12:28:20', b'1'); INSERT INTO `sys_user_session` VALUES ('505b4e7d8b0d4b40aa23bf540da81234', 1, '2021-03-14 01:25:13', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 00:31:43', NULL, '2021-03-13 07:35:26', b'1'); INSERT INTO `sys_user_session` VALUES ('5a7248bf87d14e7e9f0578b05969986c', 1, '2021-03-13 10:42:50', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-13 09:37:36', NULL, '2021-03-12 19:53:07', b'1'); +INSERT INTO `sys_user_session` VALUES ('8b3eac5e4a104a4191c8070e03d553ea', 1, '2021-04-05 02:45:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 02:15:12', NULL, '2021-04-01 11:05:25', b'1'); INSERT INTO `sys_user_session` VALUES ('9ae27346d8b7491aad1385f51e8aa196', 1, '2021-03-13 14:02:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-13 10:43:06', NULL, '2021-03-13 06:40:35', b'1'); INSERT INTO `sys_user_session` VALUES ('ae9ee7452ee54e4b983d658188c15c4d', 1, '2021-03-14 21:32:57', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 20:25:00', NULL, '2021-03-13 15:19:10', b'1'); -INSERT INTO `sys_user_session` VALUES ('d0adf48f82914212b947e5ab04d9fb65', 1, '2021-03-21 19:16:28', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 18:13:37', NULL, '2021-03-21 18:46:28', b'0'); +INSERT INTO `sys_user_session` VALUES ('d0adf48f82914212b947e5ab04d9fb65', 1, '2021-03-21 19:16:28', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 18:13:37', NULL, '2021-03-15 05:53:20', b'1'); +INSERT INTO `sys_user_session` VALUES ('e80c2400724042a2ab73732166cde8fc', 1, '2021-03-21 21:17:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 20:47:12', NULL, '2021-03-15 08:18:56', b'1'); INSERT INTO `sys_user_session` VALUES ('e8872f5192584440a548641b83c877ef', 1, '2021-03-21 18:36:01', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 17:51:48', NULL, '2021-03-15 03:54:20', b'1'); INSERT INTO `sys_user_session` VALUES ('f1ab99b09b5a475795579ff99d60ac78', 1, '2021-03-14 23:04:31', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 21:12:44', NULL, '2021-03-15 03:32:38', b'1'); INSERT INTO `sys_user_session` VALUES ('f853b50d064340a581e9a49bba9411fc', 1, '2021-03-10 01:55:41', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-10 01:11:53', NULL, '2021-03-12 18:37:05', b'1'); @@ -964,7 +1095,7 @@ CREATE TABLE `tool_codegen_column` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=389 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表字段定义'; +) ENGINE=InnoDB AUTO_INCREMENT=418 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表字段定义'; -- ---------------------------- -- Records of tool_codegen_column @@ -1126,6 +1257,35 @@ INSERT INTO `tool_codegen_column` VALUES (385, 33, 'create_time', 'datetime', ' INSERT INTO `tool_codegen_column` VALUES (386, 33, 'updater', 'varchar(64)', '更新者', b'1', b'0', '0', 6, 'String', 'updater', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); INSERT INTO `tool_codegen_column` VALUES (387, 33, 'update_time', 'datetime', '更新时间', b'0', b'0', '0', 7, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); INSERT INTO `tool_codegen_column` VALUES (388, 33, 'deleted', 'bit(1)', '是否删除', b'0', b'0', '0', 8, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); +INSERT INTO `tool_codegen_column` VALUES (389, 34, 'id', 'bigint(20)', '编号', b'0', b'1', '1', 1, 'Long', 'id', '', '1024', b'0', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (390, 34, 'signature', 'varchar(8)', '短信签名', b'0', b'0', '0', 2, 'String', 'signature', '', '芋道源码', b'1', b'1', b'1', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (391, 34, 'code', 'varchar(63)', '渠道编码', b'0', b'0', '0', 3, 'String', 'code', 'sys_sms_channel_code', 'YUN_PIAN', b'1', b'0', b'0', '=', b'1', 'select', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (392, 34, 'status', 'tinyint(4)', '启用状态', b'0', b'0', '0', 4, 'Integer', 'status', 'sys_common_status', '1', b'1', b'1', b'1', '=', b'1', 'radio', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (393, 34, 'remark', 'varchar(255)', '备注', b'1', b'0', '0', 5, 'String', 'remark', '', '好吃!', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (394, 34, 'api_key', 'varchar(63)', '短信 API 的账号', b'0', b'0', '0', 6, 'String', 'apiKey', '', 'yudao', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (395, 34, 'api_secret', 'varchar(63)', '短信 API 的秘钥', b'1', b'0', '0', 7, 'String', 'apiSecret', '', 'yuanma', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (396, 34, 'callback_url', 'varchar(255)', '短信发送回调 URL', b'1', b'0', '0', 8, 'String', 'callbackUrl', '', 'http://www.iocoder.cn', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (397, 34, 'creator', 'varchar(64)', '创建者', b'1', b'0', '0', 9, 'String', 'creator', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (398, 34, 'create_time', 'datetime', '创建时间', b'0', b'0', '0', 10, 'Date', 'createTime', '', NULL, b'0', b'0', b'1', 'BETWEEN', b'1', 'datetime', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (399, 34, 'updater', 'varchar(64)', '更新者', b'1', b'0', '0', 11, 'String', 'updater', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (400, 34, 'update_time', 'datetime', '更新时间', b'0', b'0', '0', 12, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (401, 34, 'deleted', 'bit(1)', '是否删除', b'0', b'0', '0', 13, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_column` VALUES (402, 35, 'id', 'bigint(20)', '编号', b'0', b'1', '1', 1, 'Long', 'id', '', '1024', b'0', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (403, 35, 'type', 'tinyint(4)', '短信签名', b'0', b'0', '0', 2, 'Integer', 'type', 'sys_sms_template_type', '1', b'1', b'1', b'1', '=', b'1', 'select', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (404, 35, 'status', 'tinyint(4)', '开启状态', b'0', b'0', '0', 3, 'Integer', 'status', 'sys_common_status', '1', b'1', b'1', b'1', '=', b'1', 'radio', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (405, 35, 'code', 'varchar(63)', '模板编码', b'0', b'0', '0', 4, 'String', 'code', '', 'test_01', b'1', b'1', b'1', 'LIKE', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (406, 35, 'name', 'varchar(63)', '模板名称', b'0', b'0', '0', 5, 'String', 'name', '', 'yudao', b'1', b'1', b'0', 'LIKE', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (407, 35, 'content', 'varchar(255)', '模板内容', b'0', b'0', '0', 6, 'String', 'content', '', '你好,{name}。你长的太{like}啦!', b'1', b'1', b'1', 'LIKE', b'1', 'editor', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (408, 35, 'params', 'varchar(255)', '参数数组', b'0', b'0', '0', 7, 'String', 'params', '', 'name,code', b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (409, 35, 'remark', 'varchar(255)', '备注', b'1', b'0', '0', 8, 'String', 'remark', '', '哈哈哈', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (410, 35, 'api_template_id', 'varchar(63)', '短信 API 的模板编号', b'0', b'0', '0', 9, 'String', 'apiTemplateId', '', '4383920', b'1', b'1', b'1', 'LIKE', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (411, 35, 'channel_id', 'bigint(20)', '短信渠道编号', b'0', b'0', '0', 10, 'Long', 'channelId', '', '10', b'1', b'1', b'1', '=', b'1', 'select', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (412, 35, 'channel_code', 'varchar(63)', '短信渠道编码', b'0', b'0', '0', 11, 'String', 'channelCode', 'sys_sms_channel_code', 'ALIYUN', b'0', b'0', b'0', '=', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (413, 35, 'creator', 'varchar(64)', '创建者', b'1', b'0', '0', 12, 'String', 'creator', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (414, 35, 'create_time', 'datetime', '创建时间', b'0', b'0', '0', 13, 'Date', 'createTime', '', NULL, b'0', b'0', b'1', 'BETWEEN', b'1', 'datetime', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (415, 35, 'updater', 'varchar(64)', '更新者', b'1', b'0', '0', 14, 'String', 'updater', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (416, 35, 'update_time', 'datetime', '更新时间', b'0', b'0', '0', 15, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); +INSERT INTO `tool_codegen_column` VALUES (417, 35, 'deleted', 'bit(1)', '是否删除', b'0', b'0', '0', 16, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0'); COMMIT; -- ---------------------------- @@ -1151,7 +1311,7 @@ CREATE TABLE `tool_codegen_table` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表定义'; +) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表定义'; -- ---------------------------- -- Records of tool_codegen_table @@ -1169,6 +1329,8 @@ INSERT INTO `tool_codegen_table` VALUES (28, 1, 'sys_dict_type', '字典类型 INSERT INTO `tool_codegen_table` VALUES (29, 1, 'sys_dict_type', '字典类型表', NULL, 'system', 'dict', 'SysDictType', '字典类型', '芋艿', 1, NULL, '', '2021-03-06 03:52:57', '', '2021-03-06 04:03:52', b'0'); INSERT INTO `tool_codegen_table` VALUES (30, 1, 'sys_dict_data', '字典数据表', NULL, 'system', 'type', 'SysDictData', '字典数据', '芋道源码', 1, NULL, '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:47', b'0'); INSERT INTO `tool_codegen_table` VALUES (33, 1, 'inf_file', '文件表', NULL, 'infra', 'file', 'InfFile', '文件', '芋艿', 1, 2, '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); +INSERT INTO `tool_codegen_table` VALUES (34, 1, 'sys_sms_channel', '短信渠道', NULL, 'system', 'sms', 'SysSmsChannel', '短信渠道', '芋道源码', 1, 1093, '1', '2021-04-03 13:39:06', '1', '2021-04-05 20:52:09', b'0'); +INSERT INTO `tool_codegen_table` VALUES (35, 1, 'sys_sms_template', '短信模板', NULL, 'system', 'sms', 'SysSmsTemplate', '短信模板', '芋道源码', 1, 1093, '1', '2021-04-03 13:58:55', '1', '2021-04-05 22:23:38', b'0'); COMMIT; -- ---------------------------- diff --git a/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java b/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java new file mode 100644 index 000000000..57fe08b5a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java @@ -0,0 +1,20 @@ +package cn.iocoder.dashboard.common.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Key Value 的键值对 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue { + + private K key; + private V value; + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java b/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java new file mode 100644 index 000000000..7738d40a2 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.dashboard.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通用状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DefaultBitFieldEnum { + + NO(0, "否"), + YES(1, "是"); + + /** + * 状态值 + */ + private final Integer val; + /** + * 状态名 + */ + private final String name; + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java b/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java index 670029504..065aece7c 100644 --- a/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java +++ b/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java @@ -1,12 +1,13 @@ package cn.iocoder.dashboard.common.exception; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; import lombok.Data; /** * 错误码对象 * - * 全局错误码,占用 [0, 999],参见 {@link GlobalException} + * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} * * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 @@ -21,11 +22,11 @@ public class ErrorCode { /** * 错误提示 */ - private final String message; + private final String msg; public ErrorCode(Integer code, String message) { this.code = code; - this.message = message; + this.msg = message; } } diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java b/src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java deleted file mode 100644 index d4f9c945e..000000000 --- a/src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java +++ /dev/null @@ -1,41 +0,0 @@ -package cn.iocoder.dashboard.common.exception; - -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 全局异常 Exception - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class GlobalException extends RuntimeException { - - /** - * 全局错误码 - * - * @see GlobalErrorCodeConstants - */ - private Integer code; - /** - * 错误提示 - */ - private String message; - - /** - * 空构造方法,避免反序列化问题 - */ - public GlobalException() { - } - - public GlobalException(ErrorCode errorCode) { - this.code = errorCode.getCode(); - this.message = errorCode.getMessage(); - } - - public GlobalException(Integer code, String message) { - this.code = code; - this.message = message; - } - -} diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java b/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java index 83c43ca2e..2e2adec75 100644 --- a/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java +++ b/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java @@ -30,7 +30,7 @@ public final class ServiceException extends RuntimeException { public ServiceException(ErrorCode errorCode) { this.code = errorCode.getCode(); - this.message = errorCode.getMessage(); + this.message = errorCode.getMsg(); } public ServiceException(Integer code, String message) { diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java b/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java index 6dbfe6ca6..e6367c835 100644 --- a/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java +++ b/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java @@ -47,12 +47,12 @@ public class ServiceExceptionUtil { // ========== 和 ServiceException 的集成 ========== public static ServiceException exception(ErrorCode errorCode) { - String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage()); + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); return exception0(errorCode.getCode(), messagePattern); } public static ServiceException exception(ErrorCode errorCode, Object... params) { - String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage()); + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); return exception0(errorCode.getCode(), messagePattern, params); } diff --git a/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java b/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java index 14e34070d..bb7fbc345 100644 --- a/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java +++ b/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java @@ -1,7 +1,6 @@ package cn.iocoder.dashboard.common.pojo; import cn.iocoder.dashboard.common.exception.ErrorCode; -import cn.iocoder.dashboard.common.exception.GlobalException; import cn.iocoder.dashboard.common.exception.ServiceException; import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -9,6 +8,7 @@ import lombok.Data; import org.springframework.util.Assert; import java.io.Serializable; +import java.util.Objects; /** * 通用返回 @@ -16,7 +16,7 @@ import java.io.Serializable; * @param 数据泛型 */ @Data -public final class CommonResult implements Serializable { +public class CommonResult implements Serializable { /** * 错误码 @@ -31,7 +31,7 @@ public final class CommonResult implements Serializable { /** * 错误提示,用户可阅读 * - * @see ErrorCode#getMessage() () + * @see ErrorCode#getMsg() () */ private String msg; @@ -57,7 +57,7 @@ public final class CommonResult implements Serializable { } public static CommonResult error(ErrorCode errorCode) { - return error(errorCode.getCode(), errorCode.getMessage()); + return error(errorCode.getCode(), errorCode.getMsg()); } public static CommonResult success(T data) { @@ -68,9 +68,13 @@ public final class CommonResult implements Serializable { return result; } + public static boolean isSuccess(Integer code) { + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + @JsonIgnore // 避免 jackson 序列化 public boolean isSuccess() { - return GlobalErrorCodeConstants.SUCCESS.getCode().equals(code); + return isSuccess(code); } @JsonIgnore // 避免 jackson 序列化 @@ -81,16 +85,12 @@ public final class CommonResult implements Serializable { // ========= 和 Exception 异常体系集成 ========= /** - * 判断是否有异常。如果有,则抛出 {@link GlobalException} 或 {@link ServiceException} 异常 + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 */ - public void checkError() throws GlobalException, ServiceException { + public void checkError() throws ServiceException { if (isSuccess()) { return; } - // 全局异常 - if (GlobalErrorCodeConstants.isMatch(code)) { - throw new GlobalException(code, msg); - } // 业务异常 throw new ServiceException(code, msg); } @@ -99,8 +99,4 @@ public final class CommonResult implements Serializable { return error(serviceException.getCode(), serviceException.getMessage()); } - public static CommonResult error(GlobalException globalException) { - return error(globalException.getCode(), globalException.getMessage()); - } - } diff --git a/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java b/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java index a332235d3..2bda65d2b 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java +++ b/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java @@ -13,6 +13,11 @@ import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.property.ExcelContentProperty; import lombok.extern.slf4j.Slf4j; +/** + * Excel {@link SysDictDataDO} 数据字典转换器 + * + * @author 芋道源码 + */ @Slf4j public class DictConvert implements Converter { diff --git a/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java b/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java new file mode 100644 index 000000000..d099d3c98 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java @@ -0,0 +1,39 @@ +package cn.iocoder.dashboard.framework.excel.core.convert; + +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.CellData; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * Excel Json 转换器 + * + * @author 芋道源码 + */ +public class JsonConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellData convertToExcelData(Object value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 生成 Excel 小表格 + return new CellData<>(JsonUtils.toJsonString(value)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java b/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java index 432d5618c..52bef8059 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java @@ -48,8 +48,8 @@ public class RedisConfig { * 创建 Redis Pub/Sub 广播消费的容器 */ @Bean - public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory, - List> listeners) { + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisConnectionFactory factory, List> listeners) { // 创建 RedisMessageListenerContainer 对象 RedisMessageListenerContainer container = new RedisMessageListenerContainer(); // 设置 RedisConnection 工厂。 @@ -69,8 +69,8 @@ public class RedisConfig { * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html */ @Bean(initMethod = "start", destroyMethod = "stop") - public StreamMessageListenerContainer> redisStreamMessageListenerContainer(RedisTemplate redisTemplate, - List> listeners) { + public StreamMessageListenerContainer> redisStreamMessageListenerContainer( + RedisTemplate redisTemplate, List> listeners) { // 第一步,创建 StreamMessageListenerContainer 容器 // 创建 options 配置 StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java index bb55e1e71..e20431153 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java +++ b/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java @@ -128,13 +128,13 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // 设置每个请求的权限 .authorizeRequests() // 登陆的接口,可匿名访问 - .antMatchers(webProperties.getApiPrefix() + "/login").anonymous() + .antMatchers(api("/login")).anonymous() // 通用的接口,可匿名访问 - .antMatchers( webProperties.getApiPrefix() + "/system/captcha/**").anonymous() + .antMatchers(api("/system/captcha/**")).anonymous() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() // 文件的获取接口,可匿名访问 - .antMatchers(webProperties.getApiPrefix() + "/infra/file/get/**").anonymous() + .antMatchers(api("/infra/file/get/**")).anonymous() // Swagger 接口文档 .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() @@ -148,13 +148,19 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .antMatchers("/actuator/**").anonymous() // Druid 监控 .antMatchers("/druid/**").anonymous() + // 短信回调 API + .antMatchers(api("/system/sms/callback/**")).anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); - httpSecurity.logout().logoutUrl(webProperties.getApiPrefix() + "/logout").logoutSuccessHandler(logoutSuccessHandler); + httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler); // 添加 JWT Filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } + private String api(String url) { + return webProperties.getApiPrefix() + url; + } + } diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java new file mode 100644 index 000000000..e5441c91f --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.framework.sms.config; + +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.impl.SmsClientFactoryImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 短信配置类 + * + * @author 芋道源码 + */ +@Configuration +public class SmsConfiguration { + + @Bean + public SmsClientFactory smsClientFactory() { + return new SmsClientFactoryImpl(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java new file mode 100644 index 000000000..f706e7983 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java @@ -0,0 +1,54 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; + +import java.util.List; + +/** + * 短信客户端接口 + * + * @author zzf + * @date 2021/1/25 14:14 + */ +public interface SmsClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + /** + * 发送消息 + * + * @param logId 日志编号 + * @param mobile 手机号 + * @param apiTemplateId 短信 API 的模板编号 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 + * @return 短信发送结果 + */ + SmsCommonResult sendSms(Long logId, String mobile, String apiTemplateId, + List> templateParams); + + /** + * 解析接收短信的接收结果 + * + * @param text 结果 + * @return 结果内容 + * @throws Throwable 当解析 text 发生异常时,则会抛出异常 + */ + List parseSmsReceiveStatus(String text) throws Throwable; + + /** + * 查询指定的短信模板 + * + * @param apiTemplateId 短信 API 的模板编号 + * @return 短信模板 + */ + SmsCommonResult getSmsTemplate(String apiTemplateId); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java new file mode 100644 index 000000000..83fb88c24 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java @@ -0,0 +1,36 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; + +/** + * 短信客户端工厂接口 + * + * @author zzf + * @date 2021/1/28 14:01 + */ +public interface SmsClientFactory { + + /** + * 获得短信 Client + * + * @param channelId 渠道编号 + * @return 短信 Client + */ + SmsClient getSmsClient(Long channelId); + + /** + * 获得短信 Client + * + * @param channelCode 渠道编码 + * @return 短信 Client + */ + SmsClient getSmsClient(String channelCode); + + /** + * 创建短信 Client + * + * @param properties 配置对象 + */ + void createOrUpdateSmsClient(SmsChannelProperties properties); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java new file mode 100644 index 000000000..7b6cc51f2 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import java.util.function.Function; + +/** + * 将 API 的错误码,转换为通用的错误码 + * + * @see SmsCommonResult + * @see SmsFrameworkErrorCodeConstants + * + * @author 芋道源码 + */ +public interface SmsCodeMapping extends Function { +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java new file mode 100644 index 000000000..79ebed3a2 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java @@ -0,0 +1,68 @@ +package cn.iocoder.dashboard.framework.sms.core.client; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信的 CommonResult 拓展类 + * + * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段 + * + * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsCommonResult extends CommonResult { + + /** + * API 返回错误码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiCode; + /** + * API 返回提示 + */ + private String apiMsg; + + /** + * API 请求编号 + */ + private String apiRequestId; + + private SmsCommonResult() { + } + + public static SmsCommonResult build(String apiCode, String apiMsg, String apiRequestId, + T data, SmsCodeMapping codeMapping) { + Assert.notNull(codeMapping, "参数 codeMapping 不能为空"); + SmsCommonResult result = new SmsCommonResult().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId); + result.setData(data); + // 翻译错误码 + if (codeMapping != null) { + ErrorCode errorCode = codeMapping.apply(apiCode); + if (errorCode == null) { + errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg()); + } + return result; + } + + public static SmsCommonResult error(Throwable ex) { + SmsCommonResult result = new SmsCommonResult<>(); + result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode()); + result.setMsg(ExceptionUtil.getRootCauseMessage(ex)); + return result; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java new file mode 100644 index 000000000..ecfdb045c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java @@ -0,0 +1,48 @@ +package cn.iocoder.dashboard.framework.sms.core.client.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 消息接收 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsReceiveRespDTO { + + /** + * 是否接收成功 + */ + private Boolean success; + /** + * API 接收结果的编码 + */ + private String errorCode; + /** + * API 接收结果的说明 + */ + private String errorMsg; + + /** + * 手机号 + */ + private String mobile; + /** + * 用户接收时间 + */ + private Date receiveTime; + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + /** + * 短信日志编号 + * + * 对应 SysSmsLogDO 的编号 + */ + private Long logId; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java new file mode 100644 index 000000000..c3f6b51ae --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.framework.sms.core.client.dto; + +import lombok.Data; + +/** + * 短信发送 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsSendRespDTO { + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java new file mode 100644 index 000000000..938310e71 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.dashboard.framework.sms.core.client.dto; + +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import lombok.Data; + +/** + * 短信模板 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateRespDTO { + + /** + * 模板编号 + */ + private String id; + /** + * 短信内容 + */ + private String content; + /** + * 审核状态 + * + * 枚举 {@link SmsTemplateAuditStatusEnum} + */ + private Integer auditStatus; + /** + * 审核未通过的理由 + */ + private String auditReason; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java new file mode 100644 index 000000000..068c0db7c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java @@ -0,0 +1,122 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl; + +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 短信客户端抽象类 + * + * @author zzf + * @date 2021/2/1 9:28 + */ +@Slf4j +public abstract class AbstractSmsClient implements SmsClient { + + /** + * 短信渠道配置 + */ + protected volatile SmsChannelProperties properties; + /** + * 错误码枚举类 + */ + protected final SmsCodeMapping codeMapping; + + /** + * 短信客户端有参构造函数 + * + * @param properties 短信配置 + */ + public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { + this.properties = properties; + this.codeMapping = codeMapping; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][配置({}) 初始化完成]", properties); + } + + public final void refresh(SmsChannelProperties properties) { + // 判断是否更新 + if (properties.equals(this.properties)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", properties); + this.properties = properties; + // 初始化 + this.init(); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + @Override + public Long getId() { + return properties.getId(); + } + + @Override + public final SmsCommonResult sendSms(Long logId, String mobile, + String apiTemplateId, List> templateParams) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doSendSms(logId, mobile, apiTemplateId, templateParams); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]", + logId, mobile, apiTemplateId, templateParams, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) + throws Throwable; + + @Override + public List parseSmsReceiveStatus(String text) throws Throwable { + try { + return doParseSmsReceiveStatus(text); + } catch (Throwable ex) { + log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex); + throw ex; + } + } + + protected abstract List doParseSmsReceiveStatus(String text) throws Throwable; + + @Override + public SmsCommonResult getSmsTemplate(String apiTemplateId) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doGetSmsTemplate(apiTemplateId); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java new file mode 100644 index 000000000..44f87d7df --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -0,0 +1,90 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl; + +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun.AliyunSmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian.YunpianSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 短信客户端工厂接口 + * + * @author zzf + */ +@Validated +@Slf4j +public class SmsClientFactoryImpl implements SmsClientFactory { + + /** + * 短信客户端 Map + * key:渠道编号,使用 {@link SmsChannelProperties#getId()} + */ + private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); + + /** + * 短信客户端 Map + * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} + * + * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 + * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} + */ + private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); + + public SmsClientFactoryImpl() { + // 初始化 channelCodeClients 集合 + Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { + // 创建一个空的 SmsChannelProperties 对象 + SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) + .setApiKey("default").setApiSecret("default"); + // 创建 Sms 客户端 + AbstractSmsClient smsClient = createSmsClient(properties); + channelCodeClients.put(channel.getCode(), smsClient); + }); + } + + @Override + public SmsClient getSmsClient(Long channelId) { + return channelIdClients.get(channelId); + } + + @Override + public SmsClient getSmsClient(String channelCode) { + return channelCodeClients.get(channelCode); + } + + @Override + public void createOrUpdateSmsClient(SmsChannelProperties properties) { + AbstractSmsClient client = channelIdClients.get(properties.getId()); + if (client == null) { + client = this.createSmsClient(properties); + client.init(); + channelIdClients.put(client.getId(), client); + } else { + client.refresh(properties); + } + } + + private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { + SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); + Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); + // 创建客户端 + switch (channelEnum) { + case ALIYUN: return new AliyunSmsClient(properties); + case YUN_PIAN: return new YunpianSmsClient(properties); + case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java new file mode 100644 index 000000000..24683bcb0 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java @@ -0,0 +1,212 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.collection.MapUtils; +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.AcsResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 阿里短信客户端的实现类 + * + * @author zzf + * @date 2021/1/25 14:17 + */ +@Slf4j +public class AliyunSmsClient extends AbstractSmsClient { + + /** + * REGION, 使用杭州 + */ + private static final String ENDPOINT = "cn-hangzhou"; + + /** + * 阿里云客户端 + */ + private volatile IAcsClient client; + + public AliyunSmsClient(SmsChannelProperties properties) { + super(properties, new AliyunSmsCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret()); + client = new DefaultAcsClient(profile); + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) { + // 构建参数 + SendSmsRequest request = new SendSmsRequest(); + request.setPhoneNumbers(mobile); + request.setSignName(properties.getSignature()); + request.setTemplateCode(apiTemplateId); + request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams))); + request.setOutId(String.valueOf(sendLogId)); + // 执行请求 + return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId())); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return statuses.stream().map(status -> { + SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); + resp.setSuccess(status.getSuccess()); + resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()); + resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()); + resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())); + return resp; + }).collect(Collectors.toList()); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + // 构建参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + request.setTemplateCode(apiTemplateId); + // 执行请求 + return invoke(request, response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); + return data; + }); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(Integer templateStatus) { + switch (templateStatus) { + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); + } + } + + @VisibleForTesting + SmsCommonResult invoke(AcsRequest request, Function responseConsumer) { + try { + // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射 + T sendResult = client.getAcsResponse(request); + String code = (String) ReflectUtil.getFieldValue(sendResult, "code"); + String message = (String) ReflectUtil.getFieldValue(sendResult, "message"); + String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId"); + // 解析结果 + R data = null; + if (Objects.equals(code, "OK")) { // 请求成功的情况下 + data = responseConsumer.apply(sendResult); + } + // 拼接结果 + return SmsCommonResult.build(code, message, requestId, data, codeMapping); + } catch (ClientException ex) { + return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping); + } + } + + private static String formatResultMsg(ClientException ex) { + if (StrUtil.isEmpty(ex.getErrorDescription())) { + return ex.getErrMsg(); + } + return ex.getErrMsg() + " => " + ex.getErrorDescription(); + } + + /** + * 短信接收状态 + * + * 参见 https://help.aliyun.com/document_detail/101867.html 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 手机号 + */ + @JsonProperty("phone_number") + private String phoneNumber; + /** + * 发送时间 + */ + @JsonProperty("send_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date sendTime; + /** + * 状态报告时间 + */ + @JsonProperty("report_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date reportTime; + /** + * 是否接收成功 + */ + private Boolean success; + /** + * 状态报告说明 + */ + @JsonProperty("err_msg") + private String errMsg; + /** + * 状态报告编码 + */ + @JsonProperty("err_code") + private String errCode; + /** + * 发送序列号 + */ + @JsonProperty("biz_id") + private String bizId; + /** + * 用户序列号 + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + @JsonProperty("out_id") + private String outId; + /** + * 短信长度,例如说 1、2、3 + * + * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送 + */ + @JsonProperty("sms_size") + private Integer smsSize; + + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java new file mode 100644 index 000000000..6319e257b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; + +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; + +/** + * 阿里云的 SmsCodeMapping 实现类 + * + * 参见 https://help.aliyun.com/document_detail/101346.htm 文档 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + switch (apiCode) { + case "OK": return GlobalErrorCodeConstants.SUCCESS; + case "isv.ACCOUNT_NOT_EXISTS": + case "isv.ACCOUNT_ABNORMAL": + case "MissingAccessKeyId": return SMS_ACCOUNT_INVALID; + case "isp.RAM_PERMISSION_DENY": return SMS_PERMISSION_DENY; + case "isv.INVALID_JSON_PARAM": + case "isv.INVALID_PARAMETERS": return SMS_API_PARAM_ERROR; + case "isv.BUSINESS_LIMIT_CONTROL": return SMS_SEND_BUSINESS_LIMIT_CONTROL; + case "isv.DAY_LIMIT_CONTROL": return SMS_SEND_DAY_LIMIT_CONTROL; + case "isv.SMS_CONTENT_ILLEGAL": return SMS_SEND_CONTENT_INVALID; + case "isv.SMS_TEMPLATE_ILLEGAL": return SMS_TEMPLATE_INVALID; + case "isv.SMS_SIGNATURE_ILLEGAL": + case "isv.SIGN_NAME_ILLEGAL": + case "isv.SMS_SIGN_ILLEGAL": return SMS_SIGN_INVALID; + case "isv.AMOUNT_NOT_ENOUGH": + case "isv.OUT_OF_SERVICE": return SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case "isv.MOBILE_NUMBER_ILLEGAL": return SMS_MOBILE_INVALID; + case "isv.TEMPLATE_MISSING_PARAMETERS": return SMS_TEMPLATE_PARAM_ERROR; + } + return SMS_UNKNOWN; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java new file mode 100644 index 000000000..a2aafb7c0 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java @@ -0,0 +1,23 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; + +import java.util.Objects; + +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + +/** + * 钉钉的 SmsCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class DebugDingTalkCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SMS_UNKNOWN; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java new file mode 100644 index 000000000..9215959b7 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java @@ -0,0 +1,96 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import cn.hutool.http.HttpUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.collection.MapUtils; +import cn.iocoder.dashboard.util.json.JsonUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 基于钉钉 WebHook 实现的调试的短信客户端实现类 + * + * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 + * + * @author 芋道源码 + */ +public class DebugDingTalkSmsClient extends AbstractSmsClient { + + public DebugDingTalkSmsClient(SmsChannelProperties properties) { + super(properties, new DebugDingTalkCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + String url = buildUrl("robot/send"); + Map params = new HashMap<>(); + params.put("msgtype", "text"); + String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", + mobile, sendLogId, MapUtils.convertMap(templateParams)); + params.put("text", MapUtil.builder().put("content", content).build()); + // 执行请求 + String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); + // 解析结果 + Map responseObj = JsonUtils.parseObject(responseText, Map.class); + return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"), + null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping); + } + + /** + * 构建请求地址 + * + * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档 + * + * @param path 请求路径 + * @return 请求地址 + */ + @SuppressWarnings("SameParameterValue") + private String buildUrl(String path) { + // 生成 timestamp + long timestamp = System.currentTimeMillis(); + // 生成 sign + String secret = properties.getApiSecret(); + String stringToSign = timestamp + "\n" + secret; + byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); + String sign = Base64.encode(signData); + // 构建最终 URL + return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", + path, properties.getApiKey(), timestamp, sign); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); + return SmsCommonResult.build("0", "success", null, data, codeMapping); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java new file mode 100644 index 000000000..cef38a548 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java @@ -0,0 +1,204 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.json.JsonUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.yunpian.sdk.YunpianClient; +import com.yunpian.sdk.constant.YunpianConstant; +import com.yunpian.sdk.model.Result; +import com.yunpian.sdk.model.Template; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 云片短信客户端的实现类 + * + * @author zzf + * @date 9:48 2021/3/5 + */ +@Slf4j +public class YunpianSmsClient extends AbstractSmsClient { + + /** + * 云信短信客户端 + */ + private volatile YunpianClient client; + + public YunpianSmsClient(SmsChannelProperties properties) { + super(properties, new YunpianSmsCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + } + + @Override + public void doInit() { + YunpianClient oldClient = client; + // 初始化新的客户端 + YunpianClient newClient = new YunpianClient(properties.getApiKey()); + newClient.init(); + this.client = newClient; + // 销毁老的客户端 + if (oldClient != null) { + oldClient.close(); + } + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + return invoke(() -> { + Map request = new HashMap<>(); + request.put(YunpianConstant.MOBILE, mobile); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams)); + request.put(YunpianConstant.UID, String.valueOf(sendLogId)); + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); + return client.sms().tpl_single_send(request); + }, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid()))); + } + + private static String formatTplValue(List> templateParams) { + if (CollUtil.isEmpty(templateParams)) { + return ""; + } + // 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化 + StringJoiner joiner = new StringJoiner("&"); + templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue()))))); + return joiner.toString(); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return statuses.stream().map(status -> { + SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); + resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS")); + resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail()); + resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime()); + resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid()); + return resp; + }).collect(Collectors.toList()); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable { + return invoke(() -> { + Map request = new HashMap<>(); + request.put(YunpianConstant.APIKEY, properties.getApiKey()); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + return client.tpl().get(request); + }, response -> { + Template template = response.get(0); + return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content()) + .setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason()); + }); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(String checkStatus) { + switch (checkStatus) { + case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus)); + } + } + + @VisibleForTesting + SmsCommonResult invoke(Supplier> requestConsumer, Function responseConsumer) throws Throwable { + // 执行请求 + Result result = requestConsumer.get(); + if (result.getThrowable() != null) { + throw result.getThrowable(); + } + // 解析结果 + R data = null; + if (result.getData() != null) { + data = responseConsumer.apply(result.getData()); + } + // 拼接结果 + return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping); + } + + private static String formatResultMsg(Result sendResult) { + if (StrUtil.isEmpty(sendResult.getDetail())) { + return sendResult.getMsg(); + } + return sendResult.getMsg() + " => " + sendResult.getDetail(); + } + + /** + * 短信接收状态 + * + * 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 接收状态 + * + * 目前仅有 SUCCESS / FAIL,所以使用 Boolean 接收 + */ + @JsonProperty("report_status") + private String reportStatus; + /** + * 接收手机号 + */ + private String mobile; + /** + * 运营商返回的代码,如:"DB:0103" + * + * 由于不同运营商信息不同,此字段仅供参考; + */ + @JsonProperty("error_msg") + private String errorMsg; + /** + * 运营商反馈代码的中文解释 + * + * 默认不推送此字段,如需推送,请联系客服 + */ + @JsonProperty("error_detail") + private String errorDetail; + /** + * 短信编号 + */ + private Long sid; + /** + * 用户自定义 id + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + private Long uid; + /** + * 用户接收时间 + */ + @JsonProperty("user_receive_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private Date userReceiveTime; + + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java new file mode 100644 index 000000000..ef980023d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java @@ -0,0 +1,45 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; +import static com.yunpian.sdk.constant.Code.*; + +/** + * 云片的 SmsCodeMapping 实现类 + * + * 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档 + * + * @author 芋道源码 + */ +public class YunpianSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + int code = Integer.parseInt(apiCode); + switch (code) { + case OK: return SUCCESS; + case ARGUMENT_MISSING: return SMS_API_PARAM_ERROR; + case BAD_ARGUMENT_FORMAT: return SMS_TEMPLATE_PARAM_ERROR; + case TPL_NOT_FOUND: + case TPL_NOT_VALID: return SMS_TEMPLATE_INVALID; + case MONEY_NOT_ENOUGH: return SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case BLACK_WORD: return SMS_SEND_CONTENT_INVALID; + case DUP_IN_SHORT_TIME: + case TOO_MANY_TIME_IN_5: + case DAY_LIMIT_PER_MOBILE: + case HOUR_LIMIT_PER_MOBILE: return SMS_SEND_BUSINESS_LIMIT_CONTROL; + case BLACK_PHONE_FILTER: return SMS_MOBILE_BLACK; + case SIGN_NOT_MATCH: + case BAD_SIGN_FORMAT: + case SIGN_NOT_VALID: return SMS_SIGN_INVALID; + case BAD_API_KEY: return SMS_ACCOUNT_INVALID; + case API_NOT_ALLOWED: return SMS_PERMISSION_DENY; + case IP_NOT_ALLOWED: return SMS_IP_DENY; + } + return SMS_UNKNOWN; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java new file mode 100644 index 000000000..ba2615d3d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.dashboard.framework.sms.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信渠道枚举 + * + * @author zzf + * @date 2021/1/25 10:56 + */ +@Getter +@AllArgsConstructor +public enum SmsChannelEnum { + + DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), + YUN_PIAN("YUN_PIAN", "云片"), + ALIYUN("ALIYUN", "阿里云"), +// TENCENT("TENCENT", "腾讯云"), +// HUA_WEI("HUA_WEI", "华为云"), + ; + + /** + * 编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static SmsChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java new file mode 100644 index 000000000..66653df56 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java @@ -0,0 +1,47 @@ +package cn.iocoder.dashboard.framework.sms.core.enums; + +import cn.iocoder.dashboard.common.exception.ErrorCode; + +/** + * 短信框架的错误码枚举 + * + * 短信框架,使用 2-001-000-000 段 + * + * @author 芋道源码 + */ +public interface SmsFrameworkErrorCodeConstants { + + ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析"); + + // ========== 权限 / 限流等相关 2001000100 ========== + + ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限"); + // 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信 + ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信"); + + // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 + ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流"); + // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 + ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流"); + + ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); + + // ========== 模板相关 2001000200 ========== + ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 + ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); + + // ========== 签名相关 2001000300 ========== + ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用"); + + // ========== 账户相关 2001000400 ========== + ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足"); + ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在"); + + // ========== 其它相关 2001000900 开头 ========== + ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); + ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); + ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); + + ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常"); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java new file mode 100644 index 000000000..888b2eeb5 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.framework.sms.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信模板的审核状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum SmsTemplateAuditStatusEnum { + + CHECKING(1), + SUCCESS(2), + FAIL(3); + + private final Integer status; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java b/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java new file mode 100644 index 000000000..750f2e7b4 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java @@ -0,0 +1,52 @@ +package cn.iocoder.dashboard.framework.sms.core.property; + +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信渠道配置类 + * + * @author zzf + * @date 2021/1/25 17:01 + */ +@Data +@Validated +public class SmsChannelProperties { + + /** + * 渠道编号 + */ + @NotNull(message = "短信渠道 ID 不能为空") + private Long id; + /** + * 短信签名 + */ + @NotEmpty(message = "短信签名不能为空") + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + @NotEmpty(message = "渠道编码不能为空") + private String code; + /** + * 短信 API 的账号 + */ + @NotEmpty(message = "短信 API 的账号不能为空") + private String apiKey; + /** + * 短信 API 的秘钥 + */ + @NotEmpty(message = "短信 API 的秘钥不能为空") + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java index 75704145c..faa01c641 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java +++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java @@ -3,7 +3,6 @@ package cn.iocoder.dashboard.framework.web.core.handler; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.extra.servlet.ServletUtil; -import cn.iocoder.dashboard.common.exception.GlobalException; import cn.iocoder.dashboard.common.exception.ServiceException; import cn.iocoder.dashboard.common.pojo.CommonResult; import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService; @@ -96,9 +95,6 @@ public class GlobalExceptionHandler { if (ex instanceof AccessDeniedException) { return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); } - if (ex instanceof GlobalException) { - return globalExceptionHandler(request, (GlobalException) ex); - } return defaultExceptionHandler(request, ex); } @@ -222,25 +218,6 @@ public class GlobalExceptionHandler { return CommonResult.error(ex.getCode(), ex.getMessage()); } - /** - * 处理全局异常 ServiceException - * - * 例如说,Dubbo 请求超时,调用的 Dubbo 服务系统异常 - */ - @ExceptionHandler(value = GlobalException.class) - public CommonResult globalExceptionHandler(HttpServletRequest req, GlobalException ex) { - // 系统异常时,才打印异常日志 - if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) { - // 插入异常日志 - this.createExceptionLog(req, ex); - // 普通全局异常,打印 info 日志即可 - } else { - log.info("[globalExceptionHandler]", ex); - } - // 返回 ERROR CommonResult - return CommonResult.error(ex); - } - /** * 处理系统异常,兜底处理所有的一切 */ @@ -250,7 +227,7 @@ public class GlobalExceptionHandler { // 插入异常日志 this.createExceptionLog(req, ex); // 返回 ERROR CommonResult - return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage()); + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } private void createExceptionLog(HttpServletRequest req, Throwable e) { diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictDataController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictDataController.java index cca874d7e..6eda1162d 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictDataController.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictDataController.java @@ -61,7 +61,7 @@ public class SysDictDataController { @GetMapping("/list-all-simple") @ApiOperation(value = "获得全部字典数据列表", notes = "一般用于管理后台缓存字典数据在本地") // 无需添加权限认证,因为前端全局都需要 - public CommonResult> getSimpleDictDatas() { + public CommonResult> getSimpleDictDatas() { List list = dictDataService.getDictDatas(); return success(SysDictDataConvert.INSTANCE.convertList(list)); } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictTypeController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictTypeController.java index cc53d7c5b..9e6ce7edb 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictTypeController.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictTypeController.java @@ -41,7 +41,7 @@ public class SysDictTypeController { return success(dictTypeId); } - @PostMapping("update") + @PutMapping("/update") @ApiOperation("修改字典类型") @PreAuthorize("@ss.hasPermission('system:dict:update')") public CommonResult updateDictType(@Valid @RequestBody SysDictTypeUpdateReqVO reqVO) { diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleRespVO.java similarity index 86% rename from src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleVO.java rename to src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleRespVO.java index afddd5fa9..a9e5aae8a 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleVO.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleRespVO.java @@ -4,9 +4,9 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; -@ApiModel("数据字典精简 VO") +@ApiModel("数据字典精简 Response VO") @Data -public class SysDictDataSimpleVO { +public class SysDictDataSimpleRespVO { @ApiModelProperty(value = "字典类型", required = true, example = "gender") private String dictType; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SmsCallbackController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SmsCallbackController.java new file mode 100644 index 000000000..30d75bca5 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SmsCallbackController.java @@ -0,0 +1,49 @@ +package cn.iocoder.dashboard.modules.system.controller.sms; + +import cn.hutool.core.util.URLUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +import static cn.iocoder.dashboard.common.pojo.CommonResult.success; + +@Api(tags = "短信回调") +@RestController +@RequestMapping("/system/sms/callback") +public class SmsCallbackController { + + @Resource + private SysSmsService smsService; + + @PostMapping("/sms/yunpian") + @ApiOperation(value = "云片短信的回调", notes = "参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档") + @ApiImplicitParam(name = "sms_status", value = "发送状态", required = true, example = "[{具体内容}]", dataTypeClass = Long.class) + @OperateLog(enable = false) + public String receiveYunpianSmsStatus(@RequestParam("sms_status") String smsStatus) throws Throwable { + String text = URLUtil.decode(smsStatus); // decode 解码参数,因为它被 encode + smsService.receiveSmsStatus(SmsChannelEnum.YUN_PIAN.getCode(), text); + return "SUCCESS"; // 约定返回 SUCCESS 为成功 + } + + @PostMapping("/sms/aliyun") + @ApiOperation(value = "阿里云短信的回调", notes = "参见 https://help.aliyun.com/document_detail/120998.html 文档") + @OperateLog(enable = false) + public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { + String text = ServletUtil.getBody(request); + smsService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); + return success(true); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsChannelController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsChannelController.java new file mode 100644 index 000000000..1e1a916c1 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsChannelController.java @@ -0,0 +1,80 @@ +package cn.iocoder.dashboard.modules.system.controller.sms; + +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.*; +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsChannelConvert; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.dashboard.common.pojo.CommonResult.success; + +@Api(tags = "短信渠道") +@RestController +@RequestMapping("system/sms-channel") +public class SysSmsChannelController { + + @Resource + private SysSmsChannelService smsChannelService; + + @PostMapping("/create") + @ApiOperation("创建短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:create')") + public CommonResult createSmsChannel(@Valid @RequestBody SysSmsChannelCreateReqVO createReqVO) { + return success(smsChannelService.createSmsChannel(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:update')") + public CommonResult updateSmsChannel(@Valid @RequestBody SysSmsChannelUpdateReqVO updateReqVO) { + smsChannelService.updateSmsChannel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除短信渠道") + @ApiImplicitParam(name = "id", value = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sms-channel:delete')") + public CommonResult deleteSmsChannel(@RequestParam("id") Long id) { + smsChannelService.deleteSmsChannel(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得短信渠道") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult getSmsChannel(@RequestParam("id") Long id) { + SysSmsChannelDO smsChannel = smsChannelService.getSmsChannel(id); + return success(SysSmsChannelConvert.INSTANCE.convert(smsChannel)); + } + + @GetMapping("/page") + @ApiOperation("获得短信渠道分页") + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult> getSmsChannelPage(@Valid SysSmsChannelPageReqVO pageVO) { + PageResult pageResult = smsChannelService.getSmsChannelPage(pageVO); + return success(SysSmsChannelConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @ApiOperation(value = "获得短信渠道精简列表", notes = "包含被禁用的短信渠道") + public CommonResult> getSimpleSmsChannels() { + List list = smsChannelService.getSmsChannelList(); + // 排序后,返回给前端 + list.sort(Comparator.comparing(SysSmsChannelDO::getId)); + return success(SysSmsChannelConvert.INSTANCE.convertList03(list)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsLogController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsLogController.java new file mode 100644 index 000000000..1d0a2fed0 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsLogController.java @@ -0,0 +1,60 @@ +package cn.iocoder.dashboard.modules.system.controller.sms; + +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils; +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExcelVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogRespVO; +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsLogConvert; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.dashboard.common.pojo.CommonResult.success; +import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api(tags = "短信日志") +@RestController +@RequestMapping("/system/sms-log") +@Validated +public class SysSmsLogController { + + @Resource + private SysSmsLogService smsLogService; + + @GetMapping("/page") + @ApiOperation("获得短信日志分页") + @PreAuthorize("@ss.hasPermission('system:sms-log:query')") + public CommonResult> getSmsLogPage(@Valid SysSmsLogPageReqVO pageVO) { + PageResult pageResult = smsLogService.getSmsLogPage(pageVO); + return success(SysSmsLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出短信日志 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-log:export')") + @OperateLog(type = EXPORT) + public void exportSmsLogExcel(@Valid SysSmsLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = smsLogService.getSmsLogList(exportReqVO); + // 导出 Excel + List datas = SysSmsLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "短信日志.xls", "数据", SysSmsLogExcelVO.class, datas); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.http b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.http new file mode 100644 index 000000000..d5441d057 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.http @@ -0,0 +1,12 @@ +### 请求 /menu/list 接口 => 成功 +POST {{baseUrl}}/system/sms-template/send-sms +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "code": "test_01", + "params": { + "key01": "value01", + "key02": "value02" + } +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.java new file mode 100644 index 000000000..1c442b71c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.java @@ -0,0 +1,98 @@ +package cn.iocoder.dashboard.modules.system.controller.sms; + +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils; +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.*; +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsTemplateConvert; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.dashboard.common.pojo.CommonResult.success; +import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Api("短信模板") +@RestController +@RequestMapping("/system/sms-template") +public class SysSmsTemplateController { + + @Resource + private SysSmsTemplateService smsTemplateService; + @Resource + private SysSmsService smsService; + + @PostMapping("/create") + @ApiOperation("创建短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:create')") + public CommonResult createSmsTemplate(@Valid @RequestBody SysSmsTemplateCreateReqVO createReqVO) { + return success(smsTemplateService.createSmsTemplate(createReqVO)); + } + + @PutMapping("/update") + @ApiOperation("更新短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:update')") + public CommonResult updateSmsTemplate(@Valid @RequestBody SysSmsTemplateUpdateReqVO updateReqVO) { + smsTemplateService.updateSmsTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @ApiOperation("删除短信模板") + @ApiImplicitParam(name = "id", value = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sms-template:delete')") + public CommonResult deleteSmsTemplate(@RequestParam("id") Long id) { + smsTemplateService.deleteSmsTemplate(id); + return success(true); + } + + @GetMapping("/get") + @ApiOperation("获得短信模板") + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult getSmsTemplate(@RequestParam("id") Long id) { + SysSmsTemplateDO smsTemplate = smsTemplateService.getSmsTemplate(id); + return success(SysSmsTemplateConvert.INSTANCE.convert(smsTemplate)); + } + + @GetMapping("/page") + @ApiOperation("获得短信模板分页") + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult> getSmsTemplatePage(@Valid SysSmsTemplatePageReqVO pageVO) { + PageResult pageResult = smsTemplateService.getSmsTemplatePage(pageVO); + return success(SysSmsTemplateConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @ApiOperation("导出短信模板 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-template:export')") + @OperateLog(type = EXPORT) + public void exportSmsTemplateExcel(@Valid SysSmsTemplateExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = smsTemplateService.getSmsTemplateList(exportReqVO); + // 导出 Excel + List datas = SysSmsTemplateConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "短信模板.xls", "数据", SysSmsTemplateExcelVO.class, datas); + } + + @PostMapping("/send-sms") + @ApiOperation("发送短信") + @PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')") + public CommonResult sendSms(@Valid @RequestBody SysSmsTemplateSendReqVO sendReqVO) { + return success(smsService.sendSingleSms(sendReqVO.getMobile(), null, null, + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelBaseVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelBaseVO.java new file mode 100644 index 000000000..9959b8af0 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelBaseVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotNull; + +/** +* 短信渠道 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class SysSmsChannelBaseVO { + + @ApiModelProperty(value = "短信签名", required = true, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @ApiModelProperty(value = "启用状态", required = true, example = "1") + @NotNull(message = "启用状态不能为空") + private Integer status; + + @ApiModelProperty(value = "备注", example = "好吃!") + private String remark; + + @ApiModelProperty(value = "短信 API 的账号", required = true, example = "yudao") + @NotNull(message = "短信 API 的账号不能为空") + private String apiKey; + + @ApiModelProperty(value = "短信 API 的秘钥", example = "yuanma") + private String apiSecret; + + @ApiModelProperty(value = "短信发送回调 URL", example = "http://www.iocoder.cn") + @URL(message = "回调 URL 格式不正确") + private String callbackUrl; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelCreateReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelCreateReqVO.java new file mode 100644 index 000000000..a21cbb71d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelCreateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("短信渠道创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsChannelCreateReqVO extends SysSmsChannelBaseVO { + + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类") + @NotNull(message = "渠道编码不能为空") + private String code; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelPageReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelPageReqVO.java new file mode 100644 index 000000000..523a6c375 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelPageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel; + +import cn.iocoder.dashboard.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("短信渠道分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsChannelPageReqVO extends PageParam { + + @ApiModelProperty(value = "任务状态", example = "1") + private Integer status; + + @ApiModelProperty(value = "短信签名", example = "芋道源码", notes = "模糊匹配") + private String signature; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelRespVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelRespVO.java new file mode 100644 index 000000000..20770689a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelRespVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@ApiModel("短信渠道 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsChannelRespVO extends SysSmsChannelBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类") + private String code; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelSimpleRespVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelSimpleRespVO.java new file mode 100644 index 000000000..b7d84165c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelSimpleRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@ApiModel("短信渠道精简 Response VO") +@Data +public class SysSmsChannelSimpleRespVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @ApiModelProperty(value = "短信签名", required = true, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类") + private String code; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelUpdateReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelUpdateReqVO.java new file mode 100644 index 000000000..66ab79412 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("短信渠道更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsChannelUpdateReqVO extends SysSmsChannelBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExcelVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExcelVO.java new file mode 100644 index 000000000..6a385feba --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExcelVO.java @@ -0,0 +1,101 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log; + +import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat; +import cn.iocoder.dashboard.framework.excel.core.convert.DictConvert; +import cn.iocoder.dashboard.framework.excel.core.convert.JsonConvert; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +import static cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum.*; + +/** + * 短信日志 Excel VO + * + * @author 芋道源码 + */ +@Data +public class SysSmsLogExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("短信渠道编号") + private Long channelId; + + @ExcelProperty("短信渠道编码") + private String channelCode; + + @ExcelProperty("模板编号") + private Long templateId; + + @ExcelProperty("模板编码") + private String templateCode; + + @ExcelProperty(value = "短信类型", converter = DictConvert.class) + @DictFormat(SYS_SMS_TEMPLATE_TYPE) + private Integer templateType; + + @ExcelProperty("短信内容") + private String templateContent; + + @ExcelProperty(value = "短信参数", converter = JsonConvert.class) + private Map templateParams; + + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @ExcelProperty("手机号") + private String mobile; + + @ExcelProperty("用户编号") + private Long userId; + + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(USER_TYPE) + private Integer userType; + + @ExcelProperty(value = "发送状态", converter = DictConvert.class) + @DictFormat(SYS_SMS_SEND_STATUS) + private Integer sendStatus; + + @ExcelProperty("发送时间") + private Date sendTime; + + @ExcelProperty("发送结果的编码") + private Integer sendCode; + + @ExcelProperty("发送结果的提示") + private String sendMsg; + + @ExcelProperty("短信 API 发送结果的编码") + private String apiSendCode; + + @ExcelProperty("短信 API 发送失败的提示") + private String apiSendMsg; + + @ExcelProperty("短信 API 发送返回的唯一请求 ID") + private String apiRequestId; + + @ExcelProperty("短信 API 发送返回的序号") + private String apiSerialNo; + + @ExcelProperty(value = "接收状态", converter = DictConvert.class) + @DictFormat(SYS_SMS_RECEIVE_STATUS) + private Integer receiveStatus; + + @ExcelProperty("接收时间") + private Date receiveTime; + + @ExcelProperty("API 接收结果的编码") + private String apiReceiveCode; + + @ExcelProperty("API 接收结果的说明") + private String apiReceiveMsg; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExportReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExportReqVO.java new file mode 100644 index 000000000..89add180a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExportReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "短信日志 Excel 导出 Request VO", description = "参数和 SysSmsLogPageReqVO 是一致的") +@Data +public class SysSmsLogExportReqVO { + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @ApiModelProperty(value = "模板编号", example = "20") + private Long templateId; + + @ApiModelProperty(value = "手机号", example = "15601691300") + private String mobile; + + @ApiModelProperty(value = "发送状态", example = "1") + private Integer sendStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始发送时间") + private Date beginSendTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束发送时间") + private Date endSendTime; + + @ApiModelProperty(value = "接收状态", example = "0") + private Integer receiveStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始接收时间") + private Date beginReceiveTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束接收时间") + private Date endReceiveTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogPageReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogPageReqVO.java new file mode 100644 index 000000000..6573e15a7 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogPageReqVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log; + +import cn.iocoder.dashboard.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("短信日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsLogPageReqVO extends PageParam { + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @ApiModelProperty(value = "模板编号", example = "20") + private Long templateId; + + @ApiModelProperty(value = "手机号", example = "15601691300") + private String mobile; + + @ApiModelProperty(value = "发送状态", example = "1", notes = "参见 SysSmsSendStatusEnum 枚举类") + private Integer sendStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始发送时间") + private Date beginSendTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束发送时间") + private Date endSendTime; + + @ApiModelProperty(value = "接收状态", example = "0", notes = "参见 SysSmsReceiveStatusEnum 枚举类") + private Integer receiveStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始接收时间") + private Date beginReceiveTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束接收时间") + private Date endReceiveTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogRespVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogRespVO.java new file mode 100644 index 000000000..423a57919 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogRespVO.java @@ -0,0 +1,89 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +@ApiModel("短信日志 Response VO") +@Data +public class SysSmsLogRespVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "短信渠道编号", required = true, example = "10") + private Long channelId; + + @ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN") + private String channelCode; + + @ApiModelProperty(value = "模板编号", required = true, example = "20") + private Long templateId; + + @ApiModelProperty(value = "模板编码", required = true, example = "test-01") + private String templateCode; + + @ApiModelProperty(value = "短信类型", required = true, example = "1") + private Integer templateType; + + @ApiModelProperty(value = "短信内容", required = true, example = "你好,你的验证码是 1024") + private String templateContent; + + @ApiModelProperty(value = "短信参数", required = true, example = "name,code") + private Map templateParams; + + @ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "SMS_207945135") + private String apiTemplateId; + + @ApiModelProperty(value = "手机号", required = true, example = "15601691300") + private String mobile; + + @ApiModelProperty(value = "用户编号", example = "10") + private Long userId; + + @ApiModelProperty(value = "用户类型", example = "1") + private Integer userType; + + @ApiModelProperty(value = "发送状态", required = true, example = "1") + private Integer sendStatus; + + @ApiModelProperty(value = "发送时间") + private Date sendTime; + + @ApiModelProperty(value = "发送结果的编码", example = "0") + private Integer sendCode; + + @ApiModelProperty(value = "发送结果的提示", example = "成功") + private String sendMsg; + + @ApiModelProperty(value = "短信 API 发送结果的编码", example = "SUCCESS") + private String apiSendCode; + + @ApiModelProperty(value = "短信 API 发送失败的提示", example = "成功") + private String apiSendMsg; + + @ApiModelProperty(value = "短信 API 发送返回的唯一请求 ID", example = "3837C6D3-B96F-428C-BBB2-86135D4B5B99") + private String apiRequestId; + + @ApiModelProperty(value = "短信 API 发送返回的序号", example = "62923244790") + private String apiSerialNo; + + @ApiModelProperty(value = "接收状态", required = true, example = "0") + private Integer receiveStatus; + + @ApiModelProperty(value = "接收时间") + private Date receiveTime; + + @ApiModelProperty(value = "API 接收结果的编码", example = "DELIVRD") + private String apiReceiveCode; + + @ApiModelProperty(value = "API 接收结果的说明", example = "用户接收成功") + private String apiReceiveMsg; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateBaseVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateBaseVO.java new file mode 100644 index 000000000..584050d55 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateBaseVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 短信模板 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class SysSmsTemplateBaseVO { + + @ApiModelProperty(value = "短信类型", required = true, example = "1", notes = "参见 SysSmsTemplateTypeEnum 枚举类") + @NotNull(message = "短信类型不能为空") + private Integer type; + + @ApiModelProperty(value = "开启状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类") + @NotNull(message = "开启状态不能为空") + private Integer status; + + @ApiModelProperty(value = "模板编码", required = true, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String code; + + @ApiModelProperty(value = "模板名称", required = true, example = "yudao") + @NotNull(message = "模板名称不能为空") + private String name; + + @ApiModelProperty(value = "模板内容", required = true, example = "你好,{name}。你长的太{like}啦!") + @NotNull(message = "模板内容不能为空") + private String content; + + @ApiModelProperty(value = "备注", example = "哈哈哈") + private String remark; + + @ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "4383920") + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; + + @ApiModelProperty(value = "短信渠道编号", required = true, example = "10") + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateCreateReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateCreateReqVO.java new file mode 100644 index 000000000..8f847556d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@ApiModel("短信模板创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsTemplateCreateReqVO extends SysSmsTemplateBaseVO { + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExcelVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExcelVO.java new file mode 100644 index 000000000..3eef8133b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExcelVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat; +import cn.iocoder.dashboard.framework.excel.core.convert.DictConvert; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +import static cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum.*; + +/** + * 短信模板 Excel VO + * + * @author 芋道源码 + */ +@Data +public class SysSmsTemplateExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty(value = "短信签名", converter = DictConvert.class) + @DictFormat(SYS_SMS_TEMPLATE_TYPE) + private Integer type; + + @ExcelProperty(value = "开启状态", converter = DictConvert.class) + @DictFormat(SYS_COMMON_STATUS) + private Integer status; + + @ExcelProperty("模板编码") + private String code; + + @ExcelProperty("模板名称") + private String name; + + @ExcelProperty("模板内容") + private String content; + + @ExcelProperty("备注") + private String remark; + + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @ExcelProperty("短信渠道编号") + private Long channelId; + + @ExcelProperty(value = "短信渠道编码", converter = DictConvert.class) + @DictFormat(SYS_SMS_CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExportReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExportReqVO.java new file mode 100644 index 000000000..34f940253 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExportReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel(value = "短信模板 Excel 导出 Request VO", description = "参数和 SysSmsTemplatePageReqVO 是一致的") +@Data +public class SysSmsTemplateExportReqVO { + + @ApiModelProperty(value = "短信签名", example = "1") + private Integer type; + + @ApiModelProperty(value = "开启状态", example = "1") + private Integer status; + + @ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配") + private String content; + + @ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配") + private String apiTemplateId; + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplatePageReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplatePageReqVO.java new file mode 100644 index 000000000..b5f1e5bfb --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplatePageReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import cn.iocoder.dashboard.common.pojo.PageParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("短信模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsTemplatePageReqVO extends PageParam { + + @ApiModelProperty(value = "短信签名", example = "1") + private Integer type; + + @ApiModelProperty(value = "开启状态", example = "1") + private Integer status; + + @ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配") + private String code; + + @ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配") + private String content; + + @ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配") + private String apiTemplateId; + + @ApiModelProperty(value = "短信渠道编号", example = "10") + private Long channelId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "开始创建时间") + private Date beginCreateTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @ApiModelProperty(value = "结束创建时间") + private Date endCreateTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateRespVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateRespVO.java new file mode 100644 index 000000000..fda58486f --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; +import java.util.List; + +@ApiModel("短信模板 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsTemplateRespVO extends SysSmsTemplateBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + private Long id; + + @ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN") + private String channelCode; + + @ApiModelProperty(value = "参数数组", example = "name,code") + private List params; + + @ApiModelProperty(value = "创建时间", required = true) + private Date createTime; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateSendReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateSendReqVO.java new file mode 100644 index 000000000..2857ee5d0 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateSendReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@ApiModel("短信模板的发送 Request VO") +@Data +public class SysSmsTemplateSendReqVO { + + @ApiModelProperty(value = "手机号", required = true, example = "15601691300") + @NotNull(message = "手机号不能为空") + private String mobile; + + @ApiModelProperty(value = "模板编码", required = true, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @ApiModelProperty(value = "模板参数") + private Map templateParams; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateUpdateReqVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateUpdateReqVO.java new file mode 100644 index 000000000..9b3aba840 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateUpdateReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@ApiModel("短信模板更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsTemplateUpdateReqVO extends SysSmsTemplateBaseVO { + + @ApiModelProperty(value = "编号", required = true, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/convert/dict/SysDictDataConvert.java b/src/main/java/cn/iocoder/dashboard/modules/system/convert/dict/SysDictDataConvert.java index 9c73a8c11..af8fda25e 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/convert/dict/SysDictDataConvert.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/convert/dict/SysDictDataConvert.java @@ -13,7 +13,7 @@ public interface SysDictDataConvert { SysDictDataConvert INSTANCE = Mappers.getMapper(SysDictDataConvert.class); - List convertList(List list); + List convertList(List list); SysDictDataRespVO convert(SysDictDataDO bean); diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsChannelConvert.java b/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsChannelConvert.java new file mode 100644 index 000000000..f8a0e71e2 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsChannelConvert.java @@ -0,0 +1,39 @@ +package cn.iocoder.dashboard.modules.system.convert.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelRespVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelSimpleRespVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 短信渠道 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SysSmsChannelConvert { + + SysSmsChannelConvert INSTANCE = Mappers.getMapper(SysSmsChannelConvert.class); + + SysSmsChannelDO convert(SysSmsChannelCreateReqVO bean); + + SysSmsChannelDO convert(SysSmsChannelUpdateReqVO bean); + + SysSmsChannelRespVO convert(SysSmsChannelDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + List convertList03(List list); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsLogConvert.java b/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsLogConvert.java new file mode 100644 index 000000000..6cb1f650a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsLogConvert.java @@ -0,0 +1,30 @@ +package cn.iocoder.dashboard.modules.system.convert.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExcelVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogRespVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 短信日志 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SysSmsLogConvert { + + SysSmsLogConvert INSTANCE = Mappers.getMapper(SysSmsLogConvert.class); + + SysSmsLogRespVO convert(SysSmsLogDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsTemplateConvert.java b/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsTemplateConvert.java new file mode 100644 index 000000000..5d73771eb --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsTemplateConvert.java @@ -0,0 +1,31 @@ +package cn.iocoder.dashboard.modules.system.convert.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExcelVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateRespVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface SysSmsTemplateConvert { + + SysSmsTemplateConvert INSTANCE = Mappers.getMapper(SysSmsTemplateConvert.class); + + SysSmsTemplateDO convert(SysSmsTemplateCreateReqVO bean); + + SysSmsTemplateDO convert(SysSmsTemplateUpdateReqVO bean); + + SysSmsTemplateRespVO convert(SysSmsTemplateDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsChannelDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsChannelDO.java new file mode 100644 index 000000000..7b0b3f072 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsChannelDO.java @@ -0,0 +1,60 @@ +package cn.iocoder.dashboard.modules.system.dal.dataobject.sms; + +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信渠道 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "sys_sms_channel", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsChannelDO extends BaseDO { + + /** + * 渠道编号 + */ + private Long id; + /** + * 短信签名 + */ + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + private String code; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的账号 + */ + private String apiKey; + /** + * 短信 API 的秘钥 + */ + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsLogDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsLogDO.java new file mode 100644 index 000000000..076e18f39 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsLogDO.java @@ -0,0 +1,173 @@ +package cn.iocoder.dashboard.modules.system.dal.dataobject.sms; + +import cn.iocoder.dashboard.common.enums.UserTypeEnum; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.Date; +import java.util.Map; + +/** + * 短信日志 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "sys_sms_log", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SysSmsLogDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SysSmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SysSmsChannelDO#getCode()} + */ + private String channelCode; + + // ========= 模板相关字段 ========= + + /** + * 模板编号 + * + * 关联 {@link SysSmsTemplateDO#getId()} + */ + private Long templateId; + /** + * 模板编码 + * + * 冗余 {@link SysSmsTemplateDO#getCode()} + */ + private String templateCode; + /** + * 短信类型 + * + * 冗余 {@link SysSmsTemplateDO#getType()} + */ + private Integer templateType; + /** + * 基于 {@link SysSmsTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 基于 {@link SysSmsTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + /** + * 短信 API 的模板编号 + * + * 冗余 {@link SysSmsTemplateDO#getApiTemplateId()} + */ + private String apiTemplateId; + + // ========= 手机相关字段 ========= + + /** + * 手机号 + */ + private String mobile; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + // ========= 发送相关字段 ========= + + /** + * 发送状态 + * + * 枚举 {@link SysSmsSendStatusEnum} + */ + private Integer sendStatus; + /** + * 发送时间 + */ + private Date sendTime; + /** + * 发送结果的编码 + * + * 枚举 {@link SmsFrameworkErrorCodeConstants} + */ + private Integer sendCode; + /** + * 发送结果的提示 + * + * 一般情况下,使用 {@link SmsFrameworkErrorCodeConstants} + * 异常情况下,通过格式化 Exception 的提示存储 + */ + private String sendMsg; + /** + * 短信 API 发送结果的编码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiSendCode; + /** + * 短信 API 发送失败的提示 + */ + private String apiSendMsg; + /** + * 短信 API 发送返回的唯一请求 ID + * + * 用于和短信 API 进行定位于排错 + */ + private String apiRequestId; + /** + * 短信 API 发送返回的序号 + * + * 用于和短信 API 平台的发送记录关联 + */ + private String apiSerialNo; + + // ========= 接收相关字段 ========= + + /** + * 接收状态 + * + * 枚举 {@link SysSmsReceiveStatusEnum} + */ + private Integer receiveStatus; + /** + * 接收时间 + */ + private Date receiveTime; + /** + * 短信 API 接收结果的编码 + */ + private String apiReceiveCode; + /** + * 短信 API 接收结果的提示 + */ + private String apiReceiveMsg; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsTemplateDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsTemplateDO.java new file mode 100644 index 000000000..9316358df --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsTemplateDO.java @@ -0,0 +1,89 @@ +package cn.iocoder.dashboard.modules.system.dal.dataobject.sms; + +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsTemplateTypeEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * 短信模板 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "sys_sms_template", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SysSmsTemplateDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 模板相关字段 ========= + + /** + * 短信类型 + * + * 枚举 {@link SysSmsTemplateTypeEnum} + */ + private Integer type; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 模板编码,保证唯一 + */ + private String code; + /** + * 模板名称 + */ + private String name; + /** + * 模板内容 + * + * 内容的参数,使用 {} 包括,例如说 {name} + */ + private String content; + /** + * 参数数组(自动根据内容生成) + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的模板编号 + */ + private String apiTemplateId; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SysSmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SysSmsChannelDO#getCode()} + */ + private String channelCode; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsChannelMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsChannelMapper.java new file mode 100644 index 000000000..69e329b90 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsChannelMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.modules.system.dal.mysql.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface SysSmsChannelMapper extends BaseMapperX { + + default PageResult selectPage(SysSmsChannelPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .likeIfPresent("signature", reqVO.getSignature()) + .eqIfPresent("status", reqVO.getStatus()) + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc("id")); + } + + @Select("SELECT id FROM sys_sms_channel WHERE update_time > #{maxUpdateTime} LIMIT 1") + Long selectExistsByUpdateTimeAfter(Date maxUpdateTime); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsLogMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsLogMapper.java new file mode 100644 index 000000000..e3345c835 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsLogMapper.java @@ -0,0 +1,40 @@ +package cn.iocoder.dashboard.modules.system.dal.mysql.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SysSmsLogMapper extends BaseMapperX { + + default PageResult selectPage(SysSmsLogPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .eqIfPresent("channel_id", reqVO.getChannelId()) + .eqIfPresent("template_id", reqVO.getTemplateId()) + .likeIfPresent("mobile", reqVO.getMobile()) + .eqIfPresent("send_status", reqVO.getSendStatus()) + .betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime()) + .eqIfPresent("receive_status", reqVO.getReceiveStatus()) + .betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime()) + .orderByDesc("id")); + } + + default List selectList(SysSmsLogExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .eqIfPresent("channel_id", reqVO.getChannelId()) + .eqIfPresent("template_id", reqVO.getTemplateId()) + .likeIfPresent("mobile", reqVO.getMobile()) + .eqIfPresent("send_status", reqVO.getSendStatus()) + .betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime()) + .eqIfPresent("receive_status", reqVO.getReceiveStatus()) + .betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime()) + .orderByDesc("id")); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsTemplateMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsTemplateMapper.java new file mode 100644 index 000000000..a41e38b2d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsTemplateMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.dashboard.modules.system.dal.mysql.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; + +@Mapper +public interface SysSmsTemplateMapper extends BaseMapperX { + + default SysSmsTemplateDO selectByCode(String code) { + return selectOne("code", code); + } + + default PageResult selectPage(SysSmsTemplatePageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .eqIfPresent("type", reqVO.getType()) + .eqIfPresent("status", reqVO.getStatus()) + .likeIfPresent("code", reqVO.getCode()) + .likeIfPresent("content", reqVO.getContent()) + .likeIfPresent("api_template_id", reqVO.getApiTemplateId()) + .eqIfPresent("channel_id", reqVO.getChannelId()) + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc("id")); + } + + default List selectList(SysSmsTemplateExportReqVO reqVO) { + return selectList(new QueryWrapperX() + .eqIfPresent("type", reqVO.getType()) + .eqIfPresent("status", reqVO.getStatus()) + .likeIfPresent("code", reqVO.getCode()) + .likeIfPresent("content", reqVO.getContent()) + .likeIfPresent("api_template_id", reqVO.getApiTemplateId()) + .eqIfPresent("channel_id", reqVO.getChannelId()) + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) + .orderByDesc("id")); + } + + default Integer selectCountByChannelId(Long channelId) { + return selectCount("channel_id", channelId); + } + + @Select("SELECT id FROM sys_sms_template WHERE update_time > #{maxUpdateTime} LIMIT 1") + Long selectExistsByUpdateTimeAfter(Date maxUpdateTime); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/redis/RedisKeyConstants.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/redis/RedisKeyConstants.java new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java index 0197b6c05..19e08e8a8 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java @@ -30,8 +30,8 @@ public interface SysErrorCodeConstants { // ========== 角色模块 1002003000 ========== ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1002003000, "角色不存在"); - ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1002003001, "已经存在名为【{}}】的角色"); - ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1002003002, "已经存在编码为【{}}】的角色"); + ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1002003001, "已经存在名为【{}】的角色"); + ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1002003002, "已经存在编码为【{}】的角色"); ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1002003004, "不能操作类型为系统内置的角色"); // ========== 用户模块 1002004000 ========== @@ -78,4 +78,18 @@ public interface SysErrorCodeConstants { ErrorCode FILE_UPLOAD_FAILED = new ErrorCode(1002009002, "文件上传失败"); ErrorCode FILE_IS_EMPTY= new ErrorCode(1002009003, "文件为空"); + // ========== 短信渠道 1002011000 ========== + ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1002011000, "短信渠道不存在"); + ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1002011001, "短信渠道不处于开启状态,不允许选择"); + ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1002011002, "无法删除,该短信渠道还有短信模板"); + + // ========== 短信模板 1002011000 ========== + ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1002011000, "短信模板不存在"); + ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1002011001, "已经存在编码为【{}】的短信模板"); + + // ========== 短信发送 1002012000 ========== + ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1002012000, "手机号不存在"); + ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1002012001, "模板参数({})缺失"); + + } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/enums/dict/SysDictTypeEnum.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/dict/SysDictTypeEnum.java index c31b3ae5f..bb291ac24 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/enums/dict/SysDictTypeEnum.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/enums/dict/SysDictTypeEnum.java @@ -18,6 +18,10 @@ public enum SysDictTypeEnum { SYS_LOGIN_RESULT("sys_login_result"), // 登陆结果 SYS_CONFIG_TYPE("sys_config_type"), // 参数配置类型 SYS_BOOLEAN_STRING("sys_boolean_string"), // Boolean 是否类型 + SYS_SMS_CHANNEL_CODE("sys_sms_channel_code"), // 短信渠道编码 + SYS_SMS_TEMPLATE_TYPE("sys_sms_template_type"), // 短信模板类型 + SYS_SMS_SEND_STATUS("sys_sms_send_status"), // 短信发送状态 + SYS_SMS_RECEIVE_STATUS("sys_sms_receive_status"), // 短信接收状态 INF_REDIS_TIMEOUT_TYPE("inf_redis_timeout_type"), // Redis 超时类型 INF_JOB_STATUS("inf_job_status"), // 定时任务状态的枚举 diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsReceiveStatusEnum.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsReceiveStatusEnum.java new file mode 100644 index 000000000..880238822 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsReceiveStatusEnum.java @@ -0,0 +1,23 @@ +package cn.iocoder.dashboard.modules.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的接收状态枚举 + * + * @author 芋道源码 + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SysSmsReceiveStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 接收成功 + FAILURE(20), // 接收失败 + ; + + private final int status; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsSendStatusEnum.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsSendStatusEnum.java new file mode 100644 index 000000000..1d505ee02 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsSendStatusEnum.java @@ -0,0 +1,24 @@ +package cn.iocoder.dashboard.modules.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的发送状态枚举 + * + * @author zzf + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SysSmsSendStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 发送成功 + FAILURE(20), // 发送失败 + IGNORE(30), // 忽略,即不发送 + ; + + private final int status; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsTemplateTypeEnum.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsTemplateTypeEnum.java new file mode 100644 index 000000000..8ff9c49b7 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsTemplateTypeEnum.java @@ -0,0 +1,25 @@ +package cn.iocoder.dashboard.modules.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的模板类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SysSmsTemplateTypeEnum { + + VERIFICATION_CODE(1), // 验证码 + NOTICE(2), // 通知 + PROMOTION(3), // 营销 + ; + + /** + * 类型 + */ + private final int type; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsChannelRefreshConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsChannelRefreshConsumer.java new file mode 100644 index 000000000..6105889cb --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsChannelRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.modules.system.mq.consumer.sms; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SysSmsChannelRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SysSmsChannelRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SysSmsChannelService smsChannelService; + + @Override + public void onMessage(SysSmsChannelRefreshMessage message) { + log.info("[onMessage][收到 SmsChannel 刷新消息]"); + smsChannelService.initSmsClients(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsSendConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsSendConsumer.java index e3b18ca75..70b167168 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsSendConsumer.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsSendConsumer.java @@ -2,16 +2,29 @@ package cn.iocoder.dashboard.modules.system.mq.consumer.sms; import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener; import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import javax.annotation.Resource; + +/** + * 针对 {@link SysSmsSendMessage} 的消费者 + * + * @author zzf + * @date 2021/3/9 16:35 + */ @Component @Slf4j public class SysSmsSendConsumer extends AbstractStreamMessageListener { + @Resource + private SysSmsService smsService; + @Override public void onMessage(SysSmsSendMessage message) { log.info("[onMessage][消息内容({})]", message); + smsService.doSendSms(message); } } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsTemplateRefreshConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsTemplateRefreshConsumer.java new file mode 100644 index 000000000..c310c48fa --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsTemplateRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.modules.system.mq.consumer.sms; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SysSmsTemplateRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SysSmsTemplateRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SysSmsTemplateService smsTemplateService; + + @Override + public void onMessage(SysSmsTemplateRefreshMessage message) { + log.info("[onMessage][收到 SmsTemplate 刷新消息]"); + smsTemplateService.initLocalCache(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsChannelRefreshMessage.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsChannelRefreshMessage.java new file mode 100644 index 000000000..fa2878720 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsChannelRefreshMessage.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.modules.system.mq.message.sms; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; +import lombok.Data; + +/** + * 短信渠道的数据刷新 Message + */ +@Data +public class SysSmsChannelRefreshMessage implements ChannelMessage { + + @Override + public String getChannel() { + return "system.sms-channel.refresh"; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsSendMessage.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsSendMessage.java index f47b52466..9bb30514a 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsSendMessage.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsSendMessage.java @@ -1,10 +1,11 @@ package cn.iocoder.dashboard.modules.system.mq.message.sms; +import cn.iocoder.dashboard.common.core.KeyValue; import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage; import lombok.Data; import javax.validation.constraints.NotNull; -import java.util.Map; +import java.util.List; /** * 短信发送消息 @@ -14,29 +15,30 @@ import java.util.Map; @Data public class SysSmsSendMessage implements StreamMessage { + /** + * 短信日志编号 + */ + @NotNull(message = "短信日志编号不能为空") + private Long logId; /** * 手机号 */ @NotNull(message = "手机号不能为空") private String mobile; /** - * 短信模板编号 + * 短信渠道编号 */ - @NotNull(message = "短信模板编号不能为空") - private String templateCode; + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + /** + * 短信 API 的模板编号 + */ + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; /** * 短信模板参数 */ - private Map templateParams; - - /** - * 用户编号,允许空 - */ - private Integer userId; - /** - * 用户类型,允许空 - */ - private Integer userType; + private List> templateParams; @Override public String getStreamKey() { diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsTemplateRefreshMessage.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsTemplateRefreshMessage.java new file mode 100644 index 000000000..4925b092a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsTemplateRefreshMessage.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.modules.system.mq.message.sms; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; +import lombok.Data; + +/** + * 短信模板的数据刷新 Message + */ +@Data +public class SysSmsTemplateRefreshMessage implements ChannelMessage { + + @Override + public String getChannel() { + return "system.sms-template.refresh"; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java index e11945dfe..b398a27a1 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java @@ -9,6 +9,8 @@ import javax.annotation.Resource; /** * Role 角色相关消息的 Producer + * + * @author 芋道源码 */ @Component public class SysRoleProducer { diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/sms/SysSmsProducer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/sms/SysSmsProducer.java new file mode 100644 index 000000000..d346ef02e --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/sms/SysSmsProducer.java @@ -0,0 +1,60 @@ +package cn.iocoder.dashboard.modules.system.mq.producer.sms; + +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils; +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage; +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * Sms 短信相关消息的 Producer + * + * @author zzf + * @date 2021/3/9 16:35 + */ +@Slf4j +@Component +public class SysSmsProducer { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 发送 {@link SysSmsSendMessage} 消息 + * + * @param logId 短信日志编号 + * @param mobile 手机号 + * @param channelId 渠道编号 + * @param apiTemplateId 短信模板编号 + * @param templateParams 短信模板参数 + */ + public void sendSmsSendMessage(Long logId, String mobile, + Long channelId, String apiTemplateId, List> templateParams) { + SysSmsSendMessage message = new SysSmsSendMessage().setLogId(logId).setMobile(mobile); + message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams); + RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message); + } + + /** + * 发送 {@link SysSmsChannelRefreshMessage} 消息 + */ + public void sendSmsChannelRefreshMessage() { + SysSmsChannelRefreshMessage message = new SysSmsChannelRefreshMessage(); + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); + } + + /** + * 发送 {@link SysSmsTemplateRefreshMessage} 消息 + */ + public void sendSmsTemplateRefreshMessage() { + SysSmsTemplateRefreshMessage message = new SysSmsTemplateRefreshMessage(); + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java index 5390d3294..6bb79c446 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java @@ -18,7 +18,7 @@ import java.util.Set; public interface SysPermissionService extends SecurityPermissionFrameworkService { /** - * 初始化 + * 初始化权限的本地缓存 */ void initLocalCache(); diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java index 9ed27d085..ec4ea5a9b 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java @@ -15,6 +15,7 @@ import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysMenuProduce import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService; import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService; import cn.iocoder.dashboard.util.collection.CollectionUtils; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; @@ -168,10 +169,6 @@ public class SysMenuServiceImpl implements SysMenuService { */ @Transactional(rollbackFor = Exception.class) public void deleteMenu(Long menuId) { - // 校验更新的菜单是否存在 - if (menuMapper.selectById(menuId) == null) { - throw ServiceExceptionUtil.exception(MENU_NOT_EXISTS); - } // 校验是否还有子菜单 if (menuMapper.selectCountByParentId(menuId) > 0) { throw ServiceExceptionUtil.exception(MENU_EXISTS_CHILDREN); @@ -250,7 +247,8 @@ public class SysMenuServiceImpl implements SysMenuService { * @param parentId 父菜单编号 * @param childId 当前菜单编号 */ - private void checkParentResource(Long parentId, Long childId) { + @VisibleForTesting + public void checkParentResource(Long parentId, Long childId) { if (parentId == null || MenuIdEnum.ROOT.getId().equals(parentId)) { return; } @@ -279,7 +277,8 @@ public class SysMenuServiceImpl implements SysMenuService { * @param parentId 父菜单编号 * @param id 菜单编号 */ - private void checkResource(Long parentId, String name, Long id) { + @VisibleForTesting + public void checkResource(Long parentId, String name, Long id) { SysMenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); if (menu == null) { return; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java index ba2a096b6..9116d3e55 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java @@ -3,7 +3,6 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMenuMapper; import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysUserRoleMapper; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java index e79813636..89e51ad5e 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java @@ -18,6 +18,7 @@ import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum; import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer; import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService; import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; @@ -58,7 +59,7 @@ public class SysRoleServiceImpl implements SysRoleService { */ private volatile Map roleCache; /** - * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 + * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新 */ private volatile Date maxUpdateTime; @@ -77,7 +78,7 @@ public class SysRoleServiceImpl implements SysRoleService { @Override @PostConstruct public void initLocalCache() { - // 获取菜单列表,如果有更新 + // 获取角色列表,如果有更新 List roleList = this.loadRoleIfUpdate(maxUpdateTime); if (CollUtil.isEmpty(roleList)) { return; @@ -98,23 +99,23 @@ public class SysRoleServiceImpl implements SysRoleService { } /** - * 如果菜单发生变化,从数据库中获取最新的全量菜单。 + * 如果角色发生变化,从数据库中获取最新的全量角色。 * 如果未发生变化,则返回空 * - * @param maxUpdateTime 当前菜单的最大更新时间 - * @return 菜单列表 + * @param maxUpdateTime 当前角色的最大更新时间 + * @return 角色列表 */ private List loadRoleIfUpdate(Date maxUpdateTime) { // 第一步,判断是否要更新。 if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 - log.info("[loadRoleIfUpdate][首次加载全量菜单]"); - } else { // 判断数据库中是否有更新的菜单 + log.info("[loadRoleIfUpdate][首次加载全量角色]"); + } else { // 判断数据库中是否有更新的角色 if (!roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) { return null; } - log.info("[loadRoleIfUpdate][增量加载全量菜单]"); + log.info("[loadRoleIfUpdate][增量加载全量角色]"); } - // 第二步,如果有更新,则从数据库加载所有菜单 + // 第二步,如果有更新,则从数据库加载所有角色 return roleMapper.selectList(); } @@ -245,7 +246,8 @@ public class SysRoleServiceImpl implements SysRoleService { * @param code 角色额编码 * @param id 角色编号 */ - private void checkDuplicateRole(String name, String code, Long id) { + @VisibleForTesting + public void checkDuplicateRole(String name, String code, Long id) { // 1. 该 name 名字被其它角色所使用 SysRoleDO role = roleMapper.selectByName(name); if (role != null && !role.getId().equals(id)) { @@ -258,7 +260,7 @@ public class SysRoleServiceImpl implements SysRoleService { // 该 code 编码被其它角色所使用 role = roleMapper.selectByCode(code); if (role != null && !role.getId().equals(id)) { - throw ServiceExceptionUtil.exception(ROLE_CODE_DUPLICATE, name); + throw ServiceExceptionUtil.exception(ROLE_CODE_DUPLICATE, code); } } @@ -267,7 +269,8 @@ public class SysRoleServiceImpl implements SysRoleService { * * @param id 角色编号 */ - private void checkUpdateRole(Long id) { + @VisibleForTesting + public void checkUpdateRole(Long id) { SysRoleDO roleDO = roleMapper.selectById(id); if (roleDO == null) { throw ServiceExceptionUtil.exception(ROLE_NOT_EXISTS); diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelService.java new file mode 100644 index 000000000..c53d85e05 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelService.java @@ -0,0 +1,79 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 短信渠道Service接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SysSmsChannelService { + + /** + * 初始化短信客户端 + */ + void initSmsClients(); + + /** + * 创建短信渠道 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsChannel(@Valid SysSmsChannelCreateReqVO createReqVO); + + /** + * 更新短信渠道 + * + * @param updateReqVO 更新信息 + */ + void updateSmsChannel(@Valid SysSmsChannelUpdateReqVO updateReqVO); + + /** + * 删除短信渠道 + * + * @param id 编号 + */ + void deleteSmsChannel(Long id); + + /** + * 获得短信渠道 + * + * @param id 编号 + * @return 短信渠道 + */ + SysSmsChannelDO getSmsChannel(Long id); + + /** + * 获得短信渠道列表 + * + * @param ids 编号 + * @return 短信渠道列表 + */ + List getSmsChannelList(Collection ids); + + /** + * 获得所有短信渠道列表 + * + * @return 短信渠道列表 + */ + List getSmsChannelList(); + + /** + * 获得短信渠道分页 + * + * @param pageReqVO 分页查询 + * @return 短信渠道分页 + */ + PageResult getSmsChannelPage(SysSmsChannelPageReqVO pageReqVO); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogService.java new file mode 100644 index 000000000..52bb4a624 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogService.java @@ -0,0 +1,77 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 短信日志 Service 实现类 + * + * @author zzf + * @date 13:48 2021/3/2 + */ +public interface SysSmsLogService { + + /** + * 创建短信日志 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param isSend 是否发送 + * @param template 短信模板 + * @param templateContent 短信内容 + * @param templateParams 短信参数 + * @return 发送日志编号 + */ + Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SysSmsTemplateDO template, String templateContent, Map templateParams); + + /** + * 更新日志的发送结果 + * + * @param id 日志编号 + * @param sendCode 发送结果的编码 + * @param sendMsg 发送结果的提示 + * @param apiSendCode 短信 API 发送结果的编码 + * @param apiSendMsg 短信 API 发送失败的提示 + * @param apiRequestId 短信 API 发送返回的唯一请求 ID + * @param apiSerialNo 短信 API 发送返回的序号 + */ + void updateSmsSendResult(Long id, Integer sendCode, String sendMsg, + String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo); + + /** + * 更新日志的接收结果 + * + * @param id 日志编号 + * @param success 是否接收成功 + * @param receiveTime 用户接收时间 + * @param apiReceiveCode API 接收结果的编码 + * @param apiReceiveMsg API 接收结果的说明 + */ + void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, String apiReceiveCode, String apiReceiveMsg); + + /** + * 获得短信日志分页 + * + * @param pageReqVO 分页查询 + * @return 短信日志分页 + */ + PageResult getSmsLogPage(SysSmsLogPageReqVO pageReqVO); + + /** + * 获得短信日志列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 短信日志列表 + */ + List getSmsLogList(SysSmsLogExportReqVO exportReqVO); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsService.java new file mode 100644 index 000000000..f568b11a8 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsService.java @@ -0,0 +1,62 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; + +import java.util.List; +import java.util.Map; + +/** + * 短信Service接口 + * 只支持异步,因此没有返回值 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SysSmsService { + + /** + * 发送单条短信给用户(管理员) + * + * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToAdmin(String mobile, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条短信给用户(会员) + * + * 在 mobile 为空时,使用 userId 加载对应会员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToMember(String mobile, Long userId, + String templateCode, Map templateParams); + + Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams); + + void sendBatchSms(List mobiles, List userIds, Integer userType, + String templateCode, Map templateParams); + + void doSendSms(SysSmsSendMessage message); + + /** + * 接收短信的接收结果 + * + * @param channelCode 渠道编码 + * @param text 结果内容 + * @throws Throwable 处理失败时,抛出异常 + */ + void receiveSmsStatus(String channelCode, String text) throws Throwable; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateService.java new file mode 100644 index 000000000..1af5dae12 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateService.java @@ -0,0 +1,115 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 短信模板 Service 接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SysSmsTemplateService { + + /** + * 初始化短信模板的本地缓存 + */ + void initLocalCache(); + + /** + * 获得短信模板 + * + * @param code 模板编码 + * @return 短信模板 + */ + SysSmsTemplateDO getSmsTemplateByCode(String code); + + /** + * 获得短信模板,从缓存中 + * + * @param code 模板编码 + * @return 短信模板 + */ + SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code); + + /** + * 格式化短信内容 + * + * @param content 短信模板的内容 + * @param params 内容的参数 + * @return 格式化后的内容 + */ + String formatSmsTemplateContent(String content, Map params); + + /** + * 创建短信模板 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsTemplate(@Valid SysSmsTemplateCreateReqVO createReqVO); + + /** + * 更新短信模板 + * + * @param updateReqVO 更新信息 + */ + void updateSmsTemplate(@Valid SysSmsTemplateUpdateReqVO updateReqVO); + + /** + * 删除短信模板 + * + * @param id 编号 + */ + void deleteSmsTemplate(Long id); + + /** + * 获得短信模板 + * + * @param id 编号 + * @return 短信模板 + */ + SysSmsTemplateDO getSmsTemplate(Long id); + + /** + * 获得短信模板列表 + * + * @param ids 编号 + * @return 短信模板列表 + */ + List getSmsTemplateList(Collection ids); + + /** + * 获得短信模板分页 + * + * @param pageReqVO 分页查询 + * @return 短信模板分页 + */ + PageResult getSmsTemplatePage(SysSmsTemplatePageReqVO pageReqVO); + + /** + * 获得短信模板列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 短信模板分页 + */ + List getSmsTemplateList(SysSmsTemplateExportReqVO exportReqVO); + + /** + * 获得指定短信渠道下的短信模板数量 + * + * @param channelId 短信渠道编号 + * @return 数量 + */ + Integer countByChannelId(Long channelId); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsChannelServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsChannelServiceImpl.java new file mode 100644 index 000000000..16ecdecff --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsChannelServiceImpl.java @@ -0,0 +1,172 @@ +package cn.iocoder.dashboard.modules.system.service.sms.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO; +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsChannelConvert; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsChannelMapper; +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; + +/** + * 短信渠道Service实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +@Slf4j +public class SysSmsChannelServiceImpl implements SysSmsChannelService { + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SysSmsChannelMapper smsChannelMapper; + + @Resource + private SysSmsTemplateService smsTemplateService; + + @Resource + private SysSmsProducer smsProducer; + + @Override + @PostConstruct + public void initSmsClients() { + // 获取短信渠道,如果有更新 + List smsChannels = this.loadSmsChannelIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(smsChannels)) { + return; + } + + // 创建或更新短信 Client + List propertiesList = SysSmsChannelConvert.INSTANCE.convertList02(smsChannels); + propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties)); + + // 写入缓存 + assert smsChannels.size() > 0; // 断言,避免告警 + maxUpdateTime = smsChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); + log.info("[initSmsClients][初始化 SmsChannel 数量为 {}]", smsChannels.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initSmsClients(); + } + + /** + * 如果短信渠道发生变化,从数据库中获取最新的全量短信渠道。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前短信渠道的最大更新时间 + * @return 短信渠道列表 + */ + private List loadSmsChannelIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadSmsChannelIfUpdate][首次加载全量短信渠道]"); + } else { // 判断数据库中是否有更新的短信渠道 + if (smsChannelMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) { + return null; + } + log.info("[loadSmsChannelIfUpdate][增量加载全量短信渠道]"); + } + // 第二步,如果有更新,则从数据库加载所有短信渠道 + return smsChannelMapper.selectList(); + } + + @Override + public Long createSmsChannel(SysSmsChannelCreateReqVO createReqVO) { + // 插入 + SysSmsChannelDO smsChannel = SysSmsChannelConvert.INSTANCE.convert(createReqVO); + smsChannelMapper.insert(smsChannel); + // 发送刷新消息 + smsProducer.sendSmsChannelRefreshMessage(); + // 返回 + return smsChannel.getId(); + } + + @Override + public void updateSmsChannel(SysSmsChannelUpdateReqVO updateReqVO) { + // 校验存在 + this.validateSmsChannelExists(updateReqVO.getId()); + // 更新 + SysSmsChannelDO updateObj = SysSmsChannelConvert.INSTANCE.convert(updateReqVO); + smsChannelMapper.updateById(updateObj); + // 发送刷新消息 + smsProducer.sendSmsChannelRefreshMessage(); + } + + @Override + public void deleteSmsChannel(Long id) { + // 校验存在 + this.validateSmsChannelExists(id); + // 校验是否有字典数据 + if (smsTemplateService.countByChannelId(id) > 0) { + throw exception(SMS_CHANNEL_HAS_CHILDREN); + } + // 删除 + smsChannelMapper.deleteById(id); + // 发送刷新消息 + smsProducer.sendSmsChannelRefreshMessage(); + } + + private void validateSmsChannelExists(Long id) { + if (smsChannelMapper.selectById(id) == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + } + + @Override + public SysSmsChannelDO getSmsChannel(Long id) { + return smsChannelMapper.selectById(id); + } + + @Override + public List getSmsChannelList(Collection ids) { + return smsChannelMapper.selectBatchIds(ids); + } + + @Override + public List getSmsChannelList() { + return smsChannelMapper.selectList(); + } + + @Override + public PageResult getSmsChannelPage(SysSmsChannelPageReqVO pageReqVO) { + return smsChannelMapper.selectPage(pageReqVO); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsLogServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsLogServiceImpl.java new file mode 100644 index 000000000..3145163c0 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsLogServiceImpl.java @@ -0,0 +1,86 @@ +package cn.iocoder.dashboard.modules.system.service.sms.impl; + +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsLogMapper; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 短信日志 Service 实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Slf4j +@Service +public class SysSmsLogServiceImpl implements SysSmsLogService { + + @Resource + private SysSmsLogMapper smsLogMapper; + + @Override + public Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SysSmsTemplateDO template, String templateContent, Map templateParams) { + SysSmsLogDO.SysSmsLogDOBuilder logBuilder = SysSmsLogDO.builder(); + // 根据是否要发送,设置状态 + logBuilder.sendStatus(Objects.equals(isSend, true) ? SysSmsSendStatusEnum.INIT.getStatus() + : SysSmsSendStatusEnum.IGNORE.getStatus()); + // 设置手机相关字段 + logBuilder.mobile(mobile).userId(userId).userType(userType); + // 设置模板相关字段 + logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType()); + logBuilder.templateContent(templateContent).templateParams(templateParams).apiTemplateId(template.getApiTemplateId()); + // 设置渠道相关字段 + logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode()); + // 设置接收相关字段 + logBuilder.receiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus()); + + // 插入数据库 + SysSmsLogDO logDO = logBuilder.build(); + smsLogMapper.insert(logDO); + return logDO.getId(); + } + + @Override + public void updateSmsSendResult(Long id, Integer sendCode, String sendMsg, + String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo) { + SysSmsSendStatusEnum sendStatus = CommonResult.isSuccess(sendCode) ? SysSmsSendStatusEnum.SUCCESS + : SysSmsSendStatusEnum.FAILURE; + smsLogMapper.updateById(SysSmsLogDO.builder().id(id).sendStatus(sendStatus.getStatus()).sendTime(new Date()) + .sendCode(sendCode).sendMsg(sendMsg).apiSendCode(apiSendCode).apiSendMsg(apiSendMsg) + .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build()); + } + + @Override + public void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, String apiReceiveCode, String apiReceiveMsg) { + SysSmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SysSmsReceiveStatusEnum.SUCCESS + : SysSmsReceiveStatusEnum.FAILURE; + smsLogMapper.updateById(SysSmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()).receiveTime(receiveTime) + .apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); + } + + @Override + public PageResult getSmsLogPage(SysSmsLogPageReqVO pageReqVO) { + return smsLogMapper.selectPage(pageReqVO); + } + + @Override + public List getSmsLogList(SysSmsLogExportReqVO exportReqVO) { + return smsLogMapper.selectList(exportReqVO); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsServiceImpl.java new file mode 100644 index 000000000..46534d57f --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsServiceImpl.java @@ -0,0 +1,171 @@ +package cn.iocoder.dashboard.modules.system.service.sms.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.common.enums.UserTypeEnum; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO; +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService; +import cn.iocoder.dashboard.modules.system.service.user.SysUserService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; + +/** + * 短信日志Service实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +@Slf4j +public class SysSmsServiceImpl implements SysSmsService { + + @Resource + private SysSmsTemplateService smsTemplateService; + @Resource + private SysSmsLogService smsLogService; + @Resource + private SysSmsProducer smsProducer; + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SysUserService userService; + + @Override + public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams) { + // 如果 mobile 为空,则加载用户编号对应的手机号 + if (StrUtil.isEmpty(mobile)) { + SysUserDO user = userService.getUser(userId); + if (user != null) { + mobile = user.getMobile(); + } + } + // 执行发送 + return this.sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams) { + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); + } + + @Override + public Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams) { + // 校验短信模板是否合法 + SysSmsTemplateDO template = this.checkSmsTemplateValid(templateCode); + // 校验手机号码是否存在 + mobile = this.checkMobile(mobile); + // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志 + List> newTemplateParams = this.buildTemplateParams(template, templateParams); + + // 创建发送日志 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); // 如果模板被禁用,则不发送短信,只记录日志 + String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); + Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); + + // 发送 MQ 消息,异步执行发送短信 + if (isSend) { + smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), template.getApiTemplateId(), newTemplateParams); + } + return sendLogId; + } + + @Override + public void sendBatchSms(List mobiles, List userIds, Integer userType, + String templateCode, Map templateParams) { + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); + } + + @VisibleForTesting + public SysSmsTemplateDO checkSmsTemplateValid(String templateCode) { + // 获得短信模板。考虑到效率,从缓存中获取 + SysSmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode); + // 短信模板不存在 + if (template == null) { + throw exception(SMS_TEMPLATE_NOT_EXISTS); + } + return template; + } + + /** + * 将参数模板,处理成有序的 KeyValue 数组 + * + * 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说腾讯云 https://cloud.tencent.com/document/product/382/39023 + * + * @param template 短信模板 + * @param templateParams 原始参数 + * @return 处理后的参数 + */ + @VisibleForTesting + public List> buildTemplateParams(SysSmsTemplateDO template, Map templateParams) { + return template.getParams().stream().map(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key); + } + return new KeyValue<>(key, value); + }).collect(Collectors.toList()); + } + + @VisibleForTesting + public String checkMobile(String mobile) { + if (StrUtil.isEmpty(mobile)) { + throw exception(SMS_SEND_MOBILE_NOT_EXISTS); + } + return mobile; + } + + @Override + public void doSendSms(SysSmsSendMessage message) { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsClientFactory.getSmsClient(message.getChannelId()); + Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", message.getChannelId())); + // 发送短信 + SmsCommonResult sendResult = smsClient.sendSms(message.getLogId(), message.getMobile(), + message.getApiTemplateId(), message.getTemplateParams()); + smsLogService.updateSmsSendResult(message.getLogId(), sendResult.getCode(), sendResult.getMsg(), + sendResult.getApiCode(), sendResult.getApiMsg(), sendResult.getApiRequestId(), + sendResult.getData() != null ? sendResult.getData().getSerialNo() : null); + } + + @Override + public void receiveSmsStatus(String channelCode, String text) throws Throwable { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsClientFactory.getSmsClient(channelCode); + Assert.notNull(smsClient, String.format("短信客户端(%s) 不存在", channelCode)); + // 解析内容 + List receiveResults = smsClient.parseSmsReceiveStatus(text); + if (CollUtil.isEmpty(receiveResults)) { + return; + } + // 更新短信日志的接收结果. 因为量一般不打,所以先使用 for 循环更新 + receiveResults.forEach(result -> { + smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSuccess(), result.getReceiveTime(), + result.getErrorCode(), result.getErrorCode()); + }); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsTemplateServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsTemplateServiceImpl.java new file mode 100644 index 000000000..0a070aefb --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsTemplateServiceImpl.java @@ -0,0 +1,275 @@ +package cn.iocoder.dashboard.modules.system.service.sms.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO; +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsTemplateConvert; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsTemplateMapper; +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService; +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.regex.Pattern; + +import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; + +/** + * 短信模板Service实现类 + * + * @author zzf + * @date 2021/1/25 9:25 + */ +@Service +@Slf4j +public class SysSmsTemplateServiceImpl implements SysSmsTemplateService { + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + + /** + * 短信模板缓存 + * key:短信模板编码 {@link SysSmsTemplateDO#getCode()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + private volatile Map smsTemplateCache; + /** + * 缓存短信模板的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; + + @Resource + private SysSmsTemplateMapper smsTemplateMapper; + + @Resource + private SysSmsChannelService smsChannelService; + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SysSmsProducer smsProducer; + + /** + * 初始化 {@link #smsTemplateCache} 缓存 + */ + @Override + @PostConstruct + public void initLocalCache() { + // 获取短信模板列表,如果有更新 + List smsTemplateList = this.loadSmsTemplateIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(smsTemplateList)) { + return; + } + + // 写入缓存 + ImmutableMap.Builder builder = ImmutableMap.builder(); + smsTemplateList.forEach(sysSmsTemplateDO -> builder.put(sysSmsTemplateDO.getCode(), sysSmsTemplateDO)); + smsTemplateCache = builder.build(); + assert smsTemplateList.size() > 0; // 断言,避免告警 + maxUpdateTime = smsTemplateList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); + log.info("[initLocalCache][初始化 SmsTemplate 数量为 {}]", smsTemplateList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + /** + * 如果短信模板发生变化,从数据库中获取最新的全量短信模板。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前短信模板的最大更新时间 + * @return 短信模板列表 + */ + private List loadSmsTemplateIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadSmsTemplateIfUpdate][首次加载全量短信模板]"); + } else { // 判断数据库中是否有更新的短信模板 + if (smsTemplateMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) { + return null; + } + log.info("[loadSmsTemplateIfUpdate][增量加载全量短信模板]"); + } + // 第二步,如果有更新,则从数据库加载所有短信模板 + return smsTemplateMapper.selectList(); + } + + @Override + public SysSmsTemplateDO getSmsTemplateByCode(String code) { + return smsTemplateMapper.selectByCode(code); + } + + @Override + public SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code) { + return smsTemplateCache.get(code); + } + + @Override + public String formatSmsTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + + @VisibleForTesting + public List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + + @Override + public Long createSmsTemplate(SysSmsTemplateCreateReqVO createReqVO) { + // 校验短信渠道 + SysSmsChannelDO channelDO = checkSmsChannel(createReqVO.getChannelId()); + // 校验短信编码是否重复 + checkSmsTemplateCodeDuplicate(null, createReqVO.getCode()); + // 校验短信模板 + checkApiTemplate(createReqVO.getChannelId(), createReqVO.getApiTemplateId()); + + // 插入 + SysSmsTemplateDO template = SysSmsTemplateConvert.INSTANCE.convert(createReqVO); + template.setParams(parseTemplateContentParams(template.getContent())); + template.setChannelCode(channelDO.getCode()); + smsTemplateMapper.insert(template); + // 发送刷新消息 + smsProducer.sendSmsTemplateRefreshMessage(); + // 返回 + return template.getId(); + } + + @Override + public void updateSmsTemplate(SysSmsTemplateUpdateReqVO updateReqVO) { + // 校验存在 + this.validateSmsTemplateExists(updateReqVO.getId()); + // 校验短信渠道 + SysSmsChannelDO channelDO = checkSmsChannel(updateReqVO.getChannelId()); + // 校验短信编码是否重复 + checkSmsTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); + // 校验短信模板 + checkApiTemplate(updateReqVO.getChannelId(), updateReqVO.getApiTemplateId()); + + // 更新 + SysSmsTemplateDO updateObj = SysSmsTemplateConvert.INSTANCE.convert(updateReqVO); + updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); + updateObj.setChannelCode(channelDO.getCode()); + smsTemplateMapper.updateById(updateObj); + // 发送刷新消息 + smsProducer.sendSmsTemplateRefreshMessage(); + } + + @Override + public void deleteSmsTemplate(Long id) { + // 校验存在 + this.validateSmsTemplateExists(id); + // 更新 + smsTemplateMapper.deleteById(id); + // 发送刷新消息 + smsProducer.sendSmsTemplateRefreshMessage(); + } + + private void validateSmsTemplateExists(Long id) { + if (smsTemplateMapper.selectById(id) == null) { + throw exception(SMS_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public SysSmsTemplateDO getSmsTemplate(Long id) { + return smsTemplateMapper.selectById(id); + } + + @Override + public List getSmsTemplateList(Collection ids) { + return smsTemplateMapper.selectBatchIds(ids); + } + + @Override + public PageResult getSmsTemplatePage(SysSmsTemplatePageReqVO pageReqVO) { + return smsTemplateMapper.selectPage(pageReqVO); + } + + @Override + public List getSmsTemplateList(SysSmsTemplateExportReqVO exportReqVO) { + return smsTemplateMapper.selectList(exportReqVO); + } + + @Override + public Integer countByChannelId(Long channelId) { + return smsTemplateMapper.selectCountByChannelId(channelId); + } + + @VisibleForTesting + public SysSmsChannelDO checkSmsChannel(Long channelId) { + SysSmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); + if (channelDO == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + if (!Objects.equals(channelDO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(SMS_CHANNEL_DISABLE); + } + return channelDO; + } + + @VisibleForTesting + public void checkSmsTemplateCodeDuplicate(Long id, String code) { + SysSmsTemplateDO template = smsTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + if (!template.getId().equals(id)) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + } + + /** + * 校验 API 短信平台的模板是否有效 + * + * @param channelId 渠道编号 + * @param apiTemplateId API 模板编号 + */ + @VisibleForTesting + public void checkApiTemplate(Long channelId, String apiTemplateId) { + // 获得短信模板 + SmsClient smsClient = smsClientFactory.getSmsClient(channelId); + Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId)); + SmsCommonResult templateResult = smsClient.getSmsTemplate(apiTemplateId); + // 校验短信模板是否正确 + templateResult.checkError(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java index 31f72aedc..6fefbbdb0 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java @@ -358,7 +358,7 @@ public class SysUserServiceImpl implements SysUserService { } // 如果存在,判断是否允许更新 if (!isUpdateSupport) { - respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMessage()); + respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg()); return; } SysUserDO updateUser = SysUserConvert.INSTANCE.convert(importUser); diff --git a/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java b/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java index 186bf1201..ccd075a96 100644 --- a/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java +++ b/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java @@ -4,8 +4,10 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import java.util.*; +import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -30,6 +32,20 @@ public class CollectionUtils { return from.stream().filter(predicate).collect(Collectors.toList()); } + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + public static List convertList(Collection from, Function func) { if (CollUtil.isEmpty(from)) { return new ArrayList<>(); @@ -48,30 +64,57 @@ public class CollectionUtils { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } - return from.stream().collect(Collectors.toMap(keyFunc, item -> item)); + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); } public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } - return from.stream().collect(Collectors.toMap(keyFunc, valueFunc)); + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); } public static Map> convertMultiMap(Collection from, Function keyFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } - return from.stream().collect(Collectors.groupingBy(keyFunc, - Collectors.mapping(t -> t, Collectors.toList()))); + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); } public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { if (CollUtil.isEmpty(from)) { return new HashMap<>(); } - return from.stream().collect(Collectors.groupingBy(keyFunc, - Collectors.mapping(valueFunc, Collectors.toList()))); + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); } // 暂时没想好名字,先以 2 结尾噶 diff --git a/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java b/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java index ebe29648f..ce5805db8 100644 --- a/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java +++ b/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java @@ -2,6 +2,8 @@ package cn.iocoder.dashboard.util.collection; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.dashboard.common.core.KeyValue; +import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import java.util.ArrayList; @@ -55,4 +57,10 @@ public class MapUtils { consumer.accept(value); } + public static Map convertMap(List> keyValues) { + Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); + keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); + return map; + } + } diff --git a/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java b/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java index 3ae810982..78ec48392 100644 --- a/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java +++ b/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java @@ -9,6 +9,11 @@ import java.util.Date; */ public class DateUtils { + /** + * 时区 - 默认 + */ + public static final String TIME_ZONE_DEFAULT = "GMT+8"; + public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; public static Date addTime(Duration duration) { diff --git a/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java b/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java index f6727459c..2291235dc 100644 --- a/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java +++ b/src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java @@ -7,7 +7,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; /** * JSON 工具类 @@ -20,7 +21,7 @@ public class JsonUtils { /** * 初始化 objectMapper 属性 - * + *

* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean * * @param objectMapper ObjectMapper 对象 @@ -59,7 +60,7 @@ public class JsonUtils { } } - public static Object parseObject(String text, TypeReference> typeReference) { + public static T parseObject(String text, TypeReference typeReference) { try { return objectMapper.readValue(text, typeReference); } catch (IOException e) { @@ -67,4 +68,15 @@ public class JsonUtils { } } + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } diff --git a/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java b/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java index 5e98d915b..e0ca605a4 100644 --- a/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java +++ b/src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java @@ -1,7 +1,10 @@ package cn.iocoder.dashboard.util.string; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import java.util.Map; + /** * 字符串工具类 * @@ -13,4 +16,22 @@ public class StrUtils { return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 } + /** + * 指定字符串的 + * @param str + * @param replaceMap + * @return + */ + public static String replace(String str, Map replaceMap) { + assert StrUtil.isNotBlank(str); + if (ObjectUtil.isEmpty(replaceMap)) { + return str; + } + String result = null; + for (String key : replaceMap.keySet()) { + result = str.replace(key, replaceMap.get(key)); + } + return result; + } + } diff --git a/src/main/resources/codegen/java/service/service.vm b/src/main/resources/codegen/java/service/service.vm index 04499314e..17a9b3953 100644 --- a/src/main/resources/codegen/java/service/service.vm +++ b/src/main/resources/codegen/java/service/service.vm @@ -63,7 +63,7 @@ public interface ${table.className}Service { * 获得${table.classComment}列表, 用于 Excel 导出 * * @param exportReqVO 查询条件 - * @return ${table.classComment}分页 + * @return ${table.classComment}列表 */ List<${table.className}DO> get${simpleClassName}List(${table.className}ExportReqVO exportReqVO); diff --git a/src/main/resources/codegen/vue/views/index.vue.vm b/src/main/resources/codegen/vue/views/index.vue.vm index 73115fd77..5456d88ae 100644 --- a/src/main/resources/codegen/vue/views/index.vue.vm +++ b/src/main/resources/codegen/vue/views/index.vue.vm @@ -76,7 +76,7 @@ - > + #else #end @@ -137,7 +137,7 @@ #elseif($column.htmlType == "checkbox")## 多选框 - + #if ("" != $dictType)## 有数据字典 #elseif($column.htmlType == "radio")## 单选框 - + #if ("" != $dictType)## 有数据字典 > templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); +// templateParams.put("operation", "嘿嘿"); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", + "SMS_207945135", templateParams); + System.out.println(result); + } + + @Test + public void testGetSmsTemplate() { + String apiTemplateId = "SMS_2079451351"; + SmsCommonResult result = smsClient.getSmsTemplate(apiTemplateId); + System.out.println(result); + } + +} diff --git a/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java b/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java new file mode 100644 index 000000000..5815aa75c --- /dev/null +++ b/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java @@ -0,0 +1,45 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug; + +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link DebugDingTalkSmsClient} 的集成测试 + */ +public class DebugDingTalkSmsClientIntegrationTest { + + private static DebugDingTalkSmsClient smsClient; + + @BeforeAll + public static void init() { + // 创建配置类 + SmsChannelProperties properties = new SmsChannelProperties(); + properties.setId(1L); + properties.setSignature("芋道"); + properties.setCode(SmsChannelEnum.DEBUG_DING_TALK.getCode()); + properties.setApiKey("696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859"); + properties.setApiSecret("SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67"); + // 创建客户端 + smsClient = new DebugDingTalkSmsClient(properties); + smsClient.init(); + } + + @Test + public void testSendSms() { + List> templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); + templateParams.add(new KeyValue<>("operation", "嘿嘿")); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams); + System.out.println(result); + } + +} diff --git a/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientIntegrationTest.java b/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientIntegrationTest.java new file mode 100644 index 000000000..73f1a472d --- /dev/null +++ b/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientIntegrationTest.java @@ -0,0 +1,52 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link YunpianSmsClient} 的集成测试 + */ +public class YunpianSmsClientIntegrationTest { + + private static YunpianSmsClient smsClient; + + @BeforeAll + public static void init() { + // 创建配置类 + SmsChannelProperties properties = new SmsChannelProperties(); + properties.setId(1L); + properties.setSignature("芋道"); + properties.setCode(SmsChannelEnum.YUN_PIAN.getCode()); + properties.setApiKey("1555a14277cb8a608cf45a9e6a80d510"); + // 创建客户端 + smsClient = new YunpianSmsClient(properties); + smsClient.init(); + } + + @Test + public void testSendSms() { + List> templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); + templateParams.add(new KeyValue<>("operation", "嘿嘿")); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams); + System.out.println(result); + } + + @Test + public void testGetSmsTemplate() { + String apiTemplateId = "4383920"; + SmsCommonResult result = smsClient.getSmsTemplate(apiTemplateId); + System.out.println(result); + } + +} diff --git a/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/package-info.java b/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/package-info.java new file mode 100644 index 000000000..037ce8ca2 --- /dev/null +++ b/src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.dashboard.framework.sms.core.client; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/package-info.java b/src/test-integration/java/cn/iocoder/dashboard/modules/system/service/package-info.java similarity index 100% rename from src/main/java/cn/iocoder/dashboard/modules/system/service/package-info.java rename to src/test-integration/java/cn/iocoder/dashboard/modules/system/service/package-info.java diff --git a/src/test-integration/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceIntegrationTest.java b/src/test-integration/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceIntegrationTest.java new file mode 100644 index 000000000..7ec704b5d --- /dev/null +++ b/src/test-integration/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceIntegrationTest.java @@ -0,0 +1,74 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.iocoder.dashboard.BaseDbAndRedisIntegrationTest; +import cn.iocoder.dashboard.common.enums.UserTypeEnum; +import cn.iocoder.dashboard.framework.sms.config.SmsConfiguration; +import cn.iocoder.dashboard.modules.system.mq.consumer.sms.SysSmsSendConsumer; +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsChannelServiceImpl; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsLogServiceImpl; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsServiceImpl; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsTemplateServiceImpl; +import cn.iocoder.dashboard.modules.system.service.user.SysUserService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Import({SmsConfiguration.class, + SysSmsChannelServiceImpl.class, SysSmsServiceImpl.class, SysSmsTemplateServiceImpl.class, SysSmsLogServiceImpl.class, + SysSmsProducer.class, SysSmsSendConsumer.class}) +public class SysSmsServiceIntegrationTest extends BaseDbAndRedisIntegrationTest { + + @Resource + private SysSmsServiceImpl smsService; + @Resource + private SysSmsChannelServiceImpl smsChannelService; + + @MockBean + private SysUserService userService; + + @Test + public void testSendSingleSms_yunpianSuccess() { + // 参数准备 + String mobile = "15601691399"; + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + String templateCode = "test_01"; + Map templateParams = MapUtil.builder() + .put("operation", "登陆").put("code", "1234").build(); + // 调用 + smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + + // 等待 MQ 消费 + ThreadUtil.sleep(1, TimeUnit.HOURS); + } + + @Test + public void testSendSingleSms_aliyunSuccess() { + // 参数准备 + String mobile = "15601691399"; + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + String templateCode = "test_02"; + Map templateParams = MapUtil.builder() + .put("code", "1234").build(); + // 调用 + smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + + // 等待 MQ 消费 + ThreadUtil.sleep(1, TimeUnit.HOURS); + } + +// @Test +// public void testDoSendSms() { +// // 等待 MQ 消费 +// ThreadUtil.sleep(1, TimeUnit.HOURS); +// } + +} diff --git a/src/test-integration/resources/application-integration-test.yaml b/src/test-integration/resources/application-integration-test.yaml index 88b92273c..43a846ee2 100644 --- a/src/test-integration/resources/application-integration-test.yaml +++ b/src/test-integration/resources/application-integration-test.yaml @@ -9,19 +9,15 @@ spring: # 数据源配置项 datasource: name: ruoyi-vue-pro - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 - driver-class-name: org.h2.Driver - username: sa - password: - schema: classpath:sql/create_tables.sql # MySQL 转 H2 的语句,使用 https://www.jooq.org/translate/ 工具 - druid: - async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 - initial-size: 1 # 单元测试,配置为 1,提升启动速度 + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: host: 127.0.0.1 # 地址 - port: 6379 # 端口(单元测试,使用 16379 端口) + port: 6379 # 端口 database: 0 # 数据库索引 mybatis: diff --git a/src/test/java/cn/iocoder/dashboard/BaseMockitoUnitTest.java b/src/test/java/cn/iocoder/dashboard/BaseMockitoUnitTest.java new file mode 100644 index 000000000..4a595b24e --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/BaseMockitoUnitTest.java @@ -0,0 +1,13 @@ +package cn.iocoder.dashboard; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * 纯 Mockito 的单元测试 + * + * @author 芋道源码 + */ +@ExtendWith(MockitoExtension.class) +public class BaseMockitoUnitTest { +} diff --git a/src/test/java/cn/iocoder/dashboard/framework/package-info.java b/src/test/java/cn/iocoder/dashboard/framework/package-info.java new file mode 100644 index 000000000..0274647fb --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/framework/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.dashboard.framework; diff --git a/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java new file mode 100644 index 000000000..a544d03d5 --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java @@ -0,0 +1,224 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.collection.MapUtils; +import cn.iocoder.dashboard.util.date.DateUtils; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; +import java.util.function.Function; + +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static cn.iocoder.dashboard.util.json.JsonUtils.toJsonString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AliyunSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private final AliyunSmsClient smsClient = new AliyunSmsClient(properties); + + @Mock + private IAcsClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient")); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() throws ClientException { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock 方法 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK")); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(mobile, acsRequest.getPhoneNumbers()); + assertEquals(properties.getSignature(), acsRequest.getSignName()); + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam()); + assertEquals(sendLogId.toString(), acsRequest.getOutId()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getBizId(), result.getData().getSerialNo()); + } + + @Test + public void testDoTParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[\n" + + " {\n" + + " \"phone_number\" : \"13900000001\",\n" + + " \"send_time\" : \"2017-01-01 11:12:13\",\n" + + " \"report_time\" : \"2017-02-02 22:23:24\",\n" + + " \"success\" : true,\n" + + " \"err_code\" : \"DELIVERED\",\n" + + " \"err_msg\" : \"用户接收成功\",\n" + + " \"sms_size\" : \"1\",\n" + + " \"biz_id\" : \"12345\",\n" + + " \"out_id\" : \"67890\"\n" + + " }\n" + + "]"; + // mock 方法 + + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("DELIVERED", statuses.get(0).getErrorCode()); + assertEquals("用户接收成功", statuses.get(0).getErrorMsg()); + assertEquals("13900000001", statuses.get(0).getMobile()); + assertEquals(DateUtils.buildTime(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime()); + assertEquals("12345", statuses.get(0).getSerialNo()); + assertEquals(67890L, statuses.get(0).getLogId()); + } + + @Test + public void testDoGetSmsTemplate() throws ClientException { + // 准备参数 + String apiTemplateId = randomString(); + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus(0)); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus(1)); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus(2)); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), + "未知审核状态(3)"); + } + + @Test + @SuppressWarnings("unchecked") + public void testInvoke_throwable() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + // mock 方法 + ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString()); + when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex); + + // 调用,并断言异常 + SmsCommonResult result = smsClient.invoke(request,null); + // 断言 + assertEquals(ex.getErrCode(), result.getApiCode()); + assertEquals(ex.getErrMsg(), result.getApiMsg()); + assertEquals(SMS_API_PARAM_ERROR.getCode(), result.getCode()); + assertEquals(SMS_API_PARAM_ERROR.getMsg(), result.getMsg()); + assertEquals(ex.getRequestId(), result.getApiRequestId()); + } + + @Test + public void testInvoke_success() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + Function responseConsumer = response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason()); + return data; + }; + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.invoke(request, responseConsumer); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java new file mode 100644 index 000000000..54dba079b --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun; + +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link AliyunSmsCodeMapping} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private AliyunSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL")); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java new file mode 100644 index 000000000..3e4190c0a --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java @@ -0,0 +1,202 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.dashboard.util.date.DateUtils; +import com.google.common.collect.Lists; +import com.yunpian.sdk.YunpianClient; +import com.yunpian.sdk.api.SmsApi; +import com.yunpian.sdk.api.TplApi; +import com.yunpian.sdk.constant.YunpianConstant; +import com.yunpian.sdk.model.Result; +import com.yunpian.sdk.model.SmsSingleSend; +import com.yunpian.sdk.model.Template; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static com.yunpian.sdk.constant.Code.OK; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * 对 {@link YunpianSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class YunpianSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()); // 随机一个 apiKey,避免构建报错 + + @InjectMocks + private final YunpianSmsClient smsClient = new YunpianSmsClient(properties); + + @Mock + private YunpianClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotEquals(client, ReflectUtil.getFieldValue(smsClient, "client")); + verify(client, times(1)).close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock sms 方法 + SmsApi smsApi = mock(SmsApi.class); + when(client.sms()).thenReturn(smsApi); + // mock tpl_single_send 方法 + Map request = new HashMap<>(); + request.put(YunpianConstant.MOBILE, mobile); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + request.put(YunpianConstant.TPL_VALUE, "#code#=1234&#op#=login"); + request.put(YunpianConstant.UID, String.valueOf(sendLogId)); + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl()); + Result responseResult = randomPojo(Result.class, SmsSingleSend.class, + o -> o.setCode(OK)); // API 发送成功的 code + when(smsApi.tpl_single_send(eq(request))).thenReturn(responseResult); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + // 断言结果 + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); + } + + @Test + public void testDoParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[{\"sid\":9527,\"uid\":1024,\"user_receive_time\":\"2014-03-17 22:55:21\",\"error_msg\":\"\",\"mobile\":\"15205201314\",\"report_status\":\"SUCCESS\"}]"; + // mock 方法 + + // 调用 + + // 断言 + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("", statuses.get(0).getErrorCode()); + assertNull(statuses.get(0).getErrorMsg()); + assertEquals("15205201314", statuses.get(0).getMobile()); + assertEquals(DateUtils.buildTime(2014, 3, 17, 22, 55, 21), statuses.get(0).getReceiveTime()); + assertEquals("9527", statuses.get(0).getSerialNo()); + assertEquals(1024L, statuses.get(0).getLogId()); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoGetSmsTemplate() throws Throwable { + // 准备参数 + String apiTemplateId = randomString(); + // mock tpl 方法 + TplApi tplApi = mock(TplApi.class); + when(client.tpl()).thenReturn(tplApi); + // mock get 方法 + Map request = new HashMap<>(); + request.put(YunpianConstant.APIKEY, properties.getApiKey()); + request.put(YunpianConstant.TPL_ID, apiTemplateId); + Result> responseResult = randomPojo(Result.class, List.class, o -> { + o.setCode(OK); // API 发送成功的 code + o.setData(randomPojoList(Template.class, t -> t.setCheck_status("SUCCESS"))); + }); + when(tplApi.get(eq(request))).thenReturn(responseResult); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + // 断言结果 + Template template = responseResult.getData().get(0); + assertEquals(template.getTpl_id().toString(), result.getData().getId()); + assertEquals(template.getTpl_content(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(template.getReason(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus("CHECKING")); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus("SUCCESS")); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus("FAIL")); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("test"), + "未知审核状态(test)"); + } + + @Test + public void testInvoke_throwable() { + // 准备参数 + Supplier> requestConsumer = + () -> new Result<>().setThrowable(new NullPointerException()); + // mock 方法 + + // 调用,并断言异常 + assertThrows(NullPointerException.class, + () -> smsClient.invoke(requestConsumer, null)); + } + + @Test + @SuppressWarnings("unchecked") + public void testInvoke_success() throws Throwable { + // 准备参数 + Result responseResult = randomPojo(Result.class, SmsSingleSend.class, o -> o.setCode(OK)); + Supplier> requestConsumer = () -> responseResult; + Function responseConsumer = + smsSingleSend -> new SmsSendRespDTO().setSerialNo(String.valueOf(responseResult.getData().getSid())); + // mock 方法 + + // 调用 + SmsCommonResult result = smsClient.invoke(requestConsumer, responseConsumer); + // 断言 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode()); + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertNull(result.getApiRequestId()); + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo()); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java new file mode 100644 index 000000000..de6e46432 --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian; + +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static com.yunpian.sdk.constant.Code.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link YunpianSmsCodeMapping} 的单元测试 + * + * @author 芋道源码 + */ +class YunpianSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private YunpianSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(String.valueOf(OK))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply(String.valueOf(ARGUMENT_MISSING))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply(String.valueOf(BAD_ARGUMENT_FORMAT))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply(String.valueOf(MONEY_NOT_ENOUGH))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_FOUND))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_VALID))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DUP_IN_SHORT_TIME))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(TOO_MANY_TIME_IN_5))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DAY_LIMIT_PER_MOBILE))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(HOUR_LIMIT_PER_MOBILE))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply(String.valueOf(BLACK_PHONE_FILTER))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_MATCH))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_VALID))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(BAD_SIGN_FORMAT))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply(String.valueOf(BAD_API_KEY))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply(String.valueOf(API_NOT_ALLOWED))); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply(String.valueOf(IP_NOT_ALLOWED))); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/dept/SysDeptServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/dept/SysDeptServiceTest.java index 373665546..48a80056e 100644 --- a/src/test/java/cn/iocoder/dashboard/modules/system/service/dept/SysDeptServiceTest.java +++ b/src/test/java/cn/iocoder/dashboard/modules/system/service/dept/SysDeptServiceTest.java @@ -270,4 +270,5 @@ class SysDeptServiceTest extends BaseDbUnitTest { }; return randomPojo(SysDeptDO.class, ArrayUtils.append(consumer, consumers)); } + } diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuServiceTest.java new file mode 100644 index 000000000..da680a551 --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuServiceTest.java @@ -0,0 +1,366 @@ +package cn.iocoder.dashboard.modules.system.service.permission; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.dashboard.BaseDbUnitTest; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuListReqVO; +import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysMenuDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysMenuMapper; +import cn.iocoder.dashboard.modules.system.enums.permission.MenuTypeEnum; +import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysMenuProducer; +import cn.iocoder.dashboard.modules.system.service.permission.impl.SysMenuServiceImpl; +import cn.iocoder.dashboard.util.AopTargetUtils; +import cn.iocoder.dashboard.util.RandomUtils; +import cn.iocoder.dashboard.util.object.ObjectUtils; +import com.google.common.collect.Multimap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.*; + +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; + +@Import(SysMenuServiceImpl.class) +public class SysMenuServiceTest extends BaseDbUnitTest { + + @Resource + private SysMenuServiceImpl sysMenuService; + + @MockBean + private SysPermissionService sysPermissionService; + + @MockBean + private SysMenuProducer sysMenuProducer; + + @Resource + private SysMenuMapper menuMapper; + + @Test + public void testInitLocalCache_success() throws Exception { + SysMenuDO menuDO1 = createMenuDO(MenuTypeEnum.MENU, "xxxx", 0L); + menuMapper.insert(menuDO1); + SysMenuDO menuDO2 = createMenuDO(MenuTypeEnum.MENU, "xxxx", 0L); + menuMapper.insert(menuDO2); + + // 调用 + sysMenuService.initLocalCache(); + + // 获取代理对象 + SysMenuServiceImpl target = (SysMenuServiceImpl) AopTargetUtils.getTarget(sysMenuService); + + Map menuCache = + (Map) BeanUtil.getFieldValue(target, "menuCache"); + Assert.isTrue(menuCache.size() == 2); + assertPojoEquals(menuDO1, menuCache.get(menuDO1.getId())); + assertPojoEquals(menuDO2, menuCache.get(menuDO2.getId())); + + Multimap permissionMenuCache = + (Multimap) BeanUtil.getFieldValue(target, "permissionMenuCache"); + Assert.isTrue(permissionMenuCache.size() == 2); + assertPojoEquals(menuDO1, permissionMenuCache.get(menuDO1.getPermission())); + assertPojoEquals(menuDO2, permissionMenuCache.get(menuDO2.getPermission())); + + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(target, "maxUpdateTime"); + assertEquals(ObjectUtils.max(menuDO1.getUpdateTime(), menuDO2.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testCreateMenu_success() { + //构造父目录 + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + //调用 + SysMenuCreateReqVO vo = randomPojo(SysMenuCreateReqVO.class, o -> { + o.setParentId(parentId); + o.setName("testSonName"); + o.setType(MenuTypeEnum.MENU.getType()); + o.setStatus(RandomUtils.randomCommonStatus()); + }); + Long menuId = sysMenuService.createMenu(vo); + + //断言 + Assertions.assertNotNull(menuId); + // 校验记录的属性是否正确 + SysMenuDO ret = menuMapper.selectById(menuId); + assertPojoEquals(vo, ret); + // 校验调用 + verify(sysMenuProducer).sendMenuRefreshMessage(); + } + + @Test + public void testUpdateMenu_success() { + //构造父子目录 + SysMenuDO sonMenuDO = initParentAndSonMenuDO(); + Long sonId = sonMenuDO.getId(); + Long parentId = sonMenuDO.getParentId(); + + //调用 + SysMenuUpdateReqVO vo = RandomUtils.randomPojo(SysMenuUpdateReqVO.class, o -> { + o.setId(sonId); + o.setParentId(parentId); + o.setType(MenuTypeEnum.MENU.getType()); + o.setStatus(RandomUtils.randomCommonStatus()); + o.setName("pppppp"); //修改名字 + }); + sysMenuService.updateMenu(vo); + + //断言 + // 校验记录的属性是否正确 + SysMenuDO ret = menuMapper.selectById(sonId); + assertPojoEquals(vo, ret); + // 校验调用 + verify(sysMenuProducer).sendMenuRefreshMessage(); + } + + @Test + public void testUpdateMenu_sonIdNotExist() { + Long sonId = 99999L; + Long parentId = 10000L; + + //调用 + SysMenuUpdateReqVO vo = RandomUtils.randomPojo(SysMenuUpdateReqVO.class, o -> { + o.setId(sonId); + o.setParentId(parentId); + o.setType(MenuTypeEnum.MENU.getType()); + o.setStatus(RandomUtils.randomCommonStatus()); + }); + //断言 + assertServiceException(() -> sysMenuService.updateMenu(vo), MENU_NOT_EXISTS); + } + + @Test + public void testDeleteMenu_success() { + SysMenuDO sonMenuDO = initParentAndSonMenuDO(); + Long sonId = sonMenuDO.getId(); + + //调用 + sysMenuService.deleteMenu(sonId); + + //断言 + SysMenuDO menuDO = menuMapper.selectById(sonId); + Assert.isNull(menuDO); + verify(sysPermissionService).processMenuDeleted(sonId); + verify(sysMenuProducer).sendMenuRefreshMessage(); + } + + @Test + public void testDeleteMenu_menuNotExist() { + Long sonId = 99999L; + + assertServiceException(() -> sysMenuService.deleteMenu(sonId), MENU_NOT_EXISTS); + } + + @Test + public void testDeleteMenu_existChildren() { + SysMenuDO sonMenu = initParentAndSonMenuDO(); + Long parentId = sonMenu.getParentId(); + + assertServiceException(() -> sysMenuService.deleteMenu(parentId), MENU_EXISTS_CHILDREN); + } + + @Test + public void testGetMenus_success() { + Map idMenuMap = new HashMap<>(); + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + idMenuMap.put(menuDO.getId(), menuDO); + + SysMenuDO sonMenu = createMenuDO(MenuTypeEnum.MENU, "son", menuDO.getId()); + menuMapper.insert(sonMenu); + idMenuMap.put(sonMenu.getId(), sonMenu); + + //调用 + List menuDOS = sysMenuService.getMenus(); + + //断言 + Assert.isTrue(menuDOS.size() == idMenuMap.size()); + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m)); + } + + @Test + public void testGetMenusReqVo_success() { + Map idMenuMap = new HashMap<>(); + //用于验证可以模糊搜索名称包含"name",状态为1的menu + SysMenuDO menu = createMenuDO(MenuTypeEnum.MENU, "name2", 0L, 1); + menuMapper.insert(menu); + idMenuMap.put(menu.getId(), menu); + + menu = createMenuDO(MenuTypeEnum.MENU, "11name111", 0L, 1); + menuMapper.insert(menu); + idMenuMap.put(menu.getId(), menu); + + menu = createMenuDO(MenuTypeEnum.MENU, "name", 0L, 1); + menuMapper.insert(menu); + idMenuMap.put(menu.getId(), menu); + + //以下是不符合搜索条件的的menu + menu = createMenuDO(MenuTypeEnum.MENU, "xxxxxx", 0L, 1); + menuMapper.insert(menu); + menu = createMenuDO(MenuTypeEnum.MENU, "name", 0L, 2); + menuMapper.insert(menu); + + //调用 + SysMenuListReqVO reqVO = new SysMenuListReqVO(); + reqVO.setStatus(1); + reqVO.setName("name"); + List menuDOS = sysMenuService.getMenus(reqVO); + + //断言 + Assert.isTrue(menuDOS.size() == idMenuMap.size()); + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m)); + } + + @Test + public void testListMenusFromCache_success() throws Exception { + Map mockCacheMap = new HashMap<>(); + //获取代理对象 + SysMenuServiceImpl target = (SysMenuServiceImpl) AopTargetUtils.getTarget(sysMenuService); + BeanUtil.setFieldValue(target, "menuCache", mockCacheMap); + + Map idMenuMap = new HashMap<>(); + //用于验证搜索类型为MENU,状态为1的menu + SysMenuDO menuDO = createMenuDO(1L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + idMenuMap.put(menuDO.getId(), menuDO); + + menuDO = createMenuDO(2L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + idMenuMap.put(menuDO.getId(), menuDO); + + //以下是不符合搜索条件的menu + menuDO = createMenuDO(3L, MenuTypeEnum.BUTTON, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2); + mockCacheMap.put(menuDO.getId(), menuDO); + + List menuDOS = sysMenuService.listMenusFromCache(Collections.singletonList(MenuTypeEnum.MENU.getType()), + Collections.singletonList(CommonStatusEnum.DISABLE.getStatus())); + Assert.isTrue(menuDOS.size() == idMenuMap.size()); + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m)); + } + + @Test + public void testListMenusFromCache2_success() throws Exception { + Map mockCacheMap = new HashMap<>(); + //获取代理对象 + SysMenuServiceImpl target = (SysMenuServiceImpl) AopTargetUtils.getTarget(sysMenuService); + BeanUtil.setFieldValue(target, "menuCache", mockCacheMap); + + Map idMenuMap = new HashMap<>(); + //验证搜索id为1, 类型为MENU, 状态为1 的menu + SysMenuDO menuDO = createMenuDO(1L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + idMenuMap.put(menuDO.getId(), menuDO); + + //以下是不符合搜索条件的menu + menuDO = createMenuDO(2L, MenuTypeEnum.MENU, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + menuDO = createMenuDO(3L, MenuTypeEnum.BUTTON, "name", 0L, 1); + mockCacheMap.put(menuDO.getId(), menuDO); + menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2); + mockCacheMap.put(menuDO.getId(), menuDO); + + List menuDOS = sysMenuService.listMenusFromCache(Collections.singletonList(1L), + Collections.singletonList(MenuTypeEnum.MENU.getType()), Collections.singletonList(1)); + Assert.isTrue(menuDOS.size() == idMenuMap.size()); + menuDOS.forEach(menu -> assertPojoEquals(idMenuMap.get(menu.getId()), menu)); + } + + @Test + public void testCheckParentResource_success() { + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + sysMenuService.checkParentResource(parentId, null); + } + + @Test + public void testCheckParentResource_canNotSetSelfToBeParent() { + assertServiceException(() -> sysMenuService.checkParentResource(1L, 1L), MENU_PARENT_ERROR); + } + + @Test + public void testCheckParentResource_parentNotExist() { + assertServiceException(() -> sysMenuService.checkParentResource(randomLongId(), null), MENU_PARENT_NOT_EXISTS); + } + + @Test + public void testCheckParentResource_parentTypeError() { + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.BUTTON, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + assertServiceException(() -> sysMenuService.checkParentResource(parentId, null), MENU_PARENT_NOT_DIR_OR_MENU); + } + + @Test + public void testCheckResource_success() { + SysMenuDO sonMenu = initParentAndSonMenuDO(); + Long parentId = sonMenu.getParentId(); + + Long otherSonMenuId = randomLongId(); + String otherSonMenuName = randomString(); + + sysMenuService.checkResource(parentId, otherSonMenuName, otherSonMenuId); + } + + @Test + public void testCheckResource_sonMenuNameDuplicate(){ + SysMenuDO sonMenu=initParentAndSonMenuDO(); + Long parentId=sonMenu.getParentId(); + + Long otherSonMenuId=randomLongId(); + String otherSonMenuName=sonMenu.getName(); //相同名称 + + assertServiceException(() -> sysMenuService.checkResource(parentId, otherSonMenuName, otherSonMenuId), MENU_NAME_DUPLICATE); + } + + /** + * 构造父子目录,返回子目录 + * + * @return + */ + private SysMenuDO initParentAndSonMenuDO() { + //构造父子目录 + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + + SysMenuDO sonMenuDO = createMenuDO(MenuTypeEnum.MENU, "testSonName", parentId); + menuMapper.insert(sonMenuDO); + return sonMenuDO; + } + + private SysMenuDO createMenuDO(MenuTypeEnum typeEnum, String menuName, Long parentId) { + return createMenuDO(typeEnum, menuName, parentId, RandomUtils.randomCommonStatus()); + } + + private SysMenuDO createMenuDO(MenuTypeEnum typeEnum, String menuName, Long parentId, Integer status) { + return createMenuDO(null, typeEnum, menuName, parentId, status); + } + + private SysMenuDO createMenuDO(Long id, MenuTypeEnum typeEnum, String menuName, Long parentId, Integer status) { + return RandomUtils.randomPojo(SysMenuDO.class, o -> { + o.setId(id); + o.setParentId(parentId); + o.setType(typeEnum.getType()); + o.setStatus(status); + o.setName(menuName); + }); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleServiceTest.java new file mode 100644 index 000000000..f180f2b92 --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleServiceTest.java @@ -0,0 +1,308 @@ +package cn.iocoder.dashboard.modules.system.service.permission; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.dashboard.BaseDbUnitTest; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.security.core.enums.DataScopeEnum; +import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO; +import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMapper; +import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum; +import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer; +import cn.iocoder.dashboard.modules.system.service.permission.impl.SysRoleServiceImpl; +import cn.iocoder.dashboard.util.AopTargetUtils; +import cn.iocoder.dashboard.util.AssertUtils; +import cn.iocoder.dashboard.util.RandomUtils; +import cn.iocoder.dashboard.util.object.ObjectUtils; +import com.google.common.collect.Sets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static cn.iocoder.dashboard.util.object.ObjectUtils.max; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +@Import(SysRoleServiceImpl.class) +public class SysRoleServiceTest extends BaseDbUnitTest { + + @Resource + private SysRoleServiceImpl sysRoleService; + + @Resource + private SysRoleMapper roleMapper; + + @MockBean + private SysPermissionService sysPermissionService; + + @MockBean + private SysRoleProducer sysRoleProducer; + + @Test + public void testInitLocalCache_success() throws Exception { + SysRoleDO roleDO1 = createRoleDO("role1", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO1); + SysRoleDO roleDO2 = createRoleDO("role2", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO2); + + //调用 + sysRoleService.initLocalCache(); + + //断言 + //获取代理对象 + SysRoleServiceImpl target = (SysRoleServiceImpl) AopTargetUtils.getTarget(sysRoleService); + + Map roleCache = (Map) BeanUtil.getFieldValue(target, "roleCache"); + assertPojoEquals(roleDO1, roleCache.get(roleDO1.getId())); + assertPojoEquals(roleDO2, roleCache.get(roleDO2.getId())); + + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(target, "maxUpdateTime"); + assertEquals(max(roleDO1.getUpdateTime(), roleDO2.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testCreateRole_success() { + SysRoleCreateReqVO reqVO = randomPojo(SysRoleCreateReqVO.class, o -> { + o.setCode("role_code"); + o.setName("role_name"); + o.setRemark("remark"); + o.setType(SysRoleTypeEnum.CUSTOM.getType()); + o.setSort(1); + }); + Long roleId = sysRoleService.createRole(reqVO); + + //断言 + assertNotNull(roleId); + SysRoleDO roleDO = roleMapper.selectById(roleId); + assertPojoEquals(reqVO, roleDO); + + verify(sysRoleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testUpdateRole_success() { + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + SysRoleUpdateReqVO reqVO = randomPojo(SysRoleUpdateReqVO.class, o -> { + o.setId(roleId); + o.setCode("role_code"); + o.setName("update_name"); + o.setType(SysRoleTypeEnum.SYSTEM.getType()); + o.setSort(999); + }); + sysRoleService.updateRole(reqVO); + + //断言 + SysRoleDO newRoleDO = roleMapper.selectById(roleId); + assertPojoEquals(reqVO, newRoleDO); + + verify(sysRoleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testUpdateRoleStatus_success() { + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, CommonStatusEnum.ENABLE.getStatus()); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + sysRoleService.updateRoleStatus(roleId, CommonStatusEnum.DISABLE.getStatus()); + + //断言 + SysRoleDO newRoleDO = roleMapper.selectById(roleId); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), newRoleDO.getStatus()); + + verify(sysRoleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testUpdateRoleDataScope_success() { + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + Set deptIdSet = Arrays.asList(1L, 2L, 3L, 4L, 5L).stream().collect(Collectors.toSet()); + sysRoleService.updateRoleDataScope(roleId, DataScopeEnum.DEPT_CUSTOM.getScore(), deptIdSet); + + //断言 + SysRoleDO newRoleDO = roleMapper.selectById(roleId); + assertEquals(DataScopeEnum.DEPT_CUSTOM.getScore(), newRoleDO.getDataScope()); + + Set newDeptIdSet = newRoleDO.getDataScopeDeptIds(); + assertTrue(deptIdSet.size() == newDeptIdSet.size()); + deptIdSet.stream().forEach(d -> assertTrue(newDeptIdSet.contains(d))); + + verify(sysRoleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testDeleteRole_success() { + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + //调用 + sysRoleService.deleteRole(roleId); + + //断言 + SysRoleDO newRoleDO = roleMapper.selectById(roleId); + assertNull(newRoleDO); + + verify(sysRoleProducer).sendRoleRefreshMessage(); + } + + @Test + public void testGetRoles_success() { + Map idRoleMap = new HashMap<>(); + // 验证查询状态为1的角色 + SysRoleDO roleDO1 = createRoleDO("role1", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1); + roleMapper.insert(roleDO1); + idRoleMap.put(roleDO1.getId(), roleDO1); + + SysRoleDO roleDO2 = createRoleDO("role2", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1); + roleMapper.insert(roleDO2); + idRoleMap.put(roleDO2.getId(), roleDO2); + + // 以下是排除的角色 + SysRoleDO roleDO3 = createRoleDO("role3", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 2); + roleMapper.insert(roleDO3); + + //调用 + List roles = sysRoleService.getRoles(Arrays.asList(1)); + + //断言 + assertEquals(2, roles.size()); + roles.stream().forEach(r -> assertPojoEquals(idRoleMap.get(r.getId()), r)); + + } + + @Test + public void testGetRolePage_success() { + Map idRoleMap = new HashMap<>(); + // 验证名称包含"role", 状态为1,code为"code"的角色 + // 第一页 + SysRoleDO roleDO = createRoleDO("role1", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "code"); + roleMapper.insert(roleDO); + idRoleMap.put(roleDO.getId(), roleDO); + // 第二页 + roleDO = createRoleDO("role2", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "code"); + roleMapper.insert(roleDO); + + // 以下是排除的角色 + roleDO = createRoleDO("role3", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 2, "code"); + roleMapper.insert(roleDO); + roleDO = createRoleDO("role4", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "xxxxx"); + roleMapper.insert(roleDO); + + //调用 + SysRolePageReqVO reqVO = randomPojo(SysRolePageReqVO.class, o -> { + o.setName("role"); + o.setCode("code"); + o.setStatus(1); + o.setPageNo(1); + o.setPageSize(1); + o.setBeginTime(null); + o.setEndTime(null); + }); + PageResult result = sysRoleService.getRolePage(reqVO); + assertEquals(2, result.getTotal()); + result.getList().stream().forEach(r -> assertPojoEquals(idRoleMap.get(r.getId()), r)); + } + + @Test + public void testCheckDuplicateRole_success() { + sysRoleService.checkDuplicateRole(randomString(), randomString(), null); + } + + @Test + public void testCheckDuplicateRole_nameDuplicate() { + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + + String duplicateName = "role_name"; + + assertServiceException(() -> sysRoleService.checkDuplicateRole(duplicateName, randomString(), null), ROLE_NAME_DUPLICATE, duplicateName); + } + + @Test + public void testCheckDuplicateRole_codeDuplicate() { + SysRoleDO roleDO = randomPojo(SysRoleDO.class, o -> { + o.setName("role_999"); + o.setCode("code"); + o.setType(SysRoleTypeEnum.CUSTOM.getType()); + o.setStatus(1); + o.setDataScope(DataScopeEnum.ALL.getScore()); + }); + roleMapper.insert(roleDO); + + String randomName = randomString(); + String duplicateCode = "code"; + + assertServiceException(() -> sysRoleService.checkDuplicateRole(randomName, duplicateCode, null), ROLE_CODE_DUPLICATE, duplicateCode); + } + + @Test + public void testCheckUpdateRole_success() { + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + sysRoleService.checkUpdateRole(roleId); + } + + @Test + public void testCheckUpdateRole_roleIdNotExist() { + assertServiceException(() -> sysRoleService.checkUpdateRole(randomLongId()), ROLE_NOT_EXISTS); + } + + @Test + public void testCheckUpdateRole_systemRoleCanNotBeUpdate() { + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.SYSTEM, DataScopeEnum.ALL); + roleMapper.insert(roleDO); + Long roleId = roleDO.getId(); + + assertServiceException(() -> sysRoleService.checkUpdateRole(roleId), ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); + } + + private SysRoleDO createRoleDO(String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status) { + return createRoleDO( name, typeEnum, scopeEnum, status, randomString()); + } + + private SysRoleDO createRoleDO(String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status, String code) { + return createRoleDO(null, name, typeEnum, scopeEnum, status, code); + } + + private SysRoleDO createRoleDO(String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum) { + return createRoleDO(null, name, typeEnum, scopeEnum, randomCommonStatus(), randomString()); + } + + private SysRoleDO createRoleDO(Long id, String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status, String code) { + SysRoleDO roleDO = randomPojo(SysRoleDO.class, o -> { + o.setId(id); + o.setName(name); + o.setType(typeEnum.getType()); + o.setStatus(status); + o.setDataScope(scopeEnum.getScore()); + o.setCode(code); + }); + return roleDO; + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelServiceTest.java new file mode 100644 index 000000000..a662b82aa --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelServiceTest.java @@ -0,0 +1,202 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.dashboard.BaseDbUnitTest; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsChannelMapper; +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsChannelServiceImpl; +import cn.iocoder.dashboard.util.collection.ArrayUtils; +import cn.iocoder.dashboard.util.object.ObjectUtils; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; +import static cn.iocoder.dashboard.util.AssertUtils.*; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static cn.iocoder.dashboard.util.date.DateUtils.buildTime; +import static cn.iocoder.dashboard.util.object.ObjectUtils.max; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** +* {@link SysSmsChannelServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(SysSmsChannelServiceImpl.class) +public class SysSmsChannelServiceTest extends BaseDbUnitTest { + + @Resource + private SysSmsChannelServiceImpl smsChannelService; + + @Resource + private SysSmsChannelMapper smsChannelMapper; + + @MockBean + private SmsClientFactory smsClientFactory; + @MockBean + private SysSmsTemplateService smsTemplateService; + @MockBean + private SysSmsProducer smsProducer; + + @Test + public void testInitLocalCache_success() { + // mock 数据s + SysSmsChannelDO smsChannelDO01 = randomSmsChannelDO(); + smsChannelMapper.insert(smsChannelDO01); + SysSmsChannelDO smsChannelDO02 = randomSmsChannelDO(); + smsChannelMapper.insert(smsChannelDO02); + + // 调用 + smsChannelService.initSmsClients(); + // 校验 maxUpdateTime 属性 + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(smsChannelService, "maxUpdateTime"); + assertEquals(max(smsChannelDO01.getUpdateTime(), smsChannelDO02.getUpdateTime()), maxUpdateTime); + // 校验调用 + verify(smsClientFactory, times(1)).createOrUpdateSmsClient( + argThat(properties -> isPojoEquals(smsChannelDO01, properties))); + verify(smsClientFactory, times(1)).createOrUpdateSmsClient( + argThat(properties -> isPojoEquals(smsChannelDO02, properties))); + } + + @Test + public void testCreateSmsChannel_success() { + // 准备参数 + SysSmsChannelCreateReqVO reqVO = randomPojo(SysSmsChannelCreateReqVO.class, o -> o.setStatus(randomCommonStatus())); + + // 调用 + Long smsChannelId = smsChannelService.createSmsChannel(reqVO); + // 断言 + assertNotNull(smsChannelId); + // 校验记录的属性是否正确 + SysSmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); + assertPojoEquals(reqVO, smsChannel); + // 校验调用 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage(); + } + + @Test + public void testUpdateSmsChannel_success() { + // mock 数据 + SysSmsChannelDO dbSmsChannel = randomSmsChannelDO(); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SysSmsChannelUpdateReqVO reqVO = randomPojo(SysSmsChannelUpdateReqVO.class, o -> { + o.setId(dbSmsChannel.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setCallbackUrl(randomString()); + }); + + // 调用 + smsChannelService.updateSmsChannel(reqVO); + // 校验是否更新正确 + SysSmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, smsChannel); + // 校验调用 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage(); + } + + @Test + public void testUpdateSmsChannel_notExists() { + // 准备参数 + SysSmsChannelUpdateReqVO reqVO = randomPojo(SysSmsChannelUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.updateSmsChannel(reqVO), SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testDeleteSmsChannel_success() { + // mock 数据 + SysSmsChannelDO dbSmsChannel = randomSmsChannelDO(); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsChannel.getId(); + + // 调用 + smsChannelService.deleteSmsChannel(id); + // 校验数据不存在了 + assertNull(smsChannelMapper.selectById(id)); + // 校验调用 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage(); + } + + @Test + public void testDeleteSmsChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testDeleteSmsChannel_hasChildren() { + // mock 数据 + SysSmsChannelDO dbSmsChannel = randomSmsChannelDO(); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsChannel.getId(); + // mock 方法 + when(smsTemplateService.countByChannelId(eq(id))).thenReturn(10); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_HAS_CHILDREN); + } + + @Test + public void testGetSmsChannelPage() { + // mock 数据 + SysSmsChannelDO dbSmsChannel = randomPojo(SysSmsChannelDO.class, o -> { // 等会查询到 + o.setSignature("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + }); + smsChannelMapper.insert(dbSmsChannel); + // 测试 signature 不匹配 + smsChannelMapper.insert(ObjectUtils.clone(dbSmsChannel, o -> o.setSignature("源码"))); + // 测试 status 不匹配 + smsChannelMapper.insert(ObjectUtils.clone(dbSmsChannel, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + smsChannelMapper.insert(ObjectUtils.clone(dbSmsChannel, o -> o.setCreateTime(buildTime(2020, 11, 11)))); + // 准备参数 + SysSmsChannelPageReqVO reqVO = new SysSmsChannelPageReqVO(); + reqVO.setSignature("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setBeginCreateTime(buildTime(2020, 12, 1)); + reqVO.setEndCreateTime(buildTime(2020, 12, 24)); + + // 调用 + PageResult pageResult = smsChannelService.getSmsChannelPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsChannel, pageResult.getList().get(0)); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SysSmsChannelDO randomSmsChannelDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(SysSmsChannelDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogServiceTest.java new file mode 100644 index 000000000..b7152d1ba --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogServiceTest.java @@ -0,0 +1,248 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.dashboard.BaseDbUnitTest; +import cn.iocoder.dashboard.common.enums.UserTypeEnum; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsLogMapper; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsTemplateTypeEnum; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsLogServiceImpl; +import cn.iocoder.dashboard.util.collection.ArrayUtils; +import cn.iocoder.dashboard.util.object.ObjectUtils; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomBoolean; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static cn.iocoder.dashboard.util.date.DateUtils.buildTime; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link SysSmsLogServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(SysSmsLogServiceImpl.class) +public class SysSmsLogServiceTest extends BaseDbUnitTest { + + @Resource + private SysSmsLogServiceImpl smsLogService; + + @Resource + private SysSmsLogMapper smsLogMapper; + + @Test + public void testCreateSmsLog() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + Boolean isSend = randomBoolean(); + SysSmsTemplateDO templateDO = randomPojo(SysSmsTemplateDO.class, + o -> o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType())); + String templateContent = randomString(); + Map templateParams = randomTemplateParams(); + // mock 方法 + + // 调用 + Long logId = smsLogService.createSmsLog(mobile, userId, userType, isSend, + templateDO, templateContent, templateParams); + // 断言 + SysSmsLogDO logDO = smsLogMapper.selectById(logId); + assertEquals(isSend ? SysSmsSendStatusEnum.INIT.getStatus() : SysSmsSendStatusEnum.IGNORE.getStatus(), + logDO.getSendStatus()); + assertEquals(mobile, logDO.getMobile()); + assertEquals(userType, logDO.getUserType()); + assertEquals(userId, logDO.getUserId()); + assertEquals(templateDO.getId(), logDO.getTemplateId()); + assertEquals(templateDO.getCode(), logDO.getTemplateCode()); + assertEquals(templateDO.getType(), logDO.getTemplateType()); + assertEquals(templateDO.getChannelId(), logDO.getChannelId()); + assertEquals(templateDO.getChannelCode(), logDO.getChannelCode()); + assertEquals(templateContent, logDO.getTemplateContent()); + assertEquals(templateParams, logDO.getTemplateParams()); + assertEquals(SysSmsReceiveStatusEnum.INIT.getStatus(), logDO.getReceiveStatus()); + } + + @Test + public void testUpdateSmsSendResult() { + // mock 数据 + SysSmsLogDO dbSmsLog = randomSmsLogDO( + o -> o.setSendStatus(SysSmsSendStatusEnum.IGNORE.getStatus())); + smsLogMapper.insert(dbSmsLog); + // 准备参数 + Long id = dbSmsLog.getId(); + Integer sendCode = randomInteger(); + String sendMsg = randomString(); + String apiSendCode = randomString(); + String apiSendMsg = randomString(); + String apiRequestId = randomString(); + String apiSerialNo = randomString(); + + // 调用 + smsLogService.updateSmsSendResult(id, sendCode, sendMsg, + apiSendCode, apiSendMsg, apiRequestId, apiSerialNo); + // 断言 + dbSmsLog = smsLogMapper.selectById(id); + assertEquals(CommonResult.isSuccess(sendCode) ? SysSmsSendStatusEnum.SUCCESS.getStatus() + : SysSmsSendStatusEnum.FAILURE.getStatus(), dbSmsLog.getSendStatus()); + assertNotNull(dbSmsLog.getSendTime()); + assertEquals(sendMsg, dbSmsLog.getSendMsg()); + assertEquals(apiSendCode, dbSmsLog.getApiSendCode()); + assertEquals(apiSendMsg, dbSmsLog.getApiSendMsg()); + assertEquals(apiRequestId, dbSmsLog.getApiRequestId()); + assertEquals(apiSerialNo, dbSmsLog.getApiSerialNo()); + } + + @Test + public void testUpdateSmsReceiveResult() { + // mock 数据 + SysSmsLogDO dbSmsLog = randomSmsLogDO( + o -> o.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus())); + smsLogMapper.insert(dbSmsLog); + // 准备参数 + Long id = dbSmsLog.getId(); + Boolean success = randomBoolean(); + Date receiveTime = randomDate(); + String apiReceiveCode = randomString(); + String apiReceiveMsg = randomString(); + + // 调用 + smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg); + // 断言 + dbSmsLog = smsLogMapper.selectById(id); + assertEquals(success ? SysSmsReceiveStatusEnum.SUCCESS.getStatus() + : SysSmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus()); + assertEquals(receiveTime, dbSmsLog.getReceiveTime()); + assertEquals(apiReceiveCode, dbSmsLog.getApiReceiveCode()); + assertEquals(apiReceiveMsg, dbSmsLog.getApiReceiveMsg()); + } + + @Test + public void testGetSmsLogPage() { + // mock 数据 + SysSmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 + o.setChannelId(1L); + o.setTemplateId(10L); + o.setMobile("15601691300"); + o.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2020, 11, 11)); + o.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus()); + o.setReceiveTime(buildTime(2021, 11, 11)); + }); + smsLogMapper.insert(dbSmsLog); + // 测试 channelId 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setChannelId(2L))); + // 测试 templateId 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setTemplateId(20L))); + // 测试 mobile 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setMobile("18818260999"))); + // 测试 sendStatus 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendStatus(SysSmsSendStatusEnum.IGNORE.getStatus()))); + // 测试 sendTime 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); + // 测试 receiveStatus 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveStatus(SysSmsReceiveStatusEnum.SUCCESS.getStatus()))); + // 测试 receiveTime 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); + // 准备参数 + SysSmsLogPageReqVO reqVO = new SysSmsLogPageReqVO(); + reqVO.setChannelId(1L); + reqVO.setTemplateId(10L); + reqVO.setMobile("156"); + reqVO.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus()); + reqVO.setBeginSendTime(buildTime(2020, 11, 1)); + reqVO.setEndSendTime(buildTime(2020, 11, 30)); + reqVO.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus()); + reqVO.setBeginReceiveTime(buildTime(2021, 11, 1)); + reqVO.setEndReceiveTime(buildTime(2021, 11, 30)); + + // 调用 + PageResult pageResult = smsLogService.getSmsLogPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsLog, pageResult.getList().get(0)); + } + + @Test + public void testGetSmsLogList() { + // mock 数据 + SysSmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 + o.setChannelId(1L); + o.setTemplateId(10L); + o.setMobile("15601691300"); + o.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2020, 11, 11)); + o.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus()); + o.setReceiveTime(buildTime(2021, 11, 11)); + }); + smsLogMapper.insert(dbSmsLog); + // 测试 channelId 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setChannelId(2L))); + // 测试 templateId 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setTemplateId(20L))); + // 测试 mobile 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setMobile("18818260999"))); + // 测试 sendStatus 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendStatus(SysSmsSendStatusEnum.IGNORE.getStatus()))); + // 测试 sendTime 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); + // 测试 receiveStatus 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveStatus(SysSmsReceiveStatusEnum.SUCCESS.getStatus()))); + // 测试 receiveTime 不匹配 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); + // 准备参数 + SysSmsLogExportReqVO reqVO = new SysSmsLogExportReqVO(); + reqVO.setChannelId(1L); + reqVO.setTemplateId(10L); + reqVO.setMobile("156"); + reqVO.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus()); + reqVO.setBeginSendTime(buildTime(2020, 11, 1)); + reqVO.setEndSendTime(buildTime(2020, 11, 30)); + reqVO.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus()); + reqVO.setBeginReceiveTime(buildTime(2021, 11, 1)); + reqVO.setEndReceiveTime(buildTime(2021, 11, 30)); + + // 调用 + List list = smsLogService.getSmsLogList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSmsLog, list.get(0)); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SysSmsLogDO randomSmsLogDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setTemplateParams(randomTemplateParams()); + o.setTemplateType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 templateType 的范围 + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); // 保证 userType 的范围 + o.setSendStatus(randomEle(SysSmsSendStatusEnum.values()).getStatus()); // 保证 sendStatus 的范围 + o.setReceiveStatus(randomEle(SysSmsReceiveStatusEnum.values()).getStatus()); // 保证 receiveStatus 的范围 + }; + return randomPojo(SysSmsLogDO.class, ArrayUtils.append(consumer, consumers)); + } + + + private static Map randomTemplateParams() { + return MapUtil.builder().put(randomString(), randomString()) + .put(randomString(), randomString()).build(); + } +} diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceTest.java new file mode 100644 index 000000000..f84e8753f --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceTest.java @@ -0,0 +1,201 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.dashboard.BaseMockitoUnitTest; +import cn.iocoder.dashboard.common.core.KeyValue; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.common.enums.UserTypeEnum; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsServiceImpl; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link SysSmsServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +public class SysSmsServiceTest extends BaseMockitoUnitTest { + + @InjectMocks + private SysSmsServiceImpl smsService; + + @Mock + private SysSmsTemplateService smsTemplateService; + @Mock + private SysSmsLogService smsLogService; + @Mock + private SysSmsProducer smsProducer; + @Mock + private SmsClientFactory smsClientFactory; + + /** + * 发送成功,当短信模板开启时 + */ + @Test + public void testSendSingleSms_successWhenSmsTemplateEnable() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock SmsTemplateService 的方法 + SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer, times(1)).sendSmsSendMessage(eq(smsLogId), eq(mobile), + eq(template.getChannelId()), eq(template.getApiTemplateId()), + eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); + } + + /** + * 发送成功,当短信模板关闭时 + */ + @Test + public void testSendSingleSms_successWhenSmsTemplateDisable() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock SmsTemplateService 的方法 + SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), + anyLong(), any(), anyList()); + } + + @Test + public void testCheckSmsTemplateValid_notExists() { + // 准备参数 + String templateCode = randomString(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.checkSmsTemplateValid(templateCode), + SMS_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testBuildTemplateParams_paramMiss() { + // 准备参数 + SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class, + o -> o.setParams(Lists.newArrayList("code"))); + Map templateParams = new HashMap<>(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.buildTemplateParams(template, templateParams), + SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, "code"); + } + + @Test + public void testCheckMobile_notExists() { + // 准备参数 + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.checkMobile(null), + SMS_SEND_MOBILE_NOT_EXISTS); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() { + // 准备参数 + SysSmsSendMessage message = randomPojo(SysSmsSendMessage.class); + // mock SmsClientFactory 的方法 + SmsClient smsClient = spy(SmsClient.class); + when(smsClientFactory.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient); + // mock SmsClient 的方法 + SmsCommonResult sendResult = randomPojo(SmsCommonResult.class, SmsSendRespDTO.class); + when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()), + eq(message.getTemplateParams()))).thenReturn(sendResult); + + // 调用 + smsService.doSendSms(message); + // 断言 + verify(smsLogService, times(1)).updateSmsSendResult(eq(message.getLogId()), + eq(sendResult.getCode()), eq(sendResult.getMsg()), eq(sendResult.getApiCode()), + eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getData().getSerialNo())); + } + + @Test + public void testReceiveSmsStatus() throws Throwable { + // 准备参数 + String channelCode = randomString(); + String text = randomString(); + // mock SmsClientFactory 的方法 + SmsClient smsClient = spy(SmsClient.class); + when(smsClientFactory.getSmsClient(eq(channelCode))).thenReturn(smsClient); + // mock SmsClient 的方法 + List receiveResults = randomPojoList(SmsReceiveRespDTO.class); + + // 调用 + smsService.receiveSmsStatus(channelCode, text); + // 断言 + receiveResults.forEach(result -> { + smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), + eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode())); + }); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateServiceTest.java new file mode 100644 index 000000000..f4bd9efa6 --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateServiceTest.java @@ -0,0 +1,380 @@ +package cn.iocoder.dashboard.modules.system.service.sms; + +import cn.iocoder.dashboard.BaseDbUnitTest; +import cn.iocoder.dashboard.common.enums.CommonStatusEnum; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient; +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult; +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO; +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO; +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO; +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsTemplateMapper; +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsTemplateTypeEnum; +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer; +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsTemplateServiceImpl; +import cn.iocoder.dashboard.util.collection.ArrayUtils; +import cn.iocoder.dashboard.util.object.ObjectUtils; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.hutool.core.bean.BeanUtil.getFieldValue; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException; +import static cn.iocoder.dashboard.util.RandomUtils.*; +import static cn.iocoder.dashboard.util.date.DateUtils.buildTime; +import static cn.iocoder.dashboard.util.object.ObjectUtils.max; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** +* {@link SysSmsTemplateServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(SysSmsTemplateServiceImpl.class) +public class SysSmsTemplateServiceTest extends BaseDbUnitTest { + + @Resource + private SysSmsTemplateServiceImpl smsTemplateService; + + @Resource + private SysSmsTemplateMapper smsTemplateMapper; + + @MockBean + private SysSmsChannelService smsChannelService; + @MockBean + private SmsClientFactory smsClientFactory; + @MockBean + private SmsClient smsClient; + @MockBean + private SysSmsProducer smsProducer; + + @Test + @SuppressWarnings("unchecked") + void testInitLocalCache() { + // mock 数据 + SysSmsTemplateDO smsTemplate01 = randomSmsTemplateDO(); + smsTemplateMapper.insert(smsTemplate01); + SysSmsTemplateDO smsTemplate02 = randomSmsTemplateDO(); + smsTemplateMapper.insert(smsTemplate02); + + // 调用 + smsTemplateService.initLocalCache(); + // 断言 deptCache 缓存 + Map smsTemplateCache = (Map) getFieldValue(smsTemplateService, "smsTemplateCache"); + assertEquals(2, smsTemplateCache.size()); + assertPojoEquals(smsTemplate01, smsTemplateCache.get(smsTemplate01.getCode())); + assertPojoEquals(smsTemplate02, smsTemplateCache.get(smsTemplate02.getCode())); + // 断言 maxUpdateTime 缓存 + Date maxUpdateTime = (Date) getFieldValue(smsTemplateService, "maxUpdateTime"); + assertEquals(max(smsTemplate01.getUpdateTime(), smsTemplate02.getUpdateTime()), maxUpdateTime); + } + + @Test + public void testParseTemplateContentParams() { + // 准备参数 + String content = "正在进行登录操作{operation},您的验证码是{code}"; + // mock 方法 + + // 调用 + List params = smsTemplateService.parseTemplateContentParams(content); + // 断言 + assertEquals(Lists.newArrayList("operation", "code"), params); + } + + @Test + @SuppressWarnings("unchecked") + public void testCreateSmsTemplate_success() { + // 准备参数 + SysSmsTemplateCreateReqVO reqVO = randomPojo(SysSmsTemplateCreateReqVO.class, o -> { + o.setContent("正在进行登录操作{operation},您的验证码是{code}"); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }); + // mock Channel 的方法 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> { + o.setId(reqVO.getChannelId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); + // mock 获得 API 短信模板成功 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class, + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()))); + + // 调用 + Long smsTemplateId = smsTemplateService.createSmsTemplate(reqVO); + // 断言 + assertNotNull(smsTemplateId); + // 校验记录的属性是否正确 + SysSmsTemplateDO smsTemplate = smsTemplateMapper.selectById(smsTemplateId); + assertPojoEquals(reqVO, smsTemplate); + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); + // 校验调用 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage(); + } + + @Test + @SuppressWarnings("unchecked") + public void testUpdateSmsTemplate_success() { + // mock 数据 + SysSmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SysSmsTemplateUpdateReqVO reqVO = randomPojo(SysSmsTemplateUpdateReqVO.class, o -> { + o.setId(dbSmsTemplate.getId()); // 设置更新的 ID + o.setContent("正在进行登录操作{operation},您的验证码是{code}"); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }); + // mock 方法 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> { + o.setId(reqVO.getChannelId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); + // mock 获得 API 短信模板成功 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class, + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()))); + + // 调用 + smsTemplateService.updateSmsTemplate(reqVO); + // 校验是否更新正确 + SysSmsTemplateDO smsTemplate = smsTemplateMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, smsTemplate); + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); + // 校验调用 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage(); + } + + @Test + public void testUpdateSmsTemplate_notExists() { + // 准备参数 + SysSmsTemplateUpdateReqVO reqVO = randomPojo(SysSmsTemplateUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> smsTemplateService.updateSmsTemplate(reqVO), SMS_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testDeleteSmsTemplate_success() { + // mock 数据 + SysSmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsTemplate.getId(); + + // 调用 + smsTemplateService.deleteSmsTemplate(id); + // 校验数据不存在了 + assertNull(smsTemplateMapper.selectById(id)); + // 校验调用 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage(); + } + + @Test + public void testDeleteSmsTemplate_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> smsTemplateService.deleteSmsTemplate(id), SMS_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testGetSmsTemplatePage() { + // mock 数据 + SysSmsTemplateDO dbSmsTemplate = randomPojo(SysSmsTemplateDO.class, o -> { // 等会查询到 + o.setType(SysSmsTemplateTypeEnum.PROMOTION.getType()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCode("yudaoyuanma"); + o.setContent("芋道源码"); + o.setApiTemplateId("yunai"); + o.setChannelId(1L); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + smsTemplateMapper.insert(dbSmsTemplate); + // 测试 type 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setType(SysSmsTemplateTypeEnum.VERIFICATION_CODE.getType()))); + // 测试 status 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 code 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCode("yuanma"))); + // 测试 content 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setContent("源码"))); + // 测试 apiTemplateId 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setApiTemplateId("nai"))); + // 测试 channelId 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setChannelId(2L))); + // 测试 createTime 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + SysSmsTemplatePageReqVO reqVO = new SysSmsTemplatePageReqVO(); + reqVO.setType(SysSmsTemplateTypeEnum.PROMOTION.getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCode("yudao"); + reqVO.setContent("芋道"); + reqVO.setApiTemplateId("yu"); + reqVO.setChannelId(1L); + reqVO.setBeginCreateTime(buildTime(2021, 11, 1)); + reqVO.setEndCreateTime(buildTime(2021, 12, 1)); + + // 调用 + PageResult pageResult = smsTemplateService.getSmsTemplatePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsTemplate, pageResult.getList().get(0)); + } + + @Test + public void testGetSmsTemplateList() { + // mock 数据 + SysSmsTemplateDO dbSmsTemplate = randomPojo(SysSmsTemplateDO.class, o -> { // 等会查询到 + o.setType(SysSmsTemplateTypeEnum.PROMOTION.getType()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCode("yudaoyuanma"); + o.setContent("芋道源码"); + o.setApiTemplateId("yunai"); + o.setChannelId(1L); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + smsTemplateMapper.insert(dbSmsTemplate); + // 测试 type 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setType(SysSmsTemplateTypeEnum.VERIFICATION_CODE.getType()))); + // 测试 status 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 code 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCode("yuanma"))); + // 测试 content 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setContent("源码"))); + // 测试 apiTemplateId 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setApiTemplateId("nai"))); + // 测试 channelId 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setChannelId(2L))); + // 测试 createTime 不匹配 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + SysSmsTemplateExportReqVO reqVO = new SysSmsTemplateExportReqVO(); + reqVO.setType(SysSmsTemplateTypeEnum.PROMOTION.getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCode("yudao"); + reqVO.setContent("芋道"); + reqVO.setApiTemplateId("yu"); + reqVO.setChannelId(1L); + reqVO.setBeginCreateTime(buildTime(2021, 11, 1)); + reqVO.setEndCreateTime(buildTime(2021, 12, 1)); + + // 调用 + List list = smsTemplateService.getSmsTemplateList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSmsTemplate, list.get(0)); + } + + @Test + public void testCheckSmsChannel_success() { + // 准备参数 + Long channelId = randomLongId(); + // mock 方法 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> { + o.setId(channelId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); + + // 调用 + SysSmsChannelDO returnChannelDO = smsTemplateService.checkSmsChannel(channelId); + // 断言 + assertPojoEquals(returnChannelDO, channelDO); + } + + @Test + public void testCheckSmsChannel_notExists() { + // 准备参数 + Long channelId = randomLongId(); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsChannel(channelId), + SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testCheckSmsChannel_disable() { + // 准备参数 + Long channelId = randomLongId(); + // mock 方法 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> { + o.setId(channelId); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 保证 status 禁用,触发失败 + }); + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsChannel(channelId), + SMS_CHANNEL_DISABLE); + } + + @Test + public void testCheckDictDataValueUnique_success() { + // 调用,成功 + smsTemplateService.checkSmsTemplateCodeDuplicate(randomLongId(), randomString()); + } + + @Test + public void testCheckSmsTemplateCodeDuplicate_valueDuplicateForCreate() { + // 准备参数 + String code = randomString(); + // mock 数据 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsTemplateCodeDuplicate(null, code), + SMS_TEMPLATE_CODE_DUPLICATE, code); + } + + @Test + public void testCheckDictDataValueUnique_valueDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String code = randomString(); + // mock 数据 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.checkSmsTemplateCodeDuplicate(id, code), + SMS_TEMPLATE_CODE_DUPLICATE, code); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SysSmsTemplateDO randomSmsTemplateDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }; + return randomPojo(SysSmsTemplateDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/util/AopTargetUtils.java b/src/test/java/cn/iocoder/dashboard/util/AopTargetUtils.java new file mode 100644 index 000000000..89a0d93f5 --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/util/AopTargetUtils.java @@ -0,0 +1,46 @@ +package cn.iocoder.dashboard.util; + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +/** + * Spring AOP 工具类 + * + * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现 + */ +public class AopTargetUtils { + + /** + * 获取代理的目标对象 + * + * @param proxy 代理对象 + * @return 目标对象 + */ + public static Object getTarget(Object proxy) throws Exception { + // 不是代理对象 + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + // Jdk 代理 + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } + // Cglib 代理 + return getCglibProxyTargetObject(proxy); + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java b/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java index 042208530..0d6987549 100644 --- a/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java +++ b/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.function.Executable; import java.lang.reflect.Field; import java.util.Arrays; +import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -50,6 +51,33 @@ public class AssertUtils { }); } + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + * @return 是否一致 + */ + public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + return Arrays.stream(expectedFields).allMatch(expectedField -> { + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return true; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return true; + } + return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField)); + }); + } + /** * 执行方法,校验抛出的 Service 是否符合条件 * @@ -62,7 +90,7 @@ public class AssertUtils { ServiceException serviceException = assertThrows(ServiceException.class, executable); // 校验错误码 Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); - String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMessage(), messageParams); + String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); } diff --git a/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java b/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java index c668f980c..717f6d490 100644 --- a/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java +++ b/src/test/java/cn/iocoder/dashboard/util/RandomUtils.java @@ -7,9 +7,8 @@ import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO; import uk.co.jemos.podam.api.PodamFactory; import uk.co.jemos.podam.api.PodamFactoryImpl; -import java.util.Arrays; -import java.util.Date; -import java.util.Set; +import java.lang.reflect.Type; +import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -87,4 +86,21 @@ public class RandomUtils { return pojo; } + @SafeVarargs + public static T randomPojo(Class clazz, Type type, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static List randomPojoList(Class clazz, Consumer... consumers) { + int size = RandomUtil.randomInt(0, RANDOM_COLLECTION_LENGTH); + return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) + .collect(Collectors.toList()); + } + } diff --git a/src/test/resources/sql/clean.sql b/src/test/resources/sql/clean.sql index c2d1d63d8..2eedb0ed7 100644 --- a/src/test/resources/sql/clean.sql +++ b/src/test/resources/sql/clean.sql @@ -19,3 +19,6 @@ DELETE FROM "sys_post"; DELETE FROM "sys_login_log"; DELETE FROM "sys_operate_log"; DELETE FROM "sys_user"; +DELETE FROM "sys_sms_channel"; +DELETE FROM "sys_sms_template"; +DELETE FROM "sys_sms_log"; diff --git a/src/test/resources/sql/create_tables.sql b/src/test/resources/sql/create_tables.sql index 23bf9c07b..963d18fa5 100644 --- a/src/test/resources/sql/create_tables.sql +++ b/src/test/resources/sql/create_tables.sql @@ -47,8 +47,7 @@ CREATE TABLE IF NOT EXISTS "inf_job" ( PRIMARY KEY ("id") ) COMMENT='定时任务表'; -DROP TABLE IF EXISTS "inf_job_log"; -CREATE TABLE "inf_job_log" ( +CREATE TABLE IF NOT EXISTS "inf_job_log" ( "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '日志编号', "job_id" bigint(20) NOT NULL COMMENT '任务编号', "handler_name" varchar(64) NOT NULL COMMENT '处理器的名字', @@ -192,8 +191,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_session` ( PRIMARY KEY (`id`) ) COMMENT '用户在线 Session'; -CREATE TABLE IF NOT EXISTS "sys_post" -( +CREATE TABLE IF NOT EXISTS "sys_post" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "code" varchar(64) NOT NULL, "name" varchar(50) NOT NULL, @@ -208,7 +206,6 @@ CREATE TABLE IF NOT EXISTS "sys_post" PRIMARY KEY ("id") ) COMMENT '岗位信息表'; - CREATE TABLE IF NOT EXISTS "sys_notice" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "title" varchar(50) NOT NULL COMMENT '公告标题', @@ -223,7 +220,6 @@ CREATE TABLE IF NOT EXISTS "sys_notice" ( PRIMARY KEY("id") ) COMMENT '通知公告表'; - CREATE TABLE IF NOT EXISTS `sys_login_log` ( `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, `log_type` bigint(4) NOT NULL, @@ -240,7 +236,6 @@ CREATE TABLE IF NOT EXISTS `sys_login_log` ( PRIMARY KEY (`id`) ) COMMENT ='系统访问记录'; - CREATE TABLE IF NOT EXISTS `sys_operate_log` ( `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, `trace_id` varchar(64) NOT NULL DEFAULT '', @@ -346,3 +341,73 @@ CREATE TABLE IF NOT EXISTS "inf_api_error_log" ( "deleted" bit not null default false, primary key ("id") ) COMMENT '系统异常日志'; + +CREATE TABLE IF NOT EXISTS "sys_sms_channel" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "signature" varchar(10) NOT NULL, + "code" varchar(63) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "api_key" varchar(63) NOT NULL, + "api_secret" varchar(63) DEFAULT NULL, + "callback_url" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信渠道'; + +CREATE TABLE "sys_sms_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" tinyint NOT NULL, + "status" tinyint NOT NULL, + "code" varchar(63) NOT NULL, + "name" varchar(63) NOT NULL, + "content" varchar(255) NOT NULL, + "params" varchar(255) NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "api_template_id" varchar(63) NOT NULL, + "channel_id" bigint NOT NULL, + "channel_code" varchar(63) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信模板'; + +CREATE TABLE "sys_sms_log" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "channel_id" bigint NOT NULL, + "channel_code" varchar(63) NOT NULL, + "template_id" bigint NOT NULL, + "template_code" varchar(63) NOT NULL, + "template_type" tinyint NOT NULL, + "template_content" varchar(255) NOT NULL, + "template_params" varchar(255) NOT NULL, + "api_template_id" varchar(63) NOT NULL, + "mobile" varchar(11) NOT NULL, + "user_id" bigint DEFAULT '0', + "user_type" tinyint DEFAULT '0', + "send_status" tinyint NOT NULL DEFAULT '0', + "send_time" timestamp DEFAULT NULL, + "send_code" int DEFAULT NULL, + "send_msg" varchar(255) DEFAULT NULL, + "api_send_code" varchar(63) DEFAULT NULL, + "api_send_msg" varchar(255) DEFAULT NULL, + "api_request_id" varchar(255) DEFAULT NULL, + "api_serial_no" varchar(255) DEFAULT NULL, + "receive_status" tinyint NOT NULL DEFAULT '0', + "receive_time" timestamp DEFAULT NULL, + "api_receive_code" varchar(63) DEFAULT NULL, + "api_receive_msg" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信日志';