Эх сурвалжийг харах

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

 Conflicts:
	src/main/resources/application-local.yaml
YunaiV 4 жил өмнө
parent
commit
22a09d079f
100 өөрчлөгдсөн 4049 нэмэгдсэн , 2112 устгасан
  1. 60 33
      README.md
  2. 4 0
      lombok.config
  3. 72 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. 54 0
      ruoyi-ui/src/api/system/errorCode.js
  21. 2 2
      ruoyi-ui/src/api/system/menu.js
  22. 2 2
      ruoyi-ui/src/api/system/notice.js
  23. 2 2
      ruoyi-ui/src/api/system/post.js
  24. 3 3
      ruoyi-ui/src/api/system/role.js
  25. 52 0
      ruoyi-ui/src/api/system/sms/smsChannel.js
  26. 20 0
      ruoyi-ui/src/api/system/sms/smsLog.js
  27. 64 0
      ruoyi-ui/src/api/system/sms/smsTemplate.js
  28. 8 8
      ruoyi-ui/src/api/system/user.js
  29. 7 1
      ruoyi-ui/src/main.js
  30. 1 1
      ruoyi-ui/src/store/modules/user.js
  31. 5 0
      ruoyi-ui/src/utils/dict.js
  32. 12 0
      ruoyi-ui/src/utils/request.js
  33. 15 0
      ruoyi-ui/src/utils/ruoyi.js
  34. 10 38
      ruoyi-ui/src/views/infra/config/index.vue
  35. 202 0
      ruoyi-ui/src/views/infra/file/index.vue
  36. 13 54
      ruoyi-ui/src/views/system/dept/index.vue
  37. 14 59
      ruoyi-ui/src/views/system/dict/data.vue
  38. 43 103
      ruoyi-ui/src/views/system/dict/index.vue
  39. 256 0
      ruoyi-ui/src/views/system/errorCode/index.vue
  40. 14 58
      ruoyi-ui/src/views/system/loginlog/index.vue
  41. 19 71
      ruoyi-ui/src/views/system/menu/index.vue
  42. 13 68
      ruoyi-ui/src/views/system/notice/index.vue
  43. 14 64
      ruoyi-ui/src/views/system/operatelog/index.vue
  44. 15 60
      ruoyi-ui/src/views/system/post/index.vue
  45. 23 94
      ruoyi-ui/src/views/system/role/index.vue
  46. 542 0
      ruoyi-ui/src/views/system/sms/smsChannel.vue
  47. 297 0
      ruoyi-ui/src/views/system/sms/smsLog.vue
  48. 405 0
      ruoyi-ui/src/views/system/sms/smsTemplate.vue
  49. 659 715
      ruoyi-ui/src/views/system/user/index.vue
  50. 9 7
      ruoyi-ui/src/views/system/user/profile/index.vue
  51. 8 8
      ruoyi-ui/src/views/system/user/profile/userInfo.vue
  52. 37 5
      ruoyi-ui/src/views/tool/dbDoc/index.vue
  53. 8 0
      sql/quartz.sql
  54. 30 10
      sql/ruoyi-vue-pro.sql
  55. 20 0
      src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java
  56. 27 0
      src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java
  57. 4 3
      src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java
  58. 0 41
      src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java
  59. 1 1
      src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java
  60. 1 0
      src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java
  61. 5 3
      src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java
  62. 11 15
      src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java
  63. 5 2
      src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java
  64. 27 1
      src/main/java/cn/iocoder/dashboard/framework/datasource/config/DataSourceConfiguration.java
  65. 22 0
      src/main/java/cn/iocoder/dashboard/framework/datasource/core/enums/DataSourceEnum.java
  66. 38 0
      src/main/java/cn/iocoder/dashboard/framework/datasource/core/filter/DruidAdRemoveFilter.java
  67. 36 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java
  68. 26 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java
  69. 34 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java
  70. 28 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java
  71. 15 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java
  72. 98 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java
  73. 24 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java
  74. 73 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java
  75. 35 0
      src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java
  76. 5 0
      src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java
  77. 39 0
      src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java
  78. 2 2
      src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java
  79. 18 1
      src/main/java/cn/iocoder/dashboard/framework/jackson/config/JacksonConfig.java
  80. 26 0
      src/main/java/cn/iocoder/dashboard/framework/jackson/deser/LocalDateTimeDeserializer.java
  81. 24 0
      src/main/java/cn/iocoder/dashboard/framework/jackson/ser/LocalDateTimeSerializer.java
  82. 1 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java
  83. 3 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java
  84. 3 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java
  85. 5 2
      src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/service/OperateLogFrameworkService.java
  86. 9 1
      src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java
  87. 14 4
      src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java
  88. 63 0
      src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handler/DefaultDBFieldHandler.java
  89. 8 4
      src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java
  90. 70 2
      src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java
  91. 5 16
      src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java
  92. 3 1
      src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java
  93. 88 0
      src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/AbstractStreamMessageListener.java
  94. 20 0
      src/main/java/cn/iocoder/dashboard/framework/redis/core/stream/StreamMessage.java
  95. 17 1
      src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java
  96. 0 1
      src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md
  97. 1 0
      src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md
  98. 10 4
      src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
  99. 2 1
      src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
  100. 21 3
      src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java

+ 60 - 33
README.md

@@ -1,6 +1,6 @@
 ## 平台简介
 
-**芋道** 是基于 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) **重构**,一套**全部开源**的**企业级**的快速开发平台,毫无保留给个人及企业免费使用。
+**芋道**,一套**全部开源**的**企业级**的快速开发平台,毫无保留给个人及企业免费使用。
 
 > 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。
 
@@ -17,39 +17,63 @@
 * 基础设施
 * 研发工具
 
+> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
+> 
+> 额外新增的功能,我们使用 🚀 标记。
+
+🙂 所有功能,都通过 **单元测试** 保证高质量。
+
 ### 系统功能
 
-1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置
-1. 在线用户:当前系统中活跃用户状态监控
-1. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分
-1. 菜单管理:配置系统菜单,操作权限,按钮权限标识等
-1. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限
-1. 岗位管理:配置系统用户所属担任职务
-1. 字典管理:对系统中经常使用的一些较为固定的数据进行维护
-1. 通知公告:系统通知公告信息发布维护
-1. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询
-1. 登录日志:系统登录日志记录查询包含登录异常
+
+|  | 功能 | 描述 |
+| --- | --- | --- |
+|  | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
+|  | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
+|  | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
+|  | 菜单管理 | 配置系统菜单,操作权限,按钮权限标识等 |
+|  | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
+|  | 岗位管理 | 配置系统用户所属担任职务 |
+|  | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
+| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、云片等主流短信平台 |
+| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
+|  | 登录日志 | 系统登录日志记录查询,包含登录异常 |
+| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
+|  | 通知公告 | 系统通知公告信息发布维护 |
+
+计划新增功能:
+* 邮件
+* 钉钉、飞书等通知
 
 ### 基础设施
 
-1. 配置管理:对系统动态配置常用参数
-1. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志
-1. API 日志:包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题
-1. MySQL 监控:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈
-1. Redis 监控:监控 Redis 数据库的使用情况,使用的 Redis Key 管理
-1. Java 监控:基于 Spring Boot Admin 实现 Java 应用的监控
-1. 链路追踪:基于 SkyWalking 实现性能监控,特别是链路的追踪
-1. 分布式锁:基于 Redis 实现分布式锁,满足并发场景
-1. 幂等组件:基于 Redis 实现幂等组件,解决重复请求问题
-1. 服务保障:基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能
-1. 日志服务:轻量级日志中心,查看远程服务器的日志
+|  | 功能 | 描述 |
+| --- | --- | --- |
+| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
+| | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
+| 🚀 | 文件服务 | 支持本地文件存储,同时支持兼容 Amazon S3 协议的云服务、开源组件 | 
+| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
+|  | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
+|  | Redis 监控 |监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
+| 🚀 |Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
+| 🚀 | 链路追踪 | 基于 SkyWalking 实现性能监控,特别是链路的追踪 |
+| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 |
+| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 |
+| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 |
+| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
+| 🚀 | 单元测试 |基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
+
+计划新增:
+* 工作流
 
 ### 研发工具
 
-1. 表单构建:拖动表单元素生成相应的 HTML 代码
-1. 代码生成:前后端代码的生成(Java、Vue、SQL),支持 CRUD 下载
-1. 系统接口:基于 Swagger 自动生成相关的 RESTful API 接口文档
-1. 数据库文档:基于 Screw 自动生成数据库文档
+|  | 功能 | 描述 |
+| --- | --- | --- |
+| 🚀 | 代码生成 |前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
+| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
+| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
+| | 表单构建 | 拖动表单元素生成相应的 HTML 代码 |
 
 ## 在线体验
 
@@ -63,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) |
@@ -78,14 +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) |
+| [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 | - |
 
-**前端**
+### 前端
 
 | 框架 | 说明 |  版本 |
 | --- | --- | --- |
@@ -125,7 +152,7 @@
     </tr>
     <tr>
         <td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
-        <td><img src="https://oscimg.oschina.net/oscnet/up-6d73c2140ce694e3de4c05035fdc1868d4c.png"/></td>
+        <td> - </td>
     </tr>
 </table>
 

+ 4 - 0
lombok.config

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

+ 72 - 27
pom.xml

@@ -22,14 +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 +44,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>
 
     <!-- 依赖声明 -->
@@ -101,8 +109,21 @@
                     <artifactId>mapstruct</artifactId>
                     <groupId>org.mapstruct</groupId> <!-- 避免冲突 -->
                 </exclusion>
+                <exclusion>
+                    <artifactId>guava</artifactId>
+                    <groupId>com.google.guava</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>swagger-annotations</artifactId>
+                    <groupId>io.swagger</groupId>
+                </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+            <version>${swagger-annotations.version}</version>
+        </dependency>
 
         <!-- DB 相关 -->
         <dependency>
@@ -120,6 +141,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>
@@ -145,6 +171,12 @@
             <groupId>com.baomidou</groupId>
             <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
             <version>${lock4j.version}</version>
+            <exclusions>
+                <exclusion>
+                    <artifactId>redisson-spring-boot-starter</artifactId>
+                    <groupId>org.redisson</groupId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
@@ -181,6 +213,12 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <artifactId>asm</artifactId>
+                    <groupId>org.ow2.asm</groupId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
@@ -192,14 +230,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>
 
@@ -217,33 +255,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>
 
@@ -265,6 +289,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'
   })
 }
 

+ 54 - 0
ruoyi-ui/src/api/system/errorCode.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 创建错误码
+export function createErrorCode(data) {
+  return request({
+    url: '/system/error-code/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新错误码
+export function updateErrorCode(data) {
+  return request({
+    url: '/system/error-code/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除错误码
+export function deleteErrorCode(id) {
+  return request({
+    url: '/system/error-code/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得错误码
+export function getErrorCode(id) {
+  return request({
+    url: '/system/error-code/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得错误码分页
+export function getErrorCodePage(query) {
+  return request({
+    url: '/system/error-code/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出错误码 Excel
+export function exportErrorCodeExcel(query) {
+  return request({
+    url: '/system/error-code/export-excel',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}

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

+ 5 - 0
ruoyi-ui/src/utils/dict.js

@@ -17,6 +17,11 @@ 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',
+  SYS_ERROR_CODE_TYPE: 'sys_error_code_type',
 
   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">

+ 43 - 103
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="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="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">
@@ -177,7 +115,7 @@ export default {
       // 状态数据字典
       statusOptions: [],
       // 日期范围
-      dateRange: [],
+      dateRangeCreateTime: [],
       // 查询参数
       queryParams: {
         pageNo: 1,
@@ -211,15 +149,15 @@ export default {
     /** 查询字典类型列表 */
     getList() {
       this.loading = true;
-      listType(this.addDateRange(this.queryParams, [
-        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
-        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
-      ])).then(response => {
-          this.typeList = response.data.list;
-          this.total = response.data.total;
-          this.loading = false;
-        }
-      );
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      listType(params).then(response => {
+        this.typeList = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
     },
     // 字典状态字典翻译
     statusFormat(row, column) {
@@ -248,7 +186,7 @@ export default {
     },
     /** 重置按钮操作 */
     resetQuery() {
-      this.dateRange = [];
+      this.dateRangeCreateTime = [];
       this.resetForm("queryForm");
       this.handleQuery();
     },
@@ -304,19 +242,21 @@ export default {
     },
     /** 导出按钮操作 */
     handleExport() {
-      const queryParams = this.addDateRange(this.queryParams, [
-        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
-        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
-      ]);
-      this.$confirm('是否确认导出所有类型数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return exportType(queryParams);
-        }).then(response => {
-          this.downloadExcel(response, '数据类型.xls');
-        })
+      // 处理查询参数
+      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 exportType(params);
+      }).then(response => {
+        this.downloadExcel(response, '字典类型.xls');
+      })
     }
   }
 };

+ 256 - 0
ruoyi-ui/src/views/system/errorCode/index.vue

@@ -0,0 +1,256 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <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_ERROR_CODE_TYPE)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="应用名" prop="applicationName">
+        <el-input v-model="queryParams.applicationName" placeholder="请输入应用名" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </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="错误码提示" prop="message">
+        <el-input v-model="queryParams.message" placeholder="请输入错误码提示" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </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:error-code: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:error-code: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="type" width="80">
+        <template slot-scope="scope">
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_ERROR_CODE_TYPE, scope.row.type) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="应用名" align="center" prop="applicationName" width="200" />
+      <el-table-column label="错误码编码" align="center" prop="code" width="100" />
+      <el-table-column label="错误码提示" align="center" prop="message" width="300" />
+      <el-table-column label="备注" align="center" prop="memo" width="200" />
+      <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:error-code:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['system:error-code: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="100px">
+        <el-form-item label="应用名" prop="applicationName">
+          <el-input v-model="form.applicationName" placeholder="请输入应用名" />
+        </el-form-item>
+        <el-form-item label="错误码编码" prop="code">
+          <el-input v-model="form.code" placeholder="请输入错误码编码" />
+        </el-form-item>
+        <el-form-item label="错误码提示" prop="message">
+          <el-input v-model="form.message" placeholder="请输入错误码提示" />
+        </el-form-item>
+        <el-form-item label="备注" prop="memo">
+          <el-input v-model="form.memo" 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>
+  </div>
+</template>
+
+<script>
+import { createErrorCode, updateErrorCode, deleteErrorCode, getErrorCode, getErrorCodePage, exportErrorCodeExcel } from "@/api/system/errorCode";
+
+export default {
+  name: "ErrorCode",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 错误码列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      dateRangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        type: null,
+        applicationName: null,
+        code: null,
+        message: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        applicationName: [{ required: true, message: "应用名不能为空", trigger: "blur" }],
+        code: [{ required: true, message: "错误码编码不能为空", trigger: "blur" }],
+        message: [{ required: true, message: "错误码提示不能为空", trigger: "blur" }],
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      getErrorCodePage(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,
+        applicationName: undefined,
+        code: undefined,
+        message: undefined,
+        memo: 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;
+      getErrorCode(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) {
+          updateErrorCode(this.form).then(response => {
+            this.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createErrorCode(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 deleteErrorCode(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 exportErrorCodeExcel(params);
+      }).then(response => {
+        this.downloadExcel(response, '错误码.xls');
+      })
+    }
+  }
+};
+</script>

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

+ 659 - 715
ruoyi-ui/src/views/system/user/index.vue

@@ -1,715 +1,659 @@
-<template>
-  <div class="app-container">
-    <el-row :gutter="20">
-      <!--部门数据-->
-      <el-col :span="4" :xs="24">
-        <div class="head-container">
-          <el-input
-            v-model="deptName"
-            placeholder="请输入部门名称"
-            clearable
-            size="small"
-            prefix-icon="el-icon-search"
-            style="margin-bottom: 20px"
-          />
-        </div>
-        <div class="head-container">
-          <el-tree
-            :data="deptOptions"
-            :props="defaultProps"
-            :expand-on-click-node="false"
-            :filter-node-method="filterNode"
-            ref="tree"
-            default-expand-all
-            @node-click="handleNodeClick"
-          />
-        </div>
-      </el-col>
-      <!--用户数据-->
-      <el-col :span="20" :xs="24">
-        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-          <el-form-item label="用户名称" prop="username">
-            <el-input
-              v-model="queryParams.username"
-              placeholder="请输入用户名称"
-              clearable
-              size="small"
-              style="width: 240px"
-              @keyup.enter.native="handleQuery"
-            />
-          </el-form-item>
-          <el-form-item label="手机号码" prop="mobile">
-            <el-input
-              v-model="queryParams.mobile"
-              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>
-          </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-form-item>
-          <el-form-item>
-            <el-button type="cyan" 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"
-              icon="el-icon-plus"
-              size="mini"
-              @click="handleAdd"
-              v-hasPermi="['system:user:add']"
-            >新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button
-              type="info"
-              icon="el-icon-upload2"
-              size="mini"
-              @click="handleImport"
-              v-hasPermi="['system:user:import']"
-            >导入</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button
-              type="warning"
-              icon="el-icon-download"
-              size="mini"
-              @click="handleExport"
-              v-hasPermi="['system:user:export']"
-            >导出</el-button>
-          </el-col>
-          <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-        </el-row>
-
-        <el-table v-loading="loading" :data="userList">
-          <el-table-column label="用户编号" align="center" prop="id" />
-          <el-table-column label="用户名称" align="center" prop="username" :show-overflow-tooltip="true" />
-          <el-table-column label="用户昵称" align="center" prop="nickname" :show-overflow-tooltip="true" />
-          <el-table-column label="部门" align="center" prop="dept.name" :show-overflow-tooltip="true" />
-          <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
-          <el-table-column label="状态" align="center">
-            <template slot-scope="scope">
-              <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="160">
-            <template slot-scope="scope">
-              <span>{{ parseTime(scope.row.createTime) }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column
-            label="操作"
-            align="center"
-            width="160"
-            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:user:edit']"
-              >修改</el-button>
-              <el-button
-                v-if="scope.row.id !== 1"
-                size="mini"
-                type="text"
-                icon="el-icon-delete"
-                @click="handleDelete(scope.row)"
-                v-hasPermi="['system:user:remove']"
-              >删除</el-button>
-              <el-button
-                size="mini"
-                type="text"
-                icon="el-icon-key"
-                @click="handleResetPwd(scope.row)"
-                v-hasPermi="['system:user:resetPwd']"
-              >重置</el-button>
-              <el-button
-                  size="mini"
-                  type="text"
-                  icon="el-icon-circle-check"
-                  @click="handleRole(scope.row)"
-                  v-hasPermi="['system:permission:assign-user-role']"
-              >分配角色</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-col>
-    </el-row>
-
-    <!-- 添加或修改参数配置对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-row>
-          <el-col :span="12">
-            <el-form-item label="用户昵称" prop="nickname">
-              <el-input v-model="form.nickname" placeholder="请输入用户昵称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="归属部门" prop="deptId">
-              <treeselect v-model="form.deptId" :options="deptOptions" :show-count="true"
-                          placeholder="请选择归属部门" :normalizer="normalizer"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="12">
-            <el-form-item label="手机号码" prop="mobile">
-              <el-input v-model="form.mobile" placeholder="请输入手机号码" maxlength="11" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="邮箱" prop="email">
-              <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="12">
-            <el-form-item v-if="form.id === undefined" label="用户名称" prop="username">
-              <el-input v-model="form.username" placeholder="请输入用户名称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item v-if="form.id === undefined" label="用户密码" prop="password">
-              <el-input v-model="form.password" placeholder="请输入用户密码" type="password" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="12">
-            <el-form-item label="用户性别">
-              <el-select v-model="form.sex" placeholder="请选择">
-                <el-option
-                  v-for="dict in sexDictDatas"
-                  :key="parseInt(dict.value)"
-                  :label="dict.label"
-                  :value="parseInt(dict.value)"
-                ></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="岗位">
-              <el-select v-model="form.postIds" multiple placeholder="请选择">
-                <el-option
-                    v-for="item in postOptions"
-                    :key="item.id"
-                    :label="item.name"
-                    :value="item.id"
-                ></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="24">
-            <el-form-item label="备注">
-              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </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="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
-      <el-upload
-        ref="upload"
-        :limit="1"
-        accept=".xlsx, .xls"
-        :headers="upload.headers"
-        :action="upload.url + '?updateSupport=' + upload.updateSupport"
-        :disabled="upload.isUploading"
-        :on-progress="handleFileUploadProgress"
-        :on-success="handleFileSuccess"
-        :auto-upload="false"
-        drag
-      >
-        <i class="el-icon-upload"></i>
-        <div class="el-upload__text">
-          将文件拖到此处,或
-          <em>点击上传</em>
-        </div>
-        <div class="el-upload__tip" slot="tip">
-          <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
-          <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
-        </div>
-        <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</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>
-
-    <!-- 分配角色 -->
-    <el-dialog title="分配角色" :visible.sync="openRole" width="500px" append-to-body>
-      <el-form :model="form" label-width="80px">
-        <el-form-item label="用户名称">
-          <el-input v-model="form.username" :disabled="true" />
-        </el-form-item>
-        <el-form-item label="用户昵称">
-          <el-input v-model="form.nickname" :disabled="true" />
-        </el-form-item>
-        <el-form-item label="角色">
-          <el-select v-model="form.roleIds" multiple placeholder="请选择">
-            <el-option
-                v-for="item in roleOptions"
-                :key="parseInt(item.id)"
-                :label="item.name"
-                :value="parseInt(item.id)"
-            ></el-option>
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitRole">确 定</el-button>
-        <el-button @click="cancelRole">取 消</el-button>
-      </div>
-    </el-dialog>
-
-  </div>
-</template>
-
-<script>
-import { listUser, getUser, delUser, addUser, updateUser, exportUser, resetUserPwd, changeUserStatus, importTemplate } from "@/api/system/user";
-import { getToken } from "@/utils/auth";
-import Treeselect from "@riophae/vue-treeselect";
-import "@riophae/vue-treeselect/dist/vue-treeselect.css";
-
-import {listSimpleDepts} from "@/api/system/dept";
-import {listSimplePosts} from "@/api/system/post";
-
-import {SysCommonStatusEnum} from "@/utils/constants";
-import {DICT_TYPE, getDictDatas} from "@/utils/dict";
-import {assignUserRole, listUserRoles} from "@/api/system/permission";
-import {listSimpleRoles} from "@/api/system/role";
-
-export default {
-  name: "User",
-  components: { Treeselect },
-  data() {
-    return {
-      // 遮罩层
-      loading: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 用户表格数据
-      userList: null,
-      // 弹出层标题
-      title: "",
-      // 部门树选项
-      deptOptions: undefined,
-      // 是否显示弹出层
-      open: false,
-      // 部门名称
-      deptName: undefined,
-      // 默认密码
-      initPassword: undefined,
-      // 日期范围
-      dateRange: [],
-      // 状态数据字典
-      statusOptions: [],
-      // 性别状态字典
-      sexOptions: [],
-      // 岗位选项
-      postOptions: [],
-      // 角色选项
-      roleOptions: [],
-      // 表单参数
-      form: {},
-      defaultProps: {
-        children: "children",
-        label: "name"
-      },
-      // 用户导入参数
-      upload: {
-        // 是否显示弹出层(用户导入)
-        open: false,
-        // 弹出层标题(用户导入)
-        title: "",
-        // 是否禁用上传
-        isUploading: false,
-        // 是否更新已经存在的用户数据
-        updateSupport: 0,
-        // 设置上传的请求头部
-        headers: { Authorization: "Bearer " + getToken() },
-        // 上传的地址
-        url: process.env.VUE_APP_BASE_API + '/api/' + "/system/user/import"
-      },
-      // 查询参数
-      queryParams: {
-        pageNo: 1,
-        pageSize: 10,
-        username: undefined,
-        mobile: undefined,
-        status: undefined,
-        deptId: undefined
-      },
-      // 表单校验
-      rules: {
-        username: [
-          { required: true, message: "用户名称不能为空", trigger: "blur" }
-        ],
-        nickname: [
-          { required: true, message: "用户昵称不能为空", trigger: "blur" }
-        ],
-        password: [
-          { required: true, message: "用户密码不能为空", trigger: "blur" }
-        ],
-        email: [
-          {
-            type: "email",
-            message: "'请输入正确的邮箱地址",
-            trigger: ["blur", "change"]
-          }
-        ],
-        mobile: [
-          {
-            pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
-            message: "请输入正确的手机号码",
-            trigger: "blur"
-          }
-        ]
-      },
-      // 是否显示弹出层(角色权限)
-      openRole: false,
-
-      // 枚举
-      SysCommonStatusEnum: SysCommonStatusEnum,
-      // 数据字典
-      statusDictDatas: getDictDatas(DICT_TYPE.SYS_COMMON_STATUS),
-      sexDictDatas: getDictDatas(DICT_TYPE.SYS_USER_SEX),
-    };
-  },
-  watch: {
-    // 根据名称筛选部门树
-    deptName(val) {
-      this.$refs.tree.filter(val);
-    }
-  },
-  created() {
-    this.getList();
-    this.getTreeselect();
-    this.getConfigKey("sys.user.initPassword").then(response => {
-      this.initPassword = response.msg;
-    });
-  },
-  methods: {
-    /** 查询用户列表 */
-    getList() {
-      this.loading = true;
-      listUser(this.addDateRange(this.queryParams, [
-        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
-        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
-      ])).then(response => {
-          this.userList = response.data.list;
-          this.total = response.data.total;
-          this.loading = false;
-        }
-      );
-    },
-    /** 查询部门下拉树结构 + 岗位下拉 */
-    getTreeselect() {
-      listSimpleDepts().then(response => {
-        // 处理 deptOptions 参数
-        this.deptOptions = [];
-        this.deptOptions.push(...this.handleTree(response.data, "id"));
-      });
-      listSimplePosts().then(response => {
-        // 处理 postOptions 参数
-        this.postOptions = [];
-        this.postOptions.push(...response.data);
-      });
-    },
-    // 筛选节点
-    filterNode(value, data) {
-      if (!value) return true;
-      return data.name.indexOf(value) !== -1;
-    },
-    // 节点单击事件
-    handleNodeClick(data) {
-      this.queryParams.deptId = data.id;
-      this.getList();
-    },
-    // 用户状态修改
-    handleStatusChange(row) {
-      let text = row.status === SysCommonStatusEnum.ENABLE ? "启用" : "停用";
-      this.$confirm('确认要"' + text + '""' + row.username + '"用户吗?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return changeUserStatus(row.id, row.status);
-        }).then(() => {
-          this.msgSuccess(text + "成功");
-        }).catch(function() {
-          row.status = row.status === SysCommonStatusEnum.ENABLE ? SysCommonStatusEnum.DISABLE
-              : SysCommonStatusEnum.ENABLE;
-        });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 取消按钮(角色权限)
-    cancelRole() {
-      this.openRole = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: undefined,
-        deptId: undefined,
-        username: undefined,
-        nickname: undefined,
-        password: undefined,
-        mobile: undefined,
-        email: undefined,
-        sex: undefined,
-        status: "0",
-        remark: undefined,
-        postIds: [],
-        roleIds: []
-      };
-      this.resetForm("form");
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.queryParams.pageNo = 1;
-      this.getList();
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.dateRange = [];
-      this.resetForm("queryForm");
-      this.handleQuery();
-    },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      // 获得下拉数据
-      this.getTreeselect();
-      // 打开表单,并设置初始化
-      this.open = true;
-      this.title = "添加用户";
-      this.form.password = this.initPassword;
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      this.getTreeselect();
-      const id = row.id;
-      getUser(id).then(response => {
-        this.form = response.data;
-        this.open = true;
-        this.title = "修改用户";
-        this.form.password = "";
-      });
-    },
-    /** 重置密码按钮操作 */
-    handleResetPwd(row) {
-      this.$prompt('请输入"' + row.username + '"的新密码', "提示", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消"
-      }).then(({ value }) => {
-          resetUserPwd(row.id, value).then(response => {
-            this.msgSuccess("修改成功,新密码是:" + value);
-          });
-        }).catch(() => {});
-    },
-    /** 分配用户角色操作 */
-    handleRole(row) {
-      this.reset();
-      const id = row.id
-      // 处理了 form 的用户 username 和 nickname 的展示
-      this.form.id = id;
-      this.form.username = row.username;
-      this.form.nickname = row.nickname;
-      // 打开弹窗
-      this.openRole = true;
-      // 获得角色列表
-      listSimpleRoles().then(response => {
-        // 处理 roleOptions 参数
-        this.roleOptions = [];
-        this.roleOptions.push(...response.data);
-      });
-      // 获得角色拥有的菜单集合
-      listUserRoles(id).then(response => {
-        // 设置选中
-        this.form.roleIds = response.data;
-      })
-    },
-    /** 提交按钮 */
-    submitForm: function() {
-      this.$refs["form"].validate(valid => {
-        if (valid) {
-          if (this.form.id !== undefined) {
-            updateUser(this.form).then(response => {
-              this.msgSuccess("修改成功");
-              this.open = false;
-              this.getList();
-            });
-          } else {
-            addUser(this.form).then(response => {
-              this.msgSuccess("新增成功");
-              this.open = false;
-              this.getList();
-            });
-          }
-        }
-      });
-    },
-    /** 提交按钮(角色权限) */
-    submitRole: function() {
-      if (this.form.id !== undefined) {
-        assignUserRole({
-          userId: this.form.id,
-          roleIds: this.form.roleIds,
-        }).then(response => {
-          this.msgSuccess("分配角色成功");
-          this.openRole = false;
-          this.getList();
-        });
-      }
-    },
-    /** 删除按钮操作 */
-    handleDelete(row) {
-      const ids = row.id || this.ids;
-      this.$confirm('是否确认删除用户编号为"' + ids + '"的数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return delUser(ids);
-        }).then(() => {
-          this.getList();
-          this.msgSuccess("删除成功");
-        })
-    },
-    /** 导出按钮操作 */
-    handleExport() {
-      const queryParams = this.addDateRange(this.queryParams, [
-        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
-        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
-      ]);
-      this.$confirm('是否确认导出所有用户数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return exportUser(queryParams);
-        }).then(response => {
-          this.downloadExcel(response, '用户数据.xls');
-        })
-    },
-    /** 导入按钮操作 */
-    handleImport() {
-      this.upload.title = "用户导入";
-      this.upload.open = true;
-    },
-    /** 下载模板操作 */
-    importTemplate() {
-      importTemplate().then(response => {
-        this.downloadExcel(response, '用户导入模板.xls');
-      });
-    },
-    // 文件上传中处理
-    handleFileUploadProgress(event, file, fileList) {
-      this.upload.isUploading = true;
-    },
-    // 文件上传成功处理
-    handleFileSuccess(response, file, fileList) {
-      this.upload.open = false;
-      this.upload.isUploading = false;
-      this.$refs.upload.clearFiles();
-      // 拼接提示语
-      let data = response.data;
-      let text = '创建成功数量:' + data.createUsernames.length;
-      for (const username of data.createUsernames) {
-        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
-      }
-      text += '<br />更新成功数量:' + data.updateUsernames.length;
-      for (const username of data.updateUsernames) {
-        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
-      }
-      text += '<br />更新失败数量:' + Object.keys(data.failureUsernames).length;
-      for (const username in data.failureUsernames) {
-        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username + ':' + data.failureUsernames[username];
-      }
-      this.$alert(text, "导入结果", { dangerouslyUseHTMLString: true });
-      this.getList();
-    },
-    // 提交上传文件
-    submitFileForm() {
-      this.$refs.upload.submit();
-    },
-    // 格式化部门的下拉框
-    normalizer(node) {
-      return {
-        id: node.id,
-        label: node.name,
-        children: node.children
-      }
-    }
-  }
-};
-</script>
+<template>
+  <div class="app-container">
+    <el-row :gutter="20">
+      <!--部门数据-->
+      <el-col :span="4" :xs="24">
+        <div class="head-container">
+          <el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px"/>
+        </div>
+        <div class="head-container">
+          <el-tree :data="deptOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode"
+                   ref="tree" default-expand-all @node-click="handleNodeClick"/>
+        </div>
+      </el-col>
+      <!--用户数据-->
+      <el-col :span="20" :xs="24">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+          <el-form-item label="用户名称" prop="username">
+            <el-input v-model="queryParams.username" placeholder="请输入用户名称" clearable size="small" style="width: 240px"
+                      @keyup.enter.native="handleQuery"/>
+          </el-form-item>
+          <el-form-item label="手机号码" prop="mobile">
+            <el-input v-model="queryParams.mobile" 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>
+          </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-form-item>
+          <el-form-item>
+            <el-button type="cyan" 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" icon="el-icon-plus" size="mini" @click="handleAdd"
+                       v-hasPermi="['system:user:create']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="info" icon="el-icon-upload2" size="mini" @click="handleImport"
+                       v-hasPermi="['system:user:import']">导入</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport"
+                       v-hasPermi="['system:user:export']">导出</el-button>
+          </el-col>
+          <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table v-loading="loading" :data="userList">
+          <el-table-column label="用户编号" align="center" prop="id" />
+          <el-table-column label="用户名称" align="center" prop="username" :show-overflow-tooltip="true" />
+          <el-table-column label="用户昵称" align="center" prop="nickname" :show-overflow-tooltip="true" />
+          <el-table-column label="部门" align="center" prop="dept.name" :show-overflow-tooltip="true" />
+          <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
+          <el-table-column label="状态" align="center">
+            <template slot-scope="scope">
+              <el-switch v-model="scope.row.status" :active-value="0" :inactive-value="1" @change="handleStatusChange(scope.row)" />
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" align="center" prop="createTime" width="160">
+            <template slot-scope="scope">
+              <span>{{ parseTime(scope.row.createTime) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
+            <template slot-scope="scope">
+              <el-button size="large" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                         v-hasPermi="['system:role:update']">修改</el-button>
+              <el-dropdown  @command="(command) => handleCommand(command, scope.$index, scope.row)">
+                    <span class="el-dropdown-link">
+                      更多操作<i class="el-icon-arrow-down el-icon--right"></i>
+                    </span>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="handleDelete" v-if="scope.row.id !== 1" size="mini" type="text" icon="el-icon-delete"
+                                    v-hasPermi="['system:user:delete']">删除</el-dropdown-item>
+                  <el-dropdown-item command="handleResetPwd" size="mini" type="text" icon="el-icon-key"
+                                    v-hasPermi="['system:user:update-password']">重置密码</el-dropdown-item>
+                  <el-dropdown-item command="handleRole" size="mini" type="text" icon="el-icon-circle-check"
+                                    v-hasPermi="['system:permission:assign-user-role']">分配角色</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                    @pagination="getList"/>
+      </el-col>
+    </el-row>
+
+    <!-- 添加或修改参数配置对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="用户昵称" prop="nickname">
+              <el-input v-model="form.nickname" placeholder="请输入用户昵称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="归属部门" prop="deptId">
+              <treeselect v-model="form.deptId" :options="deptOptions" :show-count="true"
+                          placeholder="请选择归属部门" :normalizer="normalizer"/>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="手机号码" prop="mobile">
+              <el-input v-model="form.mobile" placeholder="请输入手机号码" maxlength="11" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="邮箱" prop="email">
+              <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item v-if="form.id === undefined" label="用户名称" prop="username">
+              <el-input v-model="form.username" placeholder="请输入用户名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.id === undefined" label="用户密码" prop="password">
+              <el-input v-model="form.password" placeholder="请输入用户密码" type="password" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="用户性别">
+              <el-select v-model="form.sex" placeholder="请选择">
+                <el-option v-for="dict in sexDictDatas" :key="parseInt(dict.value)" :label="dict.label" :value="parseInt(dict.value)"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="岗位">
+              <el-select v-model="form.postIds" multiple placeholder="请选择">
+                <el-option
+                    v-for="item in postOptions"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </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="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+      <el-upload
+        ref="upload"
+        :limit="1"
+        accept=".xlsx, .xls"
+        :headers="upload.headers"
+        :action="upload.url + '?updateSupport=' + upload.updateSupport"
+        :disabled="upload.isUploading"
+        :on-progress="handleFileUploadProgress"
+        :on-success="handleFileSuccess"
+        :auto-upload="false"
+        drag
+      >
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">
+          将文件拖到此处,或
+          <em>点击上传</em>
+        </div>
+        <div class="el-upload__tip" slot="tip">
+          <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
+          <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
+        </div>
+        <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</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>
+
+    <!-- 分配角色 -->
+    <el-dialog title="分配角色" :visible.sync="openRole" width="500px" append-to-body>
+      <el-form :model="form" label-width="80px">
+        <el-form-item label="用户名称">
+          <el-input v-model="form.username" :disabled="true" />
+        </el-form-item>
+        <el-form-item label="用户昵称">
+          <el-input v-model="form.nickname" :disabled="true" />
+        </el-form-item>
+        <el-form-item label="角色">
+          <el-select v-model="form.roleIds" multiple placeholder="请选择">
+            <el-option
+                v-for="item in roleOptions"
+                :key="parseInt(item.id)"
+                :label="item.name"
+                :value="parseInt(item.id)"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitRole">确 定</el-button>
+        <el-button @click="cancelRole">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import {
+  addUser,
+  changeUserStatus,
+  delUser,
+  exportUser,
+  getUser,
+  importTemplate,
+  listUser,
+  resetUserPwd,
+  updateUser
+} from "@/api/system/user";
+import {getToken} from "@/utils/auth";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+
+import {listSimpleDepts} from "@/api/system/dept";
+import {listSimplePosts} from "@/api/system/post";
+
+import {SysCommonStatusEnum} from "@/utils/constants";
+import {DICT_TYPE, getDictDatas} from "@/utils/dict";
+import {assignUserRole, listUserRoles} from "@/api/system/permission";
+import {listSimpleRoles} from "@/api/system/role";
+
+export default {
+  name: "User",
+  components: { Treeselect },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 用户表格数据
+      userList: null,
+      // 弹出层标题
+      title: "",
+      // 部门树选项
+      deptOptions: undefined,
+      // 是否显示弹出层
+      open: false,
+      // 部门名称
+      deptName: undefined,
+      // 默认密码
+      initPassword: undefined,
+      // 日期范围
+      dateRange: [],
+      // 状态数据字典
+      statusOptions: [],
+      // 性别状态字典
+      sexOptions: [],
+      // 岗位选项
+      postOptions: [],
+      // 角色选项
+      roleOptions: [],
+      // 表单参数
+      form: {},
+      defaultProps: {
+        children: "children",
+        label: "name"
+      },
+      // 用户导入参数
+      upload: {
+        // 是否显示弹出层(用户导入)
+        open: false,
+        // 弹出层标题(用户导入)
+        title: "",
+        // 是否禁用上传
+        isUploading: false,
+        // 是否更新已经存在的用户数据
+        updateSupport: 0,
+        // 设置上传的请求头部
+        headers: { Authorization: "Bearer " + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + '/api/' + "/system/user/import"
+      },
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        username: undefined,
+        mobile: undefined,
+        status: undefined,
+        deptId: undefined
+      },
+      // 表单校验
+      rules: {
+        username: [
+          { required: true, message: "用户名称不能为空", trigger: "blur" }
+        ],
+        nickname: [
+          { required: true, message: "用户昵称不能为空", trigger: "blur" }
+        ],
+        password: [
+          { required: true, message: "用户密码不能为空", trigger: "blur" }
+        ],
+        email: [
+          {
+            type: "email",
+            message: "'请输入正确的邮箱地址",
+            trigger: ["blur", "change"]
+          }
+        ],
+        mobile: [
+          {
+            pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
+            message: "请输入正确的手机号码",
+            trigger: "blur"
+          }
+        ]
+      },
+      // 是否显示弹出层(角色权限)
+      openRole: false,
+
+      // 枚举
+      SysCommonStatusEnum: SysCommonStatusEnum,
+      // 数据字典
+      statusDictDatas: getDictDatas(DICT_TYPE.SYS_COMMON_STATUS),
+      sexDictDatas: getDictDatas(DICT_TYPE.SYS_USER_SEX),
+    };
+  },
+  watch: {
+    // 根据名称筛选部门树
+    deptName(val) {
+      this.$refs.tree.filter(val);
+    }
+  },
+  created() {
+    this.getList();
+    this.getTreeselect();
+    this.getConfigKey("sys.user.init-password").then(response => {
+      this.initPassword = response.msg;
+    });
+  },
+  methods: {
+    // 更多操作
+    handleCommand(command, index, row) {
+      switch (command) {
+        case 'handleUpdate':
+          this.handleUpdate(row);//修改客户信息
+          break;
+        case 'handleDelete':
+          this.handleDelete(row);//红号变更
+          break;
+        case 'handleResetPwd':
+          this.handleResetPwd(row);
+          break;
+        case 'handleRole':
+          this.handleRole(row);
+          break;
+        default:
+          break;
+      }
+    },
+    /** 查询用户列表 */
+    getList() {
+      this.loading = true;
+      listUser(this.addDateRange(this.queryParams, [
+        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
+        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
+      ])).then(response => {
+          this.userList = response.data.list;
+          this.total = response.data.total;
+          this.loading = false;
+        }
+      );
+    },
+    /** 查询部门下拉树结构 + 岗位下拉 */
+    getTreeselect() {
+      listSimpleDepts().then(response => {
+        // 处理 deptOptions 参数
+        this.deptOptions = [];
+        this.deptOptions.push(...this.handleTree(response.data, "id"));
+      });
+      listSimplePosts().then(response => {
+        // 处理 postOptions 参数
+        this.postOptions = [];
+        this.postOptions.push(...response.data);
+      });
+    },
+    // 筛选节点
+    filterNode(value, data) {
+      if (!value) return true;
+      return data.name.indexOf(value) !== -1;
+    },
+    // 节点单击事件
+    handleNodeClick(data) {
+      this.queryParams.deptId = data.id;
+      this.getList();
+    },
+    // 用户状态修改
+    handleStatusChange(row) {
+      let text = row.status === SysCommonStatusEnum.ENABLE ? "启用" : "停用";
+      this.$confirm('确认要"' + text + '""' + row.username + '"用户吗?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return changeUserStatus(row.id, row.status);
+        }).then(() => {
+          this.msgSuccess(text + "成功");
+        }).catch(function() {
+          row.status = row.status === SysCommonStatusEnum.ENABLE ? SysCommonStatusEnum.DISABLE
+              : SysCommonStatusEnum.ENABLE;
+        });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 取消按钮(角色权限)
+    cancelRole() {
+      this.openRole = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        deptId: undefined,
+        username: undefined,
+        nickname: undefined,
+        password: undefined,
+        mobile: undefined,
+        email: undefined,
+        sex: undefined,
+        status: "0",
+        remark: undefined,
+        postIds: [],
+        roleIds: []
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRange = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      // 获得下拉数据
+      this.getTreeselect();
+      // 打开表单,并设置初始化
+      this.open = true;
+      this.title = "添加用户";
+      this.form.password = this.initPassword;
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.getTreeselect();
+      const id = row.id;
+      getUser(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改用户";
+        this.form.password = "";
+      });
+    },
+    /** 重置密码按钮操作 */
+    handleResetPwd(row) {
+      this.$prompt('请输入"' + row.username + '"的新密码', "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+      }).then(({ value }) => {
+          resetUserPwd(row.id, value).then(response => {
+            this.msgSuccess("修改成功,新密码是:" + value);
+          });
+        }).catch(() => {});
+    },
+    /** 分配用户角色操作 */
+    handleRole(row) {
+      this.reset();
+      const id = row.id
+      // 处理了 form 的用户 username 和 nickname 的展示
+      this.form.id = id;
+      this.form.username = row.username;
+      this.form.nickname = row.nickname;
+      // 打开弹窗
+      this.openRole = true;
+      // 获得角色列表
+      listSimpleRoles().then(response => {
+        // 处理 roleOptions 参数
+        this.roleOptions = [];
+        this.roleOptions.push(...response.data);
+      });
+      // 获得角色拥有的菜单集合
+      listUserRoles(id).then(response => {
+        // 设置选中
+        this.form.roleIds = response.data;
+      })
+    },
+    /** 提交按钮 */
+    submitForm: function() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id !== undefined) {
+            updateUser(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addUser(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 提交按钮(角色权限) */
+    submitRole: function() {
+      if (this.form.id !== undefined) {
+        assignUserRole({
+          userId: this.form.id,
+          roleIds: this.form.roleIds,
+        }).then(response => {
+          this.msgSuccess("分配角色成功");
+          this.openRole = false;
+          this.getList();
+        });
+      }
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除用户编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delUser(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.addDateRange(this.queryParams, [
+        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
+        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
+      ]);
+      this.$confirm('是否确认导出所有用户数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportUser(queryParams);
+        }).then(response => {
+          this.downloadExcel(response, '用户数据.xls');
+        })
+    },
+    /** 导入按钮操作 */
+    handleImport() {
+      this.upload.title = "用户导入";
+      this.upload.open = true;
+    },
+    /** 下载模板操作 */
+    importTemplate() {
+      importTemplate().then(response => {
+        this.downloadExcel(response, '用户导入模板.xls');
+      });
+    },
+    // 文件上传中处理
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true;
+    },
+    // 文件上传成功处理
+    handleFileSuccess(response, file, fileList) {
+      this.upload.open = false;
+      this.upload.isUploading = false;
+      this.$refs.upload.clearFiles();
+      // 拼接提示语
+      let data = response.data;
+      let text = '创建成功数量:' + data.createUsernames.length;
+      for (const username of data.createUsernames) {
+        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
+      }
+      text += '<br />更新成功数量:' + data.updateUsernames.length;
+      for (const username of data.updateUsernames) {
+        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
+      }
+      text += '<br />更新失败数量:' + Object.keys(data.failureUsernames).length;
+      for (const username in data.failureUsernames) {
+        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username + ':' + data.failureUsernames[username];
+      }
+      this.$alert(text, "导入结果", { dangerouslyUseHTMLString: true });
+      this.getList();
+    },
+    // 提交上传文件
+    submitFileForm() {
+      this.$refs.upload.submit();
+    },
+    // 格式化部门的下拉框
+    normalizer(node) {
+      return {
+        id: node.id,
+        label: node.name,
+        children: node.children
+      }
+    }
+  }
+};
+</script>
+<style>
+  .el-dropdown-link {
+    cursor: pointer;
+    color: #1890ff;
+    margin-left: 5px;
+  }
+  .el-icon-arrow-down {
+    font-size: 14px;
+  }
+</style>

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

+ 8 - 0
sql/quartz.sql

@@ -178,3 +178,11 @@ CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIG
 CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
 
 commit;
+
+-- 初始化默认任务 用户 Session 超时 Job
+INSERT INTO QRTZ_JOB_DETAILS VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', NULL, 'cn.iocoder.dashboard.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000047400104A4F425F48414E444C45525F4E414D457400187379735573657253657373696F6E54696D656F75744A6F627800);
+commit;
+INSERT INTO QRTZ_TRIGGERS VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', 'sysUserSessionTimeoutJob', 'DEFAULT', NULL, 1615706340000, 1615706280000, 5, 'WAITING', 'CRON', 1615706125000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000007D074000F4A4F425F52455452595F434F554E547371007E0009000000037800);
+commit;
+INSERT INTO QRTZ_CRON_TRIGGERS VALUES ('schedulerName', 'sysUserSessionTimeoutJob', 'DEFAULT', '0 * * * * ? *', 'Asia/Shanghai');
+commit;

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 30 - 10
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, "未知错误");
 

+ 5 - 3
src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java

@@ -2,6 +2,7 @@ package cn.iocoder.dashboard.common.exception.util;
 
 import cn.iocoder.dashboard.common.exception.ErrorCode;
 import cn.iocoder.dashboard.common.exception.ServiceException;
+import com.google.common.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,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);
     }
 
@@ -91,7 +92,8 @@ public class ServiceExceptionUtil {
      * @param params         参数
      * @return 格式化后的提示
      */
-    private static String doFormat(int code, String messagePattern, Object... params) {
+    @VisibleForTesting
+    public static String doFormat(int code, String messagePattern, Object... params) {
         StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
         int i = 0;
         int j;

+ 11 - 15
src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java

@@ -1,7 +1,6 @@
 package cn.iocoder.dashboard.common.pojo;
 
 import cn.iocoder.dashboard.common.exception.ErrorCode;
-import cn.iocoder.dashboard.common.exception.GlobalException;
 import cn.iocoder.dashboard.common.exception.ServiceException;
 import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -9,6 +8,7 @@ import lombok.Data;
 import org.springframework.util.Assert;
 
 import java.io.Serializable;
+import java.util.Objects;
 
 /**
  * 通用返回
@@ -16,7 +16,7 @@ import java.io.Serializable;
  * @param <T> 数据泛型
  */
 @Data
-public final class CommonResult<T> implements Serializable {
+public class CommonResult<T> implements Serializable {
 
     /**
      * 错误码
@@ -31,7 +31,7 @@ public final class CommonResult<T> implements Serializable {
     /**
      * 错误提示,用户可阅读
      *
-     * @see ErrorCode#getMessage() ()
+     * @see ErrorCode#getMsg() ()
      */
     private String msg;
 
@@ -57,7 +57,7 @@ public final class CommonResult<T> implements Serializable {
     }
 
     public static <T> CommonResult<T> error(ErrorCode errorCode) {
-        return error(errorCode.getCode(), errorCode.getMessage());
+        return error(errorCode.getCode(), errorCode.getMsg());
     }
 
     public static <T> CommonResult<T> success(T data) {
@@ -68,9 +68,13 @@ public final class CommonResult<T> implements Serializable {
         return result;
     }
 
+    public static boolean isSuccess(Integer code) {
+        return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
+    }
+
     @JsonIgnore // 避免 jackson 序列化
     public boolean isSuccess() {
-        return GlobalErrorCodeConstants.SUCCESS.getCode().equals(code);
+        return isSuccess(code);
     }
 
     @JsonIgnore // 避免 jackson 序列化
@@ -81,16 +85,12 @@ public final class CommonResult<T> implements Serializable {
     // ========= 和 Exception 异常体系集成 =========
 
     /**
-     * 判断是否有异常。如果有,则抛出 {@link GlobalException} 或 {@link ServiceException} 异常
+     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
      */
-    public void checkError() throws GlobalException, ServiceException {
+    public void checkError() throws ServiceException {
         if (isSuccess()) {
             return;
         }
-        // 全局异常
-        if (GlobalErrorCodeConstants.isMatch(code)) {
-            throw new GlobalException(code, msg);
-        }
         // 业务异常
         throw new ServiceException(code, msg);
     }
@@ -99,8 +99,4 @@ public final class CommonResult<T> implements Serializable {
         return error(serviceException.getCode(), serviceException.getMessage());
     }
 
-    public static <T> CommonResult<T> error(GlobalException globalException) {
-        return error(globalException.getCode(), globalException.getMessage());
-    }
-
 }

+ 5 - 2
src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java

@@ -13,14 +13,17 @@ import java.io.Serializable;
 @Data
 public class PageParam implements Serializable {
 
+    private static final Integer PAGE_NO = 1;
+    private static final Integer PAGE_SIZE = 10;
+
     @ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1")
     @NotNull(message = "页码不能为空")
     @Min(value = 1, message = "页码最小值为 1")
-    private Integer pageNo;
+    private Integer pageNo = PAGE_NO;
 
     @ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10")
     @NotNull(message = "每页条数不能为空")
     @Range(min = 1, max = 100, message = "条数范围为 [1, 100]")
-    private Integer pageSize;
+    private Integer pageSize = PAGE_SIZE;
 
 }

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

+ 36 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeConfiguration.java

@@ -0,0 +1,36 @@
+package cn.iocoder.dashboard.framework.errorcode.config;
+
+import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGenerator;
+import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoader;
+import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService;
+import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoaderImpl;
+import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 错误码配置类
+ */
+@Configuration
+@EnableConfigurationProperties(ErrorCodeProperties.class)
+@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码
+public class ErrorCodeConfiguration {
+
+    @Bean
+    public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName,
+                                                         ErrorCodeProperties errorCodeProperties,
+                                                         ErrorCodeFrameworkService errorCodeFrameworkService) {
+        return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(),
+                errorCodeFrameworkService);
+    }
+
+    @Bean
+    public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName,
+                                           ErrorCodeFrameworkService errorCodeFrameworkService) {
+        return new ErrorCodeLoaderImpl(applicationName, errorCodeFrameworkService);
+    }
+
+}

+ 26 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/config/ErrorCodeProperties.java

@@ -0,0 +1,26 @@
+package cn.iocoder.dashboard.framework.errorcode.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 错误码的配置属性类
+ *
+ * @author dlyan
+ */
+@ConfigurationProperties("yudao.error-code")
+@Data
+@Validated
+public class ErrorCodeProperties {
+
+    /**
+     * 错误码枚举类
+     */
+    @NotNull(message = "错误码枚举类不能为空")
+    private List<String> constantsClassList;
+
+}

+ 34 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeAutoGenerateReqDTO.java

@@ -0,0 +1,34 @@
+package cn.iocoder.dashboard.framework.errorcode.core.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 错误码自动生成 DTO
+ *
+ * @author dylan
+ */
+@Data
+@Accessors(chain = true)
+public class ErrorCodeAutoGenerateReqDTO {
+
+    /**
+     * 应用名
+     */
+    @NotNull(message = "应用名不能为空")
+    private String applicationName;
+    /**
+     * 错误码编码
+     */
+    @NotNull(message = "错误码编码不能为空")
+    private Integer code;
+    /**
+     * 错误码错误提示
+     */
+    @NotEmpty(message = "错误码错误提示不能为空")
+    private String message;
+
+}

+ 28 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/core/dto/ErrorCodeRespDTO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.dashboard.framework.errorcode.core.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 错误码的 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ErrorCodeRespDTO {
+
+    /**
+     * 错误码编码
+     */
+    private Integer code;
+    /**
+     * 错误码错误提示
+     */
+    private String message;
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+}

+ 15 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java

@@ -0,0 +1,15 @@
+package cn.iocoder.dashboard.framework.errorcode.core.generator;
+
+/**
+ * 错误码的自动生成器
+ *
+ * @author dylan
+ */
+public interface ErrorCodeAutoGenerator {
+
+    /**
+     * 将配置类到错误码写入数据库
+     */
+    void execute();
+
+}

+ 98 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java

@@ -0,0 +1,98 @@
+package cn.iocoder.dashboard.framework.errorcode.core.generator;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ClassUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.iocoder.dashboard.common.exception.ErrorCode;
+import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO;
+import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * ErrorCodeAutoGenerator 的实现类
+ * 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中
+ *
+ * @author dylan
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator {
+
+    /**
+     * 应用分组
+     */
+    private final String applicationName;
+    /**
+     * 错误码枚举类
+     */
+    private final List<String> constantsClassList;
+    /**
+     * 错误码 Service
+     */
+    private final ErrorCodeFrameworkService errorCodeService;
+
+    @Override
+    @EventListener(ApplicationReadyEvent.class)
+    @Async // 异步,保证项目的启动过程,毕竟非关键流程
+    public void execute() {
+        // 第一步,解析错误码
+        List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = parseErrorCode();
+        log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size());
+
+        // 第二步,写入到 system 服务
+        errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs);
+        log.info("[execute][写入到 system 组件完成]");
+    }
+
+    /**
+     * 解析 constantsClassList 变量,转换成错误码数组
+     *
+     * @return 错误码数组
+     */
+    private List<ErrorCodeAutoGenerateReqDTO>  parseErrorCode() {
+        // 校验 errorCodeConstantsClass 参数
+        if (CollUtil.isEmpty(constantsClassList)) {
+            log.info("[execute][未配置 yudao.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]");
+            return new ArrayList<>();
+        }
+
+        // 解析错误码
+        List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
+        constantsClassList.forEach(constantsClass -> {
+            // 解析错误码枚举类
+            Class<?> errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass);
+            // 解析错误码
+            autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz));
+        });
+        return autoGenerateDTOs;
+    }
+
+    /**
+     * 解析错误码类,获得错误码数组
+     *
+     * @return 错误码数组
+     */
+    private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode(Class<?> constantsClass) {
+        List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
+        Arrays.stream(constantsClass.getFields()).forEach(field -> {
+            if (field.getType() != ErrorCode.class) {
+                return;
+            }
+            // 转换成 ErrorCodeAutoGenerateReqDTO 对象
+            ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field);
+            autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName)
+                    .setCode(errorCode.getCode()).setMessage(errorCode.getMsg()));
+        });
+        return autoGenerateDTOs;
+    }
+
+}
+

+ 24 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoader.java

@@ -0,0 +1,24 @@
+package cn.iocoder.dashboard.framework.errorcode.core.loader;
+
+import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
+
+/**
+ * 错误码加载器
+ *
+ * 注意,错误码最终加载到 {@link cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil} 的 MESSAGES 变量中!
+ *
+ * @author dlyan
+ */
+public interface ErrorCodeLoader {
+
+    /**
+     * 添加错误码
+     *
+     * @param code 错误码的编号
+     * @param msg 错误码的提示
+     */
+    default void putErrorCode(Integer code, String msg) {
+        ServiceExceptionUtil.put(code, msg);
+    }
+
+}

+ 73 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java

@@ -0,0 +1,73 @@
+package cn.iocoder.dashboard.framework.errorcode.core.loader;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO;
+import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService;
+import cn.iocoder.dashboard.util.date.DateUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。
+ *
+ * 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。
+ *
+ * @author dlyan
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class ErrorCodeLoaderImpl implements ErrorCodeLoader {
+
+    /**
+     * 刷新错误码的频率,单位:毫秒
+     */
+    private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000;
+
+    /**
+     * 应用分组
+     */
+    private final String applicationName;
+    /**
+     * 错误码 Service
+     */
+    private final ErrorCodeFrameworkService errorCodeService;
+
+    /**
+     * 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新
+     */
+    private Date maxUpdateTime;
+
+    @EventListener(ApplicationReadyEvent.class)
+    public void loadErrorCodes() {
+        this.loadErrorCodes0();
+    }
+
+    @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD)
+    public void refreshErrorCodes() {
+        this.loadErrorCodes0();
+    }
+
+    private void loadErrorCodes0() {
+        // 加载错误码
+        List<ErrorCodeRespDTO> errorCodeRespDTOs = errorCodeService.getErrorCodeList(applicationName, maxUpdateTime);
+        if (CollUtil.isEmpty(errorCodeRespDTOs)) {
+            return;
+        }
+        log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
+
+        // 刷新错误码的缓存
+        errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
+            // 写入到错误码的缓存
+            putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
+            // 记录下更新时间,方便增量更新
+            maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
+        });
+    }
+
+}

+ 35 - 0
src/main/java/cn/iocoder/dashboard/framework/errorcode/core/service/ErrorCodeFrameworkService.java

@@ -0,0 +1,35 @@
+package cn.iocoder.dashboard.framework.errorcode.core.service;
+
+import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO;
+import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO;
+
+import javax.validation.Valid;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 错误码 Framework Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErrorCodeFrameworkService {
+
+    /**
+     * 自动创建错误码
+     *
+     * @param autoGenerateDTOs 错误码信息
+     */
+    void autoGenerateErrorCodes(@Valid List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs);
+
+    /**
+     * 增量获得错误码数组
+     *
+     * 如果 minUpdateTime 为空时,则获取所有错误码
+     *
+     * @param applicationName 应用名
+     * @param minUpdateTime 最小更新时间
+     * @return 错误码数组
+     */
+    List<ErrorCodeRespDTO> getErrorCodeList(String applicationName, Date minUpdateTime);
+
+}

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

+ 9 - 1
src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java

@@ -1,5 +1,7 @@
 package cn.iocoder.dashboard.framework.mybatis.config;
 
+import cn.iocoder.dashboard.framework.mybatis.core.handler.DefaultDBFieldHandler;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
 import org.apache.ibatis.annotations.Mapper;
@@ -13,7 +15,8 @@ import org.springframework.context.annotation.Configuration;
  * @author 芋道源码
  */
 @Configuration
-@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class)
+@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class,
+        lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
 public class MybatisConfiguration {
 
     @Bean
@@ -23,4 +26,9 @@ public class MybatisConfiguration {
         return mybatisPlusInterceptor;
     }
 
+    @Bean
+    public MetaObjectHandler defaultMetaObjectHandler(){
+        return new DefaultDBFieldHandler(); // 自动填充参数类
+    }
+
 }

+ 14 - 4
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java

@@ -1,5 +1,7 @@
 package cn.iocoder.dashboard.framework.mybatis.core.dataobject;
 
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import lombok.Data;
 
@@ -15,19 +17,27 @@ public class BaseDO implements Serializable {
     /**
      * 创建时间
      */
+    @TableField(fill = FieldFill.INSERT)
     private Date createTime;
     /**
      * 最后更新时间
      */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
     private Date updateTime;
     /**
-     * 创建者 TODO 芋艿:迁移成编号
+     * 创建者,目前使用 SysUser 的 id 编号
+     *
+     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
      */
-    private String createBy;
+    @TableField(fill = FieldFill.INSERT)
+    private String creator;
     /**
-     * 更新者 TODO 芋艿:迁移成编号
+     * 更新者,目前使用 SysUser 的 id 编号
+     *
+     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
      */
-    private String updateBy;
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updater;
     /**
      * 是否删除
      */

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

@@ -0,0 +1,63 @@
+package cn.iocoder.dashboard.framework.mybatis.core.handler;
+
+import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.dashboard.framework.security.core.LoginUser;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * 通用参数填充实现类
+ *
+ * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
+ *
+ * @author hexiaowu
+ */
+public class DefaultDBFieldHandler implements MetaObjectHandler {
+
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
+            LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
+            Date current = new Date();
+
+            // 创建时间为空,则以当前时间为插入时间
+            if (Objects.isNull(baseDO.getCreateTime())) {
+                baseDO.setCreateTime(current);
+            }
+            // 更新时间为空,则以当前时间为更新时间
+            if (Objects.isNull(baseDO.getUpdateTime())) {
+                baseDO.setUpdateTime(current);
+            }
+            // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
+            if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) {
+                baseDO.setCreator(loginUser.getId().toString());
+            }
+            // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
+            if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) {
+                baseDO.setUpdater(loginUser.getId().toString());
+            }
+        }
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        Object modifyTime = getFieldValByName("updateTime", metaObject);
+        Object modifier = getFieldValByName("updater", metaObject);
+        // 获取登录用户信息
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+
+        // 更新时间为空,则以当前时间为更新时间
+        if (Objects.isNull(modifyTime)) {
+            setFieldValByName("updateTime", new Date(), metaObject);
+        }
+        // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
+        if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
+            setFieldValByName("updater", loginUser.getId().toString(), metaObject);
+        }
+    }
+}

