Browse Source

Merge remote-tracking branch 'origin/dev' into 4.X

疯狂的狮子Li 2 years ago
parent
commit
71317201b8
100 changed files with 3121 additions and 608 deletions
  1. 1 1
      .run/ruoyi-monitor-admin.run.xml
  2. 1 1
      .run/ruoyi-server.run.xml
  3. 1 1
      .run/ruoyi-xxl-job-admin.run.xml
  4. 2 3
      README.md
  5. 14 20
      pom.xml
  6. 1 1
      ruoyi-admin/pom.xml
  7. 11 13
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
  8. 1 1
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
  9. 1 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
  10. 24 9
      ruoyi-admin/src/main/resources/application-dev.yml
  11. 24 9
      ruoyi-admin/src/main/resources/application-prod.yml
  12. 3 2
      ruoyi-admin/src/main/resources/application.yml
  13. 1 1
      ruoyi-common/pom.xml
  14. 1 1
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java
  15. 5 0
      ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
  16. 27 5
      ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java
  17. 1 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
  18. 1 1
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/EmailLoginBody.java
  19. 9 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/service/DictService.java
  20. 86 79
      ruoyi-common/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java
  21. 149 0
      ruoyi-common/src/main/java/com/ruoyi/common/excel/DropDownOptions.java
  22. 370 0
      ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java
  23. 21 4
      ruoyi-common/src/main/java/com/ruoyi/common/helper/LoginHelper.java
  24. 1 2
      ruoyi-common/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java
  25. 9 6
      ruoyi-common/src/main/java/com/ruoyi/common/utils/StreamUtils.java
  26. 62 8
      ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
  27. 27 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java
  28. 1 12
      ruoyi-demo/pom.xml
  29. 13 29
      ruoyi-demo/src/main/java/com/ruoyi/demo/controller/SmsController.java
  30. 31 3
      ruoyi-demo/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
  31. 119 0
      ruoyi-demo/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java
  32. 68 0
      ruoyi-demo/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java
  33. 18 0
      ruoyi-demo/src/main/java/com/ruoyi/demo/service/IExportExcelService.java
  34. 223 0
      ruoyi-demo/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java
  35. 1 1
      ruoyi-extend/pom.xml
  36. 1 1
      ruoyi-extend/ruoyi-monitor-admin/pom.xml
  37. 1 1
      ruoyi-extend/ruoyi-xxl-job-admin/pom.xml
  38. 1 1
      ruoyi-framework/pom.xml
  39. 18 20
      ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
  40. 15 25
      ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
  41. 4 3
      ruoyi-framework/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java
  42. 22 0
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
  43. 1 1
      ruoyi-generator/pom.xml
  44. 1 1
      ruoyi-job/pom.xml
  45. 1 1
      ruoyi-oss/pom.xml
  46. 17 0
      ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java
  47. 10 10
      ruoyi-sms/pom.xml
  48. 1 34
      ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java
  49. 20 47
      ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
  50. 0 66
      ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
  51. 0 26
      ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
  52. 0 82
      ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
  53. 0 31
      ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java
  54. 0 16
      ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java
  55. 1 1
      ruoyi-system/pom.xml
  56. 3 1
      ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java
  57. 17 15
      ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java
  58. 7 2
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
  59. 23 6
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
  60. 23 4
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
  61. 21 0
      ruoyi-ui-vue3/.editorconfig
  62. 17 0
      ruoyi-ui-vue3/.env.development
  63. 20 0
      ruoyi-ui-vue3/.env.production
  64. 23 0
      ruoyi-ui-vue3/.gitignore
  65. 83 0
      ruoyi-ui-vue3/README.md
  66. 12 0
      ruoyi-ui-vue3/bin/build.bat
  67. 12 0
      ruoyi-ui-vue3/bin/package.bat
  68. 12 0
      ruoyi-ui-vue3/bin/run-web.bat
  69. 21 0
      ruoyi-ui-vue3/html/ie.html
  70. 215 0
      ruoyi-ui-vue3/index.html
  71. 43 0
      ruoyi-ui-vue3/package.json
  72. BIN
      ruoyi-ui-vue3/public/favicon.ico
  73. 15 0
      ruoyi-ui-vue3/src/App.vue
  74. 54 0
      ruoyi-ui-vue3/src/api/demo/demo.js
  75. 44 0
      ruoyi-ui-vue3/src/api/demo/tree.js
  76. 59 0
      ruoyi-ui-vue3/src/api/login.js
  77. 9 0
      ruoyi-ui-vue3/src/api/menu.js
  78. 57 0
      ruoyi-ui-vue3/src/api/monitor/cache.js
  79. 34 0
      ruoyi-ui-vue3/src/api/monitor/logininfor.js
  80. 18 0
      ruoyi-ui-vue3/src/api/monitor/online.js
  81. 26 0
      ruoyi-ui-vue3/src/api/monitor/operlog.js
  82. 72 0
      ruoyi-ui-vue3/src/api/system/config.js
  83. 52 0
      ruoyi-ui-vue3/src/api/system/dept.js
  84. 52 0
      ruoyi-ui-vue3/src/api/system/dict/data.js
  85. 60 0
      ruoyi-ui-vue3/src/api/system/dict/type.js
  86. 60 0
      ruoyi-ui-vue3/src/api/system/menu.js
  87. 44 0
      ruoyi-ui-vue3/src/api/system/notice.js
  88. 27 0
      ruoyi-ui-vue3/src/api/system/oss.js
  89. 58 0
      ruoyi-ui-vue3/src/api/system/ossConfig.js
  90. 44 0
      ruoyi-ui-vue3/src/api/system/post.js
  91. 119 0
      ruoyi-ui-vue3/src/api/system/role.js
  92. 135 0
      ruoyi-ui-vue3/src/api/system/user.js
  93. 85 0
      ruoyi-ui-vue3/src/api/tool/gen.js
  94. BIN
      ruoyi-ui-vue3/src/assets/401_images/401.gif
  95. BIN
      ruoyi-ui-vue3/src/assets/404_images/404.png
  96. BIN
      ruoyi-ui-vue3/src/assets/404_images/404_cloud.png
  97. 1 0
      ruoyi-ui-vue3/src/assets/icons/svg/404.svg
  98. 1 0
      ruoyi-ui-vue3/src/assets/icons/svg/bug.svg
  99. 1 0
      ruoyi-ui-vue3/src/assets/icons/svg/build.svg
  100. 0 0
      ruoyi-ui-vue3/src/assets/icons/svg/button.svg

+ 1 - 1
.run/ruoyi-monitor-admin.run.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:4.7.0" />
+        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:4.8.0" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
       </settings>

+ 1 - 1
.run/ruoyi-server.run.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-server:4.7.0" />
+        <option name="imageTag" value="ruoyi/ruoyi-server:4.8.0" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
       </settings>

+ 1 - 1
.run/ruoyi-xxl-job-admin.run.xml

@@ -2,7 +2,7 @@
   <configuration default="false" name="ruoyi-xxl-job-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
     <deployment type="dockerfile">
       <settings>
-        <option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:4.7.0" />
+        <option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:4.8.0" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-xxl-job-admin/Dockerfile" />
       </settings>

+ 2 - 3
README.md

@@ -10,7 +10,7 @@
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
 [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 <br>
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-4.7.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-4.8.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
 [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-2.7-blue.svg)]()
 [![JDK-8+](https://img.shields.io/badge/JDK-8-green.svg)]()
 [![JDK-11](https://img.shields.io/badge/JDK-11-green.svg)]()
@@ -53,7 +53,7 @@
 | 分布式任务调度     | 采用 Xxl-Job 天生支持分布式 统一的管理中心                                                                                        | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造                                                   | 
 | 文件存储        | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储                                                     | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应                                                    |
 | 云存储         | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家                                                                          | 不支持                                                                                |
-| 短信          | 支持 阿里、腾讯 只需在yml配置好厂家密钥即可使用 接口化支持扩展其他厂家                                                                            | 不支持                                                                                |
+| 短信          | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用                                                                 | 不支持                                                                                |
 | 邮件          | 采用 mail-api 通用协议支持大部分邮件厂商                                                                                         | 不支持                                                                                |
 | 接口文档        | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了                                                     | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成                                                | 
 | 校验框架        | 采用 Validation 支持注解与工具类校验 注解支持国际化                                                                                  | 仅支持注解 且注解不支持国际化                                                                    |
@@ -125,7 +125,6 @@
 * GitHub 地址 [RuoYi-Vue-Plus-github](https://github.com/dromara/RuoYi-Vue-Plus)
 * 单模块 分支 [RuoYi-Vue-Plus-fast](https://gitee.com/dromara/RuoYi-Vue-Plus/tree/fast/)
 * 微服务 分支 [RuoYi-Cloud-Plus](https://gitee.com/JavaLionLi/RuoYi-Cloud-Plus)
-* Vue3 分支 [RuoYi-Vue-Plus-UI](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus-UI)
 * 用户扩展项目 [扩展项目列表](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4478302&doc_id=1469725)
 
 ## 加群与捐献

+ 14 - 20
pom.xml

@@ -6,15 +6,15 @@
 
     <groupId>com.ruoyi</groupId>
     <artifactId>ruoyi-vue-plus</artifactId>
-    <version>4.7.0</version>
+    <version>4.8.0</version>
 
     <name>RuoYi-Vue-Plus</name>
     <url>https://gitee.com/dromara/RuoYi-Vue-Plus</url>
     <description>RuoYi-Vue-Plus后台管理系统</description>
 
     <properties>
-        <ruoyi-vue-plus.version>4.7.0</ruoyi-vue-plus.version>
-        <spring-boot.version>2.7.11</spring-boot.version>
+        <ruoyi-vue-plus.version>4.8.0</ruoyi-vue-plus.version>
+        <spring-boot.version>2.7.13</spring-boot.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>1.8</java.version>
@@ -22,9 +22,9 @@
         <spring-boot.mybatis>2.2.2</spring-boot.mybatis>
         <springdoc.version>1.6.15</springdoc.version>
         <poi.version>5.2.3</poi.version>
-        <easyexcel.version>3.2.1</easyexcel.version>
+        <easyexcel.version>3.3.1</easyexcel.version>
         <velocity.version>2.3</velocity.version>
-        <satoken.version>1.34.0</satoken.version>
+        <satoken.version>1.35.0.RC</satoken.version>
         <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
         <p6spy.version>3.9.1</p6spy.version>
         <hutool.version>5.8.18</hutool.version>
@@ -46,8 +46,7 @@
         <!-- OSS 配置 -->
         <aws-java-sdk-s3.version>1.12.400</aws-java-sdk-s3.version>
         <!-- SMS 配置 -->
-        <aliyun.sms.version>2.0.23</aliyun.sms.version>
-        <tencent.sms.version>3.1.687</tencent.sms.version>
+        <sms4j.version>2.2.0</sms4j.version>
     </properties>
 
     <profiles>
@@ -200,16 +199,11 @@
                 <version>${aws-java-sdk-s3.version}</version>
             </dependency>
 
+            <!--短信sms4j-->
             <dependency>
-                <groupId>com.aliyun</groupId>
-                <artifactId>dysmsapi20170525</artifactId>
-                <version>${aliyun.sms.version}</version>
-            </dependency>
-
-            <dependency>
-                <groupId>com.tencentcloudapi</groupId>
-                <artifactId>tencentcloud-sdk-java-sms</artifactId>
-                <version>${tencent.sms.version}</version>
+                <groupId>org.dromara.sms4j</groupId>
+                <artifactId>sms4j-spring-boot-starter</artifactId>
+                <version>${sms4j.version}</version>
             </dependency>
 
             <dependency>
@@ -420,8 +414,8 @@
     <repositories>
         <repository>
             <id>public</id>
-            <name>aliyun nexus</name>
-            <url>https://maven.aliyun.com/repository/public/</url>
+            <name>huawei nexus</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
             <releases>
                 <enabled>true</enabled>
             </releases>
@@ -431,8 +425,8 @@
     <pluginRepositories>
         <pluginRepository>
             <id>public</id>
-            <name>aliyun nexus</name>
-            <url>https://maven.aliyun.com/repository/public/</url>
+            <name>huawei nexus</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
             <releases>
                 <enabled>true</enabled>
             </releases>

+ 1 - 1
ruoyi-admin/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <packaging>jar</packaging>

+ 11 - 13
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java

@@ -16,12 +16,13 @@ import com.ruoyi.common.utils.reflect.ReflectUtils;
 import com.ruoyi.common.utils.spring.SpringUtils;
 import com.ruoyi.framework.config.properties.CaptchaProperties;
 import com.ruoyi.framework.config.properties.MailProperties;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.SmsTemplate;
-import com.ruoyi.sms.entity.SmsResult;
 import com.ruoyi.system.service.ISysConfigService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.sms4j.provider.enumerate.SupplierType;
 import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.validation.constraints.NotBlank;
 import java.time.Duration;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
@@ -47,7 +49,6 @@ import java.util.Map;
 public class CaptchaController {
 
     private final CaptchaProperties captchaProperties;
-    private final SmsProperties smsProperties;
     private final ISysConfigService configService;
     private final MailProperties mailProperties;
 
@@ -58,21 +59,18 @@ public class CaptchaController {
      */
     @GetMapping("/captchaSms")
     public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
-        if (!smsProperties.getEnabled()) {
-            return R.fail("当前系统没有开启短信功能!");
-        }
         String key = CacheConstants.CAPTCHA_CODE_KEY + phonenumber;
         String code = RandomUtil.randomNumbers(4);
         RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
         // 验证码模板id 自行处理 (查数据库或写死均可)
         String templateId = "";
-        Map<String, String> map = new HashMap<>(1);
+        LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
         map.put("code", code);
-        SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
-        SmsResult result = smsTemplate.send(phonenumber, templateId, map);
-        if (!result.isSuccess()) {
-            log.error("验证码短信发送异常 => {}", result);
-            return R.fail(result.getMessage());
+        SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
+        SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
+        if (!"OK".equals(smsResponse.getCode())) {
+            log.error("验证码短信发送异常 => {}", smsResponse);
+            return R.fail(smsResponse.getMessage());
         }
         return R.ok();
     }

+ 1 - 1
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java

@@ -47,7 +47,7 @@ public class SysUserOnlineController extends BaseController {
         for (String key : keys) {
             String token = StringUtils.substringAfterLast(key, ":");
             // 如果已经过期则跳过
-            if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
+            if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
                 continue;
             }
             userOnlineDTOList.add(RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token));

+ 1 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java

@@ -80,6 +80,7 @@ public class SysRoleController extends BaseController {
     @Log(title = "角色管理", businessType = BusinessType.INSERT)
     @PostMapping
     public R<Void> add(@Validated @RequestBody SysRole role) {
+        roleService.checkRoleAllowed(role);
         if (!roleService.checkRoleNameUnique(role)) {
             return R.fail("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
         } else if (!roleService.checkRoleKeyUnique(role)) {

+ 24 - 9
ruoyi-admin/src/main/resources/application-dev.yml

@@ -158,14 +158,29 @@ mail:
   # Socket连接超时值,单位毫秒,缺省值不超时
   connectionTimeout: 0
 
---- # sms 短信
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
 sms:
-  enabled: false
   # 阿里云 dysmsapi.aliyuncs.com
-  # 腾讯云 sms.tencentcloudapi.com
-  endpoint: "dysmsapi.aliyuncs.com"
-  accessKeyId: xxxxxxx
-  accessKeySecret: xxxxxx
-  signName: 测试
-  # 腾讯专用
-  sdkAppId:
+  alibaba:
+    #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
+    requestUrl: dysmsapi.aliyuncs.com
+    #阿里云的accessKey
+    accessKeyId: xxxxxxx
+    #阿里云的accessKeySecret
+    accessKeySecret: xxxxxxx
+    #短信签名
+    signature: 测试
+  tencent:
+    #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
+    requestUrl: sms.tencentcloudapi.com
+    #腾讯云的accessKey
+    accessKeyId: xxxxxxx
+    #腾讯云的accessKeySecret
+    accessKeySecret: xxxxxxx
+    #短信签名
+    signature: 测试
+    #短信sdkAppId
+    sdkAppId: appid
+    #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
+    territory: ap-guangzhou

+ 24 - 9
ruoyi-admin/src/main/resources/application-prod.yml

@@ -161,14 +161,29 @@ mail:
   # Socket连接超时值,单位毫秒,缺省值不超时
   connectionTimeout: 0
 
---- # sms 短信
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
 sms:
-  enabled: false
   # 阿里云 dysmsapi.aliyuncs.com
-  # 腾讯云 sms.tencentcloudapi.com
-  endpoint: "dysmsapi.aliyuncs.com"
-  accessKeyId: xxxxxxx
-  accessKeySecret: xxxxxx
-  signName: 测试
-  # 腾讯专用
-  sdkAppId:
+  alibaba:
+    #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
+    requestUrl: dysmsapi.aliyuncs.com
+    #阿里云的accessKey
+    accessKeyId: xxxxxxx
+    #阿里云的accessKeySecret
+    accessKeySecret: xxxxxxx
+    #短信签名
+    signature: 测试
+  tencent:
+    #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
+    requestUrl: sms.tencentcloudapi.com
+    #腾讯云的accessKey
+    accessKeyId: xxxxxxx
+    #腾讯云的accessKeySecret
+    accessKeySecret: xxxxxxx
+    #短信签名
+    signature: 测试
+    #短信sdkAppId
+    sdkAppId: appid
+    #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
+    territory: ap-guangzhou

+ 3 - 2
ruoyi-admin/src/main/resources/application.yml

@@ -104,8 +104,9 @@ sa-token:
   token-name: Authorization
   # token有效期 设为一天 (必定过期) 单位: 秒
   timeout: 86400
-  # token临时有效期 (指定时间无操作就过期) 单位: 秒
-  activity-timeout: 1800
+  # 多端不同 token 有效期 可查看 LoginHelper.loginByDevice 方法自定义
+  # token最低活跃时间 (指定时间无操作就过期) 单位: 秒
+  active-timeout: 1800
   # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
   is-concurrent: true
   # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)

+ 1 - 1
ruoyi-common/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java

@@ -32,7 +32,7 @@ public @interface EncryptField {
     String publicKey() default "";
 
     /**
-     * 钥。RSA、SM2需要
+     * 钥。RSA、SM2需要
      */
     String privateKey() default "";
 

+ 5 - 0
ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java

@@ -129,4 +129,9 @@ public interface UserConstants {
      */
     Long ADMIN_ID = 1L;
 
+    /**
+     * 管理员角色key
+     */
+    String ADMIN_ROLE_KEY = "admin";
+
 }

+ 27 - 5
ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java

@@ -37,14 +37,36 @@ public class ExcelEnumConvert implements Converter<Object> {
 
     @Override
     public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
-        Object codeValue = cellData.getData();
+        cellData.checkEmpty();
+        // Excel中填入的是枚举中指定的描述
+        Object textValue = null;
+        switch (cellData.getType()) {
+            case STRING:
+            case DIRECT_STRING:
+            case RICH_TEXT_STRING:
+                textValue = cellData.getStringValue();
+                break;
+            case NUMBER:
+                textValue = cellData.getNumberValue();
+                break;
+            case BOOLEAN:
+                textValue = cellData.getBooleanValue();
+                break;
+            default:
+                throw new IllegalArgumentException("单元格类型异常!");
+        }
         // 如果是空值
-        if (ObjectUtil.isNull(codeValue)) {
+        if (ObjectUtil.isNull(textValue)) {
             return null;
         }
-        Map<Object, String> enumValueMap = beforeConvert(contentProperty);
-        String textValue = enumValueMap.get(codeValue);
-        return Convert.convert(contentProperty.getField().getType(), textValue);
+        Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
+        // 从Java输出至Excel是code转text
+        // 因此从Excel转Java应该将text与code对调
+        Map<Object, Object> enumTextToCodeMap = new HashMap<>();
+        enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
+        // 应该从text -> code中查找
+        Object codeValue = enumTextToCodeMap.get(textValue);
+        return Convert.convert(contentProperty.getField().getType(), codeValue);
     }
 
     @Override

+ 1 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java

@@ -53,6 +53,7 @@ public class SysUser extends BaseEntity {
      * 用户昵称
      */
     @Xss(message = "用户昵称不能包含脚本字符")
+    @NotBlank(message = "用户昵称不能为空")
     @Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
     private String nickName;
 

+ 1 - 1
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/EmailLoginBody.java

@@ -6,7 +6,7 @@ import javax.validation.constraints.Email;
 import javax.validation.constraints.NotBlank;
 
 /**
- * 短信登录对象
+ * 邮箱登录对象
  *
  * @author Lion Li
  */

+ 9 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/service/DictService.java

@@ -1,5 +1,7 @@
 package com.ruoyi.common.core.service;
 
+import java.util.Map;
+
 /**
  * 通用 字典服务
  *
@@ -54,4 +56,11 @@ public interface DictService {
      */
     String getDictValue(String dictType, String dictLabel, String separator);
 
+    /**
+     * 获取字典下所有的字典值与标签
+     *
+     * @param dictType 字典类型
+     * @return dictValue为key,dictLabel为值组成的Map
+     */
+    Map<String, String> getAllDictByDictType(String dictType);
 }

+ 86 - 79
ruoyi-common/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java

@@ -1,19 +1,20 @@
 package com.ruoyi.common.excel;
 
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
 import com.alibaba.excel.metadata.Head;
 import com.alibaba.excel.write.merge.AbstractMergeStrategy;
 import com.ruoyi.common.annotation.CellMerge;
+import com.ruoyi.common.utils.reflect.ReflectUtils;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.util.CellRangeAddress;
 
 import java.lang.reflect.Field;
-import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -24,91 +25,97 @@ import java.util.Map;
  *
  * @author Lion Li
  */
-@AllArgsConstructor
 @Slf4j
 public class CellMergeStrategy extends AbstractMergeStrategy {
 
-	private List<?> list;
-	private boolean hasTitle;
+    private final List<CellRangeAddress> cellList;
+    private final boolean hasTitle;
+    private int rowIndex;
 
-	@Override
-	protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
-		List<CellRangeAddress> cellList = handle(list, hasTitle);
-		// judge the list is not null
-		if (CollectionUtils.isNotEmpty(cellList)) {
-			// the judge is necessary
-			if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
-				for (CellRangeAddress item : cellList) {
-					sheet.addMergedRegion(item);
-				}
-			}
-		}
-	}
+    public CellMergeStrategy(List<?> list, boolean hasTitle) {
+        this.hasTitle = hasTitle;
+        // 行合并开始下标
+        this.rowIndex = hasTitle ? 1 : 0;
+        this.cellList = handle(list, hasTitle);
+    }
 
-	@SneakyThrows
-	private static List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
-		List<CellRangeAddress> cellList = new ArrayList<>();
-		if (CollectionUtils.isEmpty(list)) {
-			return cellList;
-		}
-		Class<?> clazz = list.get(0).getClass();
-		Field[] fields = clazz.getDeclaredFields();
-		// 有注解的字段
-		List<Field> mergeFields = new ArrayList<>();
-		List<Integer> mergeFieldsIndex = new ArrayList<>();
-		for (int i = 0; i < fields.length; i++) {
-			Field field = fields[i];
-			if (field.isAnnotationPresent(CellMerge.class)) {
-				CellMerge cm = field.getAnnotation(CellMerge.class);
-				mergeFields.add(field);
-				mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
-			}
-		}
-		// 行合并开始下标
-		int rowIndex = hasTitle ? 1 : 0;
-		Map<Field, RepeatCell> map = new HashMap<>();
-		// 生成两两合并单元格
-		for (int i = 0; i < list.size(); i++) {
-			for (int j = 0; j < mergeFields.size(); j++) {
-				Field field = mergeFields.get(j);
-				String name = field.getName();
-				String methodName = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
-				Method readMethod = clazz.getMethod(methodName);
-				Object val = readMethod.invoke(list.get(i));
+    @Override
+    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
+        // judge the list is not null
+        if (CollUtil.isNotEmpty(cellList)) {
+            // the judge is necessary
+            if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
+                for (CellRangeAddress item : cellList) {
+                    sheet.addMergedRegion(item);
+                }
+            }
+        }
+    }
 
-				int colNum = mergeFieldsIndex.get(j);
-				if (!map.containsKey(field)) {
-					map.put(field, new RepeatCell(val, i));
-				} else {
-					RepeatCell repeatCell = map.get(field);
-					Object cellValue = repeatCell.getValue();
-					if (cellValue == null || "".equals(cellValue)) {
-						// 空值跳过不合并
-						continue;
-					}
-					if (!cellValue.equals(val)) {
-						if (i - repeatCell.getCurrent() > 1) {
-							cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
-						}
-						map.put(field, new RepeatCell(val, i));
-					} else if (i == list.size() - 1) {
-						if (i > repeatCell.getCurrent()) {
-							cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
-						}
-					}
-				}
-			}
-		}
-		return cellList;
-	}
+    @SneakyThrows
+    private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
+        List<CellRangeAddress> cellList = new ArrayList<>();
+        if (CollUtil.isEmpty(list)) {
+            return cellList;
+        }
+        Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
 
-	@Data
-	@AllArgsConstructor
-	static class RepeatCell {
+        // 有注解的字段
+        List<Field> mergeFields = new ArrayList<>();
+        List<Integer> mergeFieldsIndex = new ArrayList<>();
+        for (int i = 0; i < fields.length; i++) {
+            Field field = fields[i];
+            if (field.isAnnotationPresent(CellMerge.class)) {
+                CellMerge cm = field.getAnnotation(CellMerge.class);
+                mergeFields.add(field);
+                mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
+                if (hasTitle) {
+                    ExcelProperty property = field.getAnnotation(ExcelProperty.class);
+                    rowIndex = Math.max(rowIndex, property.value().length);
+                }
+            }
+        }
 
-		private Object value;
+        Map<Field, RepeatCell> map = new HashMap<>();
+        // 生成两两合并单元格
+        for (int i = 0; i < list.size(); i++) {
+            for (int j = 0; j < mergeFields.size(); j++) {
+                Field field = mergeFields.get(j);
+                Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
 
-		private int current;
+                int colNum = mergeFieldsIndex.get(j);
+                if (!map.containsKey(field)) {
+                    map.put(field, new RepeatCell(val, i));
+                } else {
+                    RepeatCell repeatCell = map.get(field);
+                    Object cellValue = repeatCell.getValue();
+                    if (cellValue == null || "".equals(cellValue)) {
+                        // 空值跳过不合并
+                        continue;
+                    }
+                    if (!cellValue.equals(val)) {
+                        if (i - repeatCell.getCurrent() > 1) {
+                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
+                        }
+                        map.put(field, new RepeatCell(val, i));
+                    } else if (i == list.size() - 1) {
+                        if (i > repeatCell.getCurrent()) {
+                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
+                        }
+                    }
+                }
+            }
+        }
+        return cellList;
+    }
 
-	}
+    @Data
+    @AllArgsConstructor
+    static class RepeatCell {
+
+        private Object value;
+
+        private int current;
+
+    }
 }

+ 149 - 0
ruoyi-common/src/main/java/com/ruoyi/common/excel/DropDownOptions.java

@@ -0,0 +1,149 @@
+package com.ruoyi.common.excel;
+
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.exception.ServiceException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * <h1>Excel下拉可选项</h1>
+ * 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@SuppressWarnings("unused")
+public class DropDownOptions {
+    /**
+     * 一级下拉所在列index,从0开始算
+     */
+    private int index = 0;
+    /**
+     * 二级下拉所在的index,从0开始算,不能与一级相同
+     */
+    private int nextIndex = 0;
+    /**
+     * 一级下拉所包含的数据
+     */
+    private List<String> options = new ArrayList<>();
+    /**
+     * 二级下拉所包含的数据Map
+     * <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
+     */
+    private Map<String, List<String>> nextOptions = new HashMap<>();
+    /**
+     * 分隔符
+     */
+    private static final String DELIMITER = "_";
+
+    /**
+     * 创建只有一级的下拉选
+     */
+    public DropDownOptions(int index, List<String> options) {
+        this.index = index;
+        this.options = options;
+    }
+
+    /**
+     * <h2>创建每个选项可选值</h2>
+     * <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
+     *
+     * @param vars 可选值内包含的参数
+     * @return 合规的可选值
+     */
+    public static String createOptionValue(Object... vars) {
+        StringBuilder stringBuffer = new StringBuilder();
+        String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
+        for (int i = 0; i < vars.length; i++) {
+            String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
+            if (!var.matches(regex)) {
+                throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
+            }
+            stringBuffer.append(var);
+            if (i < vars.length - 1) {
+                // 直至最后一个前,都以_作为切割线
+                stringBuffer.append(DELIMITER);
+            }
+        }
+        if (stringBuffer.toString().matches("^\\d_*$")) {
+            throw new ServiceException("禁止以数字开头");
+        }
+        return stringBuffer.toString();
+    }
+
+    /**
+     * 将处理后合理的可选值解析为原始的参数
+     *
+     * @param option 经过处理后的合理的可选项
+     * @return 原始的参数
+     */
+    public static List<String> analyzeOptionValue(String option) {
+        return StrUtil.split(option, DELIMITER, true, true);
+    }
+
+    /**
+     * 创建级联下拉选项
+     *
+     * @param parentList                  父实体可选项原始数据
+     * @param parentIndex                 父下拉选位置
+     * @param sonList                     子实体可选项原始数据
+     * @param sonIndex                    子下拉选位置
+     * @param parentHowToGetIdFunction    父类如何获取唯一标识
+     * @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
+     * @param howToBuildEveryOption       如何生成下拉选内容
+     * @return 级联下拉选项
+     */
+    public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
+                                                         int parentIndex,
+                                                         List<T> sonList,
+                                                         int sonIndex,
+                                                         Function<T, Number> parentHowToGetIdFunction,
+                                                         Function<T, Number> sonHowToGetParentIdFunction,
+                                                         Function<T, String> howToBuildEveryOption) {
+        DropDownOptions parentLinkSonOptions = new DropDownOptions();
+        // 先创建父类的下拉
+        parentLinkSonOptions.setIndex(parentIndex);
+        parentLinkSonOptions.setOptions(
+            parentList.stream()
+                .map(howToBuildEveryOption)
+                .collect(Collectors.toList())
+        );
+        // 提取父-子级联下拉
+        Map<String, List<String>> sonOptions = new HashMap<>();
+        // 父级依据自己的ID分组
+        Map<Number, List<T>> parentGroupByIdMap =
+            parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
+        // 遍历每个子集,提取到Map中
+        sonList.forEach(everySon -> {
+            if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
+                // 找到对应的上级
+                T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
+                // 提取名称和ID作为Key
+                String key = howToBuildEveryOption.apply(parentObj);
+                // Key对应的Value
+                List<String> thisParentSonOptionList;
+                if (sonOptions.containsKey(key)) {
+                    thisParentSonOptionList = sonOptions.get(key);
+                } else {
+                    thisParentSonOptionList = new ArrayList<>();
+                    sonOptions.put(key, thisParentSonOptionList);
+                }
+                // 往Value中添加当前子集选项
+                thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
+            }
+        });
+        parentLinkSonOptions.setNextIndex(sonIndex);
+        parentLinkSonOptions.setNextOptions(sonOptions);
+        return parentLinkSonOptions;
+    }
+}

+ 370 - 0
ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java

@@ -0,0 +1,370 @@
+package com.ruoyi.common.excel;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.annotation.ExcelEnumFormat;
+import com.ruoyi.common.core.service.DictService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StreamUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * <h1>Excel表格下拉选操作</h1>
+ * 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
+ * <p>
+ * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
+ *
+ * @author Emil.Zhang
+ */
+@Slf4j
+public class ExcelDownHandler implements SheetWriteHandler {
+
+    /**
+     * Excel表格中的列名英文
+     * 仅为了解析列英文,禁止修改
+     */
+    private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    /**
+     * 单选数据Sheet名
+     */
+    private static final String OPTIONS_SHEET_NAME = "options";
+    /**
+     * 联动选择数据Sheet名的头
+     */
+    private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
+    /**
+     * 下拉可选项
+     */
+    private final List<DropDownOptions> dropDownOptions;
+    /**
+     * 当前单选进度
+     */
+    private int currentOptionsColumnIndex;
+    /**
+     * 当前联动选择进度
+     */
+    private int currentLinkedOptionsSheetIndex;
+    private final DictService dictService;
+
+    public ExcelDownHandler(List<DropDownOptions> options) {
+        this.dropDownOptions = options;
+        this.currentOptionsColumnIndex = 0;
+        this.currentLinkedOptionsSheetIndex = 0;
+        this.dictService = SpringUtils.getBean(DictService.class);
+    }
+
+    /**
+     * <h2>开始创建下拉数据</h2>
+     * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
+     * 如果有且设置了value值,则将其直接置为下拉可选项
+     * <p>
+     * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
+     * <p>
+     * 3.二者并存,注意调用方式
+     */
+    @Override
+    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+        Sheet sheet = writeSheetHolder.getSheet();
+        // 开始设置下拉框 HSSFWorkbook
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
+        Workbook workbook = writeWorkbookHolder.getWorkbook();
+        int length = fields.length;
+        for (int i = 0; i < length; i++) {
+            // 循环实体中的每个属性
+            // 可选的下拉值
+            List<String> options = new ArrayList<>();
+            if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
+                // 如果指定了@ExcelDictFormat,则使用字典的逻辑
+                ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
+                String dictType = format.dictType();
+                String converterExp = format.readConverterExp();
+                if (StrUtil.isNotBlank(dictType)) {
+                    // 如果传递了字典名,则依据字典建立下拉
+                    Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
+                        .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
+                        .values();
+                    options = new ArrayList<>(values);
+                } else if (StrUtil.isNotBlank(converterExp)) {
+                    // 如果指定了确切的值,则直接解析确切的值
+                    options = StrUtil.split(converterExp, format.separator(), true, true);
+                }
+            } else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
+                // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
+                ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
+                List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
+                options = StreamUtils.toList(values, String::valueOf);
+            }
+            if (ObjectUtil.isNotEmpty(options)) {
+                // 仅当下拉可选项不为空时执行
+                // 获取列下标,默认为当前循环次数
+                int index = i;
+                if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
+                    // 如果指定了列下标,以指定的为主
+                    index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
+                }
+                if (options.size() > 20) {
+                    // 这里限制如果可选项大于20,则使用额外表形式
+                    dropDownWithSheet(helper, workbook, sheet, index, options);
+                } else {
+                    // 否则使用固定值形式
+                    dropDownWithSimple(helper, sheet, index, options);
+                }
+            }
+        }
+        dropDownOptions.forEach(everyOptions -> {
+            // 如果传递了下拉框选择器参数
+            if (!everyOptions.getNextOptions().isEmpty()) {
+                // 当二级选项不为空时,使用额外关联表的形式
+                dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
+            } else if (everyOptions.getOptions().size() > 10) {
+                // 当一级选项参数个数大于10,使用额外表的形式
+                dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+            } else if (everyOptions.getOptions().size() != 0) {
+                // 当一级选项个数不为空,使用默认形式
+                dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+            }
+        });
+    }
+
+    /**
+     * <h2>简单下拉框</h2>
+     * 直接将可选项拼接为指定列的数据校验值
+     *
+     * @param celIndex 列index
+     * @param value    下拉选可选值
+     */
+    private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
+        if (ObjectUtil.isEmpty(value)) {
+            return;
+        }
+        this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
+    }
+
+    /**
+     * <h2>额外表格形式的级联下拉框</h2>
+     *
+     * @param options 额外表格形式存储的下拉可选项
+     */
+    private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
+        String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
+        // 创建联动下拉数据表
+        Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
+        // 将下拉表隐藏
+        workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
+        // 完善横向的一级选项数据表
+        List<String> firstOptions = options.getOptions();
+        Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
+
+        // 创建名称管理器
+        Name name = workbook.createName();
+        // 设置名称管理器的别名
+        name.setNameName(linkedOptionsSheetName);
+        // 以横向第一行创建一级下拉拼接引用位置
+        String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
+            linkedOptionsSheetName,
+            getExcelColumnName(0),
+            getExcelColumnName(firstOptions.size())
+        );
+        // 设置名称管理器的引用位置
+        name.setRefersToFormula(firstOptionsFunction);
+        // 设置数据校验为序列模式,引用的是名称管理器中的别名
+        this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
+
+        for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
+            // 先提取主表中一级下拉的列名
+            String firstOptionsColumnName = getExcelColumnName(columIndex);
+            // 一次循环是每一个一级选项
+            int finalI = columIndex;
+            // 本次循环的一级选项值
+            String thisFirstOptionsValue = firstOptions.get(columIndex);
+            // 创建第一行的数据
+            Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
+                // 如果不存在则创建第一行
+                .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
+                // 第一行当前列
+                .createCell(columIndex)
+                // 设置值为当前一级选项值
+                .setCellValue(thisFirstOptionsValue);
+
+            // 第二行开始,设置第二级别选项参数
+            List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
+            if (CollUtil.isEmpty(secondOptions)) {
+                // 必须保证至少有一个关联选项,否则将导致Excel解析错误
+                secondOptions = Collections.singletonList("暂无_0");
+            }
+
+            // 以该一级选项值创建子名称管理器
+            Name sonName = workbook.createName();
+            // 设置名称管理器的别名
+            sonName.setNameName(thisFirstOptionsValue);
+            // 以第二行该列数据拼接引用位置
+            String sonFunction = String.format("%s!$%s$2:$%s$%d",
+                linkedOptionsSheetName,
+                firstOptionsColumnName,
+                firstOptionsColumnName,
+                secondOptions.size() + 1
+            );
+            // 设置名称管理器的引用位置
+            sonName.setRefersToFormula(sonFunction);
+            // 数据验证为序列模式,引用到每一个主表中的二级选项位置
+            // 创建子项的名称管理器,只是为了使得Excel可以识别到数据
+            String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
+            for (int i = 0; i < 100; i++) {
+                // 以一级选项对应的主体所在位置创建二级下拉
+                String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
+                // 二级只能主表每一行的每一列添加二级校验
+                markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
+            }
+
+            for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
+                // 从第二行开始填充二级选项
+                int finalRowIndex = rowIndex + 1;
+                int finalColumIndex = columIndex;
+
+                Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
+                    // 没有则创建
+                    .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
+                Optional
+                    // 在本级一级选项所在的列
+                    .ofNullable(row.getCell(finalColumIndex))
+                    // 不存在则创建
+                    .orElseGet(() -> row.createCell(finalColumIndex))
+                    // 设置二级选项值
+                    .setCellValue(secondOptions.get(rowIndex));
+            }
+        }
+
+        currentLinkedOptionsSheetIndex++;
+    }
+
+    /**
+     * <h2>额外表格形式的普通下拉框</h2>
+     * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
+     *
+     * @param celIndex 下拉选
+     * @param value    下拉选可选值
+     */
+    private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
+        // 创建下拉数据表
+        Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
+            .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
+        // 将下拉表隐藏
+        workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
+        // 完善纵向的一级选项数据表
+        for (int i = 0; i < value.size(); i++) {
+            int finalI = i;
+            // 获取每一选项行,如果没有则创建
+            Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
+                .orElseGet(() -> simpleDataSheet.createRow(finalI));
+            // 获取本级选项对应的选项列,如果没有则创建
+            Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
+                .orElseGet(() -> row.createCell(currentOptionsColumnIndex));
+            // 设置值
+            cell.setCellValue(value.get(i));
+        }
+
+        // 创建名称管理器
+        Name name = workbook.createName();
+        // 设置名称管理器的别名
+        String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
+        name.setNameName(nameName);
+        // 以纵向第一列创建一级下拉拼接引用位置
+        String function = String.format("%s!$%s$1:$%s$%d",
+            OPTIONS_SHEET_NAME,
+            getExcelColumnName(currentOptionsColumnIndex),
+            getExcelColumnName(currentOptionsColumnIndex),
+            value.size());
+        // 设置名称管理器的引用位置
+        name.setRefersToFormula(function);
+        // 设置数据校验为序列模式,引用的是名称管理器中的别名
+        this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
+        currentOptionsColumnIndex++;
+    }
+
+    /**
+     * 挂载下拉的列,仅限一级选项
+     */
+    private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
+                                    DataValidationConstraint constraint) {
+        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+        CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
+        markDataValidationToSheet(helper, sheet, constraint, addressList);
+    }
+
+    /**
+     * 挂载下拉的列,仅限二级选项
+     */
+    private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
+                                          Integer celIndex, DataValidationConstraint constraint) {
+        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+        CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
+        markDataValidationToSheet(helper, sheet, constraint, addressList);
+    }
+
+    /**
+     * 应用数据校验
+     */
+    private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
+                                           DataValidationConstraint constraint, CellRangeAddressList addressList) {
+        // 数据有效性对象
+        DataValidation dataValidation = helper.createValidation(constraint, addressList);
+        // 处理Excel兼容性问题
+        if (dataValidation instanceof XSSFDataValidation) {
+            //数据校验
+            dataValidation.setSuppressDropDownArrow(true);
+            //错误提示
+            dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+            dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
+            dataValidation.setShowErrorBox(true);
+            //选定提示
+            dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
+            dataValidation.setShowPromptBox(true);
+            sheet.addValidationData(dataValidation);
+        } else {
+            dataValidation.setSuppressDropDownArrow(false);
+        }
+        sheet.addValidationData(dataValidation);
+    }
+
+    /**
+     * <h2>依据列index获取列名英文</h2>
+     * 依据列index转换为Excel中的列名英文
+     * <p>例如第1列,index为0,解析出来为A列</p>
+     * 第27列,index为26,解析为AA列
+     * <p>第28列,index为27,解析为AB列</p>
+     *
+     * @param columnIndex 列index
+     * @return 列index所在得英文名
+     */
+    private String getExcelColumnName(int columnIndex) {
+        // 26一循环的次数
+        int columnCircleCount = columnIndex / 26;
+        // 26一循环内的位置
+        int thisCircleColumnIndex = columnIndex % 26;
+        // 26一循环的次数大于0,则视为栏名至少两位
+        String columnPrefix = columnCircleCount == 0
+            ? StrUtil.EMPTY
+            : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
+        // 从26一循环内取对应的栏位名
+        String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
+        // 将二者拼接即为最终的栏位名
+        return columnPrefix + columnNext;
+    }
+}

+ 21 - 4
ruoyi-common/src/main/java/com/ruoyi/common/helper/LoginHelper.java

@@ -2,6 +2,7 @@ package com.ruoyi.common.helper;
 
 import cn.dev33.satoken.context.SaHolder;
 import cn.dev33.satoken.context.model.SaStorage;
+import cn.dev33.satoken.session.SaSession;
 import cn.dev33.satoken.stp.SaLoginModel;
 import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.convert.Convert;
@@ -54,6 +55,14 @@ public class LoginHelper {
         if (ObjectUtil.isNotNull(deviceType)) {
             model.setDevice(deviceType.getDevice());
         }
+        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+        // 例如: 后台用户30分钟过期 app用户1天过期
+//        UserType userType = UserType.getUserType(loginUser.getUserType());
+//        if (userType == UserType.SYS_USER) {
+//            model.setTimeout(86400).setActiveTimeout(1800);
+//        } else if (userType == UserType.APP_USER) {
+//            model.setTimeout(86400).setActiveTimeout(1800);
+//        }
         StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
         StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
     }
@@ -66,7 +75,11 @@ public class LoginHelper {
         if (loginUser != null) {
             return loginUser;
         }
-        loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
+        SaSession session = StpUtil.getTokenSession();
+        if (ObjectUtil.isNull(session)) {
+            return null;
+        }
+        loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
         SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
         return loginUser;
     }
@@ -75,7 +88,11 @@ public class LoginHelper {
      * 获取用户基于token
      */
     public static LoginUser getLoginUser(String token) {
-        return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
+        SaSession session = StpUtil.getTokenSessionByToken(token);
+        if (ObjectUtil.isNull(session)) {
+            return null;
+        }
+        return (LoginUser) session.get(LOGIN_USER_KEY);
     }
 
     /**
@@ -113,8 +130,8 @@ public class LoginHelper {
      * 获取用户类型
      */
     public static UserType getUserType() {
-        String loginId = StpUtil.getLoginIdAsString();
-        return UserType.getUserType(loginId);
+        String loginType = StpUtil.getLoginIdAsString();
+        return UserType.getUserType(loginType);
     }
 
     /**

+ 1 - 2
ruoyi-common/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java

@@ -17,11 +17,10 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * bean拷贝工具(基于 cglib 性能优异)
+ * bean拷贝工具(基于 cglib 性能优异)
  * <p>
  * 重点 cglib 不支持 拷贝到链式对象
  * 例如: 源对象 拷贝到 目标(链式对象)
- * 请区分好`浅拷贝`和`深拷贝`再做使用
  *
  * @author Lion Li
  */

+ 9 - 6
ruoyi-common/src/main/java/com/ruoyi/common/utils/StreamUtils.java

@@ -30,6 +30,7 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
             return CollUtil.newArrayList();
         }
+        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
         return collection.stream().filter(function).collect(Collectors.toList());
     }
 
@@ -70,7 +71,8 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
             return CollUtil.newArrayList();
         }
-        return collection.stream().sorted(comparing).collect(Collectors.toList());
+        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+        return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
     }
 
     /**
@@ -87,7 +89,7 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
             return MapUtil.newHashMap();
         }
-        return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
     }
 
     /**
@@ -106,7 +108,7 @@ public class StreamUtils {
         if (CollUtil.isEmpty(collection)) {
             return MapUtil.newHashMap();
         }
-        return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l));
+        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
     }
 
     /**
@@ -124,7 +126,7 @@ public class StreamUtils {
             return MapUtil.newHashMap();
         }
         return collection
-            .stream()
+            .stream().filter(Objects::nonNull)
             .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
     }
 
@@ -145,7 +147,7 @@ public class StreamUtils {
             return MapUtil.newHashMap();
         }
         return collection
-            .stream()
+            .stream().filter(Objects::nonNull)
             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
     }
 
@@ -166,7 +168,7 @@ public class StreamUtils {
             return MapUtil.newHashMap();
         }
         return collection
-            .stream()
+            .stream().filter(Objects::nonNull)
             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
     }
 
@@ -188,6 +190,7 @@ public class StreamUtils {
             .stream()
             .map(function)
             .filter(Objects::nonNull)
+            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
             .collect(Collectors.toList());
     }
 

+ 62 - 8
ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java

@@ -11,10 +11,7 @@ import com.alibaba.excel.write.metadata.fill.FillConfig;
 import com.alibaba.excel.write.metadata.fill.FillWrapper;
 import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
 import com.ruoyi.common.convert.ExcelBigNumberConvert;
-import com.ruoyi.common.excel.CellMergeStrategy;
-import com.ruoyi.common.excel.DefaultExcelListener;
-import com.ruoyi.common.excel.ExcelListener;
-import com.ruoyi.common.excel.ExcelResult;
+import com.ruoyi.common.excel.*;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.file.FileUtils;
 import lombok.AccessLevel;
@@ -88,7 +85,26 @@ public class ExcelUtil {
         try {
             resetResponse(sheetName, response);
             ServletOutputStream os = response.getOutputStream();
-            exportExcel(list, sheetName, clazz, false, os);
+            exportExcel(list, sheetName, clazz, false, os, null);
+        } catch (IOException e) {
+            throw new RuntimeException("导出Excel异常");
+        }
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param response  响应体
+     * @param options   级联下拉选
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
+        try {
+            resetResponse(sheetName, response);
+            ServletOutputStream os = response.getOutputStream();
+            exportExcel(list, sheetName, clazz, false, os, options);
         } catch (IOException e) {
             throw new RuntimeException("导出Excel异常");
         }
@@ -107,7 +123,27 @@ public class ExcelUtil {
         try {
             resetResponse(sheetName, response);
             ServletOutputStream os = response.getOutputStream();
-            exportExcel(list, sheetName, clazz, merge, os);
+            exportExcel(list, sheetName, clazz, merge, os, null);
+        } catch (IOException e) {
+            throw new RuntimeException("导出Excel异常");
+        }
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param merge     是否合并单元格
+     * @param response  响应体
+     * @param options   级联下拉选
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
+        try {
+            resetResponse(sheetName, response);
+            ServletOutputStream os = response.getOutputStream();
+            exportExcel(list, sheetName, clazz, merge, os, options);
         } catch (IOException e) {
             throw new RuntimeException("导出Excel异常");
         }
@@ -122,7 +158,20 @@ public class ExcelUtil {
      * @param os        输出流
      */
     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
-        exportExcel(list, sheetName, clazz, false, os);
+        exportExcel(list, sheetName, clazz, false, os, null);
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param os        输出流
+     * @param options   级联下拉选内容
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
+        exportExcel(list, sheetName, clazz, false, os, options);
     }
 
     /**
@@ -134,7 +183,8 @@ public class ExcelUtil {
      * @param merge     是否合并单元格
      * @param os        输出流
      */
-    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, OutputStream os) {
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
+                                       OutputStream os, List<DropDownOptions> options) {
         ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
             .autoCloseStream(false)
             // 自动适配
@@ -146,6 +196,10 @@ public class ExcelUtil {
             // 合并处理器
             builder.registerWriteHandler(new CellMergeStrategy(list, true));
         }
+        if (CollUtil.isNotEmpty(options)) {
+            // 添加下拉框操作
+            builder.registerWriteHandler(new ExcelDownHandler(options));
+        }
         builder.doWrite(list);
     }
 

+ 27 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java

@@ -130,6 +130,18 @@ public class RedisUtils {
     }
 
     /**
+     * 如果不存在则设置 并返回 true 如果存在则返回 false
+     *
+     * @param key   缓存的键值
+     * @param value 缓存的值
+     * @return set成功或失败
+     */
+    public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
+        RBucket<T> bucket = CLIENT.getBucket(key);
+        return bucket.setIfAbsent(value, duration);
+    }
+
+    /**
      * 注册对象监听器
      * <p>
      * key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置
@@ -375,6 +387,21 @@ public class RedisUtils {
     }
 
     /**
+     * 删除Hash中的数据
+     *
+     * @param key   Redis键
+     * @param hKeys Hash键
+     */
+    public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
+        RBatch batch = CLIENT.createBatch();
+        RMapAsync<String, T> rMap = batch.getMap(key);
+        for (String hKey : hKeys) {
+            rMap.removeAsync(hKey);
+        }
+        batch.execute();
+    }
+
+    /**
      * 获取多个Hash中的数据
      *
      * @param key   Redis键

+ 1 - 12
ruoyi-demo/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -28,17 +28,6 @@
             <artifactId>ruoyi-sms</artifactId>
         </dependency>
 
-        <!-- 短信 用哪个导入哪个依赖 -->
-<!--        <dependency>-->
-<!--            <groupId>com.aliyun</groupId>-->
-<!--            <artifactId>dysmsapi20170525</artifactId>-->
-<!--        </dependency>-->
-
-<!--        <dependency>-->
-<!--            <groupId>com.tencentcloudapi</groupId>-->
-<!--            <artifactId>tencentcloud-sdk-java-sms</artifactId>-->
-<!--        </dependency>-->
-
     </dependencies>
 
 </project>

+ 13 - 29
ruoyi-demo/src/main/java/com/ruoyi/demo/controller/SmsController.java

@@ -1,17 +1,17 @@
 package com.ruoyi.demo.controller;
 
 import com.ruoyi.common.core.domain.R;
-import com.ruoyi.common.utils.spring.SpringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.SmsTemplate;
 import lombok.RequiredArgsConstructor;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.sms4j.provider.enumerate.SupplierType;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.LinkedHashMap;
 
 /**
  * 短信演示案例
@@ -26,10 +26,6 @@ import java.util.Map;
 @RequestMapping("/demo/sms")
 public class SmsController {
 
-    private final SmsProperties smsProperties;
-//    private final SmsTemplate smsTemplate; // 可以使用spring注入
-//    private final AliyunSmsTemplate smsTemplate; // 也可以注入某个厂家的模板工具
-
     /**
      * 发送短信Aliyun
      *
@@ -38,17 +34,11 @@ public class SmsController {
      */
     @GetMapping("/sendAliyun")
     public R<Object> sendAliyun(String phones, String templateId) {
-        if (!smsProperties.getEnabled()) {
-            return R.fail("当前系统没有开启短信功能!");
-        }
-        if (!SpringUtils.containsBean("aliyunSmsTemplate")) {
-            return R.fail("阿里云依赖未引入!");
-        }
-        SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
-        Map<String, String> map = new HashMap<>(1);
+        LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
         map.put("code", "1234");
-        Object send = smsTemplate.send(phones, templateId, map);
-        return R.ok(send);
+        SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
+        SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
+        return R.ok(smsResponse);
     }
 
     /**
@@ -59,18 +49,12 @@ public class SmsController {
      */
     @GetMapping("/sendTencent")
     public R<Object> sendTencent(String phones, String templateId) {
-        if (!smsProperties.getEnabled()) {
-            return R.fail("当前系统没有开启短信功能!");
-        }
-        if (!SpringUtils.containsBean("tencentSmsTemplate")) {
-            return R.fail("腾讯云依赖未引入!");
-        }
-        SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
-        Map<String, String> map = new HashMap<>(1);
+        LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
 //        map.put("2", "测试测试");
         map.put("1", "1234");
-        Object send = smsTemplate.send(phones, templateId, map);
-        return R.ok(send);
+        SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.TENCENT);
+        SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
+        return R.ok(smsResponse);
     }
 
 }

+ 31 - 3
ruoyi-demo/src/main/java/com/ruoyi/demo/controller/TestExcelController.java

@@ -1,12 +1,17 @@
 package com.ruoyi.demo.controller;
 
 import cn.hutool.core.collection.CollUtil;
+import com.ruoyi.common.excel.ExcelResult;
 import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+import com.ruoyi.demo.listener.ExportDemoListener;
+import com.ruoyi.demo.service.IExportExcelService;
 import lombok.AllArgsConstructor;
 import lombok.Data;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletResponse;
 import java.util.ArrayList;
@@ -20,9 +25,12 @@ import java.util.Map;
  * @author Lion Li
  */
 @RestController
+@RequiredArgsConstructor
 @RequestMapping("/demo/excel")
 public class TestExcelController {
 
+    private final IExportExcelService exportExcelService;
+
     /**
      * 单列表多数据
      */
@@ -76,6 +84,26 @@ public class TestExcelController {
         ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
     }
 
+    /**
+     * 导出下拉框
+     *
+     * @param response /
+     */
+    @GetMapping("/exportWithOptions")
+    public void exportWithOptions(HttpServletResponse response) {
+        exportExcelService.exportWithOptions(response);
+    }
+
+    /**
+     * 导入表格
+     */
+    @PostMapping(value = "/importWithOptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public List<ExportDemoVo> importWithOptions(@RequestPart("file") MultipartFile file) throws Exception {
+        // 处理解析结果
+        ExcelResult<ExportDemoVo> excelResult = ExcelUtil.importExcel(file.getInputStream(), ExportDemoVo.class, new ExportDemoListener());
+        return excelResult.getList();
+    }
+
     @Data
     @AllArgsConstructor
     static class TestObj1 {

+ 119 - 0
ruoyi-demo/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java

@@ -0,0 +1,119 @@
+package com.ruoyi.demo.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.annotation.ExcelEnumFormat;
+import com.ruoyi.common.convert.ExcelDictConvert;
+import com.ruoyi.common.convert.ExcelEnumConvert;
+import com.ruoyi.common.core.validate.AddGroup;
+import com.ruoyi.common.core.validate.EditGroup;
+import com.ruoyi.common.enums.UserStatus;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 带有下拉选的Excel导出
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExportDemoVo {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户昵称
+     */
+    @ExcelProperty(value = "用户名", index = 0)
+    @NotEmpty(message = "用户名不能为空", groups = AddGroup.class)
+    private String nickName;
+
+    /**
+     * 用户类型
+     * </p>
+     * 使用ExcelEnumFormat注解需要进行下拉选的部分
+     */
+    @ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
+    @ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
+    @NotEmpty(message = "用户类型不能为空", groups = AddGroup.class)
+    private String userStatus;
+
+    /**
+     * 性别
+     * <p>
+     * 使用ExcelDictFormat注解需要进行下拉选的部分
+     */
+    @ExcelProperty(value = "性别", index = 2, converter = ExcelDictConvert.class)
+    @ExcelDictFormat(dictType = "sys_user_sex")
+    @NotEmpty(message = "性别不能为空", groups = AddGroup.class)
+    private String gender;
+
+    /**
+     * 手机号
+     */
+    @ExcelProperty(value = "手机号", index = 3)
+    @NotEmpty(message = "手机号不能为空", groups = AddGroup.class)
+    private String phoneNumber;
+
+    /**
+     * Email
+     */
+    @ExcelProperty(value = "Email", index = 4)
+    @NotEmpty(message = "Email不能为空", groups = AddGroup.class)
+    private String email;
+
+    /**
+     * 省
+     * <p>
+     * 级联下拉,仅判断是否选了
+     */
+    @ExcelProperty(value = "省", index = 5)
+    @NotNull(message = "省不能为空", groups = AddGroup.class)
+    private String province;
+
+    /**
+     * 数据库中的省ID
+     * </p>
+     * 处理完毕后再判断是否市正确的值
+     */
+    @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+    private Integer provinceId;
+
+    /**
+     * 市
+     * <p>
+     * 级联下拉
+     */
+    @ExcelProperty(value = "市", index = 6)
+    @NotNull(message = "市不能为空", groups = AddGroup.class)
+    private String city;
+
+    /**
+     * 数据库中的市ID
+     */
+    @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+    private Integer cityId;
+
+    /**
+     * 县
+     * <p>
+     * 级联下拉
+     */
+    @ExcelProperty(value = "县", index = 7)
+    @NotNull(message = "县不能为空", groups = AddGroup.class)
+    private String area;
+
+    /**
+     * 数据库中的县ID
+     */
+    @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+    private Integer areaId;
+}

+ 68 - 0
ruoyi-demo/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java

@@ -0,0 +1,68 @@
+package com.ruoyi.demo.listener;
+
+import cn.hutool.core.util.NumberUtil;
+import com.alibaba.excel.context.AnalysisContext;
+import com.ruoyi.common.core.validate.AddGroup;
+import com.ruoyi.common.core.validate.EditGroup;
+import com.ruoyi.common.excel.DefaultExcelListener;
+import com.ruoyi.common.excel.DropDownOptions;
+import com.ruoyi.common.utils.ValidatorUtils;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+
+import java.util.List;
+
+/**
+ * Excel带下拉框的解析处理器
+ *
+ * @author Emil.Zhang
+ */
+public class ExportDemoListener extends DefaultExcelListener<ExportDemoVo> {
+
+    public ExportDemoListener() {
+        // 显示使用构造函数,否则将导致空指针
+        super(true);
+    }
+
+    @Override
+    public void invoke(ExportDemoVo data, AnalysisContext context) {
+        // 先校验必填
+        ValidatorUtils.validate(data, AddGroup.class);
+
+        // 处理级联下拉的部分
+        String province = data.getProvince();
+        String city = data.getCity();
+        String area = data.getArea();
+        // 本行用户选择的省
+        List<String> thisRowSelectedProvinceOption = DropDownOptions.analyzeOptionValue(province);
+        if (thisRowSelectedProvinceOption.size() == 2) {
+            String provinceIdStr = thisRowSelectedProvinceOption.get(1);
+            if (NumberUtil.isNumber(provinceIdStr)) {
+                // 严格要求数据的话可以在这里做与数据库相关的判断
+                // 例如判断省信息是否在数据库中存在等,建议结合RedisCache做缓存10s,减少数据库调用
+                data.setProvinceId(Integer.parseInt(provinceIdStr));
+            }
+        }
+        // 本行用户选择的市
+        List<String> thisRowSelectedCityOption = DropDownOptions.analyzeOptionValue(city);
+        if (thisRowSelectedCityOption.size() == 2) {
+            String cityIdStr = thisRowSelectedCityOption.get(1);
+            if (NumberUtil.isNumber(cityIdStr)) {
+                data.setCityId(Integer.parseInt(cityIdStr));
+            }
+        }
+        // 本行用户选择的县
+        List<String> thisRowSelectedAreaOption = DropDownOptions.analyzeOptionValue(area);
+        if (thisRowSelectedAreaOption.size() == 2) {
+            String areaIdStr = thisRowSelectedAreaOption.get(1);
+            if (NumberUtil.isNumber(areaIdStr)) {
+                data.setAreaId(Integer.parseInt(areaIdStr));
+            }
+        }
+
+        // 处理完毕以后判断是否符合规则
+        ValidatorUtils.validate(data, EditGroup.class);
+
+        // 添加到处理结果中
+        getExcelResult().getList().add(data);
+    }
+}

+ 18 - 0
ruoyi-demo/src/main/java/com/ruoyi/demo/service/IExportExcelService.java

@@ -0,0 +1,18 @@
+package com.ruoyi.demo.service;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+public interface IExportExcelService {
+
+    /**
+     * 导出下拉框
+     *
+     * @param response /
+     */
+    void exportWithOptions(HttpServletResponse response);
+}

+ 223 - 0
ruoyi-demo/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java

@@ -0,0 +1,223 @@
+package com.ruoyi.demo.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.enums.UserStatus;
+import com.ruoyi.common.excel.DropDownOptions;
+import com.ruoyi.common.utils.StreamUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+import com.ruoyi.demo.service.IExportExcelService;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+@Service
+@RequiredArgsConstructor
+public class ExportExcelServiceImpl implements IExportExcelService {
+
+    @Override
+    public void exportWithOptions(HttpServletResponse response) {
+        // 创建表格数据,业务中一般通过数据库查询
+        List<ExportDemoVo> excelDataList = new ArrayList<>();
+        for (int i = 0; i < 3; i++) {
+            // 模拟数据库中的一条数据
+            ExportDemoVo everyRowData = new ExportDemoVo();
+            everyRowData.setNickName("用户-" + i);
+            everyRowData.setUserStatus(UserStatus.OK.getCode());
+            everyRowData.setGender("1");
+            everyRowData.setPhoneNumber(String.format("175%08d", i));
+            everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
+            everyRowData.setProvinceId(i);
+            everyRowData.setCityId(i);
+            everyRowData.setAreaId(i);
+            excelDataList.add(everyRowData);
+        }
+
+        // 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
+        // 并通过@DropDown注解指定下拉值,或者通过创建ExcelOptions来指定下拉框
+        // 使用ExcelOptions时建议指定列index,防止出现下拉列解析不对齐
+
+        // 首先从数据库中查询下拉框内的可选项
+        // 这里模拟查询结果
+        List<DemoCityData> provinceList = getProvinceList(),
+            cityList = getCityList(provinceList),
+            areaList = getAreaList(cityList);
+        int provinceIndex = 5, cityIndex = 6, areaIndex = 7;
+
+        DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
+            provinceList,
+            provinceIndex,
+            cityList,
+            cityIndex,
+            DemoCityData::getId,
+            DemoCityData::getPid,
+            everyOptions -> DropDownOptions.createOptionValue(
+                everyOptions.getName(),
+                everyOptions.getId()
+            )
+        );
+
+        DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
+            cityList,
+            cityIndex,
+            areaList,
+            areaIndex,
+            DemoCityData::getId,
+            DemoCityData::getPid,
+            everyOptions -> DropDownOptions.createOptionValue(
+                everyOptions.getName(),
+                everyOptions.getId()
+            )
+        );
+
+        // 把所有的下拉框存储
+        List<DropDownOptions> options = new ArrayList<>();
+        options.add(provinceToCity);
+        options.add(cityToArea);
+
+        // 到此为止所有的下拉框可选项已全部配置完毕
+
+        // 接下来需要将Excel中的展示数据转换为对应的下拉选
+        List<ExportDemoVo> outList = StreamUtils.toList(excelDataList, everyRowData -> {
+            // 只需要处理没有使用@ExcelDictFormat注解的下拉框
+            // 一般来说,可以直接在数据库查询即查询出省市县信息,这里通过模拟操作赋值
+            everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
+            everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
+            everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
+            return everyRowData;
+        });
+
+        ExcelUtil.exportExcel(outList, "下拉框示例", ExportDemoVo.class, response, options);
+    }
+
+    private String buildOptions(List<DemoCityData> cityDataList, Integer id) {
+        Map<Integer, List<DemoCityData>> groupByIdMap =
+            cityDataList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+        if (groupByIdMap.containsKey(id)) {
+            DemoCityData demoCityData = groupByIdMap.get(id).get(0);
+            return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
+        } else {
+            return StrUtil.EMPTY;
+        }
+    }
+
+    /**
+     * 模拟查询数据库操作
+     *
+     * @return /
+     */
+    private List<DemoCityData> getProvinceList() {
+        List<DemoCityData> provinceList = new ArrayList<>();
+
+        // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+        provinceList.add(new DemoCityData(0, null, "安徽省"));
+        provinceList.add(new DemoCityData(1, null, "江苏省"));
+
+        return provinceList;
+    }
+
+    /**
+     * 模拟查找数据库操作,需要连带查询出省的数据
+     *
+     * @param provinceList 模拟的父省数据
+     * @return /
+     */
+    private List<DemoCityData> getCityList(List<DemoCityData> provinceList) {
+        List<DemoCityData> cityList = new ArrayList<>();
+
+        // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+        cityList.add(new DemoCityData(0, 0, "合肥市"));
+        cityList.add(new DemoCityData(1, 0, "芜湖市"));
+        cityList.add(new DemoCityData(2, 1, "南京市"));
+        cityList.add(new DemoCityData(3, 1, "无锡市"));
+        cityList.add(new DemoCityData(4, 1, "徐州市"));
+
+        selectParentData(provinceList, cityList);
+
+        return cityList;
+    }
+
+    /**
+     * 模拟查找数据库操作,需要连带查询出市的数据
+     *
+     * @param cityList 模拟的父市数据
+     * @return /
+     */
+    private List<DemoCityData> getAreaList(List<DemoCityData> cityList) {
+        List<DemoCityData> areaList = new ArrayList<>();
+
+        // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+        areaList.add(new DemoCityData(0, 0, "瑶海区"));
+        areaList.add(new DemoCityData(1, 0, "庐江区"));
+        areaList.add(new DemoCityData(2, 1, "南宁县"));
+        areaList.add(new DemoCityData(3, 1, "镜湖区"));
+        areaList.add(new DemoCityData(4, 2, "玄武区"));
+        areaList.add(new DemoCityData(5, 2, "秦淮区"));
+        areaList.add(new DemoCityData(6, 3, "宜兴市"));
+        areaList.add(new DemoCityData(7, 3, "新吴区"));
+        areaList.add(new DemoCityData(8, 4, "鼓楼区"));
+        areaList.add(new DemoCityData(9, 4, "丰县"));
+
+        selectParentData(cityList, areaList);
+
+        return areaList;
+    }
+
+    /**
+     * 模拟数据库的查询父数据操作
+     *
+     * @param parentList /
+     * @param sonList    /
+     */
+    private void selectParentData(List<DemoCityData> parentList, List<DemoCityData> sonList) {
+        Map<Integer, List<DemoCityData>> parentGroupByIdMap =
+            parentList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+
+        sonList.forEach(everySon -> {
+            if (parentGroupByIdMap.containsKey(everySon.getPid())) {
+                everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).get(0));
+            }
+        });
+    }
+
+    /**
+     * 模拟的数据库省市县
+     */
+    @Data
+    private static class DemoCityData {
+        /**
+         * 数据库id字段
+         */
+        private Integer id;
+        /**
+         * 数据库pid字段
+         */
+        private Integer pid;
+        /**
+         * 数据库name字段
+         */
+        private String name;
+        /**
+         * MyBatisPlus连带查询父数据
+         */
+        private DemoCityData pData;
+
+        public DemoCityData(Integer id, Integer pid, String name) {
+            this.id = id;
+            this.pid = pid;
+            this.name = name;
+        }
+    }
+}

+ 1 - 1
ruoyi-extend/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <artifactId>ruoyi-extend</artifactId>

+ 1 - 1
ruoyi-extend/ruoyi-monitor-admin/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-extend</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <packaging>jar</packaging>

+ 1 - 1
ruoyi-extend/ruoyi-xxl-job-admin/pom.xml

@@ -4,7 +4,7 @@
     <parent>
         <artifactId>ruoyi-extend</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <artifactId>ruoyi-xxl-job-admin</artifactId>
     <packaging>jar</packaging>

+ 1 - 1
ruoyi-framework/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 18 - 20
ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java

@@ -2,6 +2,7 @@ package com.ruoyi.framework.aspectj;
 
 import cn.hutool.core.lang.Dict;
 import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.domain.event.OperLogEvent;
@@ -25,6 +26,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.util.Collection;
 import java.util.Map;
+import java.util.StringJoiner;
 
 /**
  * 操作日志记录处理
@@ -144,26 +146,23 @@ public class LogAspect {
      * 参数拼装
      */
     private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
-        StringBuilder params = new StringBuilder();
-        if (paramsArray != null && paramsArray.length > 0) {
-            for (Object o : paramsArray) {
-                if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
-                    try {
-                        String str = JsonUtils.toJsonString(o);
-                        Dict dict = JsonUtils.parseMap(str);
-                        if (MapUtil.isNotEmpty(dict)) {
-                            MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
-                            MapUtil.removeAny(dict, excludeParamNames);
-                            str = JsonUtils.toJsonString(dict);
-                        }
-                        params.append(str).append(" ");
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                    }
+        StringJoiner params = new StringJoiner(" ");
+        if (ArrayUtil.isEmpty(paramsArray)) {
+            return params.toString();
+        }
+        for (Object o : paramsArray) {
+            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+                String str = JsonUtils.toJsonString(o);
+                Dict dict = JsonUtils.parseMap(str);
+                if (MapUtil.isNotEmpty(dict)) {
+                    MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
+                    MapUtil.removeAny(dict, excludeParamNames);
+                    str = JsonUtils.toJsonString(dict);
                 }
+                params.add(str);
             }
         }
-        return params.toString().trim();
+        return params.toString();
     }
 
     /**
@@ -184,9 +183,8 @@ public class LogAspect {
             }
         } else if (Map.class.isAssignableFrom(clazz)) {
             Map map = (Map) o;
-            for (Object value : map.entrySet()) {
-                Map.Entry entry = (Map.Entry) value;
-                return entry.getValue() instanceof MultipartFile;
+            for (Object value : map.values()) {
+                return value instanceof MultipartFile;
             }
         }
         return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse

+ 15 - 25
ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java

@@ -1,6 +1,7 @@
 package com.ruoyi.framework.aspectj;
 
 import cn.dev33.satoken.SaManager;
+import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.crypto.SecureUtil;
 import com.ruoyi.common.annotation.RepeatSubmit;
@@ -12,8 +13,6 @@ import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.common.utils.ServletUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.redis.RedisUtils;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.AfterThrowing;
@@ -28,14 +27,13 @@ import javax.servlet.http.HttpServletResponse;
 import java.time.Duration;
 import java.util.Collection;
 import java.util.Map;
+import java.util.StringJoiner;
 
 /**
  * 防止重复提交(参考美团GTIS防重系统)
  *
  * @author Lion Li
  */
-@Slf4j
-@RequiredArgsConstructor
 @Aspect
 @Component
 public class RepeatSubmitAspect {
@@ -45,10 +43,8 @@ public class RepeatSubmitAspect {
     @Before("@annotation(repeatSubmit)")
     public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
         // 如果注解不为0 则使用注解数值
-        long interval = 0;
-        if (repeatSubmit.interval() > 0) {
-            interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
-        }
+        long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
+
         if (interval < 1000) {
             throw new ServiceException("重复提交间隔时间不能小于'1'秒");
         }
@@ -64,9 +60,7 @@ public class RepeatSubmitAspect {
         submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
         // 唯一标识(指定key + url + 消息头)
         String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
-        String key = RedisUtils.getCacheObject(cacheRepeatKey);
-        if (key == null) {
-            RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));
+        if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
             KEY_CACHE.set(cacheRepeatKey);
         } else {
             String message = repeatSubmit.message();
@@ -114,19 +108,16 @@ public class RepeatSubmitAspect {
      * 参数拼装
      */
     private String argsArrayToString(Object[] paramsArray) {
-        StringBuilder params = new StringBuilder();
-        if (paramsArray != null && paramsArray.length > 0) {
-            for (Object o : paramsArray) {
-                if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
-                    try {
-                        params.append(JsonUtils.toJsonString(o)).append(" ");
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                    }
-                }
+        StringJoiner params = new StringJoiner(" ");
+        if (ArrayUtil.isEmpty(paramsArray)) {
+            return params.toString();
+        }
+        for (Object o : paramsArray) {
+            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+                params.add(JsonUtils.toJsonString(o));
             }
         }
-        return params.toString().trim();
+        return params.toString();
     }
 
     /**
@@ -147,9 +138,8 @@ public class RepeatSubmitAspect {
             }
         } else if (Map.class.isAssignableFrom(clazz)) {
             Map map = (Map) o;
-            for (Object value : map.entrySet()) {
-                Map.Entry entry = (Map.Entry) value;
-                return entry.getValue() instanceof MultipartFile;
+            for (Object value : map.values()) {
+                return value instanceof MultipartFile;
             }
         }
         return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse

+ 4 - 3
ruoyi-framework/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java

@@ -118,6 +118,10 @@ public class PlusSpringCacheManager implements CacheManager {
 
     @Override
     public Cache getCache(String name) {
+        // 重写 cacheName 支持多参数
+        String[] array = StringUtils.delimitedListToStringArray(name, "#");
+        name = array[0];
+
         Cache cache = instanceMap.get(name);
         if (cache != null) {
             return cache;
@@ -132,9 +136,6 @@ public class PlusSpringCacheManager implements CacheManager {
             configMap.put(name, config);
         }
 
-        // 重写 cacheName 支持多参数
-        String[] array = StringUtils.delimitedListToStringArray(name, "#");
-        name = array[0];
         if (array.length > 1) {
             config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
         }

+ 22 - 0
ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java

@@ -16,8 +16,10 @@ import org.springframework.dao.DuplicateKeyException;
 import org.springframework.validation.BindException;
 import org.springframework.web.HttpRequestMethodNotSupportedException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.ConstraintViolation;
@@ -109,6 +111,26 @@ public class GlobalExceptionHandler {
     }
 
     /**
+     * 请求路径中缺少必需的路径变量
+     */
+    @ExceptionHandler(MissingPathVariableException.class)
+    public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
+        return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
+    }
+
+    /**
+     * 请求参数类型不匹配
+     */
+    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+    public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
+        return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
+    }
+
+    /**
      * 拦截未知的运行时异常
      */
     @ExceptionHandler(RuntimeException.class)

+ 1 - 1
ruoyi-generator/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
ruoyi-job/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <packaging>jar</packaging>

+ 1 - 1
ruoyi-oss/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 17 - 0
ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java

@@ -24,6 +24,7 @@ import com.ruoyi.oss.exception.OssException;
 import com.ruoyi.oss.properties.OssProperties;
 
 import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Date;
@@ -115,6 +116,18 @@ public class OssClient {
         return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
     }
 
+    public UploadResult upload(File file, String path) {
+        try {
+            PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
+            // 设置上传对象的 Acl 为公共读
+            putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
+            client.putObject(putObjectRequest);
+        } catch (Exception e) {
+            throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
+        }
+        return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
+    }
+
     public void delete(String path) {
         path = path.replace(getUrl() + "/", "");
         try {
@@ -132,6 +145,10 @@ public class OssClient {
         return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
     }
 
+    public UploadResult uploadSuffix(File file, String suffix) {
+        return upload(file, getPath(properties.getPrefix(), suffix));
+    }
+
     /**
      * 获取文件元数据
      *

+ 10 - 10
ruoyi-sms/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -24,15 +24,15 @@
         </dependency>
 
         <dependency>
-            <groupId>com.aliyun</groupId>
-            <artifactId>dysmsapi20170525</artifactId>
-            <optional>true</optional>
-        </dependency>
-
-        <dependency>
-            <groupId>com.tencentcloudapi</groupId>
-            <artifactId>tencentcloud-sdk-java-sms</artifactId>
-            <optional>true</optional>
+            <groupId>org.dromara.sms4j</groupId>
+            <artifactId>sms4j-spring-boot-starter</artifactId>
+            <exclusions>
+                <!-- 排除京东短信内存在的fastjson等待作者后续修复 -->
+                <exclusion>
+                    <groupId>com.alibaba</groupId>
+                    <artifactId>fastjson</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
     </dependencies>

+ 1 - 34
ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java

@@ -1,45 +1,12 @@
 package com.ruoyi.sms.config;
 
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.AliyunSmsTemplate;
-import com.ruoyi.sms.core.SmsTemplate;
-import com.ruoyi.sms.core.TencentSmsTemplate;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
 /**
  * 短信配置类
  *
  * @author Lion Li
  * @version 4.2.0
  */
-@Configuration
+//@Configuration // 暂时用不上 留着后续扩展使用
 public class SmsConfig {
 
-    @Configuration
-    @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
-    @ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
-    static class AliyunSmsConfig {
-
-        @Bean
-        public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
-            return new AliyunSmsTemplate(smsProperties);
-        }
-
-    }
-
-    @Configuration
-    @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
-    @ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
-    static class TencentSmsConfig {
-
-        @Bean
-        public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
-            return new TencentSmsTemplate(smsProperties);
-        }
-
-    }
-
 }

+ 20 - 47
ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java

@@ -1,47 +1,20 @@
-package com.ruoyi.sms.config.properties;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-/**
- * SMS短信 配置属性
- *
- * @author Lion Li
- * @version 4.2.0
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "sms")
-public class SmsProperties {
-
-    private Boolean enabled;
-
-    /**
-     * 配置节点
-     * 阿里云 dysmsapi.aliyuncs.com
-     * 腾讯云 sms.tencentcloudapi.com
-     */
-    private String endpoint;
-
-    /**
-     * key
-     */
-    private String accessKeyId;
-
-    /**
-     * 密匙
-     */
-    private String accessKeySecret;
-
-    /*
-     * 短信签名
-     */
-    private String signName;
-
-    /**
-     * 短信应用ID (腾讯专属)
-     */
-    private String sdkAppId;
-
-}
+//package com.ruoyi.sms.config.properties;
+//
+//import lombok.Data;
+//import org.springframework.boot.context.properties.ConfigurationProperties;
+//import org.springframework.stereotype.Component;
+//
+///**
+// * SMS短信 配置属性
+// *
+// * @author Lion Li
+// * @version 4.2.0
+// */
+//@Data
+//@Component
+//@ConfigurationProperties(prefix = "sms")
+//public class SmsProperties {
+//
+//    private Boolean enabled;
+//
+//}

+ 0 - 66
ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java

@@ -1,66 +0,0 @@
-package com.ruoyi.sms.core;
-
-import com.aliyun.dysmsapi20170525.Client;
-import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
-import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
-import com.aliyun.teaopenapi.models.Config;
-import com.ruoyi.common.utils.JsonUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.entity.SmsResult;
-import com.ruoyi.sms.exception.SmsException;
-import lombok.SneakyThrows;
-
-import java.util.Map;
-
-/**
- * Aliyun 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public class AliyunSmsTemplate implements SmsTemplate {
-
-    private SmsProperties properties;
-
-    private Client client;
-
-    @SneakyThrows(Exception.class)
-    public AliyunSmsTemplate(SmsProperties smsProperties) {
-        this.properties = smsProperties;
-        Config config = new Config()
-            // 您的AccessKey ID
-            .setAccessKeyId(smsProperties.getAccessKeyId())
-            // 您的AccessKey Secret
-            .setAccessKeySecret(smsProperties.getAccessKeySecret())
-            // 访问的域名
-            .setEndpoint(smsProperties.getEndpoint());
-        this.client = new Client(config);
-    }
-
-    @Override
-    public SmsResult send(String phones, String templateId, Map<String, String> param) {
-        if (StringUtils.isBlank(phones)) {
-            throw new SmsException("手机号不能为空");
-        }
-        if (StringUtils.isBlank(templateId)) {
-            throw new SmsException("模板ID不能为空");
-        }
-        SendSmsRequest req = new SendSmsRequest()
-            .setPhoneNumbers(phones)
-            .setSignName(properties.getSignName())
-            .setTemplateCode(templateId)
-            .setTemplateParam(JsonUtils.toJsonString(param));
-        try {
-            SendSmsResponse resp = client.sendSms(req);
-            return SmsResult.builder()
-                .isSuccess("OK".equals(resp.getBody().getCode()))
-                .message(resp.getBody().getMessage())
-                .response(JsonUtils.toJsonString(resp))
-                .build();
-        } catch (Exception e) {
-            throw new SmsException(e.getMessage());
-        }
-    }
-
-}

+ 0 - 26
ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java

@@ -1,26 +0,0 @@
-package com.ruoyi.sms.core;
-
-import com.ruoyi.sms.entity.SmsResult;
-
-import java.util.Map;
-
-/**
- * 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public interface SmsTemplate {
-
-    /**
-     * 发送短信
-     *
-     * @param phones     电话号(多个逗号分割)
-     * @param templateId 模板id
-     * @param param      模板对应参数
-     *                   阿里 需使用 模板变量名称对应内容 例如: code=1234
-     *                   腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
-     */
-    SmsResult send(String phones, String templateId, Map<String, String> param);
-
-}

+ 0 - 82
ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java

@@ -1,82 +0,0 @@
-package com.ruoyi.sms.core;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ArrayUtil;
-import com.ruoyi.common.utils.JsonUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.entity.SmsResult;
-import com.ruoyi.sms.exception.SmsException;
-import com.tencentcloudapi.common.Credential;
-import com.tencentcloudapi.common.profile.ClientProfile;
-import com.tencentcloudapi.common.profile.HttpProfile;
-import com.tencentcloudapi.sms.v20190711.SmsClient;
-import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
-import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
-import com.tencentcloudapi.sms.v20190711.models.SendStatus;
-import lombok.SneakyThrows;
-
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Tencent 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public class TencentSmsTemplate implements SmsTemplate {
-
-    private SmsProperties properties;
-
-    private SmsClient client;
-
-    @SneakyThrows(Exception.class)
-    public TencentSmsTemplate(SmsProperties smsProperties) {
-        this.properties = smsProperties;
-        Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
-        HttpProfile httpProfile = new HttpProfile();
-        httpProfile.setEndpoint(smsProperties.getEndpoint());
-        ClientProfile clientProfile = new ClientProfile();
-        clientProfile.setHttpProfile(httpProfile);
-        this.client = new SmsClient(credential, "", clientProfile);
-    }
-
-    @Override
-    public SmsResult send(String phones, String templateId, Map<String, String> param) {
-        if (StringUtils.isBlank(phones)) {
-            throw new SmsException("手机号不能为空");
-        }
-        if (StringUtils.isBlank(templateId)) {
-            throw new SmsException("模板ID不能为空");
-        }
-        SendSmsRequest req = new SendSmsRequest();
-        Set<String> set = Arrays.stream(phones.split(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
-        req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
-        if (CollUtil.isNotEmpty(param)) {
-            req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
-        }
-        req.setTemplateID(templateId);
-        req.setSign(properties.getSignName());
-        req.setSmsSdkAppid(properties.getSdkAppId());
-        try {
-            SendSmsResponse resp = client.SendSms(req);
-            SmsResult.SmsResultBuilder builder = SmsResult.builder()
-                .isSuccess(true)
-                .message("send success")
-                .response(JsonUtils.toJsonString(resp));
-            for (SendStatus sendStatus : resp.getSendStatusSet()) {
-                if (!"Ok".equals(sendStatus.getCode())) {
-                    builder.isSuccess(false).message(sendStatus.getMessage());
-                    break;
-                }
-            }
-            return builder.build();
-        } catch (Exception e) {
-            throw new SmsException(e.getMessage());
-        }
-    }
-
-}

+ 0 - 31
ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java

@@ -1,31 +0,0 @@
-package com.ruoyi.sms.entity;
-
-import lombok.Builder;
-import lombok.Data;
-
-/**
- * 上传返回体
- *
- * @author Lion Li
- */
-@Data
-@Builder
-public class SmsResult {
-
-    /**
-     * 是否成功
-     */
-    private boolean isSuccess;
-
-    /**
-     * 响应消息
-     */
-    private String message;
-
-    /**
-     * 实际响应体
-     * <p>
-     * 可自行转换为 SDK 对应的 SendSmsResponse
-     */
-    private String response;
-}

+ 0 - 16
ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java

@@ -1,16 +0,0 @@
-package com.ruoyi.sms.exception;
-
-/**
- * Sms异常类
- *
- * @author Lion Li
- */
-public class SmsException extends RuntimeException {
-
-    private static final long serialVersionUID = 1L;
-
-    public SmsException(String msg) {
-        super(msg);
-    }
-
-}

+ 1 - 1
ruoyi-system/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.7.0</version>
+        <version>4.8.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 3 - 1
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java

@@ -2,12 +2,12 @@ package com.ruoyi.system.service;
 
 import com.ruoyi.common.core.domain.PageQuery;
 import com.ruoyi.common.core.page.TableDataInfo;
-import com.ruoyi.system.domain.SysOss;
 import com.ruoyi.system.domain.bo.SysOssBo;
 import com.ruoyi.system.domain.vo.SysOssVo;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletResponse;
+import java.io.File;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
@@ -27,6 +27,8 @@ public interface ISysOssService {
 
     SysOssVo upload(MultipartFile file);
 
+    SysOssVo upload(File file);
+
     void download(Long ossId, HttpServletResponse response) throws IOException;
 
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);

+ 17 - 15
ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java

@@ -8,9 +8,9 @@ import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.constant.Constants;
-import com.ruoyi.common.core.domain.event.LogininforEvent;
 import com.ruoyi.common.core.domain.dto.RoleDTO;
 import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.event.LogininforEvent;
 import com.ruoyi.common.core.domain.model.LoginUser;
 import com.ruoyi.common.core.domain.model.XcxLoginUser;
 import com.ruoyi.common.enums.DeviceType;
@@ -71,9 +71,10 @@ public class SysLoginService {
         if (captchaEnabled) {
             validateCaptcha(username, code, uuid);
         }
+        // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
         SysUser user = loadUserByUsername(username);
         checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
-        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
         LoginUser loginUser = buildLoginUser(user);
         // 生成token
         LoginHelper.loginByDevice(loginUser, DeviceType.PC);
@@ -88,7 +89,7 @@ public class SysLoginService {
         SysUser user = loadUserByPhonenumber(phonenumber);
 
         checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
-        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
         LoginUser loginUser = buildLoginUser(user);
         // 生成token
         LoginHelper.loginByDevice(loginUser, DeviceType.APP);
@@ -99,11 +100,11 @@ public class SysLoginService {
     }
 
     public String emailLogin(String email, String emailCode) {
-        // 通过手机号查找用户
+        // 通过手邮箱查找用户
         SysUser user = loadUserByEmail(email);
 
         checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
-        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
         LoginUser loginUser = buildLoginUser(user);
         // 生成token
         LoginHelper.loginByDevice(loginUser, DeviceType.APP);
@@ -118,9 +119,11 @@ public class SysLoginService {
         // todo 以下自行实现
         // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
         String openid = "";
+
+        // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
         SysUser user = loadUserByOpenid(openid);
 
-        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
         XcxLoginUser loginUser = new XcxLoginUser();
         loginUser.setUserId(user.getUserId());
         loginUser.setUsername(user.getUserName());
@@ -301,25 +304,24 @@ public class SysLoginService {
         String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
         String loginFail = Constants.LOGIN_FAIL;
 
-        // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
-        Integer errorNumber = RedisUtils.getCacheObject(errorKey);
+        // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
+        int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
         // 锁定时间内登录 则踢出
-        if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
+        if (errorNumber >= maxRetryCount) {
             recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
             throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
         }
 
         if (supplier.get()) {
-            // 是否第一次
-            errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
+            // 错误次数递增
+            errorNumber++;
+            RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
             // 达到规定错误次数 则锁定登录
-            if (errorNumber.equals(maxRetryCount)) {
-                RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
+            if (errorNumber >= maxRetryCount) {
                 recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
                 throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
             } else {
-                // 未达到规定错误次数 则递增
-                RedisUtils.setCacheObject(errorKey, errorNumber);
+                // 未达到规定错误次数
                 recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
                 throw new UserException(loginType.getRetryLimitCount(), errorNumber);
             }

+ 7 - 2
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java

@@ -116,7 +116,6 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
      * @param dictType 字典类型
      * @return 字典类型
      */
-    @Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
     @Override
     public SysDictType selectDictTypeByType(String dictType) {
         return baseMapper.selectById(new LambdaQueryWrapper<SysDictType>().eq(SysDictType::getDictType, dictType));
@@ -148,7 +147,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
         List<SysDictData> dictDataList = dictDataMapper.selectList(
             new LambdaQueryWrapper<SysDictData>().eq(SysDictData::getStatus, UserConstants.DICT_NORMAL));
         Map<String, List<SysDictData>> dictDataMap = StreamUtils.groupByKey(dictDataList, SysDictData::getDictType);
-        dictDataMap.forEach((k,v) -> {
+        dictDataMap.forEach((k, v) -> {
             List<SysDictData> dictList = StreamUtils.sorted(v, Comparator.comparing(SysDictData::getDictSort));
             CacheUtils.put(CacheNames.SYS_DICT, k, dictList);
         });
@@ -182,6 +181,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
     public List<SysDictData> insertDictType(SysDictType dict) {
         int row = baseMapper.insert(dict);
         if (row > 0) {
+            // 新增 type 下无 data 数据 返回空防止缓存穿透
             return new ArrayList<>();
         }
         throw new ServiceException("操作失败");
@@ -279,4 +279,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
         }
     }
 
+    @Override
+    public Map<String, String> getAllDictByDictType(String dictType) {
+        List<SysDictData> list = selectDictDataByType(dictType);
+        return StreamUtils.toMap(list, SysDictData::getDictValue, SysDictData::getDictLabel);
+    }
 }

+ 23 - 6
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java

@@ -1,5 +1,6 @@
 package com.ruoyi.system.service.impl;
 
+import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.util.ObjectUtil;
@@ -11,7 +12,6 @@ import com.ruoyi.common.core.domain.PageQuery;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.core.service.OssService;
 import com.ruoyi.common.exception.ServiceException;
-import com.ruoyi.common.utils.BeanCopyUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.file.FileUtils;
 import com.ruoyi.common.utils.spring.SpringUtils;
@@ -31,9 +31,13 @@ import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletResponse;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -108,7 +112,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
         }
         FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
         response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
-        OssClient storage = OssFactory.instance();
+        OssClient storage = OssFactory.instance(sysOss.getService());
         try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
             int available = inputStream.available();
             IoUtil.copy(inputStream, response.getOutputStream(), available);
@@ -130,15 +134,28 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
             throw new ServiceException(e.getMessage());
         }
         // 保存文件信息
+        return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
+    }
+
+    @Override
+    public SysOssVo upload(File file) {
+        String originalfileName = file.getName();
+        String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
+        OssClient storage = OssFactory.instance();
+        UploadResult uploadResult = storage.uploadSuffix(file, suffix);
+        // 保存文件信息
+        return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
+    }
+
+    private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
         SysOss oss = new SysOss();
         oss.setUrl(uploadResult.getUrl());
         oss.setFileSuffix(suffix);
         oss.setFileName(uploadResult.getFilename());
         oss.setOriginalName(originalfileName);
-        oss.setService(storage.getConfigKey());
+        oss.setService(configKey);
         baseMapper.insert(oss);
-        SysOssVo sysOssVo = new SysOssVo();
-        BeanCopyUtils.copy(oss, sysOssVo);
+        SysOssVo sysOssVo = BeanUtil.toBean(oss, SysOssVo.class);
         return this.matchingUrl(sysOssVo);
     }
 

+ 23 - 4
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java

@@ -186,6 +186,20 @@ public class SysRoleServiceImpl implements ISysRoleService {
         if (ObjectUtil.isNotNull(role.getRoleId()) && role.isAdmin()) {
             throw new ServiceException("不允许操作超级管理员角色");
         }
+        // 新增不允许使用 管理员标识符
+        if (ObjectUtil.isNull(role.getRoleId())
+            && StringUtils.equals(role.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
+            throw new ServiceException("不允许使用系统内置管理员角色标识符!");
+        }
+        // 修改不允许修改 管理员标识符
+        if (ObjectUtil.isNotNull(role.getRoleId())) {
+            SysRole sysRole = baseMapper.selectById(role.getRoleId());
+            // 如果标识符不相等 判断为修改了管理员标识符
+            if (!StringUtils.equals(sysRole.getRoleKey(), role.getRoleKey())
+                && StringUtils.equals(sysRole.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
+                throw new ServiceException("不允许修改系统内置管理员角色标识符!");
+            }
+        }
     }
 
     /**
@@ -342,9 +356,9 @@ public class SysRoleServiceImpl implements ISysRoleService {
     @Transactional(rollbackFor = Exception.class)
     public int deleteRoleByIds(Long[] roleIds) {
         for (Long roleId : roleIds) {
-            checkRoleAllowed(new SysRole(roleId));
-            checkRoleDataScope(roleId);
             SysRole role = selectRoleById(roleId);
+            checkRoleAllowed(role);
+            checkRoleDataScope(roleId);
             if (countUserRoleByRoleId(roleId) > 0) {
                 throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName()));
             }
@@ -420,6 +434,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
 
     @Override
     public void cleanOnlineUserByRole(Long roleId) {
+        // 如果角色未绑定用户 直接返回
+        Long num = userRoleMapper.selectCount(new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getRoleId, roleId));
+        if (num == 0) {
+            return;
+        }
         List<String> keys = StpUtil.searchTokenValue("", 0, -1, false);
         if (CollUtil.isEmpty(keys)) {
             return;
@@ -428,11 +447,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
         keys.parallelStream().forEach(key -> {
             String token = StringUtils.substringAfterLast(key, ":");
             // 如果已经过期则跳过
-            if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
+            if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
                 return;
             }
             LoginUser loginUser = LoginHelper.getLoginUser(token);
-            if (loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
+            if (ObjectUtil.isNotNull(loginUser) && loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
                 try {
                     StpUtil.logoutByTokenValue(token);
                 } catch (NotLoginException ignored) {

+ 21 - 0
ruoyi-ui-vue3/.editorconfig

@@ -0,0 +1,21 @@
+# 告诉EditorConfig插件,这是根文件,不用继续往上查找
+root = true
+
+# 匹配全部文件
+[*]
+# 缩进风格,可选space、tab
+indent_style = space
+# 缩进的空格数
+indent_size = 2
+# 设置字符集
+charset = utf-8
+# 结尾换行符,可选lf、cr、crlf
+end_of_line = lf
+# 在文件结尾插入新行
+trim_trailing_whitespace = true
+# 删除一行中的前后空格
+insert_final_newline = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 17 - 0
ruoyi-ui-vue3/.env.development

@@ -0,0 +1,17 @@
+# 页面标题
+VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
+
+# 开发环境配置
+VITE_APP_ENV = 'development'
+
+# 若依管理系统/开发环境
+VITE_APP_BASE_API = '/dev-api'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH = '/'
+
+# 监控地址
+VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
+
+# xxl-job 控制台地址
+VITE_APP_XXL_JOB_ADMIN = 'http://localhost:9100/xxl-job-admin'

+ 20 - 0
ruoyi-ui-vue3/.env.production

@@ -0,0 +1,20 @@
+# 页面标题
+VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
+
+# 生产环境配置
+VITE_APP_ENV = 'production'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH = '/'
+
+# 监控地址
+VITE_APP_MONITRO_ADMIN = '/admin/applications'
+
+# 监控地址
+VITE_APP_XXL_JOB_ADMIN = '/xxl-job-admin'
+
+# 若依管理系统/生产环境
+VITE_APP_BASE_API = '/prod-api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip

+ 23 - 0
ruoyi-ui-vue3/.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock

+ 83 - 0
ruoyi-ui-vue3/README.md

@@ -0,0 +1,83 @@
+## 平台简介
+
+* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
+* 配套后端代码仓库地址[RuoYi-Vue-Plus 4.X(注意版本号)](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus)
+* 5.X后端需要使用此项目 [plus-ui](https://gitee.com/JavaLionLi/plus-ui)
+
+## 前端运行
+
+```bash
+# 进入项目目录
+cd ruoyi-ui-vue3
+
+# 安装依赖
+npm install --registry=https://registry.npmmirror.com
+
+# 启动服务
+npm run dev
+
+# 构建测试环境 yarn build:stage
+# 构建生产环境 yarn build:prod
+# 前端访问地址 http://localhost:80
+```
+
+## 后端改造
+参考后端代码内 `ruoyi-generator/resources/vm/vue/v3/readme.txt` 说明
+
+## 内置功能
+
+1.  用户管理:用户是系统操作者,该功能主要完成系统用户配置。
+2.  部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
+3.  岗位管理:配置系统用户所属担任职务。
+4.  菜单管理:配置系统菜单,操作权限,按钮权限标识等。
+5.  角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
+6.  字典管理:对系统中经常使用的一些较为固定的数据进行维护。
+7.  参数管理:对系统动态配置常用参数。
+8.  通知公告:系统通知公告信息发布维护。
+9.  操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
+10. 登录日志:系统登录日志记录查询包含登录异常。
+11. 在线用户:当前系统中活跃用户状态监控。
+12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
+13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
+14. 系统接口:根据业务代码自动生成相关的api接口文档。
+15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
+16. 缓存监控:对系统的缓存信息查询,命令统计等。
+17. 在线构建器:拖动表单元素生成相应的HTML代码。
+18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
+
+## 演示图
+
+<table>
+    <tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
+    </tr>
+    <tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td>
+    </tr>
+    <tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td>
+    </tr>
+	<tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td>
+    </tr>	 
+    <tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
+    </tr>
+	<tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td>
+    </tr>
+	<tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
+    </tr>
+    <tr>
+        <td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
+        <td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
+    </tr>
+</table>

+ 12 - 0
ruoyi-ui-vue3/bin/build.bat

@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 打包Web工程,生成dist文件。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn build:prod
+
+pause

+ 12 - 0
ruoyi-ui-vue3/bin/package.bat

@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 安装Web工程,生成node_modules文件。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn --registry=https://registry.npmmirror.com
+
+pause

+ 12 - 0
ruoyi-ui-vue3/bin/run-web.bat

@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 使用 Vite 命令运行 Web 工程。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn dev
+
+pause

File diff suppressed because it is too large
+ 21 - 0
ruoyi-ui-vue3/html/ie.html


+ 215 - 0
ruoyi-ui-vue3/index.html

@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="renderer" content="webkit">
+  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+  <link rel="icon" href="/favicon.ico">
+  <title>RuoYi-Vue-Plus管理系统</title>
+  <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
+  <style>
+    html,
+    body,
+    #app {
+      height: 100%;
+      margin: 0px;
+      padding: 0px;
+    }
+
+    .chromeframe {
+      margin: 0.2em 0;
+      background: #ccc;
+      color: #000;
+      padding: 0.2em 0;
+    }
+
+    #loader-wrapper {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      z-index: 999999;
+    }
+
+    #loader {
+      display: block;
+      position: relative;
+      left: 50%;
+      top: 50%;
+      width: 150px;
+      height: 150px;
+      margin: -75px 0 0 -75px;
+      border-radius: 50%;
+      border: 3px solid transparent;
+      border-top-color: #FFF;
+      -webkit-animation: spin 2s linear infinite;
+      -ms-animation: spin 2s linear infinite;
+      -moz-animation: spin 2s linear infinite;
+      -o-animation: spin 2s linear infinite;
+      animation: spin 2s linear infinite;
+      z-index: 1001;
+    }
+
+    #loader:before {
+      content: "";
+      position: absolute;
+      top: 5px;
+      left: 5px;
+      right: 5px;
+      bottom: 5px;
+      border-radius: 50%;
+      border: 3px solid transparent;
+      border-top-color: #FFF;
+      -webkit-animation: spin 3s linear infinite;
+      -moz-animation: spin 3s linear infinite;
+      -o-animation: spin 3s linear infinite;
+      -ms-animation: spin 3s linear infinite;
+      animation: spin 3s linear infinite;
+    }
+
+    #loader:after {
+      content: "";
+      position: absolute;
+      top: 15px;
+      left: 15px;
+      right: 15px;
+      bottom: 15px;
+      border-radius: 50%;
+      border: 3px solid transparent;
+      border-top-color: #FFF;
+      -moz-animation: spin 1.5s linear infinite;
+      -o-animation: spin 1.5s linear infinite;
+      -ms-animation: spin 1.5s linear infinite;
+      -webkit-animation: spin 1.5s linear infinite;
+      animation: spin 1.5s linear infinite;
+    }
+
+
+    @-webkit-keyframes spin {
+      0% {
+        -webkit-transform: rotate(0deg);
+        -ms-transform: rotate(0deg);
+        transform: rotate(0deg);
+      }
+
+      100% {
+        -webkit-transform: rotate(360deg);
+        -ms-transform: rotate(360deg);
+        transform: rotate(360deg);
+      }
+    }
+
+    @keyframes spin {
+      0% {
+        -webkit-transform: rotate(0deg);
+        -ms-transform: rotate(0deg);
+        transform: rotate(0deg);
+      }
+
+      100% {
+        -webkit-transform: rotate(360deg);
+        -ms-transform: rotate(360deg);
+        transform: rotate(360deg);
+      }
+    }
+
+
+    #loader-wrapper .loader-section {
+      position: fixed;
+      top: 0;
+      width: 51%;
+      height: 100%;
+      background: #7171C6;
+      z-index: 1000;
+      -webkit-transform: translateX(0);
+      -ms-transform: translateX(0);
+      transform: translateX(0);
+    }
+
+    #loader-wrapper .loader-section.section-left {
+      left: 0;
+    }
+
+    #loader-wrapper .loader-section.section-right {
+      right: 0;
+    }
+
+
+    .loaded #loader-wrapper .loader-section.section-left {
+      -webkit-transform: translateX(-100%);
+      -ms-transform: translateX(-100%);
+      transform: translateX(-100%);
+      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+    }
+
+    .loaded #loader-wrapper .loader-section.section-right {
+      -webkit-transform: translateX(100%);
+      -ms-transform: translateX(100%);
+      transform: translateX(100%);
+      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+    }
+
+    .loaded #loader {
+      opacity: 0;
+      -webkit-transition: all 0.3s ease-out;
+      transition: all 0.3s ease-out;
+    }
+
+    .loaded #loader-wrapper {
+      visibility: hidden;
+      -webkit-transform: translateY(-100%);
+      -ms-transform: translateY(-100%);
+      transform: translateY(-100%);
+      -webkit-transition: all 0.3s 1s ease-out;
+      transition: all 0.3s 1s ease-out;
+    }
+
+    .no-js #loader-wrapper {
+      display: none;
+    }
+
+    .no-js h1 {
+      color: #222222;
+    }
+
+    #loader-wrapper .load_title {
+      font-family: 'Open Sans';
+      color: #FFF;
+      font-size: 19px;
+      width: 100%;
+      text-align: center;
+      z-index: 9999999999999;
+      position: absolute;
+      top: 60%;
+      opacity: 1;
+      line-height: 30px;
+    }
+
+    #loader-wrapper .load_title span {
+      font-weight: normal;
+      font-style: italic;
+      font-size: 13px;
+      color: #FFF;
+      opacity: 0.5;
+    }
+  </style>
+</head>
+
+<body>
+  <div id="app">
+    <div id="loader-wrapper">
+      <div id="loader"></div>
+      <div class="loader-section section-left"></div>
+      <div class="loader-section section-right"></div>
+      <div class="load_title">正在加载系统资源,请耐心等待</div>
+    </div>
+  </div>
+  <script type="module" src="/src/main.js"></script>
+</body>
+
+</html>

+ 43 - 0
ruoyi-ui-vue3/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "ruoyi-vue-plus",
+  "version": "4.8.0",
+  "description": "RuoYi-Vue-Plus后台管理系统",
+  "author": "LionLi",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vite",
+    "build:prod": "vite build",
+    "preview": "vite preview"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://gitee.com/JavaLionLi/RuoYi-Vue-Plus-UI.git"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "2.0.10",
+    "@vueup/vue-quill": "1.1.0",
+    "@vueuse/core": "9.5.0",
+    "axios": "0.27.2",
+    "echarts": "5.4.0",
+    "element-plus": "2.2.27",
+    "file-saver": "2.0.5",
+    "fuse.js": "6.6.2",
+    "js-cookie": "3.0.1",
+    "jsencrypt": "3.3.1",
+    "nprogress": "0.2.0",
+    "pinia": "2.0.22",
+    "vue": "3.2.45",
+    "vue-cropper": "1.0.3",
+    "vue-router": "4.1.4"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "3.1.0",
+    "@vue/compiler-sfc": "3.2.45",
+    "sass": "1.56.1",
+    "unplugin-auto-import": "0.11.4",
+    "vite": "3.2.3",
+    "vite-plugin-compression": "0.5.1",
+    "vite-plugin-svg-icons": "2.0.1",
+    "vite-plugin-vue-setup-extend": "0.4.0"
+  }
+}

BIN
ruoyi-ui-vue3/public/favicon.ico


+ 15 - 0
ruoyi-ui-vue3/src/App.vue

@@ -0,0 +1,15 @@
+<template>
+  <router-view />
+</template>
+
+<script setup>
+import useSettingsStore from '@/store/modules/settings'
+import { handleThemeStyle } from '@/utils/theme'
+
+onMounted(() => {
+  nextTick(() => {
+    // 初始化主题样式
+    handleThemeStyle(useSettingsStore().theme)
+  })
+})
+</script>

+ 54 - 0
ruoyi-ui-vue3/src/api/demo/demo.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 查询测试单表列表
+export function listDemo(query) {
+  return request({
+    url: '/demo/demo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 自定义分页接口
+export function pageDemo(query) {
+  return request({
+    url: '/demo/demo/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询测试单表详细
+export function getDemo(id) {
+  return request({
+    url: '/demo/demo/' + id,
+    method: 'get'
+  })
+}
+
+// 新增测试单表
+export function addDemo(data) {
+  return request({
+    url: '/demo/demo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改测试单表
+export function updateDemo(data) {
+  return request({
+    url: '/demo/demo',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除测试单表
+export function delDemo(id) {
+  return request({
+    url: '/demo/demo/' + id,
+    method: 'delete'
+  })
+}
+

+ 44 - 0
ruoyi-ui-vue3/src/api/demo/tree.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询测试树表列表
+export function listTree(query) {
+  return request({
+    url: '/demo/tree/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询测试树表详细
+export function getTree(id) {
+  return request({
+    url: '/demo/tree/' + id,
+    method: 'get'
+  })
+}
+
+// 新增测试树表
+export function addTree(data) {
+  return request({
+    url: '/demo/tree',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改测试树表
+export function updateTree(data) {
+  return request({
+    url: '/demo/tree',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除测试树表
+export function delTree(id) {
+  return request({
+    url: '/demo/tree/' + id,
+    method: 'delete'
+  })
+}

+ 59 - 0
ruoyi-ui-vue3/src/api/login.js

@@ -0,0 +1,59 @@
+import request from '@/utils/request'
+
+// 登录方法
+export function login(username, password, code, uuid) {
+  const data = {
+    username,
+    password,
+    code,
+    uuid
+  }
+  return request({
+    url: '/login',
+    headers: {
+      isToken: false
+    },
+    method: 'post',
+    data: data
+  })
+}
+
+// 注册方法
+export function register(data) {
+  return request({
+    url: '/register',
+    headers: {
+      isToken: false
+    },
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取用户详细信息
+export function getInfo() {
+  return request({
+    url: '/getInfo',
+    method: 'get'
+  })
+}
+
+// 退出方法
+export function logout() {
+  return request({
+    url: '/logout',
+    method: 'post'
+  })
+}
+
+// 获取验证码
+export function getCodeImg() {
+  return request({
+    url: '/captchaImage',
+    headers: {
+      isToken: false
+    },
+    method: 'get',
+    timeout: 20000
+  })
+}

+ 9 - 0
ruoyi-ui-vue3/src/api/menu.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 获取路由
+export const getRouters = () => {
+  return request({
+    url: '/getRouters',
+    method: 'get'
+  })
+}

+ 57 - 0
ruoyi-ui-vue3/src/api/monitor/cache.js

@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+// 查询缓存详细
+export function getCache() {
+  return request({
+    url: '/monitor/cache',
+    method: 'get'
+  })
+}
+
+// 查询缓存名称列表
+export function listCacheName() {
+  return request({
+    url: '/monitor/cache/getNames',
+    method: 'get'
+  })
+}
+
+// 查询缓存键名列表
+export function listCacheKey(cacheName) {
+  return request({
+    url: '/monitor/cache/getKeys/' + cacheName,
+    method: 'get'
+  })
+}
+
+// 查询缓存内容
+export function getCacheValue(cacheName, cacheKey) {
+  return request({
+    url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
+    method: 'get'
+  })
+}
+
+// 清理指定名称缓存
+export function clearCacheName(cacheName) {
+  return request({
+    url: '/monitor/cache/clearCacheName/' + cacheName,
+    method: 'delete'
+  })
+}
+
+// 清理指定键名缓存
+export function clearCacheKey(cacheName, cacheKey) {
+  return request({
+    url: '/monitor/cache/clearCacheKey/' + cacheName + '/'  + cacheKey,
+    method: 'delete'
+  })
+}
+
+// 清理全部缓存
+export function clearCacheAll() {
+  return request({
+    url: '/monitor/cache/clearCacheAll',
+    method: 'delete'
+  })
+}

+ 34 - 0
ruoyi-ui-vue3/src/api/monitor/logininfor.js

@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// 查询登录日志列表
+export function list(query) {
+  return request({
+    url: '/monitor/logininfor/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 删除登录日志
+export function delLogininfor(infoId) {
+  return request({
+    url: '/monitor/logininfor/' + infoId,
+    method: 'delete'
+  })
+}
+
+// 解锁用户登录状态
+export function unlockLogininfor(userName) {
+  return request({
+    url: '/monitor/logininfor/unlock/' + userName,
+    method: 'get'
+  })
+}
+
+// 清空登录日志
+export function cleanLogininfor() {
+  return request({
+    url: '/monitor/logininfor/clean',
+    method: 'delete'
+  })
+}

+ 18 - 0
ruoyi-ui-vue3/src/api/monitor/online.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 查询在线用户列表
+export function list(query) {
+  return request({
+    url: '/monitor/online/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 强退用户
+export function forceLogout(tokenId) {
+  return request({
+    url: '/monitor/online/' + tokenId,
+    method: 'delete'
+  })
+}

+ 26 - 0
ruoyi-ui-vue3/src/api/monitor/operlog.js

@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 查询操作日志列表
+export function list(query) {
+  return request({
+    url: '/monitor/operlog/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 删除操作日志
+export function delOperlog(operId) {
+  return request({
+    url: '/monitor/operlog/' + operId,
+    method: 'delete'
+  })
+}
+
+// 清空操作日志
+export function cleanOperlog() {
+  return request({
+    url: '/monitor/operlog/clean',
+    method: 'delete'
+  })
+}

+ 72 - 0
ruoyi-ui-vue3/src/api/system/config.js

@@ -0,0 +1,72 @@
+import request from '@/utils/request'
+
+// 查询参数列表
+export function listConfig(query) {
+  return request({
+    url: '/system/config/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询参数详细
+export function getConfig(configId) {
+  return request({
+    url: '/system/config/' + configId,
+    method: 'get'
+  })
+}
+
+// 根据参数键名查询参数值
+export function getConfigKey(configKey) {
+  return request({
+    url: '/system/config/configKey/' + configKey,
+    method: 'get'
+  })
+}
+
+// 新增参数配置
+export function addConfig(data) {
+  return request({
+    url: '/system/config',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改参数配置
+export function updateConfig(data) {
+  return request({
+    url: '/system/config',
+    method: 'put',
+    data: data
+  })
+}
+
+// 修改参数配置
+export function updateConfigByKey(key, value) {
+  return request({
+    url: '/system/config/updateByKey',
+    method: 'put',
+    data: {
+      configKey: key,
+      configValue: value
+    }
+  })
+}
+
+// 删除参数配置
+export function delConfig(configId) {
+  return request({
+    url: '/system/config/' + configId,
+    method: 'delete'
+  })
+}
+
+// 刷新参数缓存
+export function refreshCache() {
+  return request({
+    url: '/system/config/refreshCache',
+    method: 'delete'
+  })
+}

+ 52 - 0
ruoyi-ui-vue3/src/api/system/dept.js

@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询部门列表
+export function listDept(query) {
+  return request({
+    url: '/system/dept/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询部门列表(排除节点)
+export function listDeptExcludeChild(deptId) {
+  return request({
+    url: '/system/dept/list/exclude/' + deptId,
+    method: 'get'
+  })
+}
+
+// 查询部门详细
+export function getDept(deptId) {
+  return request({
+    url: '/system/dept/' + deptId,
+    method: 'get'
+  })
+}
+
+// 新增部门
+export function addDept(data) {
+  return request({
+    url: '/system/dept',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改部门
+export function updateDept(data) {
+  return request({
+    url: '/system/dept',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除部门
+export function delDept(deptId) {
+  return request({
+    url: '/system/dept/' + deptId,
+    method: 'delete'
+  })
+}

+ 52 - 0
ruoyi-ui-vue3/src/api/system/dict/data.js

@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询字典数据列表
+export function listData(query) {
+  return request({
+    url: '/system/dict/data/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询字典数据详细
+export function getData(dictCode) {
+  return request({
+    url: '/system/dict/data/' + dictCode,
+    method: 'get'
+  })
+}
+
+// 根据字典类型查询字典数据信息
+export function getDicts(dictType) {
+  return request({
+    url: '/system/dict/data/type/' + dictType,
+    method: 'get'
+  })
+}
+
+// 新增字典数据
+export function addData(data) {
+  return request({
+    url: '/system/dict/data',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改字典数据
+export function updateData(data) {
+  return request({
+    url: '/system/dict/data',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除字典数据
+export function delData(dictCode) {
+  return request({
+    url: '/system/dict/data/' + dictCode,
+    method: 'delete'
+  })
+}

+ 60 - 0
ruoyi-ui-vue3/src/api/system/dict/type.js

@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询字典类型列表
+export function listType(query) {
+  return request({
+    url: '/system/dict/type/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询字典类型详细
+export function getType(dictId) {
+  return request({
+    url: '/system/dict/type/' + dictId,
+    method: 'get'
+  })
+}
+
+// 新增字典类型
+export function addType(data) {
+  return request({
+    url: '/system/dict/type',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改字典类型
+export function updateType(data) {
+  return request({
+    url: '/system/dict/type',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除字典类型
+export function delType(dictId) {
+  return request({
+    url: '/system/dict/type/' + dictId,
+    method: 'delete'
+  })
+}
+
+// 刷新字典缓存
+export function refreshCache() {
+  return request({
+    url: '/system/dict/type/refreshCache',
+    method: 'delete'
+  })
+}
+
+// 获取字典选择框列表
+export function optionselect() {
+  return request({
+    url: '/system/dict/type/optionselect',
+    method: 'get'
+  })
+}

+ 60 - 0
ruoyi-ui-vue3/src/api/system/menu.js

@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询菜单列表
+export function listMenu(query) {
+  return request({
+    url: '/system/menu/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询菜单详细
+export function getMenu(menuId) {
+  return request({
+    url: '/system/menu/' + menuId,
+    method: 'get'
+  })
+}
+
+// 查询菜单下拉树结构
+export function treeselect() {
+  return request({
+    url: '/system/menu/treeselect',
+    method: 'get'
+  })
+}
+
+// 根据角色ID查询菜单下拉树结构
+export function roleMenuTreeselect(roleId) {
+  return request({
+    url: '/system/menu/roleMenuTreeselect/' + roleId,
+    method: 'get'
+  })
+}
+
+// 新增菜单
+export function addMenu(data) {
+  return request({
+    url: '/system/menu',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改菜单
+export function updateMenu(data) {
+  return request({
+    url: '/system/menu',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除菜单
+export function delMenu(menuId) {
+  return request({
+    url: '/system/menu/' + menuId,
+    method: 'delete'
+  })
+}

+ 44 - 0
ruoyi-ui-vue3/src/api/system/notice.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询公告列表
+export function listNotice(query) {
+  return request({
+    url: '/system/notice/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询公告详细
+export function getNotice(noticeId) {
+  return request({
+    url: '/system/notice/' + noticeId,
+    method: 'get'
+  })
+}
+
+// 新增公告
+export function addNotice(data) {
+  return request({
+    url: '/system/notice',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改公告
+export function updateNotice(data) {
+  return request({
+    url: '/system/notice',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除公告
+export function delNotice(noticeId) {
+  return request({
+    url: '/system/notice/' + noticeId,
+    method: 'delete'
+  })
+}

+ 27 - 0
ruoyi-ui-vue3/src/api/system/oss.js

@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+// 查询OSS对象存储列表
+export function listOss(query) {
+  return request({
+    url: '/system/oss/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询OSS对象基于id串
+export function listByIds(ossId) {
+  return request({
+    url: '/system/oss/listByIds/' + ossId,
+    method: 'get'
+  })
+}
+
+// 删除OSS对象存储
+export function delOss(ossId) {
+  return request({
+    url: '/system/oss/' + ossId,
+    method: 'delete'
+  })
+}
+

+ 58 - 0
ruoyi-ui-vue3/src/api/system/ossConfig.js

@@ -0,0 +1,58 @@
+import request from '@/utils/request'
+
+// 查询对象存储配置列表
+export function listOssConfig(query) {
+  return request({
+    url: '/system/oss/config/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询对象存储配置详细
+export function getOssConfig(ossConfigId) {
+  return request({
+    url: '/system/oss/config/' + ossConfigId,
+    method: 'get'
+  })
+}
+
+// 新增对象存储配置
+export function addOssConfig(data) {
+  return request({
+    url: '/system/oss/config',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改对象存储配置
+export function updateOssConfig(data) {
+  return request({
+    url: '/system/oss/config',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除对象存储配置
+export function delOssConfig(ossConfigId) {
+  return request({
+    url: '/system/oss/config/' + ossConfigId,
+    method: 'delete'
+  })
+}
+
+// 对象存储状态修改
+export function changeOssConfigStatus(ossConfigId, status, configKey) {
+  const data = {
+    ossConfigId,
+    status,
+    configKey
+  }
+  return request({
+    url: '/system/oss/config/changeStatus',
+    method: 'put',
+    data: data
+  })
+}

+ 44 - 0
ruoyi-ui-vue3/src/api/system/post.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询岗位列表
+export function listPost(query) {
+  return request({
+    url: '/system/post/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询岗位详细
+export function getPost(postId) {
+  return request({
+    url: '/system/post/' + postId,
+    method: 'get'
+  })
+}
+
+// 新增岗位
+export function addPost(data) {
+  return request({
+    url: '/system/post',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改岗位
+export function updatePost(data) {
+  return request({
+    url: '/system/post',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除岗位
+export function delPost(postId) {
+  return request({
+    url: '/system/post/' + postId,
+    method: 'delete'
+  })
+}

+ 119 - 0
ruoyi-ui-vue3/src/api/system/role.js

@@ -0,0 +1,119 @@
+import request from '@/utils/request'
+
+// 查询角色列表
+export function listRole(query) {
+  return request({
+    url: '/system/role/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询角色详细
+export function getRole(roleId) {
+  return request({
+    url: '/system/role/' + roleId,
+    method: 'get'
+  })
+}
+
+// 新增角色
+export function addRole(data) {
+  return request({
+    url: '/system/role',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改角色
+export function updateRole(data) {
+  return request({
+    url: '/system/role',
+    method: 'put',
+    data: data
+  })
+}
+
+// 角色数据权限
+export function dataScope(data) {
+  return request({
+    url: '/system/role/dataScope',
+    method: 'put',
+    data: data
+  })
+}
+
+// 角色状态修改
+export function changeRoleStatus(roleId, status) {
+  const data = {
+    roleId,
+    status
+  }
+  return request({
+    url: '/system/role/changeStatus',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除角色
+export function delRole(roleId) {
+  return request({
+    url: '/system/role/' + roleId,
+    method: 'delete'
+  })
+}
+
+// 查询角色已授权用户列表
+export function allocatedUserList(query) {
+  return request({
+    url: '/system/role/authUser/allocatedList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询角色未授权用户列表
+export function unallocatedUserList(query) {
+  return request({
+    url: '/system/role/authUser/unallocatedList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 取消用户授权角色
+export function authUserCancel(data) {
+  return request({
+    url: '/system/role/authUser/cancel',
+    method: 'put',
+    data: data
+  })
+}
+
+// 批量取消用户授权角色
+export function authUserCancelAll(data) {
+  return request({
+    url: '/system/role/authUser/cancelAll',
+    method: 'put',
+    params: data
+  })
+}
+
+// 授权用户选择
+export function authUserSelectAll(data) {
+  return request({
+    url: '/system/role/authUser/selectAll',
+    method: 'put',
+    params: data
+  })
+}
+
+// 根据角色ID查询部门树结构
+export function deptTreeSelect(roleId) {
+  return request({
+    url: '/system/role/deptTree/' + roleId,
+    method: 'get'
+  })
+}

+ 135 - 0
ruoyi-ui-vue3/src/api/system/user.js

@@ -0,0 +1,135 @@
+import request from '@/utils/request'
+import { parseStrEmpty } from "@/utils/ruoyi";
+
+// 查询用户列表
+export function listUser(query) {
+  return request({
+    url: '/system/user/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询用户详细
+export function getUser(userId) {
+  return request({
+    url: '/system/user/' + parseStrEmpty(userId),
+    method: 'get'
+  })
+}
+
+// 新增用户
+export function addUser(data) {
+  return request({
+    url: '/system/user',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改用户
+export function updateUser(data) {
+  return request({
+    url: '/system/user',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除用户
+export function delUser(userId) {
+  return request({
+    url: '/system/user/' + userId,
+    method: 'delete'
+  })
+}
+
+// 用户密码重置
+export function resetUserPwd(userId, password) {
+  const data = {
+    userId,
+    password
+  }
+  return request({
+    url: '/system/user/resetPwd',
+    method: 'put',
+    data: data
+  })
+}
+
+// 用户状态修改
+export function changeUserStatus(userId, status) {
+  const data = {
+    userId,
+    status
+  }
+  return request({
+    url: '/system/user/changeStatus',
+    method: 'put',
+    data: data
+  })
+}
+
+// 查询用户个人信息
+export function getUserProfile() {
+  return request({
+    url: '/system/user/profile',
+    method: 'get'
+  })
+}
+
+// 修改用户个人信息
+export function updateUserProfile(data) {
+  return request({
+    url: '/system/user/profile',
+    method: 'put',
+    data: data
+  })
+}
+
+// 用户密码重置
+export function updateUserPwd(oldPassword, newPassword) {
+  const data = {
+    oldPassword,
+    newPassword
+  }
+  return request({
+    url: '/system/user/profile/updatePwd',
+    method: 'put',
+    params: data
+  })
+}
+
+// 用户头像上传
+export function uploadAvatar(data) {
+  return request({
+    url: '/system/user/profile/avatar',
+    method: 'post',
+    data: data
+  })
+}
+
+// 查询授权角色
+export function getAuthRole(userId) {
+  return request({
+    url: '/system/user/authRole/' + userId,
+    method: 'get'
+  })
+}
+
+// 保存授权角色
+export function updateAuthRole(data) {
+  return request({
+    url: '/system/user/authRole',
+    method: 'put',
+    params: data
+  })
+}
+
+// 查询部门下拉树结构
+export function deptTreeSelect() {
+  return request({
+    url: '/system/user/deptTree',
+    method: 'get'
+  })
+}

+ 85 - 0
ruoyi-ui-vue3/src/api/tool/gen.js

@@ -0,0 +1,85 @@
+import request from '@/utils/request'
+
+// 查询生成表数据
+export function listTable(query) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/list',
+    method: 'get',
+    params: query
+  })
+}
+// 查询db数据库列表
+export function listDbTable(query) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/db/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询表详细信息
+export function getGenTable(tableId) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/' + tableId,
+    method: 'get'
+  })
+}
+
+// 修改代码生成信息
+export function updateGenTable(data) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen',
+    method: 'put',
+    data: data
+  })
+}
+
+// 导入表
+export function importTable(data) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/importTable',
+    method: 'post',
+    params: data
+  })
+}
+
+// 预览生成代码
+export function previewTable(tableId) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/preview/' + tableId,
+    method: 'get'
+  })
+}
+
+// 删除表数据
+export function delTable(tableId) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/' + tableId,
+    method: 'delete'
+  })
+}
+
+// 生成代码(自定义路径)
+export function genCode(tableName) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/genCode/' + tableName,
+    method: 'get'
+  })
+}
+
+// 同步数据库
+export function synchDb(tableName) {
+  return request({
+    headers: { 'datasource': localStorage.getItem("dataName") },
+    url: '/tool/gen/synchDb/' + tableName,
+    method: 'get'
+  })
+}

BIN
ruoyi-ui-vue3/src/assets/401_images/401.gif


BIN
ruoyi-ui-vue3/src/assets/404_images/404.png


BIN
ruoyi-ui-vue3/src/assets/404_images/404_cloud.png


+ 1 - 0
ruoyi-ui-vue3/src/assets/icons/svg/404.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>

+ 1 - 0
ruoyi-ui-vue3/src/assets/icons/svg/bug.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>

+ 1 - 0
ruoyi-ui-vue3/src/assets/icons/svg/build.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1568899741379" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2054" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M960 591.424V368.96c0-0.288 0.16-0.512 0.16-0.768S960 367.68 960 367.424V192a32 32 0 0 0-32-32H96a32 32 0 0 0-32 32v175.424c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768v222.464c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768V864a32 32 0 0 0 32 32h832a32 32 0 0 0 32-32v-271.04c0-0.288 0.16-0.512 0.16-0.768S960 591.68 960 591.424z m-560-31.232v-160H608v160h-208z m208 64V832h-208v-207.808H608z m-480-224h208v160H128v-160z m544 0h224v160h-224v-160zM896 224v112.192H128V224h768zM128 624.192h208V832H128v-207.808zM672 832v-207.808h224V832h-224z" p-id="2055"></path></svg>

File diff suppressed because it is too large
+ 0 - 0
ruoyi-ui-vue3/src/assets/icons/svg/button.svg


Some files were not shown because too many files changed in this diff