Prechádzať zdrojové kódy

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

 Conflicts:
	src/main/resources/application.yaml
YunaiV 4 rokov pred
rodič
commit
54e26d9e8b
100 zmenil súbory, kde vykonal 4664 pridanie a 2112 odobranie
  1. 9 8
      README.md
  2. 4 0
      lombok.config
  3. 46 27
      pom.xml
  4. 0 27
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java
  5. 0 145
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScopeAspect.java
  6. 0 28
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java
  7. 0 64
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSourceAspect.java
  8. 0 19
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSourceType.java
  9. 0 24
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DynamicDataSource.java
  10. 0 41
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DynamicDataSourceContextHolder.java
  11. 0 116
      ruoyi-common/src/main/java/com/ruoyi/common/config/DruidConfig.java
  12. 0 37
      ruoyi-common/src/main/resources/application-druid.yml
  13. 7 0
      ruoyi-ui/.env.demo1024
  14. 1 0
      ruoyi-ui/package.json
  15. 16 0
      ruoyi-ui/src/api/infra/dbDoc.js
  16. 18 0
      ruoyi-ui/src/api/infra/file.js
  17. 2 2
      ruoyi-ui/src/api/system/dept.js
  18. 2 2
      ruoyi-ui/src/api/system/dict/data.js
  19. 2 2
      ruoyi-ui/src/api/system/dict/type.js
  20. 2 2
      ruoyi-ui/src/api/system/menu.js
  21. 2 2
      ruoyi-ui/src/api/system/notice.js
  22. 2 2
      ruoyi-ui/src/api/system/post.js
  23. 3 3
      ruoyi-ui/src/api/system/role.js
  24. 52 0
      ruoyi-ui/src/api/system/sms/smsChannel.js
  25. 20 0
      ruoyi-ui/src/api/system/sms/smsLog.js
  26. 64 0
      ruoyi-ui/src/api/system/sms/smsTemplate.js
  27. 8 8
      ruoyi-ui/src/api/system/user.js
  28. 7 1
      ruoyi-ui/src/main.js
  29. 1 1
      ruoyi-ui/src/store/modules/user.js
  30. 4 0
      ruoyi-ui/src/utils/dict.js
  31. 12 0
      ruoyi-ui/src/utils/request.js
  32. 15 0
      ruoyi-ui/src/utils/ruoyi.js
  33. 10 38
      ruoyi-ui/src/views/infra/config/index.vue
  34. 202 0
      ruoyi-ui/src/views/infra/file/index.vue
  35. 13 54
      ruoyi-ui/src/views/system/dept/index.vue
  36. 14 59
      ruoyi-ui/src/views/system/dict/data.vue
  37. 17 79
      ruoyi-ui/src/views/system/dict/index.vue
  38. 14 58
      ruoyi-ui/src/views/system/loginlog/index.vue
  39. 19 71
      ruoyi-ui/src/views/system/menu/index.vue
  40. 13 68
      ruoyi-ui/src/views/system/notice/index.vue
  41. 14 64
      ruoyi-ui/src/views/system/operatelog/index.vue
  42. 15 60
      ruoyi-ui/src/views/system/post/index.vue
  43. 23 94
      ruoyi-ui/src/views/system/role/index.vue
  44. 542 0
      ruoyi-ui/src/views/system/sms/smsChannel.vue
  45. 297 0
      ruoyi-ui/src/views/system/sms/smsLog.vue
  46. 405 0
      ruoyi-ui/src/views/system/sms/smsTemplate.vue
  47. 659 715
      ruoyi-ui/src/views/system/user/index.vue
  48. 9 7
      ruoyi-ui/src/views/system/user/profile/index.vue
  49. 8 8
      ruoyi-ui/src/views/system/user/profile/userInfo.vue
  50. 37 5
      ruoyi-ui/src/views/tool/dbDoc/index.vue
  51. 8 0
      sql/quartz.sql
  52. 339 75
      sql/ruoyi-vue-pro.sql
  53. 20 0
      src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java
  54. 27 0
      src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java
  55. 4 3
      src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java
  56. 0 41
      src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java
  57. 1 1
      src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java
  58. 1 0
      src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java
  59. 2 2
      src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java
  60. 11 15
      src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java
  61. 27 1
      src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java
  62. 22 0
      src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java
  63. 38 0
      src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java
  64. 5 0
      src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java
  65. 39 0
      src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java
  66. 2 2
      src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java
  67. 18 1
      src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java
  68. 26 0
      src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java
  69. 24 0
      src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java
  70. 1 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java
  71. 3 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java
  72. 3 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java
  73. 5 2
      src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java
  74. 1 1
      src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java
  75. 70 2
      src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java
  76. 5 16
      src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java
  77. 3 1
      src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java
  78. 88 0
      src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java
  79. 20 0
      src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java
  80. 17 1
      src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java
  81. 10 4
      src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
  82. 21 0
      src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java
  83. 54 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java
  84. 36 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java
  85. 17 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java
  86. 68 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java
  87. 48 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java
  88. 18 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java
  89. 33 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java
  90. 122 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java
  91. 90 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java
  92. 212 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java
  93. 43 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java
  94. 23 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java
  95. 96 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java
  96. 204 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java
  97. 45 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java
  98. 37 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java
  99. 47 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java
  100. 0 0
      src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java

+ 9 - 8
README.md

@@ -35,12 +35,12 @@
 |  | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
 |  | 岗位管理 | 配置系统用户所属担任职务 |
 |  | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
-|  | 通知公告 | 系统通知公告信息发布维护 |
+| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、云片等主流短信平台 |
 | 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
 |  | 登录日志 | 系统登录日志记录查询包含登录异常 |
+|  | 通知公告 | 系统通知公告信息发布维护 |
 
 计划新增功能:
-* 短信
 * 邮件
 * 钉钉、飞书等通知
 
@@ -50,6 +50,7 @@
 | --- | --- | --- |
 | 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
 | | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
+| 🚀 | 文件服务 | 支持本地文件存储,同时支持兼容 Amazon S3 协议的云服务、开源组件 | 
 | 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
 |  | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
 | | Redis 监控 |监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
@@ -64,7 +65,6 @@
 计划新增:
 * 工作流
 * 错误码
-* 文件服务
 
 ### 研发工具
 
@@ -87,14 +87,15 @@
 
 ## 技术栈
 