+ 8 - 4
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java

@@ -24,12 +24,16 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
 
-    default List<T> selectList() {
-        return selectList(new QueryWrapper<>());
-    }
-
     default T selectOne(String field, Object value) {
         return selectOne(new QueryWrapper<T>().eq(field, value));
     }
 
+    default Integer selectCount(String field, Object value) {
+        return selectCount(new QueryWrapper<T>().eq(field, value));
+    }
+
+    default List<T> selectList() {
+        return selectList(new QueryWrapper<>());
+    }
+
 }

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

+ 0 - 1
src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md

@@ -1 +0,0 @@
-<http://www.iocoder.cn/Spring-Boot/Spring-Security/?github>

+ 1 - 0
src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md

@@ -0,0 +1 @@
+<https://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao>

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

+ 2 - 1
src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java

@@ -1,6 +1,7 @@
 package cn.iocoder.dashboard.framework.security.core.handler;
 
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
 import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
 import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
 import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
@@ -36,6 +37,6 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
             securityFrameworkService.logout(token);
         }
         // 返回成功
-        ServletUtils.writeJSON(response, null);
+        ServletUtils.writeJSON(response, CommonResult.success(null));
     }
 }

+ 21 - 3
src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java

@@ -2,7 +2,10 @@ package cn.iocoder.dashboard.framework.security.core.util;
 
 import cn.iocoder.dashboard.framework.security.core.LoginUser;
 import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
+import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.util.StringUtils;
@@ -40,9 +43,20 @@ public class SecurityFrameworkUtils {
 
     /**
      * 获取当前用户
+     *
+     * @return 当前用户
      */
+    @Nullable
     public static LoginUser getLoginUser() {
-        return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        SecurityContext context = SecurityContextHolder.getContext();
+        if (context == null) {
+            return null;
+        }
+        Authentication authentication = context.getAuthentication();
+        if (authentication == null) {
+            return null;
+        }
+        return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
     }
 
     /**
@@ -50,8 +64,10 @@ public class SecurityFrameworkUtils {
      *
      * @return 用户编号
      */
+    @Nullable
     public static Long getLoginUserId() {
-        return getLoginUser().getId();
+        LoginUser loginUser = getLoginUser();
+        return loginUser != null ? loginUser.getId() : null;
     }
 
     /**
@@ -59,8 +75,10 @@ public class SecurityFrameworkUtils {
      *
      * @return 角色编号数组
      */
+    @Nullable
     public static Set<Long> getLoginUserRoleIds() {
-        return getLoginUser().getRoleIds();
+        LoginUser loginUser = getLoginUser();
+        return loginUser != null ? loginUser.getRoleIds() : null;
     }
 
     /**

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно