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 @@ + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 新增 + + + 导出 + + + + + + + + + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }} + + > + + + {{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }} + + > + + + + + + + {{ parseTime(scope.row.createTime) }} + + + + + 修改 + 删除 + + + + + + + + + + + + + + + + + + {{dict.label}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 新增 + + + 导出 + + + + + + + + + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }} + + > + + + {{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }} + + > + + + + {{ parseTime(scope.row.createTime) }} + + + + + 修改 + 删除 + + + + + + + + + + + + + + + + + + + + {{dict.label}} + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 新增 + + + 导出 + + + + + + + + + + {{ parseTime(scope.row.createTime) }} + + + + + {{ scope.row.mobile }} + + {{ getDictDataLabel(DICT_TYPE.USER_TYPE, scope.row.userType) + '(' + scope.row.userId + ')' }} + + + + + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, scope.row.sendStatus) }} + {{ parseTime(scope.row.sendTime) }} + + + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, scope.row.receiveStatus) }} + {{ parseTime(scope.row.receiveTime) }} + + + + + {{ formatChannelSignature(scope.row.channelId) }} + 【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】 + + + + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.templateType) }} + + + + + 详细 + + + + + + + + + + + + {{ form.id }} + + + + {{ formatChannelSignature(form.channelId) }}【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, form.channelCode) }}】 + + + + + {{ form.templateId }} | {{ form.templateCode}} | {{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, form.templateType) }} + + + + {{ form.apiTemplateId }} + + + {{ form.mobile }} + | {{ getDictDataLabel(DICT_TYPE.USER_TYPE, form.userType) }} | {{ form.userId }} + + + + {{ form.templateContent }} + + + {{ form.templateParams }} + + + {{ parseTime(form.createTime) }} + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, form.sendStatus) }} + + + {{ parseTime(form.sendTime) }} + + + {{ form.sendCode }} | {{ form.sendMsg }} + + + + {{ form.apiSendCode }} | {{ form.apiSendMsg }} + + + {{ form.apiSerialNo }} + + + {{ form.apiRequestId }} + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, form.receiveStatus) }} + + + {{ parseTime(form.receiveTime) }} + + + {{ form.apiReceiveCode }} | {{ form.apiReceiveMsg }} + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 新增 + + + 导出 + + + + + + + + + + + + {{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.type) }} + + + + + {{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }} + + + + + + + {{ formatChannelSignature(scope.row.channelId) }} + 【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】 + + + + + {{ parseTime(scope.row.createTime) }} + + + + + 测试 + 修改 + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{dict.label}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 extends Map> 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 extends Map> 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 extends Map> 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