-**后端**
+### 后端
 
 | 框架 | 说明 |  版本 | 学习指南 |
 | --- | --- | --- | --- |
 | [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.4.2 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
 | [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 |  |
 | [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.4 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
-| [MyBatis-Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
+| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
+| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
 | [Redis](https://redis.io/) | key-value 数据库 | 5.0 |  |
 | [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.1.46 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
 | [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架  | 5.4.2 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
@@ -102,16 +103,16 @@
 | [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.1.7 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
 | [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
 | [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.2 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
-| [Resilience4j](https://github.com/quartz-scheduler) | 服务保障组件 | 1.7.0 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
+| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.0 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
 | [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
-| [Spring Boot Admin](https://github.com/skywalking) | Spring Boot 监控平台 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
+| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
 | [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.11.4 |  |
 | [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
 | [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
 | [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.7.0 | - |
 | [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 3.6.28 | - |
 
-**前端**
+### 前端
 
 | 框架 | 说明 |  版本 |
 | --- | --- | --- |

+ 4 - 0
lombok.config

@@ -0,0 +1,4 @@
+config.stopBubbling = true
+lombok.tostring.callsuper=true
+lombok.equalsandhashcode.callsuper=true
+lombok.accessors.chain=true

+ 46 - 27
pom.xml

@@ -22,15 +22,16 @@
         <maven.compiler.target>${java.version}</maven.compiler.target>
         <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
         <!-- 统一依赖管理 -->
-        <spring.boot.version>2.4.2</spring.boot.version>
+        <spring.boot.version>2.4.4</spring.boot.version>
         <!-- Web 相关 -->
         <knife4j.version>3.0.2</knife4j.version>
         <swagger-annotations.version>1.5.22</swagger-annotations.version>
         <!-- DB 相关 -->
         <mysql-connector-java.version>5.1.46</mysql-connector-java.version>
         <druid.version>1.2.4</druid.version>
-        <mybatis-plus.version>3.4.1</mybatis-plus.version>
-        <redisson.version>3.14.1</redisson.version>
+        <mybatis-plus.version>3.4.2</mybatis-plus.version>
+        <dynamic-datasource.version>3.3.2</dynamic-datasource.version>
+        <redisson.version>3.15.1</redisson.version>
         <!-- Config 配置中心相关 -->
         <apollo.version>1.7.0</apollo.version>
         <!-- 服务保障相关 -->
@@ -42,10 +43,16 @@
         <!-- 工具类相关 -->
         <lombok.version>1.16.14</lombok.version>
         <mapstruct.version>1.4.1.Final</mapstruct.version>
-        <hutool.version>5.5.6</hutool.version>
+        <hutool.version>5.6.1</hutool.version>
         <easyexcel.verion>2.2.7</easyexcel.verion>
         <velocity.version>2.2</velocity.version>
         <screw.version>1.0.5</screw.version>
+        <podam.version>7.2.6.RELEASE</podam.version>
+        <jedis-mock.version>0.1.16</jedis-mock.version>
+        <!-- 三方云服务相关 -->
+        <aliyun-java-sdk-core.version>4.5.18</aliyun-java-sdk-core.version>
+        <aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
+        <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
     </properties>
 
     <!-- 依赖声明 -->
@@ -133,6 +140,11 @@
             <artifactId>mybatis-plus-boot-starter</artifactId>
             <version>${mybatis-plus.version}</version>
         </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
+            <version>${dynamic-datasource.version}</version>
+        </dependency>
 
         <dependency>
             <groupId>org.redisson</groupId>
@@ -212,14 +224,14 @@
         <dependency>
             <groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
             <artifactId>jedis-mock</artifactId>
-            <version>0.1.16</version>
+            <version>${jedis-mock.version}</version>
             <scope>test</scope>
         </dependency>
 
         <dependency>
             <groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
             <artifactId>podam</artifactId>
-            <version>7.2.6.RELEASE</version>
+            <version>${podam.version}</version>
             <scope>test</scope>
         </dependency>
 
@@ -237,33 +249,19 @@
         </dependency>
         <dependency>
             <groupId>org.mapstruct</groupId>
-            <artifactId>mapstruct-jdk8</artifactId>
+            <artifactId>mapstruct-processor</artifactId>
             <version>${mapstruct.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>cn.hutool</groupId>
-            <artifactId>hutool-core</artifactId>
-            <version>${hutool.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.hutool</groupId>
-            <artifactId>hutool-extra</artifactId>
-            <version>${hutool.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.hutool</groupId>
-            <artifactId>hutool-captcha</artifactId>
-            <version>${hutool.version}</version>
+            <optional>true</optional>
         </dependency>
         <dependency>
-            <groupId>cn.hutool</groupId>
-            <artifactId>hutool-http</artifactId>
-            <version>${hutool.version}</version>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-jdk8</artifactId>
+            <version>${mapstruct.version}</version>
         </dependency>
+
         <dependency>
             <groupId>cn.hutool</groupId>
-            <artifactId>hutool-crypto</artifactId>
+            <artifactId>hutool-all</artifactId>
             <version>${hutool.version}</version>
         </dependency>
 
@@ -285,6 +283,27 @@
             <version>${screw.version}</version>
         </dependency>
 
+        <!-- 三方云服务相关 -->
+
+        <!-- SMS SDK begin -->
+        <dependency>
+            <groupId>com.yunpian.sdk</groupId>
+            <artifactId>yunpian-java-sdk</artifactId>
+            <version>${yunpian-java-sdk.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+            <version>${aliyun-java-sdk-core.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
+            <version>${aliyun-java-sdk-dysmsapi.version}</version>
+        </dependency>
+        <!-- SMS SDK end -->
+
     </dependencies>
 
 

+ 0 - 27
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java

@@ -1,27 +0,0 @@
-package com.ruoyi.common.annotation;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * 数据权限过滤注解
- *
- * @author ruoyi
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-public @interface DataScope {
-    /**
-     * 部门表的别名
-     */
-    public String deptAlias() default "";
-
-    /**
-     * 用户表的别名
-     */
-    public String userAlias() default "";
-}

+ 0 - 145
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScopeAspect.java

@@ -1,145 +0,0 @@
-package com.ruoyi.framework.aspectj;
-
-import java.lang.reflect.Method;
-
-import org.aspectj.lang.JoinPoint;
-import org.aspectj.lang.Signature;
-import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Before;
-import org.aspectj.lang.annotation.Pointcut;
-import org.aspectj.lang.reflect.MethodSignature;
-import org.springframework.stereotype.Component;
-import com.ruoyi.common.annotation.DataScope;
-import com.ruoyi.common.core.domain.BaseEntity;
-import com.ruoyi.common.core.domain.entity.SysRole;
-import com.ruoyi.common.core.domain.entity.SysUser;
-import com.ruoyi.common.core.domain.model.LoginUser;
-import com.ruoyi.common.utils.ServletUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.common.utils.spring.SpringUtils;
-import com.ruoyi.framework.web.service.TokenService;
-
-/**
- * 数据过滤处理
- *
- * @author ruoyi
- */
-@Aspect
-@Component
-public class DataScopeAspect {
-    /**
-     * 全部数据权限
-     */
-    public static final String DATA_SCOPE_ALL = "1";
-
-    /**
-     * 自定数据权限
-     */
-    public static final String DATA_SCOPE_CUSTOM = "2";
-
-    /**
-     * 部门数据权限
-     */
-    public static final String DATA_SCOPE_DEPT = "3";
-
-    /**
-     * 部门及以下数据权限
-     */
-    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
-
-    /**
-     * 仅本人数据权限
-     */
-    public static final String DATA_SCOPE_SELF = "5";
-
-    /**
-     * 数据权限过滤关键字
-     */
-    public static final String DATA_SCOPE = "dataScope";
-
-    // 配置织入点
-    @Pointcut("@annotation(com.ruoyi.common.annotation.DataScope)")
-    public void dataScopePointCut() {
-    }
-
-    @Before("dataScopePointCut()")
-    public void doBefore(JoinPoint point) throws Throwable {
-        handleDataScope(point);
-    }
-
-    protected void handleDataScope(final JoinPoint joinPoint) {
-        // 获得注解
-        DataScope controllerDataScope = getAnnotationLog(joinPoint);
-        if (controllerDataScope == null) {
-            return;
-        }
-        // 获取当前的用户
-        LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
-        if (StringUtils.isNotNull(loginUser)) {
-            SysUser currentUser = loginUser.getUser();
-            // 如果是超级管理员,则不过滤数据
-            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) {
-                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
-                        controllerDataScope.userAlias());
-            }
-        }
-    }
-
-    /**
-     * 数据范围过滤
-     *
-     * @param joinPoint 切点
-     * @param user      用户
-     * @param userAlias 别名
-     */
-    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) {
-        StringBuilder sqlString = new StringBuilder();
-
-        for (SysRole role : user.getRoles()) {
-            String dataScope = role.getDataScope();
-            if (DATA_SCOPE_ALL.equals(dataScope)) {
-                sqlString = new StringBuilder();
-                break;
-            } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
-                sqlString.append(StringUtils.format(
-                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
-                        role.getRoleId()));
-            } else if (DATA_SCOPE_DEPT.equals(dataScope)) {
-                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
-            } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
-                sqlString.append(StringUtils.format(
-                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
-                        deptAlias, user.getDeptId(), user.getDeptId()));
-            } else if (DATA_SCOPE_SELF.equals(dataScope)) {
-                if (StringUtils.isNotBlank(userAlias)) {
-                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
-                } else {
-                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
-                    sqlString.append(" OR 1=0 ");
-                }
-            }
-        }
-
-        if (StringUtils.isNotBlank(sqlString.toString())) {
-            Object params = joinPoint.getArgs()[0];
-            if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
-                BaseEntity baseEntity = (BaseEntity) params;
-                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
-            }
-        }
-    }
-
-    /**
-     * 是否存在注解,如果存在就获取
-     */
-    private DataScope getAnnotationLog(JoinPoint joinPoint) {
-        Signature signature = joinPoint.getSignature();
-        MethodSignature methodSignature = (MethodSignature) signature;
-        Method method = methodSignature.getMethod();
-
-        if (method != null) {
-            return method.getAnnotation(DataScope.class);
-        }
-        return null;
-    }
-}

+ 0 - 28
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java

@@ -1,28 +0,0 @@
-package com.ruoyi.common.annotation;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-import com.ruoyi.common.enums.DataSourceType;
-
-/**
- * 自定义多数据源切换注解
- * <p>
- * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
- *
- * @author ruoyi
- */
-@Target({ElementType.METHOD, ElementType.TYPE})
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-@Inherited
-public @interface DataSource {
-    /**
-     * 切换数据源名称
-     */
-    public DataSourceType value() default DataSourceType.MASTER;
-}

+ 0 - 64
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSourceAspect.java

@@ -1,64 +0,0 @@
-package com.ruoyi.framework.aspectj;
-
-import java.util.Objects;
-
-import org.aspectj.lang.ProceedingJoinPoint;
-import org.aspectj.lang.annotation.Around;
-import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Pointcut;
-import org.aspectj.lang.reflect.MethodSignature;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.core.annotation.AnnotationUtils;
-import org.springframework.core.annotation.Order;
-import org.springframework.stereotype.Component;
-import com.ruoyi.common.annotation.DataSource;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder;
-
-/**
- * 多数据源处理
- *
- * @author ruoyi
- */
-@Aspect
-@Order(1)
-@Component
-public class DataSourceAspect {
-    protected Logger logger = LoggerFactory.getLogger(getClass());
-
-    @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
-            + "|| @within(com.ruoyi.common.annotation.DataSource)")
-    public void dsPointCut() {
-
-    }
-
-    @Around("dsPointCut()")
-    public Object around(ProceedingJoinPoint point) throws Throwable {
-        DataSource dataSource = getDataSource(point);
-
-        if (StringUtils.isNotNull(dataSource)) {
-            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
-        }
-
-        try {
-            return point.proceed();
-        } finally {
-            // 销毁数据源 在执行方法之后
-            DynamicDataSourceContextHolder.clearDataSourceType();
-        }
-    }
-
-    /**
-     * 获取需要切换的数据源
-     */
-    public DataSource getDataSource(ProceedingJoinPoint point) {
-        MethodSignature signature = (MethodSignature) point.getSignature();
-        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
-        if (Objects.nonNull(dataSource)) {
-            return dataSource;
-        }
-
-        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
-    }
-}

+ 0 - 19
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSourceType.java

@@ -1,19 +0,0 @@
-package com.ruoyi.common.enums;
-
-/**
- * 数据源
- * 
- * @author ruoyi
- */
-public enum DataSourceType
-{
-    /**
-     * 主库
-     */
-    MASTER,
-
-    /**
-     * 从库
-     */
-    SLAVE
-}

+ 0 - 24
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DynamicDataSource.java

@@ -1,24 +0,0 @@
-package com.ruoyi.framework.datasource;
-
-import java.util.Map;
-import javax.sql.DataSource;
-
-import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
-
-/**
- * 动态数据源
- *
- * @author ruoyi
- */
-public class DynamicDataSource extends AbstractRoutingDataSource {
-    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
-        super.setDefaultTargetDataSource(defaultTargetDataSource);
-        super.setTargetDataSources(targetDataSources);
-        super.afterPropertiesSet();
-    }
-
-    @Override
-    protected Object determineCurrentLookupKey() {
-        return DynamicDataSourceContextHolder.getDataSourceType();
-    }
-}

+ 0 - 41
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DynamicDataSourceContextHolder.java

@@ -1,41 +0,0 @@
-package com.ruoyi.framework.datasource;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * 数据源切换处理
- *
- * @author ruoyi
- */
-public class DynamicDataSourceContextHolder {
-    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
-
-    /**
-     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
-     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
-     */
-    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
-
-    /**
-     * 设置数据源的变量
-     */
-    public static void setDataSourceType(String dsType) {
-        log.info("切换到{}数据源", dsType);
-        CONTEXT_HOLDER.set(dsType);
-    }
-
-    /**
-     * 获得数据源的变量
-     */
-    public static String getDataSourceType() {
-        return CONTEXT_HOLDER.get();
-    }
-
-    /**
-     * 清空数据源变量
-     */
-    public static void clearDataSourceType() {
-        CONTEXT_HOLDER.remove();
-    }
-}

+ 0 - 116
ruoyi-common/src/main/java/com/ruoyi/common/config/DruidConfig.java

@@ -1,116 +0,0 @@
-package com.ruoyi.framework.config;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.sql.DataSource;
-
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.web.servlet.FilterRegistrationBean;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
-import com.alibaba.druid.pool.DruidDataSource;
-import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
-import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
-import com.alibaba.druid.util.Utils;
-import com.ruoyi.common.enums.DataSourceType;
-import com.ruoyi.common.utils.spring.SpringUtils;
-import com.ruoyi.framework.config.properties.DruidProperties;
-import com.ruoyi.framework.datasource.DynamicDataSource;
-
-/**
- * druid 配置多数据源
- *
- * @author ruoyi
- */
-@Configuration
-public class DruidConfig {
-    @Bean
-    @ConfigurationProperties("spring.datasource.druid.master")
-    public DataSource masterDataSource(DruidProperties druidProperties) {
-        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
-        return druidProperties.dataSource(dataSource);
-    }
-
-    @Bean
-    @ConfigurationProperties("spring.datasource.druid.slave")
-    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
-    public DataSource slaveDataSource(DruidProperties druidProperties) {
-        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
-        return druidProperties.dataSource(dataSource);
-    }
-
-    @Bean(name = "dynamicDataSource")
-    @Primary
-    public DynamicDataSource dataSource(DataSource masterDataSource) {
-        Map<Object, Object> targetDataSources = new HashMap<>();
-        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
-        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
-        return new DynamicDataSource(masterDataSource, targetDataSources);
-    }
-
-    /**
-     * 设置数据源
-     *
-     * @param targetDataSources 备选数据源集合
-     * @param sourceName        数据源名称
-     * @param beanName          bean名称
-     */
-    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
-        try {
-            DataSource dataSource = SpringUtils.getBean(beanName);
-            targetDataSources.put(sourceName, dataSource);
-        } catch (Exception e) {
-        }
-    }
-
-    /**
-     * 去除监控页面底部的广告
-     */
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    @Bean
-    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
-    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
-        // 获取web监控页面的参数
-        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
-        // 提取common.js的配置路径
-        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
-        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
-        final String filePath = "support/http/resources/js/common.js";
-        // 创建filter进行过滤
-        Filter filter = new Filter() {
-            @Override
-            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
-            }
-
-            @Override
-            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
-                    throws IOException, ServletException {
-                chain.doFilter(request, response);
-                // 重置缓冲区,响应头不会被重置
-                response.resetBuffer();
-                // 获取common.js
-                String text = Utils.readFromResource(filePath);
-                // 正则替换banner, 除去底部的广告信息
-                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
-                text = text.replaceAll("powered.*?shrek.wang</a>", "");
-                response.getWriter().write(text);
-            }
-
-            @Override
-            public void destroy() {
-            }
-        };
-        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
-        registrationBean.setFilter(filter);
-        registrationBean.addUrlPatterns(commonJsPattern);
-        return registrationBean;
-    }
-}

+ 0 - 37
ruoyi-common/src/main/resources/application-druid.yml

@@ -1,37 +0,0 @@
-# 数据源配置
-spring:
-    datasource:
-        type: com.alibaba.druid.pool.DruidDataSource
-        driverClassName: com.mysql.cj.jdbc.Driver
-        druid:
-            # 主库数据源
-            master:
-                url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                username: root
-                password: password
-            # 从库数据源
-            slave:
-                # 从数据源开关/默认关闭
-                enabled: false
-                url:
-                username:
-                password:
-            # 初始连接数
-            initialSize: 5
-            # 最小连接池数量
-            minIdle: 10
-            # 最大连接池数量
-            maxActive: 20
-            # 配置获取连接等待超时的时间
-            maxWait: 60000
-            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
-            timeBetweenEvictionRunsMillis: 60000
-            # 配置一个连接在池中最小生存的时间,单位是毫秒
-            minEvictableIdleTimeMillis: 300000
-            # 配置一个连接在池中最大生存的时间,单位是毫秒
-            maxEvictableIdleTimeMillis: 900000
-            # 配置检测连接是否有效
-            validationQuery: SELECT 1 FROM DUAL
-            testWhileIdle: true
-            testOnBorrow: false
-            testOnReturn: false

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

+ 16 - 0
ruoyi-ui/src/api/infra/dbDoc.js

@@ -8,3 +8,19 @@ export function exportHtml() {
     responseType: 'blob'
   })
 }
+
+export function exportWord() {
+  return request({
+    url: '/infra/db-doc/export-word',
+    method: 'get',
+    responseType: 'blob'
+  })
+}
+
+export function exportMarkdown() {
+  return request({
+    url: '/infra/db-doc/export-markdown',
+    method: 'get',
+    responseType: 'blob'
+  })
+}

+ 18 - 0
ruoyi-ui/src/api/infra/file.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 删除文件
+export function deleteFile(id) {
+  return request({
+    url: '/infra/file/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得文件分页
+export function getFilePage(query) {
+  return request({
+    url: '/infra/file/page',
+    method: 'get',
+    params: query
+  })
+}

+ 2 - 2
ruoyi-ui/src/api/system/dept.js

@@ -46,7 +46,7 @@ export function addDept(data) {
 export function updateDept(data) {
   return request({
     url: '/system/dept/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -55,6 +55,6 @@ export function updateDept(data) {
 export function delDept(id) {
   return request({
     url: '/system/dept/delete?id=' + id,
-    method: 'post'
+    method: 'delete'
   })
 }

+ 2 - 2
ruoyi-ui/src/api/system/dict/data.js

@@ -38,7 +38,7 @@ export function addData(data) {
 export function updateData(data) {
   return request({
     url: '/system/dict-data/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -47,7 +47,7 @@ export function updateData(data) {
 export function delData(dictCode) {
   return request({
     url: '/system/dict-data/delete?id=' + dictCode,
-    method: 'post'
+    method: 'delete'
   })
 }
 

+ 2 - 2
ruoyi-ui/src/api/system/dict/type.js

@@ -30,7 +30,7 @@ export function addType(data) {
 export function updateType(data) {
   return request({
     url: '/system/dict-type/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -39,7 +39,7 @@ export function updateType(data) {
 export function delType(dictId) {
   return request({
     url: '/system/dict-type/delete?id=' + dictId,
-    method: 'post'
+    method: 'delete'
   })
 }
 

+ 2 - 2
ruoyi-ui/src/api/system/menu.js

@@ -38,7 +38,7 @@ export function addMenu(data) {
 export function updateMenu(data) {
   return request({
     url: '/system/menu/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -47,6 +47,6 @@ export function updateMenu(data) {
 export function delMenu(id) {
   return request({
     url: '/system/menu/delete?id=' + id,
-    method: 'post'
+    method: 'delete'
   })
 }

+ 2 - 2
ruoyi-ui/src/api/system/notice.js

@@ -30,7 +30,7 @@ export function addNotice(data) {
 export function updateNotice(data) {
   return request({
     url: '/system/notice/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -39,6 +39,6 @@ export function updateNotice(data) {
 export function delNotice(noticeId) {
   return request({
     url: '/system/notice/delete?id=' + noticeId,
-    method: 'post'
+    method: 'delete'
   })
 }

+ 2 - 2
ruoyi-ui/src/api/system/post.js

@@ -38,7 +38,7 @@ export function addPost(data) {
 export function updatePost(data) {
   return request({
     url: '/system/post/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -47,7 +47,7 @@ export function updatePost(data) {
 export function delPost(postId) {
   return request({
     url: '/system/post/delete?id=' + postId,
-    method: 'post'
+    method: 'delete'
   })
 }
 

+ 3 - 3
ruoyi-ui/src/api/system/role.js

@@ -38,7 +38,7 @@ export function addRole(data) {
 export function updateRole(data) {
   return request({
     url: '/system/role/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -51,7 +51,7 @@ export function changeRoleStatus(id, status) {
   }
   return request({
     url: '/system/role/update-status',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -60,7 +60,7 @@ export function changeRoleStatus(id, status) {
 export function delRole(roleId) {
   return request({
     url: '/system/role/delete?id=' + roleId,
-    method: 'post'
+    method: 'delete'
   })
 }
 

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

+ 8 - 8
ruoyi-ui/src/api/system/user.js

@@ -31,7 +31,7 @@ export function addUser(data) {
 export function updateUser(data) {
   return request({
     url: '/system/user/update',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -62,7 +62,7 @@ export function resetUserPwd(id, password) {
   }
   return request({
     url: '/system/user/update-password',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -75,7 +75,7 @@ export function changeUserStatus(id, status) {
   }
   return request({
     url: '/system/user/update-status',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -83,7 +83,7 @@ export function changeUserStatus(id, status) {
 // 查询用户个人信息
 export function getUserProfile() {
   return request({
-    url: '/system/user/profile',
+    url: '/system/user/profile/get',
     method: 'get'
   })
 }
@@ -91,7 +91,7 @@ export function getUserProfile() {
 // 修改用户个人信息
 export function updateUserProfile(data) {
   return request({
-    url: '/system/user/profile',
+    url: '/system/user/profile/update',
     method: 'put',
     data: data
   })
@@ -104,9 +104,9 @@ export function updateUserPwd(oldPassword, newPassword) {
     newPassword
   }
   return request({
-    url: '/system/user/profile/updatePwd',
+    url: '/system/user/profile/update-password',
     method: 'put',
-    params: data
+    data: data
   })
 }
 
@@ -114,7 +114,7 @@ export function updateUserPwd(oldPassword, newPassword) {
 export function uploadAvatar(data) {
   return request({
     url: '/system/user/profile/avatar',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }

+ 7 - 1
ruoyi-ui/src/main.js

@@ -25,7 +25,10 @@ import {
   download,
   handleTree,
   downloadExcel,
-  downloadZip
+  downloadWord,
+  downloadZip,
+  downloadHtml,
+  downloadMarkdown,
 } from "@/utils/ruoyi";
 import Pagination from "@/components/Pagination";
 // 自定义表格工具扩展
@@ -48,6 +51,9 @@ Vue.prototype.getDictDataLabel = getDictDataLabel
 Vue.prototype.DICT_TYPE = DICT_TYPE
 Vue.prototype.download = download
 Vue.prototype.downloadExcel = downloadExcel
+Vue.prototype.downloadWord = downloadWord
+Vue.prototype.downloadHtml = downloadHtml
+Vue.prototype.downloadMarkdown = downloadMarkdown
 Vue.prototype.downloadZip = downloadZip
 Vue.prototype.handleTree = handleTree
 

+ 1 - 1
ruoyi-ui/src/store/modules/user.js

@@ -53,7 +53,7 @@ const user = {
         getInfo(state.token).then(res => {
           res = res.data; // 读取 data 数据
           const user = res.user
-          const avatar = user.avatar === "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
+          const avatar = user.avatar === "" ? require("@/assets/images/profile.jpg") : user.avatar;
           if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
             commit('SET_ROLES', res.roles)
             commit('SET_PERMISSIONS', res.permissions)

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

+ 12 - 0
ruoyi-ui/src/utils/request.js

@@ -73,6 +73,18 @@ service.interceptors.response.use(res => {
         type: 'error'
       })
       return Promise.reject(new Error(msg))
+    } else if (code === 901) {
+      Message({
+        type: 'error',
+        duration: 0,
+        dangerouslyUseHTMLString: true,
+        message: '<div>演示模式,不发进行写操作</div>'
+          + '<div> &nbsp; </div>'
+          + '<div>参考 https://www.iocoder.cn/Yudao/build-debugger-environment 教程</div>'
+          + '<div> &nbsp; </div>'
+          + '<div>5 分钟搭建本地环境</div>',
+      })
+      return Promise.reject(new Error(msg))
     } else if (code !== 200) {
       Notification.error({
         title: msg

+ 15 - 0
ruoyi-ui/src/utils/ruoyi.js

@@ -120,11 +120,26 @@ export function downloadExcel(data, fileName) {
   download0(data, fileName, 'application/vnd.ms-excel');
 }
 
+// 下载 Word 方法
+export function downloadWord(data, fileName) {
+  download0(data, fileName, 'application/msword');
+}
+
 // 下载 Zip 方法
 export function downloadZip(data, fileName) {
   download0(data, fileName, 'application/zip');
 }
 
+// 下载 Html 方法
+export function downloadHtml(data, fileName) {
+  download0(data, fileName, 'text/html');
+}
+
+// 下载 Markdown 方法
+export function downloadMarkdown(data, fileName) {
+  download0(data, fileName, 'text/markdown');
+}
+
 function download0(data, fileName, mineType) {
   // 创建 blob
   let blob = new Blob([data], {type: mineType});

+ 10 - 38
ruoyi-ui/src/views/infra/config/index.vue

@@ -2,24 +2,12 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="参数名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入参数名称"
-          clearable
-          size="small"
-          style="width: 240px"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.name" placeholder="请输入参数名称" clearable size="small" style="width: 240px"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="参数键名" prop="key">
-        <el-input
-          v-model="queryParams.key"
-          placeholder="请输入参数键名"
-          clearable
-          size="small"
-          style="width: 240px"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.key" placeholder="请输入参数键名" clearable size="small" style="width: 240px"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="系统内置" prop="type">
         <el-select v-model="queryParams.type" placeholder="系统内置" clearable size="small">
@@ -56,7 +44,7 @@
           icon="el-icon-plus"
           size="mini"
           @click="handleAdd"
-          v-hasPermi="['infra:config:add']"
+          v-hasPermi="['infra:config:create']"
         >新增</el-button>
       </el-col>
       <el-col :span="1.5">
@@ -95,31 +83,15 @@
       </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="['infra:config:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['infra:config:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['infra:config:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:config: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"
-    />
+    <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>

+ 202 - 0
ruoyi-ui/src/views/infra/file/index.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="文件路径" prop="id">
+        <el-input v-model="queryParams.id" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="文件类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择文件类型" clearable size="small">
+          <el-option label="请选择字典生成" 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">上传文件</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" width="300" />
+      <el-table-column label="文件类型" align="center" prop="type" width="80" />
+      <el-table-column label="文件内容" align="center" prop="content">
+        <template slot-scope="scope">
+          <img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"
+               width="200px" :src="getFileUrl + scope.row.id">
+          <i v-else>非图片,无法预览</i>
+        </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">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:file: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="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+      <el-upload ref="upload" :limit="1" accept=".jpg, .png, .gif" :auto-upload="false" drag
+                 :headers="upload.headers" :action="upload.url" :data="upload.data" :disabled="upload.isUploading"
+                 :on-change="handleFileChange"
+                 :on-progress="handleFileUploadProgress"
+                 :on-success="handleFileSuccess">
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">
+          将文件拖到此处,或 <em>点击上传</em>
+        </div>
+        <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入 jpg、png、gif 格式文件!</div>
+      </el-upload>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitFileForm">确 定</el-button>
+        <el-button @click="upload.open = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { deleteFile, getFilePage } from "@/api/infra/file";
+import {getToken} from "@/utils/auth";
+
+export default {
+  name: "File",
+  data() {
+    return {
+      getFileUrl: process.env.VUE_APP_BASE_API + '/api/infra/file/get/',
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 文件列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      dateRangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        id: null,
+        type: null,
+      },
+      // 用户导入参数
+      upload: {
+        open: false, // 是否显示弹出层
+        title: "", // 弹出层标题
+        isUploading: false, // 是否禁用上传
+        url: process.env.VUE_APP_BASE_API + '/api/' + "/infra/file/upload", // 请求地址
+        headers: { Authorization: "Bearer " + getToken() }, // 设置上传的请求头部
+        data: {} // 上传的额外数据,用于文件名
+      },
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      getFilePage(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 = {
+        content: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeCreateTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.upload.open = true;
+      this.upload.title = "上传文件";
+    },
+    /** 处理上传的文件发生变化 */
+    handleFileChange(file, fileList) {
+      this.upload.data.path = file.name;
+    },
+    /** 处理文件上传中 */
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true; // 禁止修改
+    },
+    /** 发起文件上窜 */
+    submitFileForm() {
+      this.$refs.upload.submit();
+    },
+    /** 文件上传成功处理 */
+    handleFileSuccess(response, file, fileList) {
+      // 清理
+      this.upload.open = false;
+      this.upload.isUploading = false;
+      this.$refs.upload.clearFiles();
+      // 提示成功,并刷新
+      this.msgSuccess("上传成功");
+      this.getList();
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$confirm('是否确认删除文件编号为"' + id + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return deleteFile(id);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      })
+    },
+  }
+};
+</script>

+ 13 - 54
ruoyi-ui/src/views/system/dept/index.vue

@@ -2,22 +2,11 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
       <el-form-item label="部门名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入部门名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.name" 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 statusDictDatas"
-              :key="parseInt(dict.value)"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-          />
+          <el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -28,24 +17,13 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['system:dept:add']"
-        >新增</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:dept:create']">新增</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table
-      v-loading="loading"
-      :data="deptList"
-      row-key="id"
-      default-expand-all
-      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
-    >
+    <el-table v-loading="loading" :data="deptList" row-key="id" default-expand-all
+              :tree-props="{children: 'children', hasChildren: 'hasChildren'}">
       <el-table-column prop="name" label="部门名称" width="260"></el-table-column>
       <el-table-column prop="sort" label="排序" width="200"></el-table-column>
       <el-table-column prop="status" label="状态" :formatter="statusFormat" width="100"></el-table-column>
@@ -56,28 +34,12 @@
       </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:dept:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-plus"
-            @click="handleAdd(scope.row)"
-            v-hasPermi="['system:dept:add']"
-          >新增</el-button>
-          <el-button
-            v-if="scope.row.parentId !== 0"
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['system:dept:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:dept:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)"
+                     v-hasPermi="['system:dept:create']">新增</el-button>
+          <el-button v-if="scope.row.parentId !== 0" size="mini" type="text" icon="el-icon-delete"
+                     @click="handleDelete(scope.row)" v-hasPermi="['system:dept:delete']">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -119,11 +81,8 @@
           <el-col :span="12">
             <el-form-item label="部门状态" prop="status">
               <el-radio-group v-model="form.status">
-                <el-radio
-                    v-for="dict in statusDictDatas"
-                    :key="parseInt(dict.value)"
-                    :label="parseInt(dict.value)"
-                >{{dict.label}}</el-radio>
+                <el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
+                  {{dict.label}}</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>

+ 14 - 59
ruoyi-ui/src/views/system/dict/data.vue

@@ -3,31 +3,15 @@
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="字典名称" prop="dictType">
         <el-select v-model="queryParams.dictType" size="small">
-          <el-option
-            v-for="item in typeOptions"
-            :key="item.id"
-            :label="item.name"
-            :value="item.type"
-          />
+          <el-option v-for="item in typeOptions" :key="item.id" :label="item.name" :value="item.type"/>
         </el-select>
       </el-form-item>
       <el-form-item label="字典标签" prop="label">
-        <el-input
-          v-model="queryParams.label"
-          placeholder="请输入字典标签"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.label" 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 statusOptions"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
+          <el-option v-for="dict in statusOptions" :key="dict.value" :label="dict.label" :value="dict.value"/>
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -38,22 +22,12 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['system:dict:add']"
-        >新增</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:dict:create']">新增</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="warning"
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['system:dict:export']"
-        >导出</el-button>
+        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:dict:export']">导出</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
@@ -72,31 +46,16 @@
       </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:dict:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['system:dict:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:dict:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:dict: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"
-    />
+    <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>
@@ -115,11 +74,7 @@
         </el-form-item>
         <el-form-item label="状态" prop="status">
           <el-radio-group v-model="form.status">
-            <el-radio
-                v-for="dict in statusDictDatas"
-                :key="parseInt(dict.value)"
-                :label="parseInt(dict.value)"
-            >{{dict.label}}</el-radio>
+            <el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="备注" prop="remark">

+ 17 - 79
ruoyi-ui/src/views/system/dict/index.vue

@@ -2,52 +2,19 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="字典名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入字典名称"
-          clearable
-          size="small"
-          style="width: 240px"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.name" placeholder="请输入字典名称" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="字典类型" prop="type">
-        <el-input
-          v-model="queryParams.type"
-          placeholder="请输入字典类型"
-          clearable
-          size="small"
-          style="width: 240px"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.type" placeholder="请输入字典类型" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="状态" prop="status">
-        <el-select
-          v-model="queryParams.status"
-          placeholder="字典状态"
-          clearable
-          size="small"
-          style="width: 240px"
-        >
-          <el-option
-              v-for="dict in statusDictDatas"
-              :key="parseInt(dict.value)"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-          />
+        <el-select v-model="queryParams.status" placeholder="字典状态" clearable size="small" style="width: 240px">
+          <el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(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-date-picker>
+        <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-date-picker>
       </el-form-item>
       <el-form-item>
         <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -57,22 +24,12 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['system:dict:add']"
-        >新增</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:dict:create']">新增</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="warning"
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['system:dict:export']"
-        >导出</el-button>
+        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:dict:export']">导出</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
@@ -96,31 +53,16 @@
       </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:dict:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['system:dict:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:dict:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:dict: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"
-    />
+    <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>
@@ -133,11 +75,7 @@
         </el-form-item>
         <el-form-item label="状态" prop="status">
           <el-radio-group v-model="form.status">
-            <el-radio
-                v-for="dict in statusDictDatas"
-                :key="parseInt(dict.value)"
-                :label="parseInt(dict.value)"
-            >{{dict.label}}</el-radio>
+            <el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="备注" prop="remark">

+ 14 - 58
ruoyi-ui/src/views/system/loginlog/index.vue

@@ -2,56 +2,22 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="登录地址" prop="userIp">
-        <el-input
-          v-model="queryParams.userIp"
-          placeholder="请输入登录地址"
-          clearable
-          style="width: 240px;"
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.userIp" placeholder="请输入登录地址" clearable style="width: 240px;" size="small"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="用户名称" prop="username">
-        <el-input
-          v-model="queryParams.username"
-          placeholder="请输入用户名称"
-          clearable
-          style="width: 240px;"
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.username" placeholder="请输入用户名称" clearable style="width: 240px;" 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"
-          style="width: 240px"
-        >
-          <el-option
-              :key="true"
-              label="成功"
-              :value="true"
-          />
-          <el-option
-              :key="false"
-              label="失败"
-              :value="false"
-          />
+        <el-select v-model="queryParams.status" placeholder="结果" clearable size="small" style="width: 240px">
+          <el-option :key="true" label="成功" :value="true"/>
+          <el-option :key="false" label="失败" :value="false"/>
         </el-select>
       </el-form-item>
       <el-form-item label="登录时间">
-        <el-date-picker
-          v-model="dateRange"
-          size="small"
-          style="width: 240px"
-          value-format="yyyy-MM-dd"
-          type="daterange"
-          range-separator="-"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-        ></el-date-picker>
+        <el-date-picker v-model="dateRange" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+          type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
       </el-form-item>
       <el-form-item>
         <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -61,13 +27,8 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="warning"
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['system:login-log:export']"
-        >导出</el-button>
+        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:login-log:export']">导出</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
@@ -76,7 +37,7 @@
       <el-table-column label="访问编号" align="center" prop="id" />
       <el-table-column label="日志类型" align="center" prop="logType">
         <template slot-scope="scope">
-          <span>{{ scope.row.logType === 1 ? '登录' : '退出' }}</span>
+          <span>{{ scope.row.logType === 100 ? '登录' : '退出' }}</span>
         </template>
       </el-table-column>
       <el-table-column label="用户名称" align="center" prop="username" />
@@ -95,13 +56,8 @@
       </el-table-column>
     </el-table>
 
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNo"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
   </div>
 </template>
 

+ 19 - 71
ruoyi-ui/src/views/system/menu/index.vue

@@ -2,22 +2,11 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
       <el-form-item label="菜单名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入菜单名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.name" 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 statusDictDatas"
-            :key="parseInt(dict.value)"
-            :label="dict.label"
-            :value="parseInt(dict.value)"
-          />
+          <el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -28,23 +17,14 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['system:menu:add']"
-        >新增</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:menu:create']">新增</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table
-      v-loading="loading"
-      :data="menuList"
-      row-key="id"
-      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
-    >
+    <el-table v-loading="loading" :data="menuList" row-key="id"
+              :tree-props="{children: 'children', hasChildren: 'hasChildren'}">
       <el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="200"></el-table-column>
       <el-table-column prop="icon" label="图标" align="center" width="100">
         <template slot-scope="scope">
@@ -62,26 +42,12 @@
       </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:menu:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-plus"
-            @click="handleAdd(scope.row)"
-            v-hasPermi="['system:menu:add']"
-          >新增</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['system:menu:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:menu:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)"
+                     v-hasPermi="['system:menu:create']">新增</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:menu:delete']">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -92,43 +58,25 @@
         <el-row>
           <el-col :span="24">
             <el-form-item label="上级菜单">
-              <treeselect
-                v-model="form.parentId"
-                :options="menuOptions"
-                :normalizer="normalizer"
-                :show-count="true"
-                placeholder="选择上级菜单"
-              />
+              <treeselect v-model="form.parentId" :options="menuOptions" :normalizer="normalizer" :show-count="true"
+                          placeholder="选择上级菜单"/>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="菜单类型" prop="type">
               <el-radio-group v-model="form.type">
-                <el-radio
-                    v-for="dict in menuTypeDictDatas"
-                    :key="parseInt(dict.value)"
-                    :label="parseInt(dict.value)"
-                >{{dict.label}}</el-radio>
+                <el-radio v-for="dict in menuTypeDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
+                  {{dict.label}}</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item v-if="form.type != '3'" label="菜单图标">
-              <el-popover
-                placement="bottom-start"
-                width="460"
-                trigger="click"
-                @show="$refs['iconSelect'].reset()"
-              >
+              <el-popover placement="bottom-start" width="460" trigger="click" @show="$refs['iconSelect'].reset()">
                 <IconSelect ref="iconSelect" @selected="selected" />
                 <el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
-                  <svg-icon
-                    v-if="form.icon"
-                    slot="prefix"
-                    :icon-class="form.icon"
-                    class="el-input__icon"
-                    style="height: 32px;width: 16px;"
-                  />
+                  <svg-icon v-if="form.icon" slot="prefix" :icon-class="form.icon" class="el-input__icon"
+                            style="height: 32px;width: 16px;"/>
                   <i v-else slot="prefix" class="el-icon-search el-input__icon" />
                 </el-input>
               </el-popover>

+ 13 - 68
ruoyi-ui/src/views/system/notice/index.vue

@@ -2,31 +2,14 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="公告标题" prop="title">
-        <el-input
-          v-model="queryParams.title"
-          placeholder="请输入公告标题"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.title" placeholder="请输入公告标题" clearable size="small" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="操作人员" prop="createBy">
-        <el-input
-          v-model="queryParams.createBy"
-          placeholder="请输入操作人员"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.createBy" placeholder="请输入操作人员" clearable size="small" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="类型" prop="type">
         <el-select v-model="queryParams.type" placeholder="公告类型" clearable size="small">
-          <el-option
-              v-for="dict in noticeTypeDictDatas"
-              :key="parseInt(dict.value)"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-          />
+          <el-option v-for="dict in noticeTypeDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -37,39 +20,16 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['system:notice:add']"
-        >新增</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:notice:create']"s>新增</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
     <el-table v-loading="loading" :data="noticeList">
       <el-table-column label="序号" align="center" prop="id" width="100" />
-      <el-table-column
-        label="公告标题"
-        align="center"
-        prop="title"
-        :show-overflow-tooltip="true"
-      />
-      <el-table-column
-        label="公告类型"
-        align="center"
-        prop="type"
-        :formatter="typeFormat"
-        width="100"
-      />
-      <el-table-column
-        label="状态"
-        align="center"
-        prop="status"
-        :formatter="statusFormat"
-        width="100"
-      />
+      <el-table-column label="公告标题" align="center" prop="title" :show-overflow-tooltip="true"/>
+      <el-table-column label="公告类型" align="center" prop="type" :formatter="typeFormat" width="100"/>
+      <el-table-column label="状态" align="center" prop="status" :formatter="statusFormat" width="100"/>
       <el-table-column label="创建者" align="center" prop="createBy" width="100" />
       <el-table-column label="创建时间" align="center" prop="createTime" width="100">
         <template slot-scope="scope">
@@ -78,31 +38,16 @@
       </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:notice:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['system:notice:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:notice:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:notice: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"
-    />
+    <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="780px" append-to-body>

+ 14 - 64
ruoyi-ui/src/views/system/operatelog/index.vue

@@ -2,72 +2,28 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="系统模块" prop="title">
-        <el-input
-          v-model="queryParams.title"
-          placeholder="请输入系统模块"
-          clearable
-          style="width: 240px;"
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable style="width: 240px;" size="small"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="操作人员" prop="operName">
-        <el-input
-          v-model="queryParams.operName"
-          placeholder="请输入操作人员"
-          clearable
-          style="width: 240px;"
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable style="width: 240px;" size="small"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="类型" prop="type">
-        <el-select
-          v-model="queryParams.type"
-          placeholder="操作类型"
-          clearable
-          size="small"
-          style="width: 240px"
-        >
-          <el-option
-            v-for="dict in this.getDictDatas(DICT_TYPE.SYS_OPERATE_TYPE)"
-            :key="parseInt(dict.value)"
-            :label="dict.label"
-            :value="parseInt(dict.value)"
-          />
+        <el-select v-model="queryParams.type" placeholder="操作类型" clearable size="small" style="width: 240px">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_OPERATE_TYPE)" :key="parseInt(dict.value)"
+                     :label="dict.label" :value="parseInt(dict.value)"/>
         </el-select>
       </el-form-item>
       <el-form-item label="状态" prop="status">
-        <el-select
-          v-model="queryParams.success"
-          placeholder="操作状态"
-          clearable
-          size="small"
-          style="width: 240px"
-        >
-          <el-option
-              :key="true"
-              label="成功"
-              :value="true"
-          />
-          <el-option
-              :key="false"
-              label="失败"
-              :value="false"
-          />
+        <el-select v-model="queryParams.success" placeholder="操作状态" clearable size="small" style="width: 240px">
+          <el-option :key="true" label="成功" :value="true"/>
+          <el-option :key="false" label="失败" :value="false"/>
         </el-select>
       </el-form-item>
       <el-form-item label="操作时间">
-        <el-date-picker
-          v-model="dateRange"
-          size="small"
-          style="width: 240px"
-          value-format="yyyy-MM-dd"
-          type="daterange"
-          range-separator="-"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-        ></el-date-picker>
+        <el-date-picker v-model="dateRange" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+          type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
       </el-form-item>
       <el-form-item>
         <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -77,13 +33,8 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="warning"
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['system:config:export']"
-        >导出</el-button>
+        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:operate-log:export']">导出</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
@@ -212,7 +163,6 @@ export default {
         businessType: undefined,
         status: undefined
       },
-
     };
   },
   created() {

+ 15 - 60
ruoyi-ui/src/views/system/post/index.vue

@@ -2,31 +2,14 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="岗位编码" prop="code">
-        <el-input
-          v-model="queryParams.code"
-          placeholder="请输入岗位编码"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.code" placeholder="请输入岗位编码" clearable size="small" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="岗位名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入岗位名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.name" 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 statusDictDatas"
-              :key="parseInt(dict.value)"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-          />
+          <el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -37,22 +20,12 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['system:post:add']"
-        >新增</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:post:create']">新增</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="warning"
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['system:post:export']"
-        >导出</el-button>
+        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:post:export']">导出</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
@@ -70,31 +43,16 @@
       </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:post:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['system:post:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:post:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:post: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"
-    />
+    <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>
@@ -110,11 +68,8 @@
         </el-form-item>
         <el-form-item label="岗位状态" prop="status">
           <el-radio-group v-model="form.status">
-            <el-radio
-                v-for="dict in statusDictDatas"
-                :key="parseInt(dict.value)"
-                :label="parseInt(dict.value)"
-            >{{dict.label}}</el-radio>
+            <el-radio v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="parseInt(dict.value)">
+              {{dict.label}}</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="备注" prop="remark">

+ 23 - 94
ruoyi-ui/src/views/system/role/index.vue

@@ -2,52 +2,21 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" v-show="showSearch" :inline="true">
       <el-form-item label="角色名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入角色名称"
-          clearable
-          size="small"
-          style="width: 240px"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.name" placeholder="请输入角色名称" clearable size="small" style="width: 240px"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="角色标识" prop="code">
-        <el-input
-          v-model="queryParams.code"
-          placeholder="请输入角色标识"
-          clearable
-          size="small"
-          style="width: 240px"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.code" placeholder="请输入角色标识" clearable size="small" style="width: 240px"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="状态" prop="status">
-        <el-select
-          v-model="queryParams.status"
-          placeholder="角色状态"
-          clearable
-          size="small"
-          style="width: 240px"
-        >
-          <el-option
-              v-for="dict in statusDictDatas"
-              :key="parseInt(dict.value)"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-          />
+        <el-select v-model="queryParams.status" placeholder="角色状态" clearable size="small" style="width: 240px">
+          <el-option v-for="dict in statusDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
         </el-select>
       </el-form-item>
       <el-form-item label="创建时间">
-        <el-date-picker
-          v-model="dateRange"
-          size="small"
-          style="width: 240px"
-          value-format="yyyy-MM-dd"
-          type="daterange"
-          range-separator="-"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-        ></el-date-picker>
+        <el-date-picker v-model="dateRange" size="small" style="width: 240px" value-format="yyyy-MM-dd" type="daterange"
+          range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
       </el-form-item>
       <el-form-item>
         <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -57,22 +26,12 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['system:role:add']"
-        >新增</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['system:role:create']">新增</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="warning"
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['system:role:export']"
-        >导出</el-button>
+        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
+                   v-hasPermi="['system:role:export']">导出</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
@@ -85,12 +44,7 @@
       <el-table-column label="显示顺序" prop="sort" width="100" />
       <el-table-column label="状态" align="center" width="100">
         <template slot-scope="scope">
-          <el-switch
-            v-model="scope.row.status"
-            :active-value="0"
-            :inactive-value="1"
-            @change="handleStatusChange(scope.row)"
-          ></el-switch>
+          <el-switch v-model="scope.row.status" :active-value="0" :inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
         </template>
       </el-table-column>
       <el-table-column label="创建时间" align="center" prop="createTime" width="180">
@@ -100,45 +54,20 @@
       </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:role:edit']"
-          >修改</el-button>
-          <el-button
-              size="mini"
-              type="text"
-              icon="el-icon-circle-check"
-              @click="handleMenu(scope.row)"
-              v-hasPermi="['system:permission:assign-role-menu']"
-          >菜单权限</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-circle-check"
-            @click="handleDataScope(scope.row)"
-            v-hasPermi="['system:permission:assign-role-data-scope']"
-          >数据权限</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['system:role:remove']"
-          >删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['system:role:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-circle-check" @click="handleMenu(scope.row)"
+                     v-hasPermi="['system:permission:assign-role-menu']">菜单权限</el-button>
+          <el-button size="mini" type="text" icon="el-icon-circle-check" @click="handleDataScope(scope.row)"
+                     v-hasPermi="['system:permission:assign-role-data-scope']">数据权限</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:role:delete']">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
 
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNum"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
 
     <!-- 添加或修改角色配置对话框 -->
     <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>

+ 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>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 659 - 715
ruoyi-ui/src/views/system/user/index.vue


+ 9 - 7
ruoyi-ui/src/views/system/user/profile/index.vue

@@ -13,11 +13,11 @@
             <ul class="list-group list-group-striped">
               <li class="list-group-item">
                 <svg-icon icon-class="user" />用户名称
-                <div class="pull-right">{{ user.userName }}</div>
+                <div class="pull-right">{{ user.username }}</div>
               </li>
               <li class="list-group-item">
                 <svg-icon icon-class="phone" />手机号码
-                <div class="pull-right">{{ user.phonenumber }}</div>
+                <div class="pull-right">{{ user.mobile }}</div>
               </li>
               <li class="list-group-item">
                 <svg-icon icon-class="email" />用户邮箱
@@ -25,15 +25,19 @@
               </li>
               <li class="list-group-item">
                 <svg-icon icon-class="tree" />所属部门
-                <div class="pull-right" v-if="user.dept">{{ user.dept.deptName }} / {{ postGroup }}</div>
+                <div class="pull-right" v-if="user.dept">{{ user.dept.name }}</div>
+              </li>
+              <li class="list-group-item">
+                <svg-icon icon-class="tree" />所属岗位
+                <div class="pull-right" v-if="user.posts">{{ user.posts.map(post => post.name).join(',') }}</div>
               </li>
               <li class="list-group-item">
                 <svg-icon icon-class="peoples" />所属角色
-                <div class="pull-right">{{ roleGroup }}</div>
+                <div class="pull-right">{{ user.roles.map(post => post.name).join(',') }}</div>
               </li>
               <li class="list-group-item">
                 <svg-icon icon-class="date" />创建日期
-                <div class="pull-right">{{ user.createTime }}</div>
+                <div class="pull-right">{{ parseTime(user.createTime) }}</div>
               </li>
             </ul>
           </div>
@@ -82,8 +86,6 @@ export default {
     getUser() {
       getUserProfile().then(response => {
         this.user = response.data;
-        this.roleGroup = response.roleGroup;
-        this.postGroup = response.postGroup;
       });
     }
   }

+ 8 - 8
ruoyi-ui/src/views/system/user/profile/userInfo.vue

@@ -1,18 +1,18 @@
 <template>
   <el-form ref="form" :model="user" :rules="rules" label-width="80px">
     <el-form-item label="用户昵称" prop="nickName">
-      <el-input v-model="user.nickName" />
-    </el-form-item> 
-    <el-form-item label="手机号码" prop="phonenumber">
-      <el-input v-model="user.phonenumber" maxlength="11" />
+      <el-input v-model="user.nickname" />
+    </el-form-item>
+    <el-form-item label="手机号码" prop="mobile">
+      <el-input v-model="user.mobile" maxlength="11" />
     </el-form-item>
     <el-form-item label="邮箱" prop="email">
       <el-input v-model="user.email" maxlength="50" />
     </el-form-item>
     <el-form-item label="性别">
       <el-radio-group v-model="user.sex">
-        <el-radio label="0">男</el-radio>
-        <el-radio label="1">女</el-radio>
+        <el-radio :label="1">男</el-radio>
+        <el-radio :label="2">女</el-radio>
       </el-radio-group>
     </el-form-item>
     <el-form-item>
@@ -35,7 +35,7 @@ export default {
     return {
       // 表单校验
       rules: {
-        nickName: [
+        nickname: [
           { required: true, message: "用户昵称不能为空", trigger: "blur" }
         ],
         email: [
@@ -46,7 +46,7 @@ export default {
             trigger: ["blur", "change"]
           }
         ],
-        phonenumber: [
+        mobile: [
           { required: true, message: "手机号码不能为空", trigger: "blur" },
           {
             pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,

+ 37 - 5
ruoyi-ui/src/views/tool/dbDoc/index.vue

@@ -1,10 +1,21 @@
 <template>
-  <div v-loading="loading" :style="'height:'+ height">
-    <iframe :src="src" frameborder="no" style="width: 100%;height: 100%" scrolling="auto" />
+  <div class="app-container">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportHtml">导出 HTML</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportWord">导出 Word</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportMarkdown">导出 Markdown</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 展示文档 -->
+    <div v-loading="loading" :style="'height:'+ height">
+      <iframe :src="src" frameborder="no" style="width: 100%;height: 100%" scrolling="auto" />
+    </div>
   </div>
 </template>
 <script>
-import {exportHtml} from "@/api/infra/dbDoc";
+import { exportHtml, exportWord, exportMarkdown} from "@/api/infra/dbDoc";
 
 export default {
   name: "DBDoc",
@@ -25,10 +36,31 @@ export default {
     };
   },
   created() {
+    // 加载 Html,进行预览
     exportHtml().then(response => {
-      // var blob = new Blob(['<a id="a"><b id="b">hey!</b></a>'], {type : 'text/html'});
-      this.src = window.URL.createObjectURL(response);
+      let blob = new Blob([response], {type : 'text/html'});
+      this.src = window.URL.createObjectURL(blob);
     })
   },
+  methods: {
+    /** 处理导出 HTML */
+    handleExportHtml() {
+      exportHtml().then(response => {
+        this.downloadHtml(response, '数据库文档.html');
+      })
+    },
+    /** 处理导出 Word */
+    handleExportWord() {
+      exportWord().then(response => {
+        this.downloadWord(response, '数据库文档.doc');
+      })
+    },
+    /** 处理导出 Markdown */
+    handleExportMarkdown() {
+      exportMarkdown().then(response => {
+        this.downloadMarkdown(response, '数据库文档.md');
+      })
+    }
+  }
 };
 </script>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 8 - 0
sql/quartz.sql


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 339 - 75
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) {

+ 1 - 0
src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java

@@ -32,6 +32,7 @@ public interface GlobalErrorCodeConstants {
 
     // ========== 自定义错误段 ==========
     ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
+    ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
 
     ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
 

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

+ 27 - 1
src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java

@@ -1,12 +1,38 @@
 package cn.iocoder.dashboard.framework.datasource.config;
 
+import cn.iocoder.dashboard.framework.datasource.core.filter.DruidAdRemoveFilter;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
- * 数据库匹配类
+ * 数据库配置类
+ *
+ * @author 芋道源码
  */
 @Configuration
 @EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理
 public class DataSourceConfiguration {
+
+    /**
+     * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告
+     */
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true")
+    public FilterRegistrationBean<DruidAdRemoveFilter> druidAdRemoveFilterFilter(DruidStatProperties properties) {
+        // 获取 druid web 监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取 common.js 的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        // 创建 DruidAdRemoveFilter Bean
+        FilterRegistrationBean<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new DruidAdRemoveFilter());
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+
 }

+ 22 - 0
src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java

@@ -0,0 +1,22 @@
+package cn.iocoder.dashboard.framework.datasource.core.enums;
+
+/**
+ * 对应于多数据源中不同数据源配置
+ *
+ * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。
+ * 注意,默认是 {@link #MASTER} 数据源
+ *
+ * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html
+ */
+public interface DataSourceEnum {
+
+    /**
+     * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解
+     */
+    String MASTER = "master";
+    /**
+     * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解
+     */
+    String SLAVE = "slave";
+
+}

+ 38 - 0
src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java

@@ -0,0 +1,38 @@
+package cn.iocoder.dashboard.framework.datasource.core.filter;
+
+import com.alibaba.druid.util.Utils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Druid 底部广告过滤器
+ *
+ * @author 芋道源码
+ */
+public class DruidAdRemoveFilter extends OncePerRequestFilter {
+
+    /**
+     * common.js 的路径
+     */
+    private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js";
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        chain.doFilter(request, response);
+        // 重置缓冲区,响应头不会被重置
+        response.resetBuffer();
+        // 获取 common.js
+        String text = Utils.readFromResource(COMMON_JS_ILE_PATH);
+        // 正则替换 banner, 除去底部的广告信息
+        text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+        text = text.replaceAll("powered.*?shrek.wang</a>", "");
+        response.getWriter().write(text);
+    }
+
+}

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

+ 2 - 2
src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.framework.file.config;
 
-import cn.iocoder.dashboard.modules.system.controller.common.SysFileController;
+import cn.iocoder.dashboard.modules.infra.controller.file.InfFileController;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.validation.annotation.Validated;
@@ -13,7 +13,7 @@ import javax.validation.constraints.NotNull;
 public class FileProperties {
 
     /**
-     * 对应 {@link SysFileController#}
+     * 对应 {@link InfFileController#}
      */
     @NotNull(message = "基础文件路径不能为空")
     private String basePath;

+ 18 - 1
src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java

@@ -1,18 +1,35 @@
 package cn.iocoder.dashboard.framework.jackson.config;
 
+import cn.iocoder.dashboard.framework.jackson.deser.LocalDateTimeDeserializer;
+import cn.iocoder.dashboard.framework.jackson.ser.LocalDateTimeSerializer;
 import cn.iocoder.dashboard.util.json.JsonUtils;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
+import java.time.LocalDateTime;
+
 @Configuration
 public class JacksonConfig {
 
     @Bean
     @SuppressWarnings("InstantiationOfUtilityClass")
     public JsonUtils jsonUtils(ObjectMapper objectMapper) {
+        SimpleModule simpleModule = new SimpleModule();
+        /*
+         * 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型
+         * 2. 新增LocalDateTime序列化、反序列化规则
+         */
+        simpleModule
+//                .addSerializer(Long.class, ToStringSerializer.instance)
+//                    .addSerializer(Long.TYPE, ToStringSerializer.instance)
+                    .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE)
+                    .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
+
+        objectMapper.registerModules(simpleModule);
+
         JsonUtils.init(objectMapper);
         return new JsonUtils();
     }
-
 }

+ 26 - 0
src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java

@@ -0,0 +1,26 @@
+package cn.iocoder.dashboard.framework.jackson.deser;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+/**
+ * LocalDateTime反序列化规则
+ * <p>
+ * 会将毫秒级时间戳反序列化为LocalDateTime
+ */
+public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
+
+    public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
+
+    @Override
+    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+        return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
+    }
+}

+ 24 - 0
src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java

@@ -0,0 +1,24 @@
+package cn.iocoder.dashboard.framework.jackson.ser;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+/**
+ * LocalDateTime序列化规则
+ * <p>
+ * 会将LocalDateTime序列化为毫秒级时间戳
+ */
+public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
+
+    public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();
+
+    @Override
+    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+    }
+}

+ 1 - 1
src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java

@@ -81,7 +81,7 @@ public class ApiAccessLogFilter extends OncePerRequestFilter {
                                       Map<String, String> queryString, String requestBody, Exception ex) {
         // 处理用户信息
         accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
-        accessLog.setUserType(WebFrameworkUtils.getUesrType(request));
+        accessLog.setUserType(WebFrameworkUtils.getUserType(request));
         // 设置访问结果
         CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
         if (result != null) {

+ 3 - 1
src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.framework.logger.apilog.core.service;
 import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
 
 import javax.validation.Valid;
+import java.util.concurrent.Future;
 
 /**
  * API 访问日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiAccessLogFrameworkService {
      * 创建 API 访问日志
      *
      * @param createDTO 创建信息
+     * @return 是否创建成功
      */
-    void createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
+    Future<Boolean> createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
 
 }

+ 3 - 1
src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.framework.logger.apilog.core.service;
 import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
 
 import javax.validation.Valid;
+import java.util.concurrent.Future;
 
 /**
  * API 错误日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiErrorLogFrameworkService {
      * 创建 API 错误日志
      *
      * @param createDTO 创建信息
+     * @return 是否创建成功
      */
-    void createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
+    Future<Boolean> createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
 
 }

+ 5 - 2
src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java

@@ -2,13 +2,16 @@ package cn.iocoder.dashboard.framework.logger.operatelog.core.service;
 
 import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO;
 
+import java.util.concurrent.Future;
+
 public interface OperateLogFrameworkService {
 
     /**
-     * 要不记录操作日志
+     * 异步记录操作日志
      *
      * @param reqVO 操作日志请求
+     * @return true: 记录成功,false: 记录失败
      */
-    void createOperateLogAsync(SysOperateLogCreateReqVO reqVO);
+    Future<Boolean> createOperateLogAsync(SysOperateLogCreateReqVO reqVO);
 
 }

+ 1 - 1
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java

@@ -57,7 +57,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
         }
         // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
         if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
-            setFieldValByName("updater", loginUser.getId(), metaObject);
+            setFieldValByName("updater", loginUser.getId().toString(), metaObject);
         }
     }
 }

+ 70 - 2
src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java

@@ -1,14 +1,21 @@
 package cn.iocoder.dashboard.framework.redis.config;
 
+import cn.hutool.system.SystemUtil;
 import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
+import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.stream.Consumer;
+import org.springframework.data.redis.connection.stream.ObjectRecord;
+import org.springframework.data.redis.connection.stream.ReadOffset;
+import org.springframework.data.redis.connection.stream.StreamOffset;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.listener.ChannelTopic;
 import org.springframework.data.redis.listener.RedisMessageListenerContainer;
 import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.stream.StreamMessageListenerContainer;
 
 import java.util.List;
 
@@ -19,6 +26,9 @@ import java.util.List;
 @Slf4j
 public class RedisConfig {
 
+    /**
+     * 创建 RedisTemplate Bean,使用 JSON 序列化方式
+     */
     @Bean
     public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
         // 创建 RedisTemplate 对象
@@ -27,14 +37,19 @@ public class RedisConfig {
         template.setConnectionFactory(factory);
         // 使用 String 序列化方式,序列化 KEY 。
         template.setKeySerializer(RedisSerializer.string());
+        template.setHashKeySerializer(RedisSerializer.string());
         // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
         template.setValueSerializer(RedisSerializer.json());
+        template.setHashValueSerializer(RedisSerializer.json());
         return template;
     }
 
+    /**
+     * 创建 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 工厂。
@@ -48,4 +63,57 @@ public class RedisConfig {
         return container;
     }
 
+    /**
+     * 创建 Redis Stream 集群消费的容器
+     *
+     * 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) {
+        // 第一步,创建 StreamMessageListenerContainer 容器
+        // 创建 options 配置
+        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
+                StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
+                        .batchSize(10) // 一次性最多拉取多少条消息
+                        .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
+                        .build();
+        // 创建 container 对象
+        StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer.create(
+                redisTemplate.getRequiredConnectionFactory(), containerOptions);
+
+        // 第二步,注册监听器,消费对应的 Stream 主题
+        String consumerName = buildConsumerName();
+//        String consumerName = "110";
+        listeners.forEach(listener -> {
+            // 创建 listener 对应的消费者分组
+            try {
+                redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
+            } catch (Exception ignore) {}
+            // 设置 listener 对应的 redisTemplate
+            listener.setRedisTemplate(redisTemplate);
+            // 创建 Consumer 对象
+            Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
+            // 设置 Consumer 消费进度,以最小消费进度为准
+            StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed());
+            // 设置 Consumer 监听
+            StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
+                    .builder(streamOffset).consumer(consumer)
+                    .autoAcknowledge(false) // 不自动 ack
+                    .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
+            container.register(builder.build(), listener);
+        });
+        return container;
+    }
+
+    /**
+     * 构建消费者名字,使用本地 IP + 进程编号的方式。
+     * 参考自 RocketMQ clientId 的实现
+     *
+     * @return 消费者名字
+     */
+    private static String buildConsumerName() {
+        return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
+    }
+
 }

+ 5 - 16
src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java

@@ -1,11 +1,10 @@
 package cn.iocoder.dashboard.framework.redis.core.pubsub;
 
-import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.TypeUtil;
 import cn.iocoder.dashboard.util.json.JsonUtils;
 import lombok.SneakyThrows;
 import org.springframework.data.redis.connection.Message;
 import org.springframework.data.redis.connection.MessageListener;
-import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
 
 import java.lang.reflect.Type;
 
@@ -62,21 +61,11 @@ public abstract class AbstractChannelMessageListener<T extends ChannelMessage> i
      */
     @SuppressWarnings("unchecked")
     private Class<T> getMessageClass() {
-        Class<?> targetClass = getClass();
-        while (targetClass.getSuperclass() != null) {
-            // 如果不是 AbstractMessageListener 父类,继续向上查找
-            if (targetClass.getSuperclass() != AbstractChannelMessageListener.class) {
-                targetClass = targetClass.getSuperclass();
-                continue;
-            }
-            // 如果是 AbstractMessageListener 父类,则解析泛型
-            Type[] types = ((ParameterizedTypeImpl) targetClass.getGenericSuperclass()).getActualTypeArguments();
-            if (ArrayUtil.isEmpty(types)) {
-                throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
-            }
-            return (Class<T>) types[0];
+        Type type = TypeUtil.getTypeArgument(getClass(), 0);
+        if (type == null) {
+            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
         }
-        throw new IllegalStateException(String.format("类型(%s) 找不到 AbstractMessageListener 父类", getClass().getName()));
+        return (Class<T>) type;
     }
 
 }

+ 3 - 1
src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java

@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 
 /**
  * Redis Channel Message 接口
+ *
+ * @author 芋道源码
  */
 public interface ChannelMessage {
 
@@ -12,7 +14,7 @@ public interface ChannelMessage {
      *
      * @return Channel
      */
-    @JsonIgnore // 必须序列化
+    @JsonIgnore // 避免序列化
     String getChannel();
 
 }

+ 88 - 0
src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java

@@ -0,0 +1,88 @@
+package cn.iocoder.dashboard.framework.redis.core.stream;
+
+import cn.hutool.core.util.TypeUtil;
+import cn.iocoder.dashboard.util.json.JsonUtils;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.connection.stream.ObjectRecord;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.stream.StreamListener;
+
+import java.lang.reflect.Type;
+
+/**
+ * Redis Stream 监听器抽象类,用于实现集群消费
+ *
+ * @param <T> 消息类型。一定要填写噢,不然会报错
+ *
+ * @author 芋道源码
+ */
+public abstract class AbstractStreamMessageListener<T extends StreamMessage>
+        implements StreamListener<String, ObjectRecord<String, String>> {
+
+    /**
+     * 消息类型
+     */
+    private final Class<T> messageType;
+    /**
+     * Redis Channel
+     */
+    @Getter
+    private final String streamKey;
+
+    /**
+     * Redis 消费者分组,默认使用 spring.application.name 名字
+     */
+    @Value("${spring.application.name}")
+    @Getter
+    private String group;
+    /**
+     *
+     */
+    @Setter
+    private RedisTemplate<String, ?> redisTemplate;
+
+    @SneakyThrows
+    protected AbstractStreamMessageListener() {
+        this.messageType = getMessageClass();
+        this.streamKey = messageType.newInstance().getStreamKey();
+    }
+
+    @Override
+    public void onMessage(ObjectRecord<String, String> message) {
+        // 消费消息
+        T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
+        this.onMessage(messageObj);
+        // ack 消息消费完成
+        redisTemplate.opsForStream().acknowledge(group, message);
+        // TODO 芋艿:需要额外考虑以下几个点:
+        // 1. 处理异常的情况
+        // 2. 发送日志;以及事务的结合
+        // 3. 消费日志;以及通用的幂等性
+        // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
+    }
+
+    /**
+     * 处理消息
+     *
+     * @param message 消息
+     */
+    public abstract void onMessage(T message);
+
+    /**
+     * 通过解析类上的泛型,获得消息类型
+     *
+     * @return 消息类型
+     */
+    @SuppressWarnings("unchecked")
+    private Class<T> getMessageClass() {
+        Type type = TypeUtil.getTypeArgument(getClass(), 0);
+        if (type == null) {
+            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
+        }
+        return (Class<T>) type;
+    }
+
+}

+ 20 - 0
src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java

@@ -0,0 +1,20 @@
+package cn.iocoder.dashboard.framework.redis.core.stream;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Redis Stream Message 接口
+ *
+ * @author 芋道源码
+ */
+public interface StreamMessage {
+
+    /**
+     * 获得 Redis Stream Key
+     *
+     * @return Channel
+     */
+    @JsonIgnore // 避免序列化
+    String getStreamKey();
+
+}

+ 17 - 1
src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java

@@ -1,7 +1,10 @@
 package cn.iocoder.dashboard.framework.redis.core.util;
 
 import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
+import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage;
 import cn.iocoder.dashboard.util.json.JsonUtils;
+import org.springframework.data.redis.connection.stream.RecordId;
+import org.springframework.data.redis.connection.stream.StreamRecords;
 import org.springframework.data.redis.core.RedisTemplate;
 
 /**
@@ -17,8 +20,21 @@ public class RedisMessageUtils {
      * @param redisTemplate Redis 操作模板
      * @param message 消息
      */
-    public static <T extends ChannelMessage>  void sendChannelMessage(RedisTemplate<?, ?> redisTemplate, T message) {
+    public static <T extends ChannelMessage> void sendChannelMessage(RedisTemplate<?, ?> redisTemplate, T message) {
         redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
     }
 
+    /**
+     * 发送 Redis 消息,基于 Redis Stream 实现
+     *
+     * @param redisTemplate Redis 操作模板
+     * @param message 消息
+     * @return 消息记录的编号对象
+     */
+    public static <T extends StreamMessage> RecordId sendStreamMessage(RedisTemplate<String, ?> redisTemplate, T message) {
+        return redisTemplate.opsForStream().add(StreamRecords.newRecord()
+                .ofObject(JsonUtils.toJsonString(message)) // 设置内容
+                .withStreamKey(message.getStreamKey())); // 设置 stream key
+    }
+
 }

+ 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() + "/system/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, "调用异常");
+
+}

+ 0 - 0
src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov