Kaynağa Gözat

!29 【需求】短信功能
Merge pull request !29 from 芋道源码/feature/sms-send

芋道源码 4 yıl önce
ebeveyn
işleme
860d0d0163
100 değiştirilmiş dosya ile 5023 ekleme ve 139 silme
  1. 22 0
      pom.xml
  2. 7 0
      ruoyi-ui/.env.demo1024
  3. 1 0
      ruoyi-ui/package.json
  4. 52 0
      ruoyi-ui/src/api/system/sms/smsChannel.js
  5. 20 0
      ruoyi-ui/src/api/system/sms/smsLog.js
  6. 64 0
      ruoyi-ui/src/api/system/sms/smsTemplate.js
  7. 4 0
      ruoyi-ui/src/utils/dict.js
  8. 0 1
      ruoyi-ui/src/views/system/operatelog/index.vue
  9. 542 0
      ruoyi-ui/src/views/system/sms/smsChannel.vue
  10. 297 0
      ruoyi-ui/src/views/system/sms/smsLog.vue
  11. 405 0
      ruoyi-ui/src/views/system/sms/smsTemplate.vue
  12. 177 15
      sql/ruoyi-vue-pro.sql
  13. 20 0
      src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java
  14. 27 0
      src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java
  15. 4 3
      src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java
  16. 0 41
      src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java
  17. 1 1
      src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java
  18. 2 2
      src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java
  19. 11 15
      src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java
  20. 5 0
      src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java
  21. 39 0
      src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java
  22. 4 4
      src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java
  23. 10 4
      src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
  24. 21 0
      src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java
  25. 54 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java
  26. 36 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java
  27. 17 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java
  28. 68 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java
  29. 48 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java
  30. 18 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java
  31. 33 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java
  32. 122 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java
  33. 90 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java
  34. 212 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java
  35. 43 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java
  36. 23 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java
  37. 96 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java
  38. 204 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java
  39. 45 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java
  40. 37 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java
  41. 47 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java
  42. 21 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java
  43. 52 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java
  44. 1 24
      src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java
  45. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictDataController.java
  46. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictTypeController.java
  47. 2 2
      src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleVO.java
  48. 49 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SmsCallbackController.java
  49. 80 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsChannelController.java
  50. 60 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsLogController.java
  51. 12 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.http
  52. 98 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.java
  53. 38 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelBaseVO.java
  54. 21 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelCreateReqVO.java
  55. 35 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelPageReqVO.java
  56. 26 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelRespVO.java
  57. 24 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelSimpleRespVO.java
  58. 21 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelUpdateReqVO.java
  59. 101 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExcelVO.java
  60. 47 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExportReqVO.java
  61. 52 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogPageReqVO.java
  62. 89 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogRespVO.java
  63. 46 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateBaseVO.java
  64. 14 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateCreateReqVO.java
  65. 56 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExcelVO.java
  66. 42 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExportReqVO.java
  67. 47 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplatePageReqVO.java
  68. 30 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateRespVO.java
  69. 25 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateSendReqVO.java
  70. 21 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateUpdateReqVO.java
  71. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/convert/dict/SysDictDataConvert.java
  72. 39 0
      src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsChannelConvert.java
  73. 30 0
      src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsLogConvert.java
  74. 31 0
      src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsTemplateConvert.java
  75. 60 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsChannelDO.java
  76. 173 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsLogDO.java
  77. 89 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsTemplateDO.java
  78. 29 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsChannelMapper.java
  79. 40 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsLogMapper.java
  80. 53 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsTemplateMapper.java
  81. 0 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/redis/RedisKeyConstants.java
  82. 14 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java
  83. 4 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/dict/SysDictTypeEnum.java
  84. 23 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsReceiveStatusEnum.java
  85. 24 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsSendStatusEnum.java
  86. 25 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsTemplateTypeEnum.java
  87. 29 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsChannelRefreshConsumer.java
  88. 13 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsSendConsumer.java
  89. 29 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsTemplateRefreshConsumer.java
  90. 17 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsChannelRefreshMessage.java
  91. 15 13
      src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsSendMessage.java
  92. 17 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsTemplateRefreshMessage.java
  93. 2 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java
  94. 60 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/sms/SysSmsProducer.java
  95. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java
  96. 0 1
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java
  97. 9 9
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java
  98. 79 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelService.java
  99. 77 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogService.java
  100. 0 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsService.java

+ 22 - 0
pom.xml

@@ -46,6 +46,7 @@
         <easyexcel.verion>2.2.7</easyexcel.verion>
         <velocity.version>2.2</velocity.version>
         <screw.version>1.0.5</screw.version>
+        <!-- 三方云服务相关 -->
     </properties>
 
     <!-- 依赖声明 -->
@@ -271,6 +272,27 @@
             <version>${screw.version}</version>
         </dependency>
 
+        <!-- 三方云服务相关 -->
+
+        <!-- SMS SDK begin -->
+        <dependency>
+            <groupId>com.yunpian.sdk</groupId>
+            <artifactId>yunpian-java-sdk</artifactId>
+            <version>1.2.7</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+            <version>4.5.18</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
+            <version>2.1.0</version>
+        </dependency>
+        <!-- SMS SDK end -->
+
     </dependencies>
 
 

+ 7 - 0
ruoyi-ui/.env.demo1024

@@ -0,0 +1,7 @@
+NODE_ENV = production
+
+# 测试环境配置
+ENV = 'staging'
+
+# 芋道管理系统/测试环境
+VUE_APP_BASE_API = 'http://127.0.0.1:48080'

+ 1 - 0
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"
   },

+ 52 - 0
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',
+  })
+}

+ 20 - 0
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'
+  })
+}

+ 64 - 0
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'
+  })
+}
+

+ 4 - 0
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',

+ 0 - 1
ruoyi-ui/src/views/system/operatelog/index.vue

@@ -163,7 +163,6 @@ export default {
         businessType: undefined,
         status: undefined
       },
-
     };
   },
   created() {

+ 542 - 0
ruoyi-ui/src/views/system/sms/smsChannel.vue

@@ -0,0 +1,542 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="短信签名" prop="signature">
+        <el-input v-model="queryParams.signature" placeholder="请输入短信签名" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="启用状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间">
+        <el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:sms-channel:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:sms-channel:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="短信签名" align="center" prop="signature" />
+      <el-table-column label="渠道编码" align="center" prop="code">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }}</span>
+        </template>
+      </el-table-column>>
+      <el-table-column label="启用状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
+        </template>
+      </el-table-column>>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="短信 API 的账号" align="center" prop="apiKey" />
+      <el-table-column label="短信 API 的秘钥" align="center" prop="apiSecret" />
+      <el-table-column label="短信发送回调 URL" align="center" prop="callbackUrl" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:sms-channel:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:sms-channel:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="短信签名" prop="signature">
+          <el-input v-model="form.signature" placeholder="请输入短信签名" />
+        </el-form-item>
+        <el-form-item label="渠道编码" prop="code">
+          <el-input v-model="form.code" placeholder="请输入渠道编码" />
+        </el-form-item>
+        <el-form-item label="启用状态">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
+                      :key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注" />
+        </el-form-item>
+        <el-form-item label="短信 API 的账号" prop="apiKey">
+          <el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" />
+        </el-form-item>
+        <el-form-item label="短信 API 的秘钥" prop="apiSecret">
+          <el-input v-model="form.apiSecret" placeholder="请输入短信 API 的秘钥" />
+        </el-form-item>
+        <el-form-item label="短信发送回调 URL" prop="callbackUrl">
+          <el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { createSmsChannel, updateSmsChannel, deleteSmsChannel, getSmsChannel, getSmsChannelPage,
+  getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
+
+export default {
+  name: "SmsChannel",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 短信渠道列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      dateRangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        signature: null,
+        status: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        signature: [{ required: true, message: "短信签名不能为空", trigger: "blur" }],
+        code: [{ required: true, message: "渠道编码不能为空", trigger: "blur" }],
+        status: [{ required: true, message: "启用状态不能为空", trigger: "blur" }],
+        apiKey: [{ required: true, message: "短信 API 的账号不能为空", trigger: "blur" }],
+      },
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      getSmsChannelPage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        signature: undefined,
+        code: undefined,
+        status: undefined,
+        remark: undefined,
+        apiKey: undefined,
+        apiSecret: undefined,
+        callbackUrl: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeCreateTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加短信渠道";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id;
+      getSmsChannel(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改短信渠道";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        if (this.form.id != null) {
+          updateSmsChannel(this.form).then(response => {
+            this.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createSmsChannel(this.form).then(response => {
+          this.msgSuccess("新增成功");
+          this.open = false;
+          this.getList();
+        });
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$confirm('是否确认删除短信渠道编号为"' + id + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return deleteSmsChannel(id);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行导出
+      this.$confirm('是否确认导出所有短信渠道数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return exportSmsChannelExcel(params);
+      }).then(response => {
+        this.downloadExcel(response, '短信渠道.xls');
+      })
+    }
+  }
+};
+</script><template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="短信签名" prop="signature">
+        <el-input v-model="queryParams.signature" placeholder="请输入短信签名" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="启用状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间">
+        <el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:sms-channel:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:sms-channel:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="短信签名" align="center" prop="signature" />
+      <el-table-column label="渠道编码" align="center" prop="code">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }}</span>
+        </template>
+      </el-table-column>>
+      <el-table-column label="启用状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
+        </template>
+      </el-table-column>>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:sms-channel:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:sms-channel:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="130px">
+        <el-form-item label="短信签名" prop="signature">
+          <el-input v-model="form.signature" placeholder="请输入短信签名" />
+        </el-form-item>
+        <el-form-item label="渠道编码" prop="code">
+          <el-select v-model="form.code" placeholder="请选择渠道编码" :disabled="form.id > 0">
+            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_CHANNEL_CODE)"
+                       :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="启用状态">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
+                      :key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注" />
+        </el-form-item>
+        <el-form-item label="短信 API 的账号" prop="apiKey">
+          <el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" />
+        </el-form-item>
+        <el-form-item v-if="form.code !== 'YUN_PIAN'" label="短信 API 的秘钥" prop="apiSecret">
+          <el-input v-model="form.apiSecret" placeholder="请输入短信 API 的秘钥" />
+        </el-form-item>
+        <el-form-item label="短信发送回调 URL" prop="callbackUrl">
+          <el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { createSmsChannel, updateSmsChannel, deleteSmsChannel, getSmsChannel, getSmsChannelPage } from "@/api/system/sms/smsChannel";
+
+export default {
+  name: "SmsChannel",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 短信渠道列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      dateRangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        signature: null,
+        status: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      apiKeyEnableChannelCodes: ['YUN_PIAN'],
+      rules: {
+        signature: [{ required: true, message: "短信签名不能为空", trigger: "blur" }],
+        code: [{ required: true, message: "渠道编码不能为空", trigger: "blur" }],
+        status: [{ required: true, message: "启用状态不能为空", trigger: "blur" }],
+        apiKey: [{ required: true, message: "短信 API 的账号不能为空", trigger: "blur" }],
+        apiSecret: [{ required: true, message: "短信 API 的秘钥不能为空", trigger: "blur" }],
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      getSmsChannelPage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        signature: undefined,
+        code: undefined,
+        status: undefined,
+        remark: undefined,
+        apiKey: undefined,
+        apiSecret: undefined,
+        callbackUrl: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeCreateTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加短信渠道";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id;
+      getSmsChannel(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改短信渠道";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        if (this.form.id != null) {
+          updateSmsChannel(this.form).then(response => {
+            this.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createSmsChannel(this.form).then(response => {
+          this.msgSuccess("新增成功");
+          this.open = false;
+          this.getList();
+        });
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$confirm('是否确认删除短信渠道编号为"' + id + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return deleteSmsChannel(id);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行导出
+      this.$confirm('是否确认导出所有短信渠道数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return exportSmsChannelExcel(params);
+      }).then(response => {
+        this.downloadExcel(response, '短信渠道.xls');
+      })
+    }
+  }
+};
+</script>

+ 297 - 0
ruoyi-ui/src/views/system/sms/smsLog.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="手机号" prop="mobile">
+        <el-input v-model="queryParams.mobile" placeholder="请输入手机号" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="短信渠道" prop="channelId">
+        <el-select v-model="queryParams.channelId" placeholder="请选择短信渠道" clearable size="small">
+          <el-option v-for="channel in channelOptions"
+                     :key="channel.id" :value="channel.id"
+                     :label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="模板编号" prop="templateId">
+        <el-input v-model="queryParams.templateId" placeholder="请输入模板编号" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="发送状态" prop="sendStatus">
+        <el-select v-model="queryParams.sendStatus" placeholder="请选择发送状态" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_SEND_STATUS)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发送时间">
+        <el-date-picker v-model="dateRangeSendTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item label="接收状态" prop="receiveStatus">
+        <el-select v-model="queryParams.receiveStatus" placeholder="请选择接收状态" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_RECEIVE_STATUS)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="接收时间">
+        <el-date-picker v-model="dateRangeReceiveTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:sms-log:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:sms-log:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号" align="center" prop="mobile" width="120">
+        <template slot-scope="scope">
+          <div>{{ scope.row.mobile }}</div>
+          <div v-if="scope.row.userType && scope.row.userId">
+            {{ getDictDataLabel(DICT_TYPE.USER_TYPE, scope.row.userType) + '(' + scope.row.userId + ')' }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="短信内容" align="center" prop="templateContent" width="300" />
+      <el-table-column label="发送状态" align="center" width="180">
+        <template slot-scope="scope">
+          <div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, scope.row.sendStatus) }}</div>
+          <div>{{ parseTime(scope.row.sendTime) }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="接收状态" align="center" width="180">
+        <template slot-scope="scope">
+          <div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, scope.row.receiveStatus) }}</div>
+          <div>{{ parseTime(scope.row.receiveTime) }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="短信渠道" align="center" width="120">
+        <template slot-scope="scope">
+          <div>{{ formatChannelSignature(scope.row.channelId) }}</div>
+          <div>【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="模板编号" align="center" prop="templateId" />
+      <el-table-column label="短信类型" align="center" prop="templateType">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.templateType) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row,scope.index)"
+                     v-hasPermi="['system:sms-log:query']">详细</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 短信日志详细 -->
+    <el-dialog title="短信日志详细" :visible.sync="open" width="700px" append-to-body>
+      <el-form ref="form" :model="form" label-width="140px" size="mini">
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="日志主键:">{{ form.id }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="短信渠道:">
+              {{ formatChannelSignature(form.channelId) }}【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, form.channelCode) }}】
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="短信模板:">
+              {{ form.templateId }} | {{ form.templateCode}} | {{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, form.templateType) }}
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="API 的模板编号:">{{ form.apiTemplateId }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="用户信息:">{{ form.mobile }}
+              <span v-if="form.userType && form.userId"> | {{ getDictDataLabel(DICT_TYPE.USER_TYPE, form.userType) }} | {{ form.userId }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="短信内容:">{{ form.templateContent }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="短信参数:">{{ form.templateParams }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="创建时间:">{{ parseTime(form.createTime) }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="发送状态:">{{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, form.sendStatus) }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="发送时间:">{{ parseTime(form.sendTime) }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="发送结果:">{{ form.sendCode }} | {{ form.sendMsg }}
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="API 发送结果:">{{ form.apiSendCode }} | {{ form.apiSendMsg }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="API 短信编号:">{{ form.apiSerialNo }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="API 请求编号:">{{ form.apiRequestId }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="接收状态:">{{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, form.receiveStatus) }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="接收时间:">{{ parseTime(form.receiveTime) }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="API 接收结果:">{{ form.apiReceiveCode }} | {{ form.apiReceiveMsg }}
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="open = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { getSmsLogPage, exportSmsLogExcel } from "@/api/system/sms/smsLog";
+import {  getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
+
+export default {
+  name: "SmsLog",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 短信日志列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      dateRangeSendTime: [],
+      dateRangeReceiveTime: [],
+      // 表单参数
+      form: {},
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        channelId: null,
+        templateId: null,
+        mobile: null,
+        sendStatus: null,
+        receiveStatus: null,
+      },
+      // 短信渠道
+      channelOptions: [],
+    };
+  },
+  created() {
+    this.getList();
+    // 获得短信渠道
+    getSimpleSmsChannels().then(response => {
+      this.channelOptions = response.data;
+    })
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeSendTime, 'sendTime');
+      this.addBeginAndEndTime(params, this.dateRangeReceiveTime, 'receiveTime');
+      // 执行查询
+      getSmsLogPage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeSendTime = [];
+      this.dateRangeReceiveTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.addBeginAndEndTime(params, this.dateRangeSendTime, 'sendTime');
+      this.addBeginAndEndTime(params, this.dateRangeReceiveTime, 'receiveTime');
+      // 执行导出
+      this.$confirm('是否确认导出所有短信日志数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return exportSmsLogExcel(params);
+      }).then(response => {
+        this.downloadExcel(response, '短信日志.xls');
+      })
+    },
+    /** 详细按钮操作 */
+    handleView(row) {
+      this.open = true;
+      this.form = row;
+    },
+    /** 格式化短信渠道 */
+    formatChannelSignature(channelId) {
+      for (const channel of this.channelOptions) {
+        if (channel.id === channelId) {
+          return channel.signature;
+        }
+      }
+      return '找不到签名:' + channelId;
+    }
+  }
+};
+</script>

+ 405 - 0
ruoyi-ui/src/views/system/sms/smsTemplate.vue

@@ -0,0 +1,405 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="150px">
+      <el-form-item label="短信类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择短信类型" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="开启状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择开启状态" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="模板编码" prop="code">
+        <el-input v-model="queryParams.code" placeholder="请输入模板编码" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="短信 API 的模板编号" prop="apiTemplateId">
+        <el-input v-model="queryParams.apiTemplateId" placeholder="请输入短信 API 的模板编号" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="短信渠道" prop="channelId">
+        <el-select v-model="queryParams.channelId" placeholder="请选择短信渠道" clearable size="small">
+          <el-option v-for="channel in channelOptions"
+                     :key="channel.id" :value="channel.id"
+                     :label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间">
+        <el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:sms-template:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:sms-template:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="模板编码" align="center" prop="code" />
+      <el-table-column label="模板名称" align="center" prop="name" />
+      <el-table-column label="模板内容" align="center" prop="content" width="300" />
+      <el-table-column label="短信类型" align="center" prop="type">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.type) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="开启状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="短信 API 的模板编号" align="center" prop="apiTemplateId" width="180" />
+      <el-table-column label="短信渠道" align="center" width="120">
+        <template slot-scope="scope">
+          <div>{{ formatChannelSignature(scope.row.channelId) }}</div>
+          <div>【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-share" @click="handleSendSms(scope.row)"
+                     v-hasPermi="['system:sms-template:send-sms']">测试</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:sms-template:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:sms-template:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="140px">
+        <el-form-item label="短信渠道编号" prop="channelId">
+          <el-select v-model="form.channelId" placeholder="请选择短信渠道编号">
+            <el-option v-for="channel in channelOptions"
+                       :key="channel.id" :value="channel.id"
+                       :label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="短信类型" prop="type">
+          <el-select v-model="form.type" placeholder="请选择短信类型">
+            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE)"
+                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="模板编号" prop="code">
+          <el-input v-model="form.code" placeholder="请输入模板编号" />
+        </el-form-item>
+        <el-form-item label="模板名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板名称" />
+        </el-form-item>
+        <el-form-item label="模板内容" prop="content">
+          <el-input type="textarea" v-model="form.content" placeholder="请输入模板内容" />
+        </el-form-item>
+        <el-form-item label="开启状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
+                      :key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="短信 API 模板编号" prop="apiTemplateId">
+          <el-input v-model="form.apiTemplateId" placeholder="请输入短信 API 的模板编号" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 对话框(发送短信) -->
+    <el-dialog title="测试发送短信" :visible.sync="sendSmsOpen" width="500px" append-to-body>
+      <el-form ref="sendSmsForm" :model="sendSmsForm" :rules="sendSmsRules" label-width="140px">
+        <el-form-item label="模板内容" prop="content">
+          <el-input v-model="sendSmsForm.content" type="textarea" placeholder="请输入模板内容" readonly />
+        </el-form-item>
+        <el-form-item label="手机号" prop="mobile">
+          <el-input v-model="sendSmsForm.mobile" placeholder="请输入手机号" />
+        </el-form-item>
+        <el-form-item v-for="param in sendSmsForm.params" :label="'参数 {' + param + '}'" :prop="'templateParams.' + param">
+          <el-input v-model="sendSmsForm.templateParams[param]" :placeholder="'请输入 ' + param + ' 参数'" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitSendSmsForm">确 定</el-button>
+        <el-button @click="cancelSendSms">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { createSmsTemplate, updateSmsTemplate, deleteSmsTemplate, getSmsTemplate, getSmsTemplatePage,
+  exportSmsTemplateExcel, sendSms } from "@/api/system/sms/smsTemplate";
+import {  getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
+
+export default {
+  name: "SmsTemplate",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 短信模板列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      dateRangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        type: null,
+        status: null,
+        code: null,
+        content: null,
+        apiTemplateId: null,
+        channelId: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        type: [{ required: true, message: "短信类型不能为空", trigger: "change" }],
+        status: [{ required: true, message: "开启状态不能为空", trigger: "blur" }],
+        code: [{ required: true, message: "模板编码不能为空", trigger: "blur" }],
+        name: [{ required: true, message: "模板名称不能为空", trigger: "blur" }],
+        content: [{ required: true, message: "模板内容不能为空", trigger: "blur" }],
+        apiTemplateId: [{ required: true, message: "短信 API 的模板编号不能为空", trigger: "blur" }],
+        channelId: [{ required: true, message: "短信渠道编号不能为空", trigger: "change" }],
+      },
+      // 短信渠道
+      channelOptions: [],
+      // 发送短信
+      sendSmsOpen: false,
+      sendSmsForm: {
+        params: [], // 模板的参数列表
+      },
+      sendSmsRules: {
+        mobile: [{ required: true, message: "手机不能为空", trigger: "blur" }],
+        templateCode: [{ required: true, message: "手机不能为空", trigger: "blur" }],
+        templateParams: { }
+      }
+    };
+  },
+  created() {
+    this.getList();
+    // 获得短信渠道
+    getSimpleSmsChannels().then(response => {
+      this.channelOptions = response.data;
+    })
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      getSmsTemplatePage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        type: undefined,
+        status: undefined,
+        code: undefined,
+        name: undefined,
+        content: undefined,
+        remark: undefined,
+        apiTemplateId: undefined,
+        channelId: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeCreateTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加短信模板";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id;
+      getSmsTemplate(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改短信模板";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        if (this.form.id != null) {
+          updateSmsTemplate(this.form).then(response => {
+            this.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createSmsTemplate(this.form).then(response => {
+          this.msgSuccess("新增成功");
+          this.open = false;
+          this.getList();
+        });
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$confirm('是否确认删除短信模板编号为"' + id + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return deleteSmsTemplate(id);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行导出
+      this.$confirm('是否确认导出所有短信模板数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return exportSmsTemplateExcel(params);
+      }).then(response => {
+        this.downloadExcel(response, '短信模板.xls');
+      })
+    },
+    /** 发送短息按钮 */
+    handleSendSms(row) {
+      this.resetSendSms(row);
+      // 设置参数
+      this.sendSmsForm.content = row.content;
+      this.sendSmsForm.params = row.params;
+      this.sendSmsForm.templateCode = row.code;
+      this.sendSmsForm.templateParams = row.params.reduce(function(obj, item) {
+        obj[item] = undefined;
+        return obj;
+      }, {});
+      // 根据 row 重置 rules
+      this.sendSmsRules.templateParams = row.params.reduce(function(obj, item) {
+        obj[item] = { required: true, message: '参数 ' + item + " 不能为空", trigger: "change" };
+        return obj;
+      }, {});
+      // 设置打开
+      this.sendSmsOpen = true;
+    },
+    /** 重置发送短信的表单 */
+    resetSendSms() {
+      // 根据 row 重置表单
+      this.sendSmsForm = {
+        content: undefined,
+        params: undefined,
+        mobile: undefined,
+        templateCode: undefined,
+        templateParams: {}
+      };
+      this.resetForm("sendSmsForm");
+    },
+    /** 取消发送短信 */
+    cancelSendSms() {
+      this.sendSmsOpen = false;
+      this.resetSendSms();
+    },
+    /** 提交按钮 */
+    submitSendSmsForm() {
+      this.$refs["sendSmsForm"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 添加的提交
+        sendSms(this.sendSmsForm).then(response => {
+          this.msgSuccess("提交发送成功!发送结果,见发送日志编号:" + response.data);
+          this.sendSmsOpen = false;
+        });
+      });
+    },
+    /** 格式化短信渠道 */
+    formatChannelSignature(channelId) {
+      for (const channel of this.channelOptions) {
+        if (channel.id === channelId) {
+          return channel.signature;
+        }
+      }
+      return '找不到签名:' + channelId;
+    }
+  }
+};
+</script>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 177 - 15
sql/ruoyi-vue-pro.sql


+ 20 - 0
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<K, V> {
+
+    private K key;
+    private V value;
+
+}

+ 27 - 0
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;
+
+}

+ 4 - 3
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;
     }
 
 }

+ 0 - 41
src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java

@@ -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;
-    }
-
-}

+ 1 - 1
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) {

+ 2 - 2
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);
     }
 

+ 11 - 15
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 <T> 数据泛型
  */
 @Data
-public final class CommonResult<T> implements Serializable {
+public class CommonResult<T> implements Serializable {
 
     /**
      * 错误码
@@ -31,7 +31,7 @@ public final class CommonResult<T> implements Serializable {
     /**
      * 错误提示,用户可阅读
      *
-     * @see ErrorCode#getMessage() ()
+     * @see ErrorCode#getMsg() ()
      */
     private String msg;
 
@@ -57,7 +57,7 @@ public final class CommonResult<T> implements Serializable {
     }
 
     public static <T> CommonResult<T> error(ErrorCode errorCode) {
-        return error(errorCode.getCode(), errorCode.getMessage());
+        return error(errorCode.getCode(), errorCode.getMsg());
     }
 
     public static <T> CommonResult<T> success(T data) {
@@ -68,9 +68,13 @@ public final class CommonResult<T> 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<T> 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<T> implements Serializable {
         return error(serviceException.getCode(), serviceException.getMessage());
     }
 
-    public static <T> CommonResult<T> error(GlobalException globalException) {
-        return error(globalException.getCode(), globalException.getMessage());
-    }
-
 }

+ 5 - 0
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<Object> {
 

+ 39 - 0
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<Object> {
+
+    @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<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
+                                               GlobalConfiguration globalConfiguration) {
+        // 生成 Excel 小表格
+        return new CellData<>(JsonUtils.toJsonString(value));
+    }
+
+}

+ 4 - 4
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<AbstractChannelMessageListener<?>> listeners) {
+    public RedisMessageListenerContainer redisMessageListenerContainer(
+            RedisConnectionFactory factory, List<AbstractChannelMessageListener<?>> 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<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(RedisTemplate<String, Object> redisTemplate,
-                                                                                                                    List<AbstractStreamMessageListener<?>> listeners) {
+    public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
+            RedisTemplate<String, Object> redisTemplate, List<AbstractStreamMessageListener<?>> listeners) {
         // 第一步,创建 StreamMessageListenerContainer 容器
         // 创建 options 配置
         StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =

+ 10 - 4
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;
+    }
+
 }

+ 21 - 0
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();
+    }
+
+}

+ 54 - 0
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<SmsSendRespDTO> sendSms(Long logId, String mobile, String apiTemplateId,
+                                            List<KeyValue<String, Object>> templateParams);
+
+    /**
+     * 解析接收短信的接收结果
+     *
+     * @param text 结果
+     * @return 结果内容
+     * @throws Throwable 当解析 text 发生异常时,则会抛出异常
+     */
+    List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable;
+
+    /**
+     * 查询指定的短信模板
+     *
+     * @param apiTemplateId 短信 API 的模板编号
+     * @return 短信模板
+     */
+    SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId);
+
+}

+ 36 - 0
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);
+
+}

+ 17 - 0
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<String, ErrorCode> {
+}

+ 68 - 0
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<T> extends CommonResult<T> {
+
+    /**
+     * API 返回错误码
+     *
+     * 由于第三方的错误码可能是字符串,所以使用 String 类型
+     */
+    private String apiCode;
+    /**
+     * API 返回提示
+     */
+    private String apiMsg;
+
+    /**
+     * API 请求编号
+     */
+    private String apiRequestId;
+
+    private SmsCommonResult() {
+    }
+
+    public static <T> SmsCommonResult<T> build(String apiCode, String apiMsg, String apiRequestId,
+                                               T data, SmsCodeMapping codeMapping) {
+        Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
+        SmsCommonResult<T> result = new SmsCommonResult<T>().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 <T> SmsCommonResult<T> error(Throwable ex) {
+        SmsCommonResult<T> result = new SmsCommonResult<>();
+        result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode());
+        result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
+        return result;
+    }
+
+}

+ 48 - 0
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;
+
+}

+ 18 - 0
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;
+
+}

+ 33 - 0
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;
+
+}

+ 122 - 0
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<SmsSendRespDTO> sendSms(Long logId, String mobile,
+                                                         String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
+        // 执行短信发送
+        SmsCommonResult<SmsSendRespDTO> 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<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
+                                                                 String apiTemplateId, List<KeyValue<String, Object>> templateParams)
+            throws Throwable;
+
+    @Override
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
+        try {
+            return doParseSmsReceiveStatus(text);
+        } catch (Throwable ex) {
+            log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex);
+            throw ex;
+        }
+    }
+
+    protected abstract List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable;
+
+    @Override
+    public SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId) {
+        // 执行短信发送
+        SmsCommonResult<SmsTemplateRespDTO> result;
+        try {
+            result = doGetSmsTemplate(apiTemplateId);
+        } catch (Throwable ex) {
+            // 打印异常日志
+            log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex);
+            // 封装返回
+            return SmsCommonResult.error(ex);
+        }
+        return result;
+    }
+
+    protected abstract SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable;
+
+}

+ 90 - 0
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<Long, AbstractSmsClient> channelIdClients = new ConcurrentHashMap<>();
+
+    /**
+     * 短信客户端 Map
+     * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()}
+     *
+     * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。
+     * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients}
+     */
+    private final ConcurrentMap<String, AbstractSmsClient> 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));
+    }
+
+}

+ 212 - 0
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<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
+                                                        String apiTemplateId, List<KeyValue<String, Object>> 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<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
+        List<SmsReceiveStatus> 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<SmsTemplateRespDTO> 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
+    <T extends AcsResponse, R> SmsCommonResult<R> invoke(AcsRequest<T> request, Function<T, R> 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;
+
+    }
+
+}

+ 43 - 0
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;
+    }
+
+}

+ 23 - 0
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;
+    }
+
+}

+ 96 - 0
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<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
+                                                        String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
+        // 构建请求
+        String url = buildUrl("robot/send");
+        Map<String, Object> 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&timestamp=%d&sign=%s",
+                path, properties.getApiKey(), timestamp, sign);
+    }
+
+    @Override
+    protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
+        throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调");
+    }
+
+    @Override
+    protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
+        SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
+                .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason("");
+        return SmsCommonResult.build("0", "success", null, data, codeMapping);
+    }
+
+}

+ 204 - 0
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<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
+                                                        String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
+        return invoke(() -> {
+            Map<String, String> 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<KeyValue<String, Object>> 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<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
+        List<SmsReceiveStatus> 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<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable {
+        return invoke(() -> {
+            Map<String, String> 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
+    <T, R> SmsCommonResult<R> invoke(Supplier<Result<T>> requestConsumer, Function<T, R> responseConsumer) throws Throwable {
+        // 执行请求
+        Result<T> 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;
+
+    }
+
+}

+ 45 - 0
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;
+    }
+
+}

+ 37 - 0
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());
+    }
+
+}

+ 47 - 0
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, "调用异常");
+
+}

+ 21 - 0
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;
+
+}

+ 52 - 0
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;
+
+}

+ 1 - 24
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);
     }
 
@@ -223,25 +219,6 @@ public class GlobalExceptionHandler {
     }
 
     /**
-     * 处理全局异常 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);
-    }
-
-    /**
      * 处理系统异常,兜底处理所有的一切
      */
     @ExceptionHandler(value = Exception.class)
@@ -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) {

+ 1 - 1
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<List<SysDictDataSimpleVO>> getSimpleDictDatas() {
+    public CommonResult<List<SysDictDataSimpleRespVO>> getSimpleDictDatas() {
         List<SysDictDataDO> list = dictDataService.getDictDatas();
         return success(SysDictDataConvert.INSTANCE.convertList(list));
     }

+ 1 - 1
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<Boolean> updateDictType(@Valid @RequestBody SysDictTypeUpdateReqVO reqVO) {

+ 2 - 2
src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleVO.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;

+ 49 - 0
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<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
+        String text = ServletUtil.getBody(request);
+        smsService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
+        return success(true);
+    }
+
+}

+ 80 - 0
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<Long> createSmsChannel(@Valid @RequestBody SysSmsChannelCreateReqVO createReqVO) {
+        return success(smsChannelService.createSmsChannel(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @ApiOperation("更新短信渠道")
+    @PreAuthorize("@ss.hasPermission('system:sms-channel:update')")
+    public CommonResult<Boolean> 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<Boolean> 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<SysSmsChannelRespVO> 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<PageResult<SysSmsChannelRespVO>> getSmsChannelPage(@Valid SysSmsChannelPageReqVO pageVO) {
+        PageResult<SysSmsChannelDO> pageResult = smsChannelService.getSmsChannelPage(pageVO);
+        return success(SysSmsChannelConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/list-all-simple")
+    @ApiOperation(value = "获得短信渠道精简列表", notes = "包含被禁用的短信渠道")
+    public CommonResult<List<SysSmsChannelSimpleRespVO>> getSimpleSmsChannels() {
+        List<SysSmsChannelDO> list = smsChannelService.getSmsChannelList();
+        // 排序后,返回给前端
+        list.sort(Comparator.comparing(SysSmsChannelDO::getId));
+        return success(SysSmsChannelConvert.INSTANCE.convertList03(list));
+    }
+
+}

+ 60 - 0
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<PageResult<SysSmsLogRespVO>> getSmsLogPage(@Valid SysSmsLogPageReqVO pageVO) {
+        PageResult<SysSmsLogDO> 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<SysSmsLogDO> list = smsLogService.getSmsLogList(exportReqVO);
+        // 导出 Excel
+        List<SysSmsLogExcelVO> datas = SysSmsLogConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "短信日志.xls", "数据", SysSmsLogExcelVO.class, datas);
+    }
+
+}

+ 12 - 0
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"
+  }
+}

+ 98 - 0
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<Long> createSmsTemplate(@Valid @RequestBody SysSmsTemplateCreateReqVO createReqVO) {
+        return success(smsTemplateService.createSmsTemplate(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @ApiOperation("更新短信模板")
+    @PreAuthorize("@ss.hasPermission('system:sms-template:update')")
+    public CommonResult<Boolean> 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<Boolean> 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<SysSmsTemplateRespVO> 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<PageResult<SysSmsTemplateRespVO>> getSmsTemplatePage(@Valid SysSmsTemplatePageReqVO pageVO) {
+        PageResult<SysSmsTemplateDO> 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<SysSmsTemplateDO> list = smsTemplateService.getSmsTemplateList(exportReqVO);
+        // 导出 Excel
+        List<SysSmsTemplateExcelVO> 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<Long> sendSms(@Valid @RequestBody SysSmsTemplateSendReqVO sendReqVO) {
+        return success(smsService.sendSingleSms(sendReqVO.getMobile(), null, null,
+                sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
+    }
+
+}

+ 38 - 0
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;
+
+}

+ 21 - 0
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;
+
+}

+ 35 - 0
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;
+
+}

+ 26 - 0
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;
+
+}

+ 24 - 0
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;
+
+}

+ 21 - 0
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;
+
+}

+ 101 - 0
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<String, Object> 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;
+
+}

+ 47 - 0
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;
+
+}

+ 52 - 0
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;
+
+}

+ 89 - 0
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<String, Object> 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;
+
+}

+ 46 - 0
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;
+
+}

+ 14 - 0
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 {
+
+}

+ 56 - 0
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;
+
+}

+ 42 - 0
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;
+
+}

+ 47 - 0
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;
+
+}

+ 30 - 0
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<String> params;
+
+    @ApiModelProperty(value = "创建时间", required = true)
+    private Date createTime;
+
+}

+ 25 - 0
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<String, Object> templateParams;
+
+}

+ 21 - 0
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;
+
+}

+ 1 - 1
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<SysDictDataSimpleVO> convertList(List<SysDictDataDO> list);
+    List<SysDictDataSimpleRespVO> convertList(List<SysDictDataDO> list);
 
     SysDictDataRespVO convert(SysDictDataDO bean);
 

+ 39 - 0
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<SysSmsChannelRespVO> convertList(List<SysSmsChannelDO> list);
+
+    PageResult<SysSmsChannelRespVO> convertPage(PageResult<SysSmsChannelDO> page);
+
+    List<SmsChannelProperties> convertList02(List<SysSmsChannelDO> list);
+
+    List<SysSmsChannelSimpleRespVO> convertList03(List<SysSmsChannelDO> list);
+
+}

+ 30 - 0
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<SysSmsLogRespVO> convertList(List<SysSmsLogDO> list);
+
+    PageResult<SysSmsLogRespVO> convertPage(PageResult<SysSmsLogDO> page);
+
+    List<SysSmsLogExcelVO> convertList02(List<SysSmsLogDO> list);
+
+}

+ 31 - 0
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<SysSmsTemplateRespVO> convertList(List<SysSmsTemplateDO> list);
+
+    PageResult<SysSmsTemplateRespVO> convertPage(PageResult<SysSmsTemplateDO> page);
+
+    List<SysSmsTemplateExcelVO> convertList02(List<SysSmsTemplateDO> list);
+
+}

+ 60 - 0
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;
+
+}

+ 173 - 0
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<String, Object> 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;
+
+}

+ 89 - 0
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<String> params;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 短信 API 的模板编号
+     */
+    private String apiTemplateId;
+
+    // ========= 渠道相关字段 =========
+
+    /**
+     * 短信渠道编号
+     *
+     * 关联 {@link SysSmsChannelDO#getId()}
+     */
+    private Long channelId;
+    /**
+     * 短信渠道编码
+     *
+     * 冗余 {@link SysSmsChannelDO#getCode()}
+     */
+    private String channelCode;
+
+}

