Browse Source

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

# Conflicts:
#	README.md
疯狂的狮子li 2 years ago
parent
commit
c631a084f3
92 changed files with 883 additions and 305 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 2
      README.md
  5. 15 6
      pom.xml
  6. 1 1
      ruoyi-admin/pom.xml
  7. 26 2
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
  8. 0 1
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java
  9. 1 1
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
  10. 17 1
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
  11. 1 9
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssController.java
  12. 1 26
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
  13. 1 1
      ruoyi-admin/src/main/resources/application.yml
  14. 4 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  15. 4 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  16. 4 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  17. BIN
      ruoyi-admin/src/main/resources/ip2region.xdb
  18. 0 0
      ruoyi-admin/src/main/resources/logback-plus.xml
  19. 7 1
      ruoyi-common/pom.xml
  20. 0 5
      ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
  21. 30 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/EmailLoginBody.java
  22. 2 2
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/SmsLoginBody.java
  23. 5 0
      ruoyi-common/src/main/java/com/ruoyi/common/enums/LoginType.java
  24. 1 1
      ruoyi-common/src/main/java/com/ruoyi/common/excel/DefaultExcelListener.java
  25. 4 4
      ruoyi-common/src/main/java/com/ruoyi/common/excel/DefaultExcelResult.java
  26. 2 2
      ruoyi-common/src/main/java/com/ruoyi/common/helper/DataBaseHelper.java
  27. 29 0
      ruoyi-common/src/main/java/com/ruoyi/common/helper/DataPermissionHelper.java
  28. 2 1
      ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/DeptNameTranslationImpl.java
  29. 1 0
      ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/DictTypeTranslationImpl.java
  30. 1 0
      ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/OssUrlTranslationImpl.java
  31. 1 0
      ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/UserNameTranslationImpl.java
  32. 243 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/EncryptUtils.java
  33. 2 29
      ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java
  34. 66 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/RegionUtils.java
  35. 1 1
      ruoyi-demo/pom.xml
  36. 12 0
      ruoyi-demo/src/main/java/com/ruoyi/demo/controller/RedisRateLimiterController.java
  37. 1 1
      ruoyi-extend/pom.xml
  38. 1 1
      ruoyi-extend/ruoyi-monitor-admin/pom.xml
  39. 3 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml
  40. 0 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/resources/logback-plus.xml
  41. 1 1
      ruoyi-extend/ruoyi-xxl-job-admin/pom.xml
  42. 7 0
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java
  43. 10 6
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java
  44. 3 0
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application.yml
  45. 1 1
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/i18n/message_en.properties
  46. 1 1
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties
  47. 1 1
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties
  48. 0 0
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/logback-plus.xml
  49. 0 2
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js
  50. 1 3
      ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl
  51. 1 1
      ruoyi-framework/pom.xml
  52. 9 1
      ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
  53. 6 3
      ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisDecryptInterceptor.java
  54. 6 3
      ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisEncryptInterceptor.java
  55. 1 1
      ruoyi-framework/src/main/java/com/ruoyi/framework/handler/CreateAndUpdateMetaObjectHandler.java
  56. 1 1
      ruoyi-generator/pom.xml
  57. 2 4
      ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
  58. 1 1
      ruoyi-job/pom.xml
  59. 1 1
      ruoyi-oss/pom.xml
  60. 1 1
      ruoyi-sms/pom.xml
  61. 1 1
      ruoyi-system/pom.xml
  62. 8 0
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
  63. 2 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java
  64. 42 5
      ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java
  65. 2 7
      ruoyi-system/src/main/java/com/ruoyi/system/service/SysRegisterService.java
  66. 40 3
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
  67. 5 0
      ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
  68. 5 3
      ruoyi-ui/.env.production
  69. 2 2
      ruoyi-ui/package.json
  70. BIN
      ruoyi-ui/src/assets/images/profile.jpg
  71. 0 9
      ruoyi-ui/src/assets/styles/index.scss
  72. 1 1
      ruoyi-ui/src/assets/styles/sidebar.scss
  73. 0 4
      ruoyi-ui/src/assets/styles/transition.scss
  74. 43 3
      ruoyi-ui/src/components/DictTag/index.vue
  75. 54 18
      ruoyi-ui/src/components/IconSelect/index.vue
  76. 7 1
      ruoyi-ui/src/components/TopNav/index.vue
  77. 15 1
      ruoyi-ui/src/layout/components/AppMain.vue
  78. 1 1
      ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue
  79. 1 1
      ruoyi-ui/src/layout/components/TagsView/index.vue
  80. 5 5
      ruoyi-ui/src/layout/index.vue
  81. 71 67
      ruoyi-ui/src/plugins/tab.js
  82. 6 0
      ruoyi-ui/src/router/index.js
  83. 1 1
      ruoyi-ui/src/views/index.vue
  84. 1 1
      ruoyi-ui/src/views/monitor/cache/list.vue
  85. 2 3
      ruoyi-ui/src/views/system/menu/index.vue
  86. 1 3
      ruoyi-ui/src/views/system/oss/index.vue
  87. 0 13
      ruoyi-ui/src/views/tool/build/index.vue
  88. 5 5
      script/docker/docker-compose.yml
  89. 6 6
      script/sql/oracle/oracle_ry_vue_4.X.sql
  90. 6 6
      script/sql/postgres/postgres_ry_vue_4.X.sql
  91. 1 1
      script/sql/ry_vue_4.X.sql
  92. 1 1
      script/sql/sqlserver/sqlserver_ry_vue_4.X.sql

+ 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.6.0" />
+        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:4.7.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.6.0" />
+        <option name="imageTag" value="ruoyi/ruoyi-server:4.7.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.6.0" />
+        <option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:4.7.0" />
         <option name="buildOnly" value="true" />
         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-xxl-job-admin/Dockerfile" />
       </settings>

+ 2 - 2
README.md

@@ -2,6 +2,7 @@
 <div style="height: 10px; clear: both;"></div>
 
 - - -
+
 ## 平台简介
 
 [![码云Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Vue-Plus)
@@ -9,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.6.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-4.7.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)]()
@@ -161,4 +162,3 @@
 | ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png "屏幕截图") |
 
 
-

+ 15 - 6
pom.xml

@@ -6,15 +6,15 @@
 
     <groupId>com.ruoyi</groupId>
     <artifactId>ruoyi-vue-plus</artifactId>
-    <version>4.6.0</version>
+    <version>4.7.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.6.0</ruoyi-vue-plus.version>
-        <spring-boot.version>2.7.9</spring-boot.version>
+        <ruoyi-vue-plus.version>4.7.0</ruoyi-vue-plus.version>
+        <spring-boot.version>2.7.11</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>
@@ -27,16 +27,18 @@
         <satoken.version>1.34.0</satoken.version>
         <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
         <p6spy.version>3.9.1</p6spy.version>
-        <hutool.version>5.8.15</hutool.version>
+        <hutool.version>5.8.18</hutool.version>
         <okhttp.version>4.10.0</okhttp.version>
         <spring-boot-admin.version>2.7.10</spring-boot-admin.version>
-        <redisson.version>3.20.0</redisson.version>
+        <redisson.version>3.20.1</redisson.version>
         <lock4j.version>2.2.3</lock4j.version>
         <dynamic-ds.version>3.5.2</dynamic-ds.version>
         <alibaba-ttl.version>2.14.2</alibaba-ttl.version>
-        <xxl-job.version>2.3.1</xxl-job.version>
+        <xxl-job.version>2.4.0</xxl-job.version>
         <lombok.version>1.18.26</lombok.version>
         <bouncycastle.version>1.72</bouncycastle.version>
+        <!-- 离线IP地址定位库 -->
+        <ip2region.version>2.7.0</ip2region.version>
 
         <!-- 临时修复 snakeyaml 漏洞 -->
         <snakeyaml.version>1.33</snakeyaml.version>
@@ -258,6 +260,13 @@
                 <version>${alibaba-ttl.version}</version>
             </dependency>
 
+            <!-- 离线IP地址定位库 ip2region -->
+            <dependency>
+                <groupId>org.lionsoul</groupId>
+                <artifactId>ip2region</artifactId>
+                <version>${ip2region.version}</version>
+            </dependency>
+
             <!-- 临时修复 snakeyaml 漏洞 -->
             <dependency>
                 <groupId>org.yaml</groupId>

+ 1 - 1
ruoyi-admin/pom.xml

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

+ 26 - 2
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java

@@ -10,10 +10,12 @@ import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.enums.CaptchaType;
 import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.email.MailUtils;
 import com.ruoyi.common.utils.redis.RedisUtils;
 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;
@@ -47,6 +49,7 @@ public class CaptchaController {
     private final CaptchaProperties captchaProperties;
     private final SmsProperties smsProperties;
     private final ISysConfigService configService;
+    private final MailProperties mailProperties;
 
     /**
      * 短信验证码
@@ -54,8 +57,7 @@ public class CaptchaController {
      * @param phonenumber 用户手机号
      */
     @GetMapping("/captchaSms")
-    public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}")
-                              String phonenumber) {
+    public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
         if (!smsProperties.getEnabled()) {
             return R.fail("当前系统没有开启短信功能!");
         }
@@ -76,6 +78,28 @@ public class CaptchaController {
     }
 
     /**
+     * 邮箱验证码
+     *
+     * @param email 邮箱
+     */
+    @GetMapping("/captchaEmail")
+    public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
+        if (!mailProperties.getEnabled()) {
+            return R.fail("当前系统没有开启邮箱功能!");
+        }
+        String key = CacheConstants.CAPTCHA_CODE_KEY + email;
+        String code = RandomUtil.randomNumbers(4);
+        RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+        try {
+            MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
+        } catch (Exception e) {
+            log.error("验证码短信发送异常 => {}", e.getMessage());
+            return R.fail(e.getMessage());
+        }
+        return R.ok();
+    }
+
+    /**
      * 生成验证码
      */
     @GetMapping("/captchaImage")

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

@@ -33,7 +33,6 @@ public class CacheController {
     private final static List<SysCache> CACHES = new ArrayList<>();
 
     static {
-        CACHES.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
         CACHES.add(new SysCache(CacheConstants.ONLINE_TOKEN_KEY, "在线用户"));
         CACHES.add(new SysCache(CacheNames.SYS_CONFIG, "配置信息"));
         CACHES.add(new SysCache(CacheNames.SYS_DICT, "数据字典"));

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

@@ -45,7 +45,7 @@ public class SysUserOnlineController extends BaseController {
         List<String> keys = StpUtil.searchTokenValue("", 0, -1, false);
         List<UserOnlineDTO> userOnlineDTOList = new ArrayList<>();
         for (String key : keys) {
-            String token = key.replace(CacheConstants.LOGIN_TOKEN_KEY, "");
+            String token = StringUtils.substringAfterLast(key, ":");
             // 如果已经过期则跳过
             if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
                 continue;

+ 17 - 1
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java

@@ -5,6 +5,7 @@ import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.domain.entity.SysMenu;
 import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.EmailLoginBody;
 import com.ruoyi.common.core.domain.model.LoginBody;
 import com.ruoyi.common.core.domain.model.LoginUser;
 import com.ruoyi.common.core.domain.model.SmsLoginBody;
@@ -57,7 +58,7 @@ public class SysLoginController {
     }
 
     /**
-     * 短信登录(示例)
+     * 短信登录
      *
      * @param smsLoginBody 登录信息
      * @return 结果
@@ -73,6 +74,21 @@ public class SysLoginController {
     }
 
     /**
+     * 邮件登录
+     *
+     * @param body 登录信息
+     * @return 结果
+     */
+    @PostMapping("/emailLogin")
+    public R<Map<String, Object>> emailLogin(@Validated @RequestBody EmailLoginBody body) {
+        Map<String, Object> ajax = new HashMap<>();
+        // 生成令牌
+        String token = loginService.emailLogin(body.getEmail(), body.getEmailCode());
+        ajax.put(Constants.TOKEN, token);
+        return R.ok(ajax);
+    }
+
+    /**
      * 小程序登录(示例)
      *
      * @param xcxCode 小程序code

+ 1 - 9
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssController.java

@@ -2,10 +2,7 @@ package com.ruoyi.web.controller.system;
 
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
-import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.http.HttpException;
-import cn.hutool.http.HttpUtil;
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.PageQuery;
@@ -13,11 +10,6 @@ import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.core.validate.QueryGroup;
 import com.ruoyi.common.enums.BusinessType;
-import com.ruoyi.common.exception.ServiceException;
-import com.ruoyi.common.utils.file.FileUtils;
-import com.ruoyi.oss.core.OssClient;
-import com.ruoyi.oss.factory.OssFactory;
-import com.ruoyi.system.domain.SysOss;
 import com.ruoyi.system.domain.bo.SysOssBo;
 import com.ruoyi.system.domain.vo.SysOssVo;
 import com.ruoyi.system.service.ISysOssService;
@@ -80,7 +72,7 @@ public class SysOssController extends BaseController {
     @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public R<Map<String, String>> upload(@RequestPart("file") MultipartFile file) {
         if (ObjectUtil.isNull(file)) {
-            throw new ServiceException("上传文件不能为空");
+            return R.fail("上传文件不能为空");
         }
         SysOssVo oss = iSysOssService.upload(file);
         Map<String, String> map = new HashMap<>(2);

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

@@ -1,22 +1,15 @@
 package com.ruoyi.web.controller.system;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
-import cn.dev33.satoken.exception.NotLoginException;
-import cn.dev33.satoken.stp.StpUtil;
-import cn.hutool.core.collection.CollUtil;
 import com.ruoyi.common.annotation.Log;
-import com.ruoyi.common.constant.CacheConstants;
-import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.PageQuery;
 import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.domain.entity.SysDept;
 import com.ruoyi.common.core.domain.entity.SysRole;
 import com.ruoyi.common.core.domain.entity.SysUser;
-import com.ruoyi.common.core.domain.model.LoginUser;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.enums.BusinessType;
-import com.ruoyi.common.helper.LoginHelper;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.system.domain.SysUserRole;
 import com.ruoyi.system.service.ISysDeptService;
@@ -112,25 +105,7 @@ public class SysRoleController extends BaseController {
         }
 
         if (roleService.updateRole(role) > 0) {
-            List<String> keys = StpUtil.searchTokenValue("", 0, -1, false);
-            if (CollUtil.isEmpty(keys)) {
-                return R.ok();
-            }
-            // 角色关联的在线用户量过大会导致redis阻塞卡顿 谨慎操作
-            keys.parallelStream().forEach(key -> {
-                String token = key.replace(CacheConstants.LOGIN_TOKEN_KEY, "");
-                // 如果已经过期则跳过
-                if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
-                    return;
-                }
-                LoginUser loginUser = LoginHelper.getLoginUser(token);
-                if (loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(role.getRoleId()))) {
-                    try {
-                        StpUtil.logoutByTokenValue(token);
-                    } catch (NotLoginException ignored) {
-                    }
-                }
-            });
+            roleService.cleanOnlineUserByRole(role.getRoleId());
             return R.ok();
         }
         return R.fail("修改角色'" + role.getRoleName() + "'失败,请联系管理员");

+ 1 - 1
ruoyi-admin/src/main/resources/application.yml

@@ -51,7 +51,7 @@ logging:
   level:
     com.ruoyi: @logging.level@
     org.springframework: warn
-  config: classpath:logback.xml
+  config: classpath:logback-plus.xml
 
 # 用户配置
 user:

+ 4 - 0
ruoyi-admin/src/main/resources/i18n/messages.properties

@@ -18,6 +18,7 @@ user.password.not.blank=用户密码不能为空
 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 user.password.not.valid=* 5-50个字符
 user.email.not.valid=邮箱格式错误
+user.email.not.blank=邮箱不能为空
 user.phonenumber.not.blank=用户手机号不能为空
 user.mobile.phone.number.not.valid=手机号格式错误
 user.login.success=登录成功
@@ -42,4 +43,7 @@ rate.limiter.message=访问过于频繁,请稍候再试
 sms.code.not.blank=短信验证码不能为空
 sms.code.retry.limit.count=短信验证码输入错误{0}次
 sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
+email.code.not.blank=邮箱验证码不能为空
+email.code.retry.limit.count=邮箱验证码输入错误{0}次
+email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
 xcx.code.not.blank=小程序code不能为空

+ 4 - 0
ruoyi-admin/src/main/resources/i18n/messages_en_US.properties

@@ -18,6 +18,7 @@ user.password.not.blank=Password cannot be empty
 user.password.length.valid=Password length must be between {min} and {max} characters
 user.password.not.valid=* 5-50 characters
 user.email.not.valid=Mailbox format error
+user.email.not.blank=Mailbox cannot be blank
 user.phonenumber.not.blank=Phone number cannot be blank
 user.mobile.phone.number.not.valid=Phone number format error
 user.login.success=Login successful
@@ -42,4 +43,7 @@ rate.limiter.message=Visit too frequently, please try again later
 sms.code.not.blank=Sms code cannot be blank
 sms.code.retry.limit.count=Sms code input error {0} times
 sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes
+email.code.not.blank=Email code cannot be blank
+email.code.retry.limit.count=Email code input error {0} times
+email.code.retry.limit.exceed=Email code input error {0} times, account locked for {1} minutes
 xcx.code.not.blank=Mini program code cannot be blank

+ 4 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties

@@ -18,6 +18,7 @@ user.password.not.blank=用户密码不能为空
 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 user.password.not.valid=* 5-50个字符
 user.email.not.valid=邮箱格式错误
+user.email.not.blank=邮箱不能为空
 user.phonenumber.not.blank=用户手机号不能为空
 user.mobile.phone.number.not.valid=手机号格式错误
 user.login.success=登录成功
@@ -42,4 +43,7 @@ rate.limiter.message=访问过于频繁,请稍候再试
 sms.code.not.blank=短信验证码不能为空
 sms.code.retry.limit.count=短信验证码输入错误{0}次
 sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
+email.code.not.blank=邮箱验证码不能为空
+email.code.retry.limit.count=邮箱验证码输入错误{0}次
+email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
 xcx.code.not.blank=小程序code不能为空

BIN
ruoyi-admin/src/main/resources/ip2region.xdb


+ 0 - 0
ruoyi-admin/src/main/resources/logback.xml → ruoyi-admin/src/main/resources/logback-plus.xml


+ 7 - 1
ruoyi-common/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.6.0</version>
+        <version>4.7.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -159,6 +159,12 @@
             <artifactId>bcprov-jdk15to18</artifactId>
         </dependency>
 
+        <!-- 离线IP地址定位库 -->
+        <dependency>
+            <groupId>org.lionsoul</groupId>
+            <artifactId>ip2region</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

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

@@ -8,11 +8,6 @@ package com.ruoyi.common.constant;
 public interface CacheConstants {
 
     /**
-     * 登录用户 redis key
-     */
-    String LOGIN_TOKEN_KEY = "Authorization:login:token:";
-
-    /**
      * 在线用户 redis key
      */
     String ONLINE_TOKEN_KEY = "online_tokens:";

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

@@ -0,0 +1,30 @@
+package com.ruoyi.common.core.domain.model;
+
+import lombok.Data;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class EmailLoginBody {
+
+    /**
+     * 邮箱
+     */
+    @NotBlank(message = "{user.email.not.blank}")
+    @Email(message = "{user.email.not.valid}")
+    private String email;
+
+    /**
+     * 邮箱code
+     */
+    @NotBlank(message = "{email.code.not.blank}")
+    private String emailCode;
+
+}

+ 2 - 2
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/SmsLoginBody.java

@@ -14,13 +14,13 @@ import javax.validation.constraints.NotBlank;
 public class SmsLoginBody {
 
     /**
-     * 用户名
+     * 手机号
      */
     @NotBlank(message = "{user.phonenumber.not.blank}")
     private String phonenumber;
 
     /**
-     * 用户密码
+     * 短信code
      */
     @NotBlank(message = "{sms.code.not.blank}")
     private String smsCode;

+ 5 - 0
ruoyi-common/src/main/java/com/ruoyi/common/enums/LoginType.java

@@ -23,6 +23,11 @@ public enum LoginType {
     SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
 
     /**
+     * 邮箱登录
+     */
+    EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
+
+    /**
      * 小程序登录
      */
     XCX("", "");

+ 1 - 1
ruoyi-common/src/main/java/com/ruoyi/common/excel/DefaultExcelListener.java

@@ -42,7 +42,7 @@ public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements
     private ExcelResult<T> excelResult;
 
     public DefaultExcelListener(boolean isValidate) {
-        this.excelResult = new DefautExcelResult<>();
+        this.excelResult = new DefaultExcelResult<>();
         this.isValidate = isValidate;
     }
 

+ 4 - 4
ruoyi-common/src/main/java/com/ruoyi/common/excel/DefautExcelResult.java → ruoyi-common/src/main/java/com/ruoyi/common/excel/DefaultExcelResult.java

@@ -12,7 +12,7 @@ import java.util.List;
  * @author Yjoioooo
  * @author Lion Li
  */
-public class DefautExcelResult<T> implements ExcelResult<T> {
+public class DefaultExcelResult<T> implements ExcelResult<T> {
 
     /**
      * 数据对象list
@@ -26,17 +26,17 @@ public class DefautExcelResult<T> implements ExcelResult<T> {
     @Setter
     private List<String> errorList;
 
-    public DefautExcelResult() {
+    public DefaultExcelResult() {
         this.list = new ArrayList<>();
         this.errorList = new ArrayList<>();
     }
 
-    public DefautExcelResult(List<T> list, List<String> errorList) {
+    public DefaultExcelResult(List<T> list, List<String> errorList) {
         this.list = list;
         this.errorList = errorList;
     }
 
-    public DefautExcelResult(ExcelResult<T> excelResult) {
+    public DefaultExcelResult(ExcelResult<T> excelResult) {
         this.list = excelResult.getList();
         this.errorList = excelResult.getErrorList();
     }

+ 2 - 2
ruoyi-common/src/main/java/com/ruoyi/common/helper/DataBaseHelper.java

@@ -66,7 +66,7 @@ public class DataBaseHelper {
             // instr(',0,100,101,' , ',100,') <> 0
             return "instr(','||" + var2 + "||',' , '," + var + ",') <> 0";
         }
-        // find_in_set(100 , '0,100,101')
-        return "find_in_set(" + var + " , " + var2 + ") <> 0";
+        // find_in_set('100' , '0,100,101')
+        return "find_in_set('" + var + "' , " + var2 + ") <> 0";
     }
 }

+ 29 - 0
ruoyi-common/src/main/java/com/ruoyi/common/helper/DataPermissionHelper.java

@@ -10,6 +10,7 @@ import lombok.NoArgsConstructor;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Supplier;
 
 /**
  * 数据权限助手
@@ -61,4 +62,32 @@ public class DataPermissionHelper {
         InterceptorIgnoreHelper.clearIgnoreStrategy();
     }
 
+    /**
+     * 在忽略数据权限中执行
+     *
+     * @param handle 处理执行方法
+     */
+    public static void ignore(Runnable handle) {
+        enableIgnore();
+        try {
+            handle.run();
+        } finally {
+            disableIgnore();
+        }
+    }
+
+    /**
+     * 在忽略数据权限中执行
+     *
+     * @param handle 处理执行方法
+     */
+    public static <T> T ignore(Supplier<T> handle) {
+        enableIgnore();
+        try {
+            return handle.get();
+        } finally {
+            disableIgnore();
+        }
+    }
+
 }

+ 2 - 1
ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/DeptNameTranslationImpl.java

@@ -18,7 +18,8 @@ import org.springframework.stereotype.Component;
 public class DeptNameTranslationImpl implements TranslationInterface<String> {
 
     private final DeptService deptService;
-
+    
+    @Override
     public String translation(Object key, String other) {
         return deptService.selectDeptNameByIds(key.toString());
     }

+ 1 - 0
ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/DictTypeTranslationImpl.java

@@ -20,6 +20,7 @@ public class DictTypeTranslationImpl implements TranslationInterface<String> {
 
     private final DictService dictService;
 
+    @Override
     public String translation(Object key, String other) {
         if (key instanceof String && StringUtils.isNotBlank(other)) {
             return dictService.getDictLabel(other, key.toString());

+ 1 - 0
ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/OssUrlTranslationImpl.java

@@ -19,6 +19,7 @@ public class OssUrlTranslationImpl implements TranslationInterface<String> {
 
     private final OssService ossService;
 
+    @Override
     public String translation(Object key, String other) {
         return ossService.selectUrlByIds(key.toString());
     }

+ 1 - 0
ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/UserNameTranslationImpl.java

@@ -19,6 +19,7 @@ public class UserNameTranslationImpl implements TranslationInterface<String> {
 
     private final UserService userService;
 
+    @Override
     public String translation(Object key, String other) {
         if (key instanceof Long) {
             return userService.selectUserNameById((Long) key);

+ 243 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/EncryptUtils.java

@@ -0,0 +1,243 @@
+package com.ruoyi.common.utils;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.RSA;
+import cn.hutool.crypto.asymmetric.SM2;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 安全相关工具类
+ *
+ * @author 老马
+ */
+public class EncryptUtils {
+    /**
+     * 公钥
+     */
+    public static final String PUBLIC_KEY = "publicKey";
+    /**
+     * 私钥
+     */
+    public static final String PRIVATE_KEY = "privateKey";
+
+    /**
+     * Base64加密
+     *
+     * @param data 待加密数据
+     * @return 加密后字符串
+     */
+    public static String encryptByBase64(String data) {
+        return Base64.encode(data, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Base64解密
+     *
+     * @param data 待解密数据
+     * @return 解密后字符串
+     */
+    public static String decryptByBase64(String data) {
+        return Base64.decodeStr(data, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * AES加密
+     *
+     * @param data     待解密数据
+     * @param password 秘钥字符串
+     * @return 加密后字符串, 采用Base64编码
+     */
+    public static String encryptByAes(String data, String password) {
+        if (StrUtil.isBlank(password)) {
+            throw new IllegalArgumentException("AES需要传入秘钥信息");
+        }
+        // aes算法的秘钥要求是16位、24位、32位
+        int[] array = {16, 24, 32};
+        if (!ArrayUtil.contains(array, password.length())) {
+            throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
+        }
+        return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * AES解密
+     *
+     * @param data     待解密数据
+     * @param password 秘钥字符串
+     * @return 解密后字符串
+     */
+    public static String decryptByAes(String data, String password) {
+        if (StrUtil.isBlank(password)) {
+            throw new IllegalArgumentException("AES需要传入秘钥信息");
+        }
+        // aes算法的秘钥要求是16位、24位、32位
+        int[] array = {16, 24, 32};
+        if (!ArrayUtil.contains(array, password.length())) {
+            throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
+        }
+        return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * sm4加密
+     *
+     * @param data     待加密数据
+     * @param password 秘钥字符串
+     * @return 加密后字符串, 采用Base64编码
+     */
+    public static String encryptBySm4(String data, String password) {
+        if (StrUtil.isBlank(password)) {
+            throw new IllegalArgumentException("SM4需要传入秘钥信息");
+        }
+        // sm4算法的秘钥要求是16位长度
+        int sm4PasswordLength = 16;
+        if (sm4PasswordLength != password.length()) {
+            throw new IllegalArgumentException("SM4秘钥长度要求为16位");
+        }
+        return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * sm4解密
+     *
+     * @param data     待解密数据
+     * @param password 秘钥字符串
+     * @return 解密后字符串
+     */
+    public static String decryptBySm4(String data, String password) {
+        if (StrUtil.isBlank(password)) {
+            throw new IllegalArgumentException("SM4需要传入秘钥信息");
+        }
+        // sm4算法的秘钥要求是16位长度
+        int sm4PasswordLength = 16;
+        if (sm4PasswordLength != password.length()) {
+            throw new IllegalArgumentException("SM4秘钥长度要求为16位");
+        }
+        return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 产生sm2加解密需要的公钥和私钥
+     *
+     * @return 公私钥Map
+     */
+    public static Map<String, String> generateSm2Key() {
+        Map<String, String> keyMap = new HashMap<>(2);
+        SM2 sm2 = SmUtil.sm2();
+        keyMap.put(PRIVATE_KEY, sm2.getPrivateKeyBase64());
+        keyMap.put(PUBLIC_KEY, sm2.getPublicKeyBase64());
+        return keyMap;
+    }
+
+    /**
+     * sm2公钥加密
+     *
+     * @param data      待加密数据
+     * @param publicKey 公钥
+     * @return 加密后字符串, 采用Base64编码
+     */
+    public static String encryptBySm2(String data, String publicKey) {
+        if (StrUtil.isBlank(publicKey)) {
+            throw new IllegalArgumentException("SM2需要传入公钥进行加密");
+        }
+        SM2 sm2 = SmUtil.sm2(null, publicKey);
+        return sm2.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+    }
+
+    /**
+     * sm2私钥解密
+     *
+     * @param data       待加密数据
+     * @param privateKey 私钥
+     * @return 解密后字符串
+     */
+    public static String decryptBySm2(String data, String privateKey) {
+        if (StrUtil.isBlank(privateKey)) {
+            throw new IllegalArgumentException("SM2需要传入私钥进行解密");
+        }
+        SM2 sm2 = SmUtil.sm2(privateKey, null);
+        return sm2.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 产生RSA加解密需要的公钥和私钥
+     *
+     * @return 公私钥Map
+     */
+    public static Map<String, String> generateRsaKey() {
+        Map<String, String> keyMap = new HashMap<>(2);
+        RSA rsa = SecureUtil.rsa();
+        keyMap.put(PRIVATE_KEY, rsa.getPrivateKeyBase64());
+        keyMap.put(PUBLIC_KEY, rsa.getPublicKeyBase64());
+        return keyMap;
+    }
+
+    /**
+     * rsa公钥加密
+     *
+     * @param data      待加密数据
+     * @param publicKey 公钥
+     * @return 加密后字符串, 采用Base64编码
+     */
+    public static String encryptByRsa(String data, String publicKey) {
+        if (StrUtil.isBlank(publicKey)) {
+            throw new IllegalArgumentException("RSA需要传入公钥进行加密");
+        }
+        RSA rsa = SecureUtil.rsa(null, publicKey);
+        return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
+    }
+
+    /**
+     * rsa私钥解密
+     *
+     * @param data       待加密数据
+     * @param privateKey 私钥
+     * @return 解密后字符串
+     */
+    public static String decryptByRsa(String data, String privateKey) {
+        if (StrUtil.isBlank(privateKey)) {
+            throw new IllegalArgumentException("RSA需要传入私钥进行解密");
+        }
+        RSA rsa = SecureUtil.rsa(privateKey, null);
+        return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * md5加密
+     *
+     * @param data 待加密数据
+     * @return 加密后字符串, 采用Hex编码
+     */
+    public static String encryptByMd5(String data) {
+        return SecureUtil.md5(data);
+    }
+
+    /**
+     * sha256加密
+     *
+     * @param data 待加密数据
+     * @return 加密后字符串, 采用Hex编码
+     */
+    public static String encryptBySha256(String data) {
+        return SecureUtil.sha256(data);
+    }
+
+    /**
+     * sm3加密
+     *
+     * @param data 待加密数据
+     * @return 加密后字符串, 采用Hex编码
+     */
+    public static String encryptBySm3(String data) {
+        return SmUtil.sm3(data);
+    }
+
+}

+ 2 - 29
ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java

@@ -1,12 +1,7 @@
 package com.ruoyi.common.utils.ip;
 
-import cn.hutool.core.lang.Dict;
 import cn.hutool.core.net.NetUtil;
 import cn.hutool.http.HtmlUtil;
-import cn.hutool.http.HttpUtil;
-import com.ruoyi.common.config.RuoYiConfig;
-import com.ruoyi.common.constant.Constants;
-import com.ruoyi.common.utils.JsonUtils;
 import com.ruoyi.common.utils.StringUtils;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
@@ -21,40 +16,18 @@ import lombok.extern.slf4j.Slf4j;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class AddressUtils {
 
-    // IP地址查询
-    public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp";
-
     // 未知地址
     public static final String UNKNOWN = "XX XX";
 
     public static String getRealAddressByIP(String ip) {
-        String address = UNKNOWN;
         if (StringUtils.isBlank(ip)) {
-            return address;
+            return UNKNOWN;
         }
         // 内网不查询
         ip = "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip);
         if (NetUtil.isInnerIP(ip)) {
             return "内网IP";
         }
-        if (RuoYiConfig.isAddressEnabled()) {
-            try {
-                String rspStr = HttpUtil.createGet(IP_URL)
-                    .body("ip=" + ip + "&json=true", Constants.GBK)
-                    .execute()
-                    .body();
-                if (StringUtils.isEmpty(rspStr)) {
-                    log.error("获取地理位置异常 {}", ip);
-                    return UNKNOWN;
-                }
-                Dict obj = JsonUtils.parseMap(rspStr);
-                String region = obj.getStr("pro");
-                String city = obj.getStr("city");
-                return String.format("%s %s", region, city);
-            } catch (Exception e) {
-                log.error("获取地理位置异常 {}", ip);
-            }
-        }
-        return UNKNOWN;
+        return RegionUtils.getCityInfo(ip);
     }
 }

+ 66 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/RegionUtils.java

@@ -0,0 +1,66 @@
+package com.ruoyi.common.utils.ip;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ClassPathResource;
+import cn.hutool.core.util.ObjectUtil;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.file.FileUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.lionsoul.ip2region.xdb.Searcher;
+
+import java.io.File;
+
+/**
+ * 根据ip地址定位工具类,离线方式
+ * 参考地址:<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">集成 ip2region 实现离线IP地址定位库</a>
+ *
+ * @author lishuyan
+ */
+@Slf4j
+public class RegionUtils {
+
+    private static final Searcher SEARCHER;
+
+    static {
+        String fileName = "/ip2region.xdb";
+        File existFile = FileUtils.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + fileName);
+        if (!FileUtils.exist(existFile)) {
+            ClassPathResource fileStream = new ClassPathResource(fileName);
+            if (ObjectUtil.isEmpty(fileStream.getStream())) {
+                throw new ServiceException("RegionUtils初始化失败,原因:IP地址库数据不存在!");
+            }
+            FileUtils.writeFromStream(fileStream.getStream(), existFile);
+        }
+
+        String dbPath = existFile.getPath();
+
+        // 1、从 dbPath 加载整个 xdb 到内存。
+        byte[] cBuff;
+        try {
+            cBuff = Searcher.loadContentFromFile(dbPath);
+        } catch (Exception e) {
+            throw new ServiceException("RegionUtils初始化失败,原因:从ip2region.xdb文件加载内容失败!" + e.getMessage());
+        }
+        // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
+        try {
+            SEARCHER = Searcher.newWithBuffer(cBuff);
+        } catch (Exception e) {
+            throw new ServiceException("RegionUtils初始化失败,原因:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据IP地址离线获取城市
+     */
+    public static String getCityInfo(String ip) {
+        try {
+            ip = ip.trim();
+            // 3、执行查询
+            String region = SEARCHER.search(ip);
+            return region.replace("0|", "").replace("|0", "");
+        } catch (Exception e) {
+            log.error("IP地址离线获取城市异常 {}", ip);
+            return "未知";
+        }
+    }
+}

+ 1 - 1
ruoyi-demo/pom.xml

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

+ 12 - 0
ruoyi-demo/src/main/java/com/ruoyi/demo/controller/RedisRateLimiterController.java

@@ -49,4 +49,16 @@ public class RedisRateLimiterController {
         return R.ok("操作成功", value);
     }
 
+    /**
+     * 测试请求IP限流(key基于参数获取)
+     * 同一IP请求受影响
+     *
+     * 简单变量获取 #变量 复杂表达式 #{#变量 != 1 ? 1 : 0}
+     */
+    @RateLimiter(count = 2, time = 10, limitType = LimitType.IP, key = "#value")
+    @GetMapping("/testObj")
+    public R<String> testObj(String value) {
+        return R.ok("操作成功", value);
+    }
+
 }

+ 1 - 1
ruoyi-extend/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.6.0</version>
+        <version>4.7.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.6.0</version>
+        <version>4.7.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <packaging>jar</packaging>

+ 3 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml

@@ -6,6 +6,9 @@ spring:
   profiles:
     active: @profiles.active@
 
+logging:
+  config: classpath:logback-plus.xml
+
 --- # 监控中心服务端配置
 spring:
   security:

+ 0 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/resources/logback.xml → ruoyi-extend/ruoyi-monitor-admin/src/main/resources/logback-plus.xml


+ 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.6.0</version>
+        <version>4.7.0</version>
     </parent>
     <artifactId>ruoyi-xxl-job-admin</artifactId>
     <packaging>jar</packaging>

+ 7 - 0
ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java

@@ -1,5 +1,6 @@
 package com.xxl.job.admin.controller;
 
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
 import com.xxl.job.admin.core.model.XxlJobGroup;
 import com.xxl.job.admin.core.model.XxlJobRegistry;
 import com.xxl.job.admin.core.util.I18nUtil;
@@ -35,12 +36,14 @@ public class JobGroupController {
     private XxlJobRegistryDao xxlJobRegistryDao;
 
     @RequestMapping
+    @PermissionLimit(adminuser = true)
     public String index(Model model) {
         return "jobgroup/jobgroup.index";
     }
 
     @RequestMapping("/pageList")
     @ResponseBody
+    @PermissionLimit(adminuser = true)
     public Map<String, Object> pageList(HttpServletRequest request,
                                         @RequestParam(required = false, defaultValue = "0") int start,
                                         @RequestParam(required = false, defaultValue = "10") int length,
@@ -60,6 +63,7 @@ public class JobGroupController {
 
     @RequestMapping("/save")
     @ResponseBody
+    @PermissionLimit(adminuser = true)
     public ReturnT<String> save(XxlJobGroup xxlJobGroup) {
 
         // valid
@@ -103,6 +107,7 @@ public class JobGroupController {
 
     @RequestMapping("/update")
     @ResponseBody
+    @PermissionLimit(adminuser = true)
     public ReturnT<String> update(XxlJobGroup xxlJobGroup) {
         // valid
         if (xxlJobGroup.getAppname() == null || xxlJobGroup.getAppname().trim().length() == 0) {
@@ -171,6 +176,7 @@ public class JobGroupController {
 
     @RequestMapping("/remove")
     @ResponseBody
+    @PermissionLimit(adminuser = true)
     public ReturnT<String> remove(int id) {
 
         // valid
@@ -190,6 +196,7 @@ public class JobGroupController {
 
     @RequestMapping("/loadById")
     @ResponseBody
+    @PermissionLimit(adminuser = true)
     public ReturnT<XxlJobGroup> loadById(int id) {
         XxlJobGroup jobGroup = xxlJobGroupDao.load(id);
         return jobGroup != null ? new ReturnT<XxlJobGroup>(jobGroup) : new ReturnT<XxlJobGroup>(ReturnT.FAIL_CODE, null);

+ 10 - 6
ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java

@@ -130,22 +130,26 @@ public class JobLogController {
 
         model.addAttribute("triggerCode", jobLog.getTriggerCode());
         model.addAttribute("handleCode", jobLog.getHandleCode());
-        model.addAttribute("executorAddress", jobLog.getExecutorAddress());
-        model.addAttribute("triggerTime", jobLog.getTriggerTime().getTime());
         model.addAttribute("logId", jobLog.getId());
         return "joblog/joblog.detail";
     }
 
     @RequestMapping("/logDetailCat")
     @ResponseBody
-    public ReturnT<LogResult> logDetailCat(String executorAddress, long triggerTime, long logId, int fromLineNum) {
+    public ReturnT<LogResult> logDetailCat(long logId, int fromLineNum) {
         try {
-            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(executorAddress);
-            ReturnT<LogResult> logResult = executorBiz.log(new LogParam(triggerTime, logId, fromLineNum));
+            // valid
+            XxlJobLog jobLog = xxlJobLogDao.load(logId);	// todo, need to improve performance
+            if (jobLog == null) {
+                return new ReturnT<LogResult>(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_logid_unvalid"));
+            }
+
+            // log cat
+            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(jobLog.getExecutorAddress());
+            ReturnT<LogResult> logResult = executorBiz.log(new LogParam(jobLog.getTriggerTime().getTime(), logId, fromLineNum));
 
             // is end
             if (logResult.getContent() != null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) {
-                XxlJobLog jobLog = xxlJobLogDao.load(logId);
                 if (jobLog.getHandleCode() > 0) {
                     logResult.getContent().setEnd(true);
                 }

+ 3 - 0
ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application.yml

@@ -16,6 +16,9 @@ spring:
     resources:
       static-locations: classpath:/static/
 
+logging:
+  config: classpath:logback-plus.xml
+
 --- # mybatis 配置
 mybatis:
   mapper-locations: classpath:/mybatis-mapper/*Mapper.xml

+ 1 - 1
ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/i18n/message_en.properties

@@ -1,6 +1,6 @@
 admin_name=Scheduling Center
 admin_name_full=Distributed Task Scheduling Platform XXL-JOB
-admin_version=2.3.1
+admin_version=2.4.0
 admin_i18n=en
 
 ## system

+ 1 - 1
ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties

@@ -1,6 +1,6 @@
 admin_name=任务调度中心
 admin_name_full=分布式任务调度平台XXL-JOB
-admin_version=2.3.1
+admin_version=2.4.0
 admin_i18n=
 
 ## system

+ 1 - 1
ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties

@@ -1,6 +1,6 @@
 admin_name=任務調度中心
 admin_name_full=分布式任務調度平臺XXL-JOB
-admin_version=2.3.1
+admin_version=2.4.0
 admin_i18n=
 
 ## system

+ 0 - 0
ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/logback.xml → ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/logback-plus.xml


+ 0 - 2
ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js

@@ -25,8 +25,6 @@ $(function() {
             async: false,   // sync, make log ordered
             url : base_url + '/joblog/logDetailCat',
             data : {
-                "executorAddress":executorAddress,
-                "triggerTime":triggerTime,
                 "logId":logId,
                 "fromLineNum":fromLineNum
             },

+ 1 - 3
ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl

@@ -62,11 +62,9 @@
     // 参数
     var triggerCode = '${triggerCode}';
     var handleCode = '${handleCode}';
-    var executorAddress = '${executorAddress!}';
-    var triggerTime = '${triggerTime?c}';
     var logId = '${logId}';
 </script>
 <script src="${request.contextPath}/static/js/joblog.detail.1.js"></script>
 
 </body>
-</html>
+</html>

+ 1 - 1
ruoyi-framework/pom.xml

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

+ 9 - 1
ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java

@@ -18,6 +18,7 @@ import org.redisson.api.RateType;
 import org.springframework.core.DefaultParameterNameDiscoverer;
 import org.springframework.core.ParameterNameDiscoverer;
 import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.ParserContext;
 import org.springframework.expression.common.TemplateParserContext;
@@ -102,7 +103,14 @@ public class RateLimiterAspect {
             }
             // 解析返回给key
             try {
-                key = parser.parseExpression(key, parserContext).getValue(context, String.class) + ":";
+                Expression expression;
+                if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
+                    && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
+                    expression = parser.parseExpression(key, parserContext);
+                } else {
+                    expression = parser.parseExpression(key);
+                }
+                key = expression.getValue(context, String.class) + ":";
             } catch (Exception e) {
                 throw new ServiceException("限流key解析异常!请联系管理员!");
             }

+ 6 - 3
ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisDecryptInterceptor.java

@@ -1,6 +1,6 @@
 package com.ruoyi.framework.encrypt;
 
-import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.ruoyi.common.annotation.EncryptField;
 import com.ruoyi.common.encrypt.EncryptContext;
@@ -62,12 +62,12 @@ public class MybatisDecryptInterceptor implements Interceptor {
         }
         if (sourceObject instanceof List<?>) {
             List<?> sourceList = (List<?>) sourceObject;
-            if(CollectionUtil.isEmpty(sourceList)) {
+            if(CollUtil.isEmpty(sourceList)) {
                 return;
             }
             // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
             Object firstItem = sourceList.get(0);
-            if (CollectionUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
+            if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
                 return;
             }
             ((List<?>) sourceObject).forEach(this::decryptHandler);
@@ -91,6 +91,9 @@ public class MybatisDecryptInterceptor implements Interceptor {
      * @return 加密后结果
      */
     private String decryptField(String value, Field field) {
+        if (ObjectUtil.isNull(value)) {
+            return null;
+        }
         EncryptField encryptField = field.getAnnotation(EncryptField.class);
         EncryptContext encryptContext = new EncryptContext();
         encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());

+ 6 - 3
ruoyi-framework/src/main/java/com/ruoyi/framework/encrypt/MybatisEncryptInterceptor.java

@@ -1,6 +1,6 @@
 package com.ruoyi.framework.encrypt;
 
-import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.ruoyi.common.annotation.EncryptField;
 import com.ruoyi.common.encrypt.EncryptContext;
@@ -72,12 +72,12 @@ public class MybatisEncryptInterceptor implements Interceptor {
         }
         if (sourceObject instanceof List<?>) {
             List<?> sourceList = (List<?>) sourceObject;
-            if(CollectionUtil.isEmpty(sourceList)) {
+            if(CollUtil.isEmpty(sourceList)) {
                 return;
             }
             // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
             Object firstItem = sourceList.get(0);
-            if (CollectionUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
+            if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
                 return;
             }
             ((List<?>) sourceObject).forEach(this::encryptHandler);
@@ -101,6 +101,9 @@ public class MybatisEncryptInterceptor implements Interceptor {
      * @return 加密后结果
      */
     private String encryptField(String value, Field field) {
+        if (ObjectUtil.isNull(value)) {
+            return null;
+        }
         EncryptField encryptField = field.getAnnotation(EncryptField.class);
         EncryptContext encryptContext = new EncryptContext();
         encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());

+ 1 - 1
ruoyi-framework/src/main/java/com/ruoyi/framework/handler/CreateAndUpdateMetaObjectHandler.java

@@ -73,7 +73,7 @@ public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
             log.warn("自动注入警告 => 用户未登录");
             return null;
         }
-        return loginUser.getUsername();
+        return ObjectUtil.isNotNull(loginUser) ? loginUser.getUsername() : null;
     }
 
 }

+ 1 - 1
ruoyi-generator/pom.xml

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

+ 2 - 4
ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java

@@ -317,13 +317,11 @@ public class GenTableServiceImpl implements IGenTableService {
                     column.setIsRequired(prevColumn.getIsRequired());
                     column.setHtmlType(prevColumn.getHtmlType());
                 }
-                genTableColumnMapper.updateById(column);
-            } else {
-                genTableColumnMapper.insert(column);
             }
+            saveColumns.add(column);
         });
         if (CollUtil.isNotEmpty(saveColumns)) {
-            genTableColumnMapper.insertBatch(saveColumns);
+            genTableColumnMapper.insertOrUpdateBatch(saveColumns);
         }
         List<GenTableColumn> delColumns = StreamUtils.filter(tableColumns, column -> !dbTableColumnNames.contains(column.getColumnName()));
         if (CollUtil.isNotEmpty(delColumns)) {

+ 1 - 1
ruoyi-job/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>4.6.0</version>
+        <version>4.7.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.6.0</version>
+        <version>4.7.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
ruoyi-sms/pom.xml

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

+ 1 - 1
ruoyi-system/pom.xml

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

+ 8 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java

@@ -77,6 +77,14 @@ public interface SysUserMapper extends BaseMapperPlus<SysUserMapper, SysUser, Sy
     SysUser selectUserByPhonenumber(String phonenumber);
 
     /**
+     * 通过邮箱查询用户
+     *
+     * @param email 邮箱
+     * @return 用户对象信息
+     */
+    SysUser selectUserByEmail(String email);
+
+    /**
      * 通过用户ID查询用户
      *
      * @param userId 用户ID

+ 2 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java

@@ -176,4 +176,6 @@ public interface ISysRoleService {
      * @return 结果
      */
     int insertAuthUsers(Long roleId, Long[] userIds);
+
+    void cleanOnlineUserByRole(Long roleId);
 }

+ 42 - 5
ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java

@@ -32,7 +32,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
-import javax.servlet.http.HttpServletRequest;
 import java.time.Duration;
 import java.util.List;
 import java.util.function.Supplier;
@@ -67,11 +66,10 @@ public class SysLoginService {
      * @return 结果
      */
     public String login(String username, String password, String code, String uuid) {
-        HttpServletRequest request = ServletUtils.getRequest();
         boolean captchaEnabled = configService.selectCaptchaEnabled();
         // 验证码开关
         if (captchaEnabled) {
-            validateCaptcha(username, code, uuid, request);
+            validateCaptcha(username, code, uuid);
         }
         SysUser user = loadUserByUsername(username);
         checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
@@ -100,6 +98,20 @@ public class SysLoginService {
         return StpUtil.getTokenValue();
     }
 
+    public String emailLogin(String email, String emailCode) {
+        // 通过手机号查找用户
+        SysUser user = loadUserByEmail(email);
+
+        checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
+        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        LoginUser loginUser = buildLoginUser(user);
+        // 生成token
+        LoginHelper.loginByDevice(loginUser, DeviceType.APP);
+
+        recordLogininfor(user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
+        recordLoginInfo(user.getUserId(), user.getUserName());
+        return StpUtil.getTokenValue();
+    }
 
     public String xcxLogin(String xcxCode) {
         // xcxCode 为 小程序调用 wx.login 授权后获取
@@ -140,7 +152,6 @@ public class SysLoginService {
      * @param username 用户名
      * @param status   状态
      * @param message  消息内容
-     * @return
      */
     private void recordLogininfor(String username, String status, String message) {
         LogininforEvent logininforEvent = new LogininforEvent();
@@ -164,13 +175,25 @@ public class SysLoginService {
     }
 
     /**
+     * 校验邮箱验证码
+     */
+    private boolean validateEmailCode(String email, String emailCode) {
+        String code = RedisUtils.getCacheObject(CacheConstants.CAPTCHA_CODE_KEY + email);
+        if (StringUtils.isBlank(code)) {
+            recordLogininfor(email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+            throw new CaptchaExpireException();
+        }
+        return code.equals(emailCode);
+    }
+
+    /**
      * 校验验证码
      *
      * @param username 用户名
      * @param code     验证码
      * @param uuid     唯一标识
      */
-    public void validateCaptcha(String username, String code, String uuid, HttpServletRequest request) {
+    public void validateCaptcha(String username, String code, String uuid) {
         String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
         String captcha = RedisUtils.getCacheObject(verifyKey);
         RedisUtils.deleteObject(verifyKey);
@@ -212,6 +235,20 @@ public class SysLoginService {
         return userMapper.selectUserByPhonenumber(phonenumber);
     }
 
+    private SysUser loadUserByEmail(String email) {
+        SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
+            .select(SysUser::getPhonenumber, SysUser::getStatus)
+            .eq(SysUser::getEmail, email));
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", email);
+            throw new UserException("user.not.exists", email);
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", email);
+            throw new UserException("user.blocked", email);
+        }
+        return userMapper.selectUserByEmail(email);
+    }
+
     private SysUser loadUserByOpenid(String openid) {
         // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
         // todo 自行实现 userService.selectUserByOpenid(openid);

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

@@ -3,7 +3,6 @@ package com.ruoyi.system.service;
 import cn.dev33.satoken.secure.BCrypt;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.constant.Constants;
-import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.domain.event.LogininforEvent;
 import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.core.domain.model.RegisterBody;
@@ -19,8 +18,6 @@ import com.ruoyi.common.utils.spring.SpringUtils;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
-import javax.servlet.http.HttpServletRequest;
-
 /**
  * 注册校验方法
  *
@@ -37,7 +34,6 @@ public class SysRegisterService {
      * 注册
      */
     public void register(RegisterBody registerBody) {
-        HttpServletRequest request = ServletUtils.getRequest();
         String username = registerBody.getUsername();
         String password = registerBody.getPassword();
         // 校验用户类型是否存在
@@ -46,7 +42,7 @@ public class SysRegisterService {
         boolean captchaEnabled = configService.selectCaptchaEnabled();
         // 验证码开关
         if (captchaEnabled) {
-            validateCaptcha(username, registerBody.getCode(), registerBody.getUuid(), request);
+            validateCaptcha(username, registerBody.getCode(), registerBody.getUuid());
         }
         SysUser sysUser = new SysUser();
         sysUser.setUserName(username);
@@ -70,9 +66,8 @@ public class SysRegisterService {
      * @param username 用户名
      * @param code     验证码
      * @param uuid     唯一标识
-     * @return 结果
      */
-    public void validateCaptcha(String username, String code, String uuid, HttpServletRequest request) {
+    public void validateCaptcha(String username, String code, String uuid) {
         String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
         String captcha = RedisUtils.getCacheObject(verifyKey);
         RedisUtils.deleteObject(verifyKey);

+ 40 - 3
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java

@@ -1,5 +1,7 @@
 package com.ruoyi.system.service.impl;
 
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
@@ -10,6 +12,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.domain.PageQuery;
 import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.model.LoginUser;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.helper.LoginHelper;
@@ -70,7 +73,7 @@ public class SysRoleServiceImpl implements ISysRoleService {
             .like(StringUtils.isNotBlank(role.getRoleKey()), "r.role_key", role.getRoleKey())
             .between(params.get("beginTime") != null && params.get("endTime") != null,
                 "r.create_time", params.get("beginTime"), params.get("endTime"))
-            .orderByAsc("r.role_sort");
+            .orderByAsc("r.role_sort").orderByAsc("r.create_time");
         return wrapper;
     }
 
@@ -362,9 +365,13 @@ public class SysRoleServiceImpl implements ISysRoleService {
      */
     @Override
     public int deleteAuthUser(SysUserRole userRole) {
-        return userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
+        int rows = userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
             .eq(SysUserRole::getRoleId, userRole.getRoleId())
             .eq(SysUserRole::getUserId, userRole.getUserId()));
+        if (rows > 0) {
+            cleanOnlineUserByRole(userRole.getRoleId());
+        }
+        return rows;
     }
 
     /**
@@ -376,9 +383,13 @@ public class SysRoleServiceImpl implements ISysRoleService {
      */
     @Override
     public int deleteAuthUsers(Long roleId, Long[] userIds) {
-        return userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
+        int rows = userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
             .eq(SysUserRole::getRoleId, roleId)
             .in(SysUserRole::getUserId, Arrays.asList(userIds)));
+        if (rows > 0) {
+            cleanOnlineUserByRole(roleId);
+        }
+        return rows;
     }
 
     /**
@@ -401,6 +412,32 @@ public class SysRoleServiceImpl implements ISysRoleService {
         if (CollUtil.isNotEmpty(list)) {
             rows = userRoleMapper.insertBatch(list) ? list.size() : 0;
         }
+        if (rows > 0) {
+            cleanOnlineUserByRole(roleId);
+        }
         return rows;
     }
+
+    @Override
+    public void cleanOnlineUserByRole(Long roleId) {
+        List<String> keys = StpUtil.searchTokenValue("", 0, -1, false);
+        if (CollUtil.isEmpty(keys)) {
+            return;
+        }
+        // 角色关联的在线用户量过大会导致redis阻塞卡顿 谨慎操作
+        keys.parallelStream().forEach(key -> {
+            String token = StringUtils.substringAfterLast(key, ":");
+            // 如果已经过期则跳过
+            if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
+                return;
+            }
+            LoginUser loginUser = LoginHelper.getLoginUser(token);
+            if (loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
+                try {
+                    StpUtil.logoutByTokenValue(token);
+                } catch (NotLoginException ignored) {
+                }
+            }
+        });
+    }
 }

+ 5 - 0
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml

@@ -128,6 +128,11 @@
         where u.del_flag = '0' and u.phonenumber = #{phonenumber}
     </select>
 
+    <select id="selectUserByEmail" parameterType="String" resultMap="SysUserResult">
+        <include refid="selectUserVo"/>
+        where u.del_flag = '0' and u.email = #{email}
+    </select>
+
     <select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
         <include refid="selectUserVo"/>
         where u.del_flag = '0' and u.user_id = #{userId}

+ 5 - 3
ruoyi-ui/.env.production

@@ -4,14 +4,16 @@ VUE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
 # 生产环境配置
 ENV = 'production'
 
+# 若依管理系统/生产环境
+VUE_APP_BASE_API = '/prod-api'
+
 # 应用访问路径 例如使用前缀 /admin/
 VUE_APP_CONTEXT_PATH = '/'
 
 # 监控地址
 VUE_APP_MONITRO_ADMIN = '/admin/login'
 
-# 监控地址
+# xxl-job 控制台地址
 VUE_APP_XXL_JOB_ADMIN = '/xxl-job-admin'
 
-# 若依管理系统/生产环境
-VUE_APP_BASE_API = '/prod-api'
+

+ 2 - 2
ruoyi-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "ruoyi-vue-plus",
-  "version": "4.6.0",
+  "version": "4.7.0",
   "description": "RuoYi-Vue-Plus后台管理系统",
   "author": "LionLi",
   "license": "MIT",
@@ -32,7 +32,7 @@
   ],
   "repository": {
     "type": "git",
-    "url": "https://gitee.com/y_project/RuoYi-Vue.git"
+    "url": "https://gitee.com/dromara/RuoYi-Vue-Plus.git"
   },
   "dependencies": {
     "@riophae/vue-treeselect": "0.4.0",

BIN
ruoyi-ui/src/assets/images/profile.jpg


+ 0 - 9
ruoyi-ui/src/assets/styles/index.scss

@@ -180,12 +180,3 @@ aside {
     margin-bottom: 10px;
   }
 }
-
-//refine vue-multiselect plugin
-.multiselect {
-  line-height: 16px;
-}
-
-.multiselect--active {
-  z-index: 1000 !important;
-}

+ 1 - 1
ruoyi-ui/src/assets/styles/sidebar.scss

@@ -1,7 +1,7 @@
 #app {
 
   .main-container {
-    min-height: 100%;
+    height: 100%;
     transition: margin-left .28s;
     margin-left: $base-sidebar-width;
     position: relative;

+ 0 - 4
ruoyi-ui/src/assets/styles/transition.scss

@@ -18,10 +18,6 @@
   transition: all .5s;
 }
 
-.fade-transform-leave-active {
-  position: absolute;
-}
-
 .fade-transform-enter {
   opacity: 0;
   transform: translateX(-30px);

+ 43 - 3
ruoyi-ui/src/components/DictTag/index.vue

@@ -7,7 +7,7 @@
           :key="item.value"
           :index="index"
           :class="item.raw.cssClass"
-          >{{ item.label }}</span
+          >{{ item.label + ' ' }}</span
         >
         <el-tag
           v-else
@@ -17,10 +17,13 @@
           :type="item.raw.listClass == 'primary' ? '' : item.raw.listClass"
           :class="item.raw.cssClass"
         >
-          {{ item.label }}
+          {{ item.label + ' ' }}
         </el-tag>
       </template>
     </template>
+    <template v-if="unmatch && showValue">
+      {{ unmatchArray | handleArray }}
+    </template>
   </div>
 </template>
 
@@ -33,6 +36,16 @@ export default {
       default: null,
     },
     value: [Number, String, Array],
+    // 当未找到匹配的数据时,显示value
+    showValue: {
+      type: Boolean,
+      default: true,
+    }
+  },
+  data() {
+    return {
+      unmatchArray: [], // 记录未匹配的项
+    }
   },
   computed: {
     values() {
@@ -42,11 +55,38 @@ export default {
         return [];
       }
     },
+    unmatch(){
+      this.unmatchArray = [];
+      if (this.value !== null && typeof this.value !== 'undefined') {
+        // 传入值为非数组
+        if(!Array.isArray(this.value)){
+          if(this.options.some(v=> v.value == this.value )) return false;
+          this.unmatchArray.push(this.value);
+          return true;
+        }
+        // 传入值为Array
+        this.value.forEach(item => {
+          if (!this.options.some(v=> v.value == item )) this.unmatchArray.push(item)
+        });
+        return true;
+      }
+      // 没有value不显示
+      return false;
+    },
+
   },
+  filters: {
+    handleArray(array) {
+      if(array.length===0) return '';
+      return array.reduce((pre, cur) => {
+        return pre + ' ' + cur;
+      })
+    },
+  }
 };
 </script>
 <style scoped>
 .el-tag + .el-tag {
   margin-left: 10px;
 }
-</style>
+</style>

+ 54 - 18
ruoyi-ui/src/components/IconSelect/index.vue

@@ -1,13 +1,17 @@
 <!-- @author zhengjie -->
 <template>
   <div class="icon-body">
-    <el-input v-model="name" style="position: relative;" clearable placeholder="请输入图标名称" @clear="filterIcons" @input.native="filterIcons">
+    <el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons">
       <i slot="suffix" class="el-icon-search el-input__icon" />
     </el-input>
     <div class="icon-list">
-      <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
-        <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
-        <span>{{ item }}</span>
+      <div class="list-container">
+        <div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)">
+          <div :class="['icon-item', { active: activeIcon === item }]">
+            <svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
+            <span>{{ item }}</span>
+          </div>
+        </div>
       </div>
     </div>
   </div>
@@ -17,6 +21,11 @@
 import icons from './requireIcons'
 export default {
   name: 'IconSelect',
+  props: {
+    activeIcon: {
+      type: String
+    }
+  },
   data() {
     return {
       name: '',
@@ -46,22 +55,49 @@ export default {
   .icon-body {
     width: 100%;
     padding: 10px;
+    .icon-search {
+      position: relative;
+      margin-bottom: 5px;
+    }
     .icon-list {
       height: 200px;
-      overflow-y: scroll;
-      div {
-        height: 30px;
-        line-height: 30px;
-        margin-bottom: -5px;
-        cursor: pointer;
-        width: 33%;
-        float: left;
-      }
-      span {
-        display: inline-block;
-        vertical-align: -0.15em;
-        fill: currentColor;
-        overflow: hidden;
+      overflow: auto;
+      .list-container {
+        display: flex;
+        flex-wrap: wrap;
+        .icon-item-wrapper {
+          width: calc(100% / 3);
+          height: 25px;
+          line-height: 25px;
+          cursor: pointer;
+          display: flex;
+          .icon-item {
+            display: flex;
+            max-width: 100%;
+            height: 100%;
+            padding: 0 5px;
+            &:hover {
+              background: #ececec;
+              border-radius: 5px;
+            }
+            .icon {
+              flex-shrink: 0;
+            }
+            span {
+              display: inline-block;
+              vertical-align: -0.15em;
+              fill: currentColor;
+              padding-left: 2px;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+            }
+          }
+          .icon-item.active {
+            background: #ececec;
+            border-radius: 5px;
+          }
+        }
       }
     }
   }

+ 7 - 1
ruoyi-ui/src/components/TopNav/index.vue

@@ -127,7 +127,13 @@ export default {
         window.open(key, "_blank");
       } else if (!route || !route.children) {
         // 没有子路由路径内部打开
-        this.$router.push({ path: key });
+        const routeMenu = this.childrenMenus.find(item => item.path === key);
+        if (routeMenu && routeMenu.query) {
+          let query = JSON.parse(routeMenu.query);
+          this.$router.push({ path: key, query: query });
+        } else {
+          this.$router.push({ path: key });
+        }
         this.$store.dispatch('app/toggleSideBarHide', true);
       } else {
         // 显示左侧联动菜单

+ 15 - 1
ruoyi-ui/src/layout/components/AppMain.vue

@@ -55,7 +55,21 @@ export default {
 // fix css style bug in open el-dialog
 .el-popup-parent--hidden {
   .fixed-header {
-    padding-right: 17px;
+    padding-right: 6px;
   }
 }
+
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background-color: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: #c0c0c0;
+  border-radius: 3px;
+}
 </style>

+ 1 - 1
ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue

@@ -87,7 +87,7 @@ export default {
       bottom: 0px;
     }
     .el-scrollbar__wrap {
-      height: 49px;
+      height: 39px;
     }
   }
 }

+ 1 - 1
ruoyi-ui/src/layout/components/TagsView/index.vue

@@ -182,7 +182,7 @@ export default {
       })
     },
     closeOthersTags() {
-      this.$router.push(this.selectedTag).catch(()=>{});
+      this.$router.push(this.selectedTag.fullPath).catch(()=>{});
       this.$tab.closeOtherPage(this.selectedTag).then(() => {
         this.moveToCurrentTag()
       })

+ 5 - 5
ruoyi-ui/src/layout/index.vue

@@ -1,15 +1,15 @@
 <template>
   <div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
     <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
-    <sidebar v-if="!sidebar.hide" class="sidebar-container" />
+    <sidebar v-if="!sidebar.hide" class="sidebar-container"/>
     <div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
       <div :class="{'fixed-header':fixedHeader}">
-        <navbar />
-        <tags-view v-if="needTagsView" />
+        <navbar/>
+        <tags-view v-if="needTagsView"/>
       </div>
-      <app-main />
+      <app-main/>
       <right-panel>
-        <settings />
+        <settings/>
       </right-panel>
     </div>
   </div>

+ 71 - 67
ruoyi-ui/src/plugins/tab.js

@@ -1,67 +1,71 @@
-import store from '@/store'
-import router from '@/router';
-
-export default {
-  // 刷新当前tab页签
-  refreshPage(obj) {
-    const { path, query, matched } = router.currentRoute;
-    if (obj === undefined) {
-      matched.forEach((m) => {
-        if (m.components && m.components.default && m.components.default.name) {
-          if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
-            obj = { name: m.components.default.name, path: path, query: query };
-          }
-        }
-      });
-    }
-    return store.dispatch('tagsView/delCachedView', obj).then(() => {
-      const { path, query } = obj
-      router.replace({
-        path: '/redirect' + path,
-        query: query
-      })
-    })
-  },
-  // 关闭当前tab页签,打开新页签
-  closeOpenPage(obj) {
-    store.dispatch("tagsView/delView", router.currentRoute);
-    if (obj !== undefined) {
-      return router.push(obj);
-    }
-  },
-  // 关闭指定tab页签
-  closePage(obj) {
-    if (obj === undefined) {
-      return store.dispatch('tagsView/delView', router.currentRoute).then(({ lastPath }) => {
-        return router.push(lastPath || '/');
-      });
-    }
-    return store.dispatch('tagsView/delView', obj);
-  },
-  // 关闭所有tab页签
-  closeAllPage() {
-    return store.dispatch('tagsView/delAllViews');
-  },
-  // 关闭左侧tab页签
-  closeLeftPage(obj) {
-    return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute);
-  },
-  // 关闭右侧tab页签
-  closeRightPage(obj) {
-    return store.dispatch('tagsView/delRightTags', obj || router.currentRoute);
-  },
-  // 关闭其他tab页签
-  closeOtherPage(obj) {
-    return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute);
-  },
-  // 添加tab页签
-  openPage(title, url, params) {
-    var obj = { path: url, meta: { title: title } }
-    store.dispatch('tagsView/addView', obj);
-    return router.push({ path: url, query: params });
-  },
-  // 修改tab页签
-  updatePage(obj) {
-    return store.dispatch('tagsView/updateVisitedView', obj);
-  }
-}
+import store from '@/store'
+import router from '@/router';
+
+export default {
+  // 刷新当前tab页签
+  refreshPage(obj) {
+    const { path, query, matched } = router.currentRoute;
+    if (obj === undefined) {
+      matched.forEach((m) => {
+        if (m.components && m.components.default && m.components.default.name) {
+          if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
+            obj = { name: m.components.default.name, path: path, query: query };
+          }
+        }
+      });
+    }
+    return store.dispatch('tagsView/delCachedView', obj).then(() => {
+      const { path, query } = obj
+      router.replace({
+        path: '/redirect' + path,
+        query: query
+      })
+    })
+  },
+  // 关闭当前tab页签,打开新页签
+  closeOpenPage(obj) {
+    store.dispatch("tagsView/delView", router.currentRoute);
+    if (obj !== undefined) {
+      return router.push(obj);
+    }
+  },
+  // 关闭指定tab页签
+  closePage(obj) {
+    if (obj === undefined) {
+      return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => {
+        const latestView = visitedViews.slice(-1)[0]
+        if (latestView) {
+          return router.push(latestView.fullPath)
+        }
+        return router.push('/');
+      });
+    }
+    return store.dispatch('tagsView/delView', obj);
+  },
+  // 关闭所有tab页签
+  closeAllPage() {
+    return store.dispatch('tagsView/delAllViews');
+  },
+  // 关闭左侧tab页签
+  closeLeftPage(obj) {
+    return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute);
+  },
+  // 关闭右侧tab页签
+  closeRightPage(obj) {
+    return store.dispatch('tagsView/delRightTags', obj || router.currentRoute);
+  },
+  // 关闭其他tab页签
+  closeOtherPage(obj) {
+    return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute);
+  },
+  // 添加tab页签
+  openPage(title, url, params) {
+    var obj = { path: url, meta: { title: title } }
+    store.dispatch('tagsView/addView', obj);
+    return router.push({ path: url, query: params });
+  },
+  // 修改tab页签
+  updatePage(obj) {
+    return store.dispatch('tagsView/updateVisitedView', obj);
+  }
+}

+ 6 - 0
ruoyi-ui/src/router/index.js

@@ -166,9 +166,15 @@ export const dynamicRoutes = [
 
 // 防止连续点击多次路由报错
 let routerPush = Router.prototype.push;
+let routerReplace = Router.prototype.replace;
+// push
 Router.prototype.push = function push(location) {
   return routerPush.call(this, location).catch(err => err)
 }
+// replace
+Router.prototype.replace = function push(location) {
+  return routerReplace.call(this, location).catch(err => err)
+}
 
 export default new Router({
   base: process.env.VUE_APP_CONTEXT_PATH,

+ 1 - 1
ruoyi-ui/src/views/index.vue

@@ -114,7 +114,7 @@ export default {
   data() {
     return {
       // 版本号
-      version: "4.6.0",
+      version: "4.7.0",
     };
   },
   methods: {

+ 1 - 1
ruoyi-ui/src/views/monitor/cache/list.vue

@@ -187,7 +187,7 @@ export default {
     /** 清理指定名称缓存 */
     handleClearCacheName(row) {
       clearCacheName(row.cacheName).then(response => {
-        this.$modal.msgSuccess("清理缓存名称[" + this.nowCacheName + "]成功");
+        this.$modal.msgSuccess("清理缓存名称[" + row.cacheName + "]成功");
         this.getCacheKeys();
       });
     },

+ 2 - 3
ruoyi-ui/src/views/system/menu/index.vue

@@ -134,14 +134,13 @@
                 trigger="click"
                 @show="$refs['iconSelect'].reset()"
               >
-                <IconSelect ref="iconSelect" @selected="selected" />
+                <IconSelect ref="iconSelect" @selected="selected" :active-icon="form.icon" />
                 <el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
                   <svg-icon
                     v-if="form.icon"
                     slot="prefix"
                     :icon-class="form.icon"
-                    class="el-input__icon"
-                    style="height: 32px;width: 16px;"
+                    style="width: 25px;"
                   />
                   <i v-else slot="prefix" class="el-icon-search el-input__icon" />
                 </el-input>

+ 1 - 3
ruoyi-ui/src/views/system/oss/index.vue

@@ -406,9 +406,7 @@ export default {
       }).then(() => {
         this.getList()
         this.$modal.msgSuccess(text + "成功");
-      }).catch(() => {
-        this.previewListResource = previewListResource !== true;
-      })
+      }).catch(() => {})
     }
   }
 };

+ 0 - 13
ruoyi-ui/src/views/tool/build/index.vue

@@ -371,19 +371,6 @@ export default {
 </script>
 
 <style lang='scss'>
-body, html{
-  margin: 0;
-  padding: 0;
-  background: #fff;
-  -moz-osx-font-smoothing: grayscale;
-  -webkit-font-smoothing: antialiased;
-  text-rendering: optimizeLegibility;
-  font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
-}
-
-input, textarea{
-  font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
-}
 
 .editor-tabs{
   background: #121315;

+ 5 - 5
script/docker/docker-compose.yml

@@ -67,7 +67,7 @@ services:
     network_mode: "host"
 
   minio:
-    image: minio/minio:RELEASE.2022-05-26T05-48-41Z
+    image: minio/minio:RELEASE.2023-03-24T21-41-23Z
     container_name: minio
     ports:
       # api 端口
@@ -100,7 +100,7 @@ services:
     network_mode: "host"
 
   ruoyi-server1:
-    image: ruoyi/ruoyi-server:4.6.0
+    image: ruoyi/ruoyi-server:4.7.0
     container_name: ruoyi-server1
     environment:
       # 时区上海
@@ -115,7 +115,7 @@ services:
     network_mode: "host"
 
   ruoyi-server2:
-    image: "ruoyi/ruoyi-server:4.6.0"
+    image: "ruoyi/ruoyi-server:4.7.0"
     container_name: ruoyi-server2
     environment:
       # 时区上海
@@ -130,7 +130,7 @@ services:
     network_mode: "host"
 
   ruoyi-monitor-admin:
-    image: ruoyi/ruoyi-monitor-admin:4.6.0
+    image: ruoyi/ruoyi-monitor-admin:4.7.0
     container_name: ruoyi-monitor-admin
     environment:
       # 时区上海
@@ -142,7 +142,7 @@ services:
     network_mode: "host"
 
   ruoyi-xxl-job-admin:
-    image: ruoyi/ruoyi-xxl-job-admin:4.6.0
+    image: ruoyi/ruoyi-xxl-job-admin:4.7.0
     container_name: ruoyi-xxl-job-admin
     environment:
       # 时区上海

+ 6 - 6
script/sql/oracle/oracle_ry_vue_4.X.sql

@@ -531,9 +531,9 @@ create table sys_oper_log (
 );
 
 alter table sys_oper_log add constraint pk_sys_oper_log primary key (oper_id);
-create unique index idx_sys_oper_log_bt on sys_oper_log (business_type);
-create unique index idx_sys_oper_log_s on sys_oper_log (status);
-create unique index idx_sys_oper_log_ot on sys_oper_log (oper_time);
+create index idx_sys_oper_log_bt on sys_oper_log (business_type);
+create index idx_sys_oper_log_s on sys_oper_log (status);
+create index idx_sys_oper_log_ot on sys_oper_log (oper_time);
 
 comment on table  sys_oper_log                is '操作日志记录';
 comment on column sys_oper_log.oper_id        is '日志主键';
@@ -711,8 +711,8 @@ create table sys_logininfor (
 );
 
 alter table sys_logininfor add constraint pk_sys_logininfor primary key (info_id);
-create unique index idx_sys_logininfor_s on sys_logininfor (status);
-create unique index idx_sys_logininfor_lt on sys_logininfor (login_time);
+create index idx_sys_logininfor_s on sys_logininfor (status);
+create index idx_sys_logininfor_lt on sys_logininfor (login_time);
 
 comment on table  sys_logininfor                is '系统访问记录';
 comment on column sys_logininfor.info_id        is '访问ID';
@@ -938,7 +938,7 @@ comment on column sys_oss_config.domain is '自定义域名';
 comment on column sys_oss_config.is_https is '是否https(Y=是,N=否)';
 comment on column sys_oss_config.region is '域';
 comment on column sys_oss_config.access_policy is '桶权限类型(0=private 1=public 2=custom)';
-comment on column sys_oss_config.status is '状态(0=正常,1=停用)';
+comment on column sys_oss_config.status is '是否默认(0=是,1=否)';
 comment on column sys_oss_config.ext1 is '扩展字段';
 comment on column sys_oss_config.remark is '备注';
 comment on column sys_oss_config.create_by is '创建者';

+ 6 - 6
script/sql/postgres/postgres_ry_vue_4.X.sql

@@ -540,9 +540,9 @@ create table if not exists sys_oper_log
     constraint sys_oper_log_pk primary key (oper_id)
 );
 
-create unique index idx_sys_oper_log_bt ON sys_oper_log (business_type);
-create unique index idx_sys_oper_log_s ON sys_oper_log (status);
-create unique index idx_sys_oper_log_ot ON sys_oper_log (oper_time);
+create index idx_sys_oper_log_bt ON sys_oper_log (business_type);
+create index idx_sys_oper_log_s ON sys_oper_log (status);
+create index idx_sys_oper_log_ot ON sys_oper_log (oper_time);
 
 comment on table sys_oper_log is '操作日志记录';
 comment on column sys_oper_log.oper_id is '日志主键';
@@ -726,8 +726,8 @@ create table if not exists sys_logininfor
     constraint sys_logininfor_pk primary key (info_id)
 );
 
-create unique index idx_sys_logininfor_s ON sys_logininfor (status);
-create unique index idx_sys_logininfor_lt ON sys_logininfor (login_time);
+create index idx_sys_logininfor_s ON sys_logininfor (status);
+create index idx_sys_logininfor_lt ON sys_logininfor (login_time);
 
 comment on table sys_logininfor is '系统访问记录';
 comment on column sys_logininfor.info_id is '访问ID';
@@ -954,7 +954,7 @@ comment on column sys_oss_config.domain is '自定义域名';
 comment on column sys_oss_config.is_https is '是否https(Y=是,N=否)';
 comment on column sys_oss_config.region is '域';
 comment on column sys_oss_config.access_policy is '桶权限类型(0=private 1=public 2=custom)';
-comment on column sys_oss_config.status is '状态(0=正常,1=停用)';
+comment on column sys_oss_config.status is '是否默认(0=是,1=否)';
 comment on column sys_oss_config.ext1 is '扩展字段';
 comment on column sys_oss_config.create_by is '创建者';
 comment on column sys_oss_config.create_time is '创建时间';

+ 1 - 1
script/sql/ry_vue_4.X.sql

@@ -679,7 +679,7 @@ create table sys_oss_config (
   is_https         char(1)                default 'N'     comment '是否https(Y=是,N=否)',
   region           varchar(255)           default ''      comment '域',
   access_policy    char(1)     not null   default '1'     comment '桶权限类型(0=private 1=public 2=custom)',
-  status           char(1)                default '1'     comment '状态(0=正常,1=停用)',
+  status           char(1)                default '1'     comment '是否默认(0=是,1=否)',
   ext1             varchar(255)           default ''      comment '扩展字段',
   create_by       varchar(64)             default ''      comment '创建者',
   create_time     datetime                default null    comment '创建时间',

+ 1 - 1
script/sql/sqlserver/sqlserver_ry_vue_4.X.sql

@@ -2285,7 +2285,7 @@ EXEC sp_addextendedproperty
      'COLUMN', N'access_policy'
 GO
 EXEC sp_addextendedproperty
-    'MS_Description', N'状态(0=正常,1=停用)',
+    'MS_Description', N'是否默认(0=是,1=否)',
     'SCHEMA', N'dbo',
     'TABLE', N'sys_oss_config',
     'COLUMN', N'status'