+ 29 - 0
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<SysSmsChannelDO> {
+
+    default PageResult<SysSmsChannelDO> selectPage(SysSmsChannelPageReqVO reqVO) {
+        return selectPage(reqVO, new QueryWrapperX<SysSmsChannelDO>()
+                .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);
+
+}

+ 40 - 0
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<SysSmsLogDO> {
+
+    default PageResult<SysSmsLogDO> selectPage(SysSmsLogPageReqVO reqVO) {
+        return selectPage(reqVO, new QueryWrapperX<SysSmsLogDO>()
+                .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<SysSmsLogDO> selectList(SysSmsLogExportReqVO reqVO) {
+        return selectList(new QueryWrapperX<SysSmsLogDO>()
+                .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"));
+    }
+
+}

+ 53 - 0
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<SysSmsTemplateDO> {
+
+    default SysSmsTemplateDO selectByCode(String code) {
+        return selectOne("code", code);
+    }
+
+    default PageResult<SysSmsTemplateDO> selectPage(SysSmsTemplatePageReqVO reqVO) {
+        return selectPage(reqVO, new QueryWrapperX<SysSmsTemplateDO>()
+                .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<SysSmsTemplateDO> selectList(SysSmsTemplateExportReqVO reqVO) {
+        return selectList(new QueryWrapperX<SysSmsTemplateDO>()
+                .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);
+
+}

+ 0 - 0
src/main/java/cn/iocoder/dashboard/modules/system/dal/redis/RedisKeyConstants.java


+ 14 - 0
src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java

@@ -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, "模板参数({})缺失");
+
+
 }

+ 4 - 0
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"), // 定时任务状态的枚举

+ 23 - 0
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;
+
+}

+ 24 - 0
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;
+
+}

+ 25 - 0
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;
+
+}

+ 29 - 0
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<SysSmsChannelRefreshMessage> {
+
+    @Resource
+    private SysSmsChannelService smsChannelService;
+
+    @Override
+    public void onMessage(SysSmsChannelRefreshMessage message) {
+        log.info("[onMessage][收到 SmsChannel 刷新消息]");
+        smsChannelService.initSmsClients();
+    }
+
+}

+ 13 - 0
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<SysSmsSendMessage> {
 
+    @Resource
+    private SysSmsService smsService;
+
     @Override
     public void onMessage(SysSmsSendMessage message) {
         log.info("[onMessage][消息内容({})]", message);
+        smsService.doSendSms(message);
     }
 
 }

+ 29 - 0
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<SysSmsTemplateRefreshMessage> {
+
+    @Resource
+    private SysSmsTemplateService smsTemplateService;
+
+    @Override
+    public void onMessage(SysSmsTemplateRefreshMessage message) {
+        log.info("[onMessage][收到 SmsTemplate 刷新消息]");
+        smsTemplateService.initLocalCache();
+    }
+
+}

+ 17 - 0
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";
+    }
+
+}

+ 15 - 13
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;
 
 /**
  * 短信发送消息
@@ -15,28 +16,29 @@ import java.util.Map;
 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 的模板编号
      */
-    private Map<String, Object> templateParams;
-
+    @NotNull(message = "短信 API 的模板编号不能为空")
+    private String apiTemplateId;
     /**
-     * 用户编号,允许空
-     */
-    private Integer userId;
-    /**
-     * 用户类型,允许空
+     * 短信模板参数
      */
-    private Integer userType;
+    private List<KeyValue<String, Object>> templateParams;
 
     @Override
     public String getStreamKey() {

+ 17 - 0
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";
+    }
+
+}

+ 2 - 0
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 {

+ 60 - 0
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<KeyValue<String, Object>> 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);
+    }
+
+}

+ 1 - 1
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();
 

+ 0 - 1
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;

+ 9 - 9
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java

@@ -59,7 +59,7 @@ public class SysRoleServiceImpl implements SysRoleService {
      */
     private volatile Map<Long, SysRoleDO> roleCache;
     /**
-     * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
+     * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新
      */
     private volatile Date maxUpdateTime;
 
@@ -78,7 +78,7 @@ public class SysRoleServiceImpl implements SysRoleService {
     @Override
     @PostConstruct
     public void initLocalCache() {
-        // 获取菜单列表,如果有更新
+        // 获取角色列表,如果有更新
         List<SysRoleDO> roleList = this.loadRoleIfUpdate(maxUpdateTime);
         if (CollUtil.isEmpty(roleList)) {
             return;
@@ -99,23 +99,23 @@ public class SysRoleServiceImpl implements SysRoleService {
     }
 
     /**
-     * 如果菜单发生变化,从数据库中获取最新的全量菜单
+     * 如果角色发生变化,从数据库中获取最新的全量角色
      * 如果未发生变化,则返回空
      *
-     * @param maxUpdateTime 当前菜单的最大更新时间
-     * @return 菜单列表
+     * @param maxUpdateTime 当前角色的最大更新时间
+     * @return 角色列表
      */
     private List<SysRoleDO> 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();
     }
 

+ 79 - 0
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<SysSmsChannelDO> getSmsChannelList(Collection<Long> ids);
+
+    /**
+     * 获得所有短信渠道列表
+     *
+     * @return 短信渠道列表
+     */
+    List<SysSmsChannelDO> getSmsChannelList();
+
+    /**
+     * 获得短信渠道分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 短信渠道分页
+     */
+    PageResult<SysSmsChannelDO> getSmsChannelPage(SysSmsChannelPageReqVO pageReqVO);
+
+}

+ 77 - 0
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<String, Object> 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<SysSmsLogDO> getSmsLogPage(SysSmsLogPageReqVO pageReqVO);
+
+    /**
+     * 获得短信日志列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 短信日志列表
+     */
+    List<SysSmsLogDO> getSmsLogList(SysSmsLogExportReqVO exportReqVO);
+
+}

+ 0 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsService.java


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor