luogang 10 сар өмнө
parent
commit
0756d5af99
100 өөрчлөгдсөн 10386 нэмэгдсэн , 0 устгасан
  1. 20 0
      LICENSE
  2. 526 0
      pom.xml
  3. 26 0
      ruoyi-admin/Dockerfile
  4. 159 0
      ruoyi-admin/pom.xml
  5. 514 0
      ruoyi-admin/src/main/java/liquibase/database/core/OracleDatabase.java
  6. 23 0
      ruoyi-admin/src/main/java/org/dromara/DromaraApplication.java
  7. 18 0
      ruoyi-admin/src/main/java/org/dromara/DromaraServletInitializer.java
  8. 234 0
      ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java
  9. 136 0
      ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java
  10. 32 0
      ruoyi-admin/src/main/java/org/dromara/web/controller/IndexController.java
  11. 25 0
      ruoyi-admin/src/main/java/org/dromara/web/domain/vo/CaptchaVo.java
  12. 25 0
      ruoyi-admin/src/main/java/org/dromara/web/domain/vo/LoginTenantVo.java
  13. 54 0
      ruoyi-admin/src/main/java/org/dromara/web/domain/vo/LoginVo.java
  14. 31 0
      ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java
  15. 154 0
      ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java
  16. 46 0
      ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java
  17. 248 0
      ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java
  18. 115 0
      ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java
  19. 106 0
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java
  20. 124 0
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
  21. 106 0
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java
  22. 133 0
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
  23. 92 0
      ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java
  24. 1946 0
      ruoyi-admin/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java
  25. 264 0
      ruoyi-admin/src/main/resources/application-dev.yml
  26. 260 0
      ruoyi-admin/src/main/resources/application-prod.yml
  27. 321 0
      ruoyi-admin/src/main/resources/application.yml
  28. 8 0
      ruoyi-admin/src/main/resources/banner.txt
  29. 61 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  30. 61 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  31. 61 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  32. BIN
      ruoyi-admin/src/main/resources/ip2region.xdb
  33. 129 0
      ruoyi-admin/src/main/resources/logback-plus.xml
  34. 45 0
      ruoyi-common/pom.xml
  35. 178 0
      ruoyi-common/ruoyi-common-bom/pom.xml
  36. 112 0
      ruoyi-common/ruoyi-common-core/pom.xml
  37. 17 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java
  38. 52 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java
  39. 33 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/RuoYiConfig.java
  40. 78 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java
  41. 40 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java
  42. 30 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/properties/ThreadPoolProperties.java
  43. 25 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java
  44. 73 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
  45. 81 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
  46. 39 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java
  47. 93 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java
  48. 54 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java
  49. 45 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java
  50. 142 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java
  51. 110 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java
  52. 46 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/OssDTO.java
  53. 42 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/RoleDTO.java
  54. 73 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java
  55. 72 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java
  56. 41 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java
  57. 40 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java
  58. 31 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/EmailLoginBody.java
  59. 48 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java
  60. 142 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java
  61. 33 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
  62. 35 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java
  63. 29 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SmsLoginBody.java
  64. 35 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SocialLoginBody.java
  65. 28 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginBody.java
  66. 27 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginUser.java
  67. 152 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/BusinessStatusEnum.java
  68. 37 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java
  69. 44 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java
  70. 30 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java
  71. 30 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java
  72. 37 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java
  73. 59 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java
  74. 74 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java
  75. 21 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java
  76. 18 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java
  77. 18 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java
  78. 18 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java
  79. 18 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java
  80. 20 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java
  81. 52 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java
  82. 31 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java
  83. 18 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/ConfigService.java
  84. 18 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java
  85. 67 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
  86. 29 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/OssService.java
  87. 69 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java
  88. 76 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java
  89. 168 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
  90. 93 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java
  91. 33 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java
  92. 228 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java
  93. 67 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java
  94. 283 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java
  95. 323 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java
  96. 75 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java
  97. 35 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java
  98. 35 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java
  99. 43 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
  100. 40 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java

+ 20 - 0
LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 RuoYi-Vue-Plus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 526 - 0
pom.xml

@@ -0,0 +1,526 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.dromara</groupId>
+    <artifactId>ruoyi-vue-plus</artifactId>
+    <version>${revision}</version>
+
+    <name>RuoYi-Vue-Plus</name>
+    <url>https://gitee.com/dromara/RuoYi-Vue-Plus</url>
+    <description>RuoYi-Vue-Plus多租户管理系统</description>
+
+    <properties>
+        <revision>5.2.1</revision>
+        <spring-boot.version>3.2.6</spring-boot.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <java.version>17</java.version>
+        <mybatis.version>3.5.16</mybatis.version>
+        <springdoc.version>2.5.0</springdoc.version>
+        <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
+        <poi.version>5.2.3</poi.version>
+        <easyexcel.version>3.3.4</easyexcel.version>
+        <velocity.version>2.3</velocity.version>
+        <satoken.version>1.38.0</satoken.version>
+        <mybatis-plus.version>3.5.7</mybatis-plus.version>
+        <p6spy.version>3.9.1</p6spy.version>
+        <hutool.version>5.8.27</hutool.version>
+        <okhttp.version>4.10.0</okhttp.version>
+        <spring-boot-admin.version>3.2.3</spring-boot-admin.version>
+        <redisson.version>3.31.0</redisson.version>
+        <lock4j.version>2.2.7</lock4j.version>
+        <dynamic-ds.version>4.3.1</dynamic-ds.version>
+        <alibaba-ttl.version>2.14.4</alibaba-ttl.version>
+        <snailjob.version>1.0.1</snailjob.version>
+        <mapstruct-plus.version>1.3.6</mapstruct-plus.version>
+        <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
+        <lombok.version>1.18.32</lombok.version>
+        <bouncycastle.version>1.76</bouncycastle.version>
+        <justauth.version>1.16.6</justauth.version>
+        <!-- 离线IP地址定位库 -->
+        <ip2region.version>2.7.0</ip2region.version>
+
+        <!-- OSS 配置 -->
+        <aws.sdk.version>2.25.15</aws.sdk.version>
+        <aws.crt.version>0.29.13</aws.crt.version>
+        <!-- SMS 配置 -->
+        <sms4j.version>3.2.1</sms4j.version>
+        <!-- 限制框架中的fastjson版本 -->
+        <fastjson.version>1.2.83</fastjson.version>
+        <!--工作流配置-->
+        <flowable.version>7.0.0</flowable.version>
+
+        <camel.version>4.4.0</camel.version>
+
+        <!-- 插件版本 -->
+        <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
+        <maven-war-plugin.version>3.2.2</maven-war-plugin.version>
+        <maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
+        <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
+        <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
+    </properties>
+
+    <profiles>
+        <profile>
+            <id>local</id>
+            <properties>
+                <!-- 环境标识,需要与配置文件的名称相对应 -->
+                <profiles.active>local</profiles.active>
+                <logging.level>info</logging.level>
+            </properties>
+        </profile>
+        <profile>
+            <id>dev</id>
+            <properties>
+                <!-- 环境标识,需要与配置文件的名称相对应 -->
+                <profiles.active>dev</profiles.active>
+                <logging.level>info</logging.level>
+            </properties>
+            <activation>
+                <!-- 默认环境 -->
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+        <profile>
+            <id>prod</id>
+            <properties>
+                <profiles.active>prod</profiles.active>
+                <logging.level>warn</logging.level>
+            </properties>
+        </profile>
+    </profiles>
+
+    <!-- 依赖声明 -->
+    <dependencyManagement>
+        <dependencies>
+
+            <!-- SpringBoot的依赖配置-->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- hutool 的依赖配置-->
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-bom</artifactId>
+                <version>${hutool.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.flowable</groupId>
+                <artifactId>flowable-bom</artifactId>
+                <version>${flowable.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.liquibase</groupId>
+                <artifactId>liquibase-core</artifactId>
+            </dependency>
+
+            <!-- JustAuth 的依赖配置-->
+            <dependency>
+                <groupId>me.zhyd.oauth</groupId>
+                <artifactId>JustAuth</artifactId>
+                <version>${justauth.version}</version>
+            </dependency>
+
+            <!-- common 的依赖配置-->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-bom</artifactId>
+                <version>${revision}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.springdoc</groupId>
+                <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
+                <version>${springdoc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.therapi</groupId>
+                <artifactId>therapi-runtime-javadoc</artifactId>
+                <version>${therapi-javadoc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi</artifactId>
+                <version>${poi.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi-ooxml</artifactId>
+                <version>${poi.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>easyexcel</artifactId>
+                <version>${easyexcel.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.apache.poi</groupId>
+                        <artifactId>poi-ooxml-schemas</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!-- velocity代码生成使用模板 -->
+            <dependency>
+                <groupId>org.apache.velocity</groupId>
+                <artifactId>velocity-engine-core</artifactId>
+                <version>${velocity.version}</version>
+            </dependency>
+
+            <!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-spring-boot3-starter</artifactId>
+                <version>${satoken.version}</version>
+            </dependency>
+            <!-- Sa-Token 整合 jwt -->
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-jwt</artifactId>
+                <version>${satoken.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>cn.hutool</groupId>
+                        <artifactId>hutool-all</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-core</artifactId>
+                <version>${satoken.version}</version>
+            </dependency>
+
+            <!-- dynamic-datasource 多数据源-->
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
+                <version>${dynamic-ds.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.mybatis</groupId>
+                <artifactId>mybatis</artifactId>
+                <version>${mybatis.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-annotation</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+            <!-- sql性能分析插件 -->
+            <dependency>
+                <groupId>p6spy</groupId>
+                <artifactId>p6spy</artifactId>
+                <version>${p6spy.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>okhttp</artifactId>
+                <version>${okhttp.version}</version>
+            </dependency>
+
+            <!--  AWS SDK for Java 2.x  -->
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3</artifactId>
+                <version>${aws.sdk.version}</version>
+            </dependency>
+            <!-- 使用AWS基于 CRT 的 S3 客户端 -->
+            <dependency>
+                <groupId>software.amazon.awssdk.crt</groupId>
+                <artifactId>aws-crt</artifactId>
+                <version>${aws.crt.version}</version>
+            </dependency>
+            <!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3-transfer-manager</artifactId>
+                <version>${aws.sdk.version}</version>
+            </dependency>
+            <!--短信sms4j-->
+            <dependency>
+                <groupId>org.dromara.sms4j</groupId>
+                <artifactId>sms4j-spring-boot-starter</artifactId>
+                <version>${sms4j.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>de.codecentric</groupId>
+                <artifactId>spring-boot-admin-starter-server</artifactId>
+                <version>${spring-boot-admin.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>de.codecentric</groupId>
+                <artifactId>spring-boot-admin-starter-client</artifactId>
+                <version>${spring-boot-admin.version}</version>
+            </dependency>
+
+            <!--redisson-->
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
+                <version>${lock4j.version}</version>
+            </dependency>
+
+            <!-- SnailJob Client -->
+            <dependency>
+                <groupId>com.aizuda</groupId>
+                <artifactId>snail-job-client-starter</artifactId>
+                <version>${snailjob.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.aizuda</groupId>
+                <artifactId>snail-job-client-job-core</artifactId>
+                <version>${snailjob.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>transmittable-thread-local</artifactId>
+                <version>${alibaba-ttl.version}</version>
+            </dependency>
+
+            <!-- 加密包引入 -->
+            <dependency>
+                <groupId>org.bouncycastle</groupId>
+                <artifactId>bcprov-jdk15to18</artifactId>
+                <version>${bouncycastle.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.github.linpeilie</groupId>
+                <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
+                <version>${mapstruct-plus.version}</version>
+            </dependency>
+
+            <!-- 离线IP地址定位库 ip2region -->
+            <dependency>
+                <groupId>org.lionsoul</groupId>
+                <artifactId>ip2region</artifactId>
+                <version>${ip2region.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>fastjson</artifactId>
+                <version>${fastjson.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-system</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-job</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-generator</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-demo</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-iot</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--  工作流模块  -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-workflow</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+    <modules>
+        <module>ruoyi-admin</module>
+        <module>ruoyi-common</module>
+        <module>ruoyi-extend</module>
+        <module>ruoyi-modules</module>
+    </modules>
+    <packaging>pom</packaging>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>${maven-compiler-plugin.verison}</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>com.github.therapi</groupId>
+                            <artifactId>therapi-runtime-javadoc-scribe</artifactId>
+                            <version>${therapi-javadoc.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${lombok.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.springframework.boot</groupId>
+                            <artifactId>spring-boot-configuration-processor</artifactId>
+                            <version>${spring-boot.version}</version>
+                        </path>
+                        <path>
+                            <groupId>io.github.linpeilie</groupId>
+                            <artifactId>mapstruct-plus-processor</artifactId>
+                            <version>${mapstruct-plus.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok-mapstruct-binding</artifactId>
+                            <version>${mapstruct-plus.lombok.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                    <compilerArgs>
+                        <arg>-parameters</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+            <!-- 单元测试使用 -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>${maven-surefire-plugin.version}</version>
+                <configuration>
+                    <argLine>-Dfile.encoding=UTF-8</argLine>
+                    <!-- 根据打包环境执行对应的@Tag测试方法 -->
+                    <groups>${profiles.active}</groups>
+                    <!-- 排除标签 -->
+                    <excludedGroups>exclude</excludedGroups>
+                </configuration>
+            </plugin>
+            <!-- 统一版本号管理 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <updatePomFile>true</updatePomFile>
+                    <flattenMode>resolveCiFriendliesOnly</flattenMode>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <!-- 关闭过滤 -->
+                <filtering>false</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+                <!-- 引入所有 匹配文件进行过滤 -->
+                <includes>
+                    <include>application*</include>
+                    <include>bootstrap*</include>
+                    <include>banner*</include>
+                </includes>
+                <!-- 启用过滤 即该资源中的变量将会被过滤器中的值替换 -->
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+    </build>
+
+    <repositories>
+        <repository>
+            <id>public</id>
+            <name>huawei nexus</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </repository>
+    </repositories>
+
+    <pluginRepositories>
+        <pluginRepository>
+            <id>public</id>
+            <name>huawei nexus</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </pluginRepository>
+    </pluginRepositories>
+
+</project>
+
+

+ 26 - 0
ruoyi-admin/Dockerfile

@@ -0,0 +1,26 @@
+# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
+FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
+#FROM bellsoft/liberica-openjdk-debian:21.0.3-cds
+#FROM findepi/graalvm:java17-native
+
+LABEL maintainer="Lion Li"
+
+RUN mkdir -p /ruoyi/server/logs \
+    /ruoyi/server/temp \
+    /ruoyi/skywalking/agent
+
+WORKDIR /ruoyi/server
+
+ENV SERVER_PORT=8080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
+
+EXPOSE ${SERVER_PORT}
+
+ADD ./target/ruoyi-admin.jar ./app.jar
+
+ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
+           # 应用名称 如果想区分集群节点监控 改成不同的名称即可
+           #-Dskywalking.agent.service_name=ruoyi-server \
+           #-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
+           -XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
+           -jar app.jar
+

+ 159 - 0
ruoyi-admin/pom.xml

@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ruoyi-vue-plus</artifactId>
+        <groupId>org.dromara</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+    <artifactId>ruoyi-admin</artifactId>
+
+    <description>
+        web服务入口
+    </description>
+
+    <dependencies>
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+        </dependency>
+        <!-- Oracle -->
+        <dependency>
+            <groupId>com.oracle.database.jdbc</groupId>
+            <artifactId>ojdbc8</artifactId>
+        </dependency>
+        <!-- PostgreSql -->
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+        </dependency>
+        <!-- SqlServer -->
+        <dependency>
+            <groupId>com.microsoft.sqlserver</groupId>
+            <artifactId>mssql-jdbc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.dameng</groupId>
+            <artifactId>DmJdbcDriver18</artifactId>
+            <version>8.1.1.193</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-doc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-social</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-ratelimiter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-mail</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-system</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-job</artifactId>
+        </dependency>
+
+        <!-- 代码生成-->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-generator</artifactId>
+        </dependency>
+
+        <!--  demo模块  -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-demo</artifactId>
+        </dependency>
+
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-iot</artifactId>
+        </dependency>
+
+
+        <!--  工作流模块  -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-workflow</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-starter-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- skywalking 整合 logback -->
+<!--        <dependency>-->
+<!--            <groupId>org.apache.skywalking</groupId>-->
+<!--            <artifactId>apm-toolkit-logback-1.x</artifactId>-->
+<!--            <version>${与你的agent探针版本保持一致}</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>org.apache.skywalking</groupId>-->
+<!--            <artifactId>apm-toolkit-trace</artifactId>-->
+<!--            <version>${与你的agent探针版本保持一致}</version>-->
+<!--        </dependency>-->
+
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>${maven-jar-plugin.version}</version>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>${maven-war-plugin.version}</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 514 - 0
ruoyi-admin/src/main/java/liquibase/database/core/OracleDatabase.java

@@ -0,0 +1,514 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by FernFlower decompiler)
+//
+
+package liquibase.database.core;
+
+import dm.jdbc.driver.DmdbConnection;
+import liquibase.CatalogAndSchema;
+import liquibase.Scope;
+import liquibase.database.AbstractJdbcDatabase;
+import liquibase.database.DatabaseConnection;
+import liquibase.database.OfflineConnection;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.DatabaseException;
+import liquibase.exception.UnexpectedLiquibaseException;
+import liquibase.exception.ValidationErrors;
+import liquibase.executor.ExecutorService;
+import liquibase.statement.DatabaseFunction;
+import liquibase.statement.SequenceCurrentValueFunction;
+import liquibase.statement.SequenceNextValueFunction;
+import liquibase.statement.UniqueConstraint;
+import liquibase.statement.core.RawCallStatement;
+import liquibase.statement.core.RawSqlStatement;
+import liquibase.structure.DatabaseObject;
+import liquibase.structure.core.Catalog;
+import liquibase.structure.core.Index;
+import liquibase.structure.core.PrimaryKey;
+import liquibase.structure.core.Schema;
+import liquibase.util.JdbcUtils;
+import liquibase.util.StringUtil;
+
+import java.lang.reflect.Method;
+import java.sql.*;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class OracleDatabase extends AbstractJdbcDatabase {
+    public static final Pattern PROXY_USER = Pattern.compile(".*(?:thin|oci)\\:(.+)/@.*");
+    public static final String PRODUCT_NAME = "oracle";
+    private static final ResourceBundle coreBundle = ResourceBundle.getBundle("liquibase/i18n/liquibase-core");
+    protected final int SHORT_IDENTIFIERS_LENGTH = 30;
+    protected final int LONG_IDENTIFIERS_LEGNTH = 128;
+    public static final int ORACLE_12C_MAJOR_VERSION = 12;
+    private final Set<String> reservedWords = new HashSet();
+    private Set<String> userDefinedTypes;
+    private Map<String, String> savedSessionNlsSettings;
+    private Boolean canAccessDbaRecycleBin;
+    private Integer databaseMajorVersion;
+    private Integer databaseMinorVersion;
+
+    public OracleDatabase() {
+        super.unquotedObjectsAreUppercased = true;
+        super.setCurrentDateTimeFunction("SYSTIMESTAMP");
+        this.dateFunctions.add(new DatabaseFunction("SYSDATE"));
+        this.dateFunctions.add(new DatabaseFunction("SYSTIMESTAMP"));
+        this.dateFunctions.add(new DatabaseFunction("CURRENT_TIMESTAMP"));
+        super.sequenceNextValueFunction = "%s.nextval";
+        super.sequenceCurrentValueFunction = "%s.currval";
+    }
+
+    public int getPriority() {
+        return 1;
+    }
+
+    private void tryProxySession(String url, Connection con) {
+        Matcher m = PROXY_USER.matcher(url);
+        if (m.matches()) {
+            Properties props = new Properties();
+            props.put("PROXY_USER_NAME", m.group(1));
+            Method method;
+            try {
+                method = con.getClass().getMethod("openProxySession", Integer.TYPE, Properties.class);
+                method.setAccessible(true);
+                method.invoke(con, 1, props);
+            } catch (Exception var8) {
+                Scope.getCurrentScope().getLog(this.getClass()).info("Could not open proxy session on OracleDatabase: " + var8.getCause().getMessage());
+                return;
+            }
+
+            try {
+                method = con.getClass().getMethod("isProxySession");
+                method.setAccessible(true);
+                boolean b = (Boolean) method.invoke(con);
+                if (!b) {
+                    Scope.getCurrentScope().getLog(this.getClass()).info("Proxy session not established on OracleDatabase: ");
+                }
+            } catch (Exception var7) {
+                Scope.getCurrentScope().getLog(this.getClass()).info("Could not open proxy session on OracleDatabase: " + var7.getCause().getMessage());
+            }
+        }
+
+    }
+
+    public void setConnection(DatabaseConnection conn) {
+        this.reservedWords.addAll(Arrays.asList("GROUP", "USER", "SESSION", "PASSWORD", "RESOURCE", "START", "SIZE", "UID", "DESC", "ORDER"));
+        Connection sqlConn = null;
+        if (!(conn instanceof OfflineConnection)) {
+            try {
+                if (conn instanceof JdbcConnection) {
+                    sqlConn = ((JdbcConnection) conn).getWrappedConnection();
+                }
+            } catch (Exception var16) {
+                throw new UnexpectedLiquibaseException(var16);
+            }
+
+            if (sqlConn != null) {
+                this.tryProxySession(conn.getURL(), sqlConn);
+
+                try {
+                    this.reservedWords.addAll(Arrays.asList(sqlConn.getMetaData().getSQLKeywords().toUpperCase().split(",\\s*")));
+                } catch (SQLException var15) {
+                    Scope.getCurrentScope().getLog(this.getClass()).info("Could get sql keywords on OracleDatabase: " + var15.getMessage());
+                }
+
+                try {
+                    Method method = sqlConn.getClass().getMethod("setRemarksReporting", Boolean.TYPE);
+                    method.setAccessible(true);
+                    method.invoke(sqlConn, true);
+                } catch (Exception var14) {
+                    Scope.getCurrentScope().getLog(this.getClass()).info("Could not set remarks reporting on OracleDatabase: " + var14.getMessage());
+                }
+
+                CallableStatement statement = null;
+
+                try {
+                    DatabaseMetaData metaData = sqlConn.getMetaData();
+                    Connection connection = metaData.getConnection();
+                    if (connection instanceof DmdbConnection) {
+                        String compatibleVersion = "11.2.0.4.0";
+                        Matcher majorVersionMatcher = Pattern.compile("(\\d+)\\.(\\d+)\\..*").matcher(compatibleVersion);
+                        if (majorVersionMatcher.matches()) {
+                            this.databaseMajorVersion = Integer.valueOf(majorVersionMatcher.group(1));
+                            this.databaseMinorVersion = Integer.valueOf(majorVersionMatcher.group(2));
+                        }
+                    } else {
+                        statement = sqlConn.prepareCall("{call DBMS_UTILITY.DB_VERSION(?,?)}");
+                        statement.registerOutParameter(1, 12);
+                        statement.registerOutParameter(2, 12);
+                        statement.execute();
+                        String compatibleVersion = statement.getString(2);
+                        if (compatibleVersion != null) {
+                            Matcher majorVersionMatcher = Pattern.compile("(\\d+)\\.(\\d+)\\..*").matcher(compatibleVersion);
+                            if (majorVersionMatcher.matches()) {
+                                this.databaseMajorVersion = Integer.valueOf(majorVersionMatcher.group(1));
+                                this.databaseMinorVersion = Integer.valueOf(majorVersionMatcher.group(2));
+                            }
+                        }
+                    }
+                } catch (SQLException var12) {
+                    String message = "Cannot read from DBMS_UTILITY.DB_VERSION: " + var12.getMessage();
+                    Scope.getCurrentScope().getLog(this.getClass()).info("Could not set check compatibility mode on OracleDatabase, assuming not running in any sort of compatibility mode: " + message);
+                } finally {
+                    JdbcUtils.closeStatement(statement);
+                }
+            }
+        }
+
+        super.setConnection(conn);
+    }
+
+    public String getShortName() {
+        return "oracle";
+    }
+
+    protected String getDefaultDatabaseProductName() {
+        return "Oracle";
+    }
+
+    public int getDatabaseMajorVersion() throws DatabaseException {
+        return this.databaseMajorVersion == null ? super.getDatabaseMajorVersion() : this.databaseMajorVersion;
+    }
+
+    public int getDatabaseMinorVersion() throws DatabaseException {
+        return this.databaseMinorVersion == null ? super.getDatabaseMinorVersion() : this.databaseMinorVersion;
+    }
+
+    public Integer getDefaultPort() {
+        return 1521;
+    }
+
+    public String getJdbcCatalogName(CatalogAndSchema schema) {
+        return null;
+    }
+
+    public String getJdbcSchemaName(CatalogAndSchema schema) {
+        return this.correctObjectName(schema.getCatalogName() == null ? schema.getSchemaName() : schema.getCatalogName(), Schema.class);
+    }
+
+    protected String getAutoIncrementClause(String generationType, Boolean defaultOnNull) {
+        if (StringUtil.isEmpty(generationType)) {
+            return super.getAutoIncrementClause();
+        } else {
+            String autoIncrementClause = "GENERATED %s AS IDENTITY";
+            String generationStrategy = generationType;
+            if (Boolean.TRUE.equals(defaultOnNull) && generationType.equalsIgnoreCase("BY DEFAULT")) {
+                generationStrategy = generationType + " ON NULL";
+            }
+
+            return String.format(autoIncrementClause, generationStrategy);
+        }
+    }
+
+    public String generatePrimaryKeyName(String tableName) {
+        return tableName.length() > 27 ? "PK_" + tableName.toUpperCase(Locale.US).substring(0, 27) : "PK_" + tableName.toUpperCase(Locale.US);
+    }
+
+    public boolean supportsInitiallyDeferrableColumns() {
+        return true;
+    }
+
+    public boolean isReservedWord(String objectName) {
+        return this.reservedWords.contains(objectName.toUpperCase());
+    }
+
+    public boolean supportsSequences() {
+        return true;
+    }
+
+    public boolean supportsSchemas() {
+        return false;
+    }
+
+    protected String getConnectionCatalogName() throws DatabaseException {
+        if (this.getConnection() instanceof OfflineConnection) {
+            return this.getConnection().getCatalog();
+        } else if (!(this.getConnection() instanceof JdbcConnection)) {
+            return this.defaultCatalogName;
+        } else {
+            try {
+                return Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", this).queryForObject(new RawCallStatement("select sys_context( 'userenv', 'current_schema' ) from dual"), String.class);
+            } catch (Exception var2) {
+                Scope.getCurrentScope().getLog(this.getClass()).info("Error getting default schema", var2);
+                return null;
+            }
+        }
+    }
+
+    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
+        return "oracle".equalsIgnoreCase(conn.getDatabaseProductName());
+    }
+
+    public String getDefaultDriver(String url) {
+        return url.startsWith("jdbc:oracle") ? "oracle.jdbc.OracleDriver" : null;
+    }
+
+    public String getDefaultCatalogName() {
+        return super.getDefaultCatalogName() == null ? null : super.getDefaultCatalogName().toUpperCase(Locale.US);
+    }
+
+    public String getDateLiteral(String isoDate) {
+        String normalLiteral = super.getDateLiteral(isoDate);
+        if (this.isDateOnly(isoDate)) {
+            return "TO_DATE(" + normalLiteral + ", 'YYYY-MM-DD')";
+        } else if (this.isTimeOnly(isoDate)) {
+            return "TO_DATE(" + normalLiteral + ", 'HH24:MI:SS')";
+        } else if (this.isTimestamp(isoDate)) {
+            return "TO_TIMESTAMP(" + normalLiteral + ", 'YYYY-MM-DD HH24:MI:SS.FF')";
+        } else if (this.isDateTime(isoDate)) {
+            int seppos = normalLiteral.lastIndexOf(46);
+            if (seppos != -1) {
+                normalLiteral = normalLiteral.substring(0, seppos) + "'";
+            }
+
+            return "TO_DATE(" + normalLiteral + ", 'YYYY-MM-DD HH24:MI:SS')";
+        } else {
+            return "UNSUPPORTED:" + isoDate;
+        }
+    }
+
+    public boolean isSystemObject(DatabaseObject example) {
+        if (example == null) {
+            return false;
+        } else if (this.isLiquibaseObject(example)) {
+            return false;
+        } else {
+            if (example instanceof Schema) {
+                label131:
+                {
+                    if (!"SYSTEM".equals(example.getName()) && !"SYS".equals(example.getName()) && !"CTXSYS".equals(example.getName()) && !"XDB".equals(example.getName())) {
+                        if (!"SYSTEM".equals(example.getSchema().getCatalogName()) && !"SYS".equals(example.getSchema().getCatalogName()) && !"CTXSYS".equals(example.getSchema().getCatalogName()) && !"XDB".equals(example.getSchema().getCatalogName())) {
+                            break label131;
+                        }
+
+                        return true;
+                    }
+
+                    return true;
+                }
+            } else if (this.isSystemObject(example.getSchema())) {
+                return true;
+            }
+
+            if (example instanceof Catalog) {
+                if ("SYSTEM".equals(example.getName()) || "SYS".equals(example.getName()) || "CTXSYS".equals(example.getName()) || "XDB".equals(example.getName())) {
+                    return true;
+                }
+            } else if (example.getName() != null) {
+                if (example.getName().startsWith("BIN$")) {
+                    boolean filteredInOriginalQuery = this.canAccessDbaRecycleBin();
+                    if (!filteredInOriginalQuery) {
+                        filteredInOriginalQuery = StringUtil.trimToEmpty(example.getSchema().getName()).equalsIgnoreCase(this.getConnection().getConnectionUserName());
+                    }
+
+                    if (!filteredInOriginalQuery) {
+                        return true;
+                    }
+
+                    return !(example instanceof PrimaryKey) && !(example instanceof Index) && !(example instanceof UniqueConstraint);
+                }
+
+                if (example.getName().startsWith("AQ$")) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("DR$")) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("SYS_IOT_OVER")) {
+                    return true;
+                }
+
+                if ((example.getName().startsWith("MDRT_") || example.getName().startsWith("MDRS_")) && example.getName().endsWith("$")) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("MLOG$_")) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("RUPD$_")) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("WM$_")) {
+                    return true;
+                }
+
+                if ("CREATE$JAVA$LOB$TABLE".equals(example.getName())) {
+                    return true;
+                }
+
+                if ("JAVA$CLASS$MD5$TABLE".equals(example.getName())) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("ISEQ$$_")) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("USLOG$")) {
+                    return true;
+                }
+
+                if (example.getName().startsWith("SYS_FBA")) {
+                    return true;
+                }
+            }
+
+            return super.isSystemObject(example);
+        }
+    }
+
+    public boolean supportsTablespaces() {
+        return true;
+    }
+
+    public boolean supportsAutoIncrement() {
+        boolean isAutoIncrementSupported = false;
+
+        try {
+            if (this.getDatabaseMajorVersion() >= 12) {
+                isAutoIncrementSupported = true;
+            }
+        } catch (DatabaseException var3) {
+            isAutoIncrementSupported = false;
+        }
+
+        return isAutoIncrementSupported;
+    }
+
+    public boolean supportsRestrictForeignKeys() {
+        return false;
+    }
+
+    public int getDataTypeMaxParameters(String dataTypeName) {
+        if ("BINARY_FLOAT".equalsIgnoreCase(dataTypeName)) {
+            return 0;
+        } else {
+            return "BINARY_DOUBLE".equalsIgnoreCase(dataTypeName) ? 0 : super.getDataTypeMaxParameters(dataTypeName);
+        }
+    }
+
+    public String getSystemTableWhereClause(String tableNameColumn) {
+        List<String> clauses = new ArrayList(Arrays.asList("BIN$", "AQ$", "DR$", "SYS_IOT_OVER", "MLOG$_", "RUPD$_", "WM$_", "ISEQ$$_", "USLOG$", "SYS_FBA"));
+
+        for (int i = 0; i < clauses.size(); ++i) {
+            clauses.set(i, tableNameColumn + " NOT LIKE '" + clauses.get(i) + "%'");
+        }
+
+        return "(" + StringUtil.join(clauses, " AND ") + ")";
+    }
+
+    public boolean jdbcCallsCatalogsSchemas() {
+        return true;
+    }
+
+    public Set<String> getUserDefinedTypes() {
+        if (this.userDefinedTypes == null) {
+            this.userDefinedTypes = new HashSet();
+            if (this.getConnection() != null && !(this.getConnection() instanceof OfflineConnection)) {
+                try {
+                    try {
+                        this.userDefinedTypes.addAll(Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", this).queryForList(new RawSqlStatement("SELECT DISTINCT TYPE_NAME FROM ALL_TYPES"), String.class));
+                    } catch (DatabaseException var2) {
+                        this.userDefinedTypes.addAll(Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", this).queryForList(new RawSqlStatement("SELECT TYPE_NAME FROM USER_TYPES"), String.class));
+                    }
+                } catch (DatabaseException var3) {
+                }
+            }
+        }
+
+        return this.userDefinedTypes;
+    }
+
+    public String generateDatabaseFunctionValue(DatabaseFunction databaseFunction) {
+        if (databaseFunction != null && "current_timestamp".equalsIgnoreCase(databaseFunction.toString())) {
+            return databaseFunction.toString();
+        } else if (!(databaseFunction instanceof SequenceNextValueFunction) && !(databaseFunction instanceof SequenceCurrentValueFunction)) {
+            return super.generateDatabaseFunctionValue(databaseFunction);
+        } else {
+            String quotedSeq = super.generateDatabaseFunctionValue(databaseFunction);
+            return quotedSeq.replaceFirst("\"([^\\.\"]+)\\.([^\\.\"]+)\"", "\"$1\".\"$2\"");
+        }
+    }
+
+    public ValidationErrors validate() {
+        ValidationErrors errors = super.validate();
+        DatabaseConnection connection = this.getConnection();
+        if (connection != null && !(connection instanceof OfflineConnection)) {
+            if (!this.canAccessDbaRecycleBin()) {
+                errors.addWarning(this.getDbaRecycleBinWarning());
+            }
+
+            return errors;
+        } else {
+            Scope.getCurrentScope().getLog(this.getClass()).info("Cannot validate offline database");
+            return errors;
+        }
+    }
+
+    public String getDbaRecycleBinWarning() {
+        return "Liquibase needs to access the DBA_RECYCLEBIN table so we can automatically handle the case where constraints are deleted and restored. Since Oracle doesn't properly restore the original table names referenced in the constraint, we use the information from the DBA_RECYCLEBIN to automatically correct this issue.\n\nThe user you used to connect to the database (" + this.getConnection().getConnectionUserName() + ") needs to have \"SELECT ON SYS.DBA_RECYCLEBIN\" permissions set before we can perform this operation. Please run the following SQL to set the appropriate permissions, and try running the command again.\n\n     GRANT SELECT ON SYS.DBA_RECYCLEBIN TO " + this.getConnection().getConnectionUserName() + ";";
+    }
+
+    public boolean canAccessDbaRecycleBin() {
+        if (this.canAccessDbaRecycleBin == null) {
+            DatabaseConnection connection = this.getConnection();
+            if (connection == null || connection instanceof OfflineConnection) {
+                return false;
+            }
+
+            Statement statement = null;
+
+            try {
+                statement = ((JdbcConnection) connection).createStatement();
+                ResultSet resultSet = statement.executeQuery("select 1 from dba_recyclebin where 0=1");
+                resultSet.close();
+                this.canAccessDbaRecycleBin = true;
+            } catch (Exception var7) {
+                if (var7 instanceof SQLException && var7.getMessage().startsWith("ORA-00942")) {
+                    this.canAccessDbaRecycleBin = false;
+                } else {
+                    Scope.getCurrentScope().getLog(this.getClass()).warning("Cannot check dba_recyclebin access", var7);
+                    this.canAccessDbaRecycleBin = false;
+                }
+            } finally {
+                JdbcUtils.close(null, statement);
+            }
+        }
+
+        return this.canAccessDbaRecycleBin;
+    }
+
+    public boolean supportsNotNullConstraintNames() {
+        return true;
+    }
+
+    public boolean isValidOracleIdentifier(String identifier, Class<? extends DatabaseObject> type) {
+        if (identifier != null && identifier.length() >= 1) {
+            if (!identifier.matches("^(i?)[A-Z][A-Z0-9\\$\\_\\#]*$")) {
+                return false;
+            } else {
+                return identifier.length() <= 128;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    public int getIdentifierMaximumLength() {
+        try {
+            if (this.getDatabaseMajorVersion() < 12) {
+                return 30;
+            } else {
+                return this.getDatabaseMajorVersion() == 12 && this.getDatabaseMinorVersion() <= 1 ? 30 : 128;
+            }
+        } catch (DatabaseException var2) {
+            throw new UnexpectedLiquibaseException("Cannot determine the Oracle database version number", var2);
+        }
+    }
+}

+ 23 - 0
ruoyi-admin/src/main/java/org/dromara/DromaraApplication.java

@@ -0,0 +1,23 @@
+package org.dromara;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
+
+/**
+ * 启动程序
+ *
+ * @author Lion Li
+ */
+
+@SpringBootApplication
+public class DromaraApplication {
+
+    public static void main(String[] args) {
+        SpringApplication application = new SpringApplication(DromaraApplication.class);
+        application.setApplicationStartup(new BufferingApplicationStartup(2048));
+        application.run(args);
+        System.out.println("(♥◠‿◠)ノ゙  RuoYi-Vue-Plus启动成功   ლ(´ڡ`ლ)゙");
+    }
+
+}

+ 18 - 0
ruoyi-admin/src/main/java/org/dromara/DromaraServletInitializer.java

@@ -0,0 +1,18 @@
+package org.dromara;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+/**
+ * web容器中进行部署
+ *
+ * @author Lion Li
+ */
+public class DromaraServletInitializer extends SpringBootServletInitializer {
+
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+        return application.sources(DromaraApplication.class);
+    }
+
+}

+ 234 - 0
ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java

@@ -0,0 +1,234 @@
+package org.dromara.web.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.utils.AuthStateUtils;
+import org.dromara.common.core.constant.UserConstants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.model.LoginBody;
+import org.dromara.common.core.domain.model.RegisterBody;
+import org.dromara.common.core.domain.model.SocialLoginBody;
+import org.dromara.common.core.utils.*;
+import org.dromara.common.encrypt.annotation.ApiEncrypt;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.utils.SocialUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.common.websocket.dto.WebSocketMessageDto;
+import org.dromara.common.websocket.utils.WebSocketUtils;
+import org.dromara.system.domain.bo.SysTenantBo;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysTenantVo;
+import org.dromara.system.service.ISysClientService;
+import org.dromara.system.service.ISysConfigService;
+import org.dromara.system.service.ISysSocialService;
+import org.dromara.system.service.ISysTenantService;
+import org.dromara.web.domain.vo.LoginTenantVo;
+import org.dromara.web.domain.vo.LoginVo;
+import org.dromara.web.domain.vo.TenantListVo;
+import org.dromara.web.service.IAuthStrategy;
+import org.dromara.web.service.SysLoginService;
+import org.dromara.web.service.SysRegisterService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 认证
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@SaIgnore
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/auth")
+public class AuthController {
+
+    private final SocialProperties socialProperties;
+    private final SysLoginService loginService;
+    private final SysRegisterService registerService;
+    private final ISysConfigService configService;
+    private final ISysTenantService tenantService;
+    private final ISysSocialService socialUserService;
+    private final ISysClientService clientService;
+    private final ScheduledExecutorService scheduledExecutorService;
+
+
+    /**
+     * 登录方法
+     *
+     * @param body 登录信息
+     * @return 结果
+     */
+    @ApiEncrypt
+    @PostMapping("/login")
+    public R<LoginVo> login(@RequestBody String body) {
+        LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
+        ValidatorUtils.validate(loginBody);
+        // 授权类型和客户端id
+        String clientId = loginBody.getClientId();
+        String grantType = loginBody.getGrantType();
+        SysClientVo client = clientService.queryByClientId(clientId);
+        // 查询不到 client 或 client 内不包含 grantType
+        if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
+            log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
+            return R.fail(MessageUtils.message("auth.grant.type.error"));
+        } else if (!UserConstants.NORMAL.equals(client.getStatus())) {
+            return R.fail(MessageUtils.message("auth.grant.type.blocked"));
+        }
+        // 校验租户
+        loginService.checkTenant(loginBody.getTenantId());
+        // 登录
+        LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
+
+        Long userId = LoginHelper.getUserId();
+        scheduledExecutorService.schedule(() -> {
+            WebSocketMessageDto dto = new WebSocketMessageDto();
+            dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
+            dto.setSessionKeys(List.of(userId));
+            WebSocketUtils.publishMessage(dto);
+        }, 3, TimeUnit.SECONDS);
+        return R.ok(loginVo);
+    }
+
+    /**
+     * 第三方登录请求
+     *
+     * @param source 登录来源
+     * @return 结果
+     */
+    @GetMapping("/binding/{source}")
+    public R<String> authBinding(@PathVariable("source") String source,
+                                 @RequestParam String tenantId, @RequestParam String domain) {
+        SocialLoginConfigProperties obj = socialProperties.getType().get(source);
+        if (ObjectUtil.isNull(obj)) {
+            return R.fail(source + "平台账号暂不支持");
+        }
+        AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
+        Map<String, String> map = new HashMap<>();
+        map.put("tenantId", tenantId);
+        map.put("domain", domain);
+        map.put("state", AuthStateUtils.createState());
+        String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
+        return R.ok("操作成功", authorizeUrl);
+    }
+
+    /**
+     * 第三方登录回调业务处理 绑定授权
+     *
+     * @param loginBody 请求体
+     * @return 结果
+     */
+    @PostMapping("/social/callback")
+    public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
+        // 获取第三方登录信息
+        AuthResponse<AuthUser> response = SocialUtils.loginAuth(
+                loginBody.getSource(), loginBody.getSocialCode(),
+                loginBody.getSocialState(), socialProperties);
+        AuthUser authUserData = response.getData();
+        // 判断授权响应是否成功
+        if (!response.ok()) {
+            return R.fail(response.getMsg());
+        }
+        loginService.socialRegister(authUserData);
+        return R.ok();
+    }
+
+
+    /**
+     * 取消授权
+     *
+     * @param socialId socialId
+     */
+    @DeleteMapping(value = "/unlock/{socialId}")
+    public R<Void> unlockSocial(@PathVariable Long socialId) {
+        Boolean rows = socialUserService.deleteWithValidById(socialId);
+        return rows ? R.ok() : R.fail("取消授权失败");
+    }
+
+
+    /**
+     * 退出登录
+     */
+    @PostMapping("/logout")
+    public R<Void> logout() {
+        loginService.logout();
+        return R.ok("退出成功");
+    }
+
+    /**
+     * 用户注册
+     */
+    @ApiEncrypt
+    @PostMapping("/register")
+    public R<Void> register(@Validated @RequestBody RegisterBody user) {
+        if (!configService.selectRegisterEnabled(user.getTenantId())) {
+            return R.fail("当前系统没有开启注册功能!");
+        }
+        registerService.register(user);
+        return R.ok();
+    }
+
+    /**
+     * 登录页面租户下拉框
+     *
+     * @return 租户列表
+     */
+    @GetMapping("/tenant/list")
+    public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
+        // 返回对象
+        LoginTenantVo result = new LoginTenantVo();
+        boolean enable = TenantHelper.isEnable();
+        result.setTenantEnabled(enable);
+        // 如果未开启租户这直接返回
+        if (!enable) {
+            return R.ok(result);
+        }
+
+        List<SysTenantVo> tenantList = tenantService.queryList(new SysTenantBo());
+        List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
+        try {
+            // 如果只超管返回所有租户
+            if (LoginHelper.isSuperAdmin()) {
+                result.setVoList(voList);
+                return R.ok(result);
+            }
+        } catch (NotLoginException ignored) {
+        }
+
+        // 获取域名
+        String host;
+        String referer = request.getHeader("referer");
+        if (StringUtils.isNotBlank(referer)) {
+            // 这里从referer中取值是为了本地使用hosts添加虚拟域名,方便本地环境调试
+            host = referer.split("//")[1].split("/")[0];
+        } else {
+            host = new URL(request.getRequestURL().toString()).getHost();
+        }
+        // 根据域名进行筛选
+        List<TenantListVo> list = StreamUtils.filter(voList, vo ->
+                StringUtils.equals(vo.getDomain1(), host));
+        result.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
+        return R.ok(result);
+    }
+
+}

+ 136 - 0
ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java

@@ -0,0 +1,136 @@
+package org.dromara.web.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.mail.config.properties.MailProperties;
+import org.dromara.common.mail.utils.MailUtils;
+import org.dromara.common.ratelimiter.annotation.RateLimiter;
+import org.dromara.common.ratelimiter.enums.LimitType;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.web.config.properties.CaptchaProperties;
+import org.dromara.common.web.enums.CaptchaType;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.web.domain.vo.CaptchaVo;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+import java.util.LinkedHashMap;
+
+/**
+ * 验证码操作处理
+ *
+ * @author Lion Li
+ */
+@SaIgnore
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+public class CaptchaController {
+
+    private final CaptchaProperties captchaProperties;
+    private final MailProperties mailProperties;
+
+    /**
+     * 短信验证码
+     *
+     * @param phonenumber 用户手机号
+     */
+    @RateLimiter(key = "#phonenumber", time = 60, count = 1)
+    @GetMapping("/resource/sms/code")
+    public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
+        String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
+        String code = RandomUtil.randomNumbers(4);
+        RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+        // 验证码模板id 自行处理 (查数据库或写死均可)
+        String templateId = "";
+        LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
+        map.put("code", code);
+        SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
+        SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
+        if (!smsResponse.isSuccess()) {
+            log.error("验证码短信发送异常 => {}", smsResponse);
+            return R.fail(smsResponse.getData().toString());
+        }
+        return R.ok();
+    }
+
+    /**
+     * 邮箱验证码
+     *
+     * @param email 邮箱
+     */
+    @RateLimiter(key = "#email", time = 60, count = 1)
+    @GetMapping("/resource/email/code")
+    public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
+        if (!mailProperties.getEnabled()) {
+            return R.fail("当前系统没有开启邮箱功能!");
+        }
+        String key = GlobalConstants.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();
+    }
+
+    /**
+     * 生成验证码
+     */
+    @RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
+    @GetMapping("/auth/code")
+    public R<CaptchaVo> getCode() {
+        CaptchaVo captchaVo = new CaptchaVo();
+        boolean captchaEnabled = captchaProperties.getEnable();
+        if (!captchaEnabled) {
+            captchaVo.setCaptchaEnabled(false);
+            return R.ok(captchaVo);
+        }
+        // 保存验证码信息
+        String uuid = IdUtil.simpleUUID();
+        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
+        // 生成验证码
+        CaptchaType captchaType = captchaProperties.getType();
+        boolean isMath = CaptchaType.MATH == captchaType;
+        Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
+        CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
+        AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
+        captcha.setGenerator(codeGenerator);
+        captcha.createCode();
+        // 如果是数学验证码,使用SpEL表达式处理验证码结果
+        String code = captcha.getCode();
+        if (isMath) {
+            ExpressionParser parser = new SpelExpressionParser();
+            Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
+            code = exp.getValue(String.class);
+        }
+        RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+        captchaVo.setUuid(uuid);
+        captchaVo.setImg(captcha.getImageBase64());
+        return R.ok(captchaVo);
+    }
+
+}

+ 32 - 0
ruoyi-admin/src/main/java/org/dromara/web/controller/IndexController.java

@@ -0,0 +1,32 @@
+package org.dromara.web.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import org.dromara.common.core.config.RuoYiConfig;
+import org.dromara.common.core.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 首页
+ *
+ * @author Lion Li
+ */
+@SaIgnore
+@RequiredArgsConstructor
+@RestController
+public class IndexController {
+
+    /**
+     * 系统基础配置
+     */
+    private final RuoYiConfig ruoyiConfig;
+
+    /**
+     * 访问首页,提示语
+     */
+    @GetMapping("/")
+    public String index() {
+        return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
+    }
+}

+ 25 - 0
ruoyi-admin/src/main/java/org/dromara/web/domain/vo/CaptchaVo.java

@@ -0,0 +1,25 @@
+package org.dromara.web.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 验证码信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class CaptchaVo {
+
+    /**
+     * 是否开启验证码
+     */
+    private Boolean captchaEnabled = true;
+
+    private String uuid;
+
+    /**
+     * 验证码图片
+     */
+    private String img;
+
+}

+ 25 - 0
ruoyi-admin/src/main/java/org/dromara/web/domain/vo/LoginTenantVo.java

@@ -0,0 +1,25 @@
+package org.dromara.web.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 登录租户对象
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginTenantVo {
+
+    /**
+     * 租户开关
+     */
+    private Boolean tenantEnabled;
+
+    /**
+     * 租户对象列表
+     */
+    private List<TenantListVo> voList;
+
+}

+ 54 - 0
ruoyi-admin/src/main/java/org/dromara/web/domain/vo/LoginVo.java

@@ -0,0 +1,54 @@
+package org.dromara.web.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 登录验证信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginVo {
+
+    /**
+     * 授权令牌
+     */
+    @JsonProperty("access_token")
+    private String accessToken;
+
+    /**
+     * 刷新令牌
+     */
+    @JsonProperty("refresh_token")
+    private String refreshToken;
+
+    /**
+     * 授权令牌 access_token 的有效期
+     */
+    @JsonProperty("expire_in")
+    private Long expireIn;
+
+    /**
+     * 刷新令牌 refresh_token 的有效期
+     */
+    @JsonProperty("refresh_expire_in")
+    private Long refreshExpireIn;
+
+    /**
+     * 应用id
+     */
+    @JsonProperty("client_id")
+    private String clientId;
+
+    /**
+     * 令牌权限
+     */
+    private String scope;
+
+    /**
+     * 用户 openid
+     */
+    private String openid;
+
+}

+ 31 - 0
ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java

@@ -0,0 +1,31 @@
+package org.dromara.web.domain.vo;
+
+import org.dromara.system.domain.vo.SysTenantVo;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+/**
+ * 租户列表
+ *
+ * @author Lion Li
+ */
+@Data
+@AutoMapper(target = SysTenantVo.class)
+public class TenantListVo {
+
+    /**
+     * 租户编号
+     */
+    private String tenantId;
+
+    /**
+     * 企业名称
+     */
+    private String companyName;
+
+    /**
+     * 域名
+     */
+    private String domain1;
+
+}

+ 154 - 0
ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java

@@ -0,0 +1,154 @@
+package org.dromara.web.listener;
+
+import cn.dev33.satoken.config.SaTokenConfig;
+import cn.dev33.satoken.listener.SaTokenListener;
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.CacheConstants;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.domain.dto.UserOnlineDTO;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.ip.AddressUtils;
+import org.dromara.common.log.event.LogininforEvent;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.web.service.SysLoginService;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+/**
+ * 用户行为 侦听器的实现
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class UserActionListener implements SaTokenListener {
+
+    private final SaTokenConfig tokenConfig;
+    private final SysLoginService loginService;
+
+    /**
+     * 每次登录时触发
+     */
+    @Override
+    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
+        UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
+        String ip = ServletUtils.getClientIP();
+        UserOnlineDTO dto = new UserOnlineDTO();
+        dto.setIpaddr(ip);
+        dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
+        dto.setBrowser(userAgent.getBrowser().getName());
+        dto.setOs(userAgent.getOs().getName());
+        dto.setLoginTime(System.currentTimeMillis());
+        dto.setTokenId(tokenValue);
+        String username = (String) loginModel.getExtra(LoginHelper.USER_NAME_KEY);
+        String tenantId = (String) loginModel.getExtra(LoginHelper.TENANT_KEY);
+        dto.setUserName(username);
+        dto.setClientKey((String) loginModel.getExtra(LoginHelper.CLIENT_KEY));
+        dto.setDeviceType(loginModel.getDevice());
+        dto.setDeptName((String) loginModel.getExtra(LoginHelper.DEPT_NAME_KEY));
+        TenantHelper.dynamic(tenantId, () -> {
+            if(tokenConfig.getTimeout() == -1) {
+                RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
+            } else {
+                RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
+            }
+        });
+        // 记录登录日志
+        LogininforEvent logininforEvent = new LogininforEvent();
+        logininforEvent.setTenantId(tenantId);
+        logininforEvent.setUsername(username);
+        logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
+        logininforEvent.setMessage(MessageUtils.message("user.login.success"));
+        logininforEvent.setRequest(ServletUtils.getRequest());
+        SpringUtils.context().publishEvent(logininforEvent);
+        // 更新登录信息
+        loginService.recordLoginInfo((Long) loginModel.getExtra(LoginHelper.USER_KEY), ip);
+        log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
+    }
+
+    /**
+     * 每次注销时触发
+     */
+    @Override
+    public void doLogout(String loginType, Object loginId, String tokenValue) {
+        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
+    }
+
+    /**
+     * 每次被踢下线时触发
+     */
+    @Override
+    public void doKickout(String loginType, Object loginId, String tokenValue) {
+        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
+    }
+
+    /**
+     * 每次被顶下线时触发
+     */
+    @Override
+    public void doReplaced(String loginType, Object loginId, String tokenValue) {
+        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+        log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
+    }
+
+    /**
+     * 每次被封禁时触发
+     */
+    @Override
+    public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
+    }
+
+    /**
+     * 每次被解封时触发
+     */
+    @Override
+    public void doUntieDisable(String loginType, Object loginId, String service) {
+    }
+
+    /**
+     * 每次打开二级认证时触发
+     */
+    @Override
+    public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
+    }
+
+    /**
+     * 每次创建Session时触发
+     */
+    @Override
+    public void doCloseSafe(String loginType, String tokenValue, String service) {
+    }
+
+    /**
+     * 每次创建Session时触发
+     */
+    @Override
+    public void doCreateSession(String id) {
+    }
+
+    /**
+     * 每次注销Session时触发
+     */
+    @Override
+    public void doLogoutSession(String id) {
+    }
+
+    /**
+     * 每次Token续期时触发
+     */
+    @Override
+    public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
+    }
+}

+ 46 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java

@@ -0,0 +1,46 @@
+package org.dromara.web.service;
+
+
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.system.domain.SysClient;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.web.domain.vo.LoginVo;
+
+/**
+ * 授权策略
+ *
+ * @author Michelle.Chung
+ */
+public interface IAuthStrategy {
+
+    String BASE_NAME = "AuthStrategy";
+
+    /**
+     * 登录
+     *
+     * @param body      登录对象
+     * @param client    授权管理视图对象
+     * @param grantType 授权类型
+     * @return 登录验证信息
+     */
+    static LoginVo login(String body, SysClientVo client, String grantType) {
+        // 授权类型和客户端id
+        String beanName = grantType + BASE_NAME;
+        if (!SpringUtils.containsBean(beanName)) {
+            throw new ServiceException("授权类型不正确!");
+        }
+        IAuthStrategy instance = SpringUtils.getBean(beanName);
+        return instance.login(body, client);
+    }
+
+    /**
+     * 登录
+     *
+     * @param body   登录对象
+     * @param client 授权管理视图对象
+     * @return 登录验证信息
+     */
+    LoginVo login(String body, SysClientVo client);
+
+}

+ 248 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java

@@ -0,0 +1,248 @@
+package org.dromara.web.service;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.lock.annotation.Lock4j;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthUser;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.constant.TenantConstants;
+import org.dromara.common.core.domain.dto.RoleDTO;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.enums.TenantStatus;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.*;
+import org.dromara.common.log.event.LogininforEvent;
+import org.dromara.common.mybatis.helper.DataPermissionHelper;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.exception.TenantException;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.bo.SysSocialBo;
+import org.dromara.system.domain.vo.*;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.system.service.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * 登录校验方法
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Slf4j
+@Service
+public class SysLoginService {
+
+    @Value("${user.password.maxRetryCount}")
+    private Integer maxRetryCount;
+
+    @Value("${user.password.lockTime}")
+    private Integer lockTime;
+
+    private final ISysTenantService tenantService;
+    private final ISysPermissionService permissionService;
+    private final ISysSocialService sysSocialService;
+    private final ISysRoleService roleService;
+    private final ISysDeptService deptService;
+    private final SysUserMapper userMapper;
+
+
+    /**
+     * 绑定第三方用户
+     *
+     * @param authUserData 授权响应实体
+     */
+    @Lock4j
+    public void socialRegister(AuthUser authUserData) {
+        String authId = authUserData.getSource() + authUserData.getUuid();
+        // 第三方用户信息
+        SysSocialBo bo = BeanUtil.toBean(authUserData, SysSocialBo.class);
+        BeanUtil.copyProperties(authUserData.getToken(), bo);
+        Long userId = LoginHelper.getUserId();
+        bo.setUserId(userId);
+        bo.setAuthId(authId);
+        bo.setOpenId(authUserData.getUuid());
+        bo.setUserName(authUserData.getUsername());
+        bo.setNickName(authUserData.getNickname());
+        List<SysSocialVo> checkList = sysSocialService.selectByAuthId(authId);
+        if (CollUtil.isNotEmpty(checkList)) {
+            throw new ServiceException("此三方账号已经被绑定!");
+        }
+        // 查询是否已经绑定用户
+        SysSocialBo params = new SysSocialBo();
+        params.setUserId(userId);
+        params.setSource(bo.getSource());
+        List<SysSocialVo> list = sysSocialService.queryList(params);
+        if (CollUtil.isEmpty(list)) {
+            // 没有绑定用户, 新增用户信息
+            sysSocialService.insertByBo(bo);
+        } else {
+            // 更新用户信息
+            bo.setId(list.get(0).getId());
+            sysSocialService.updateByBo(bo);
+            // 如果要绑定的平台账号已经被绑定过了 是否抛异常自行决断
+            // throw new ServiceException("此平台账号已经被绑定!");
+        }
+    }
+
+
+    /**
+     * 退出登录
+     */
+    public void logout() {
+        try {
+            LoginUser loginUser = LoginHelper.getLoginUser();
+            if (ObjectUtil.isNull(loginUser)) {
+                return;
+            }
+            if (TenantHelper.isEnable() && LoginHelper.isSuperAdmin()) {
+                // 超级管理员 登出清除动态租户
+                TenantHelper.clearDynamic();
+            }
+            recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
+        } catch (NotLoginException ignored) {
+        } finally {
+            try {
+                StpUtil.logout();
+            } catch (NotLoginException ignored) {
+            }
+        }
+    }
+
+    /**
+     * 记录登录信息
+     *
+     * @param tenantId 租户ID
+     * @param username 用户名
+     * @param status   状态
+     * @param message  消息内容
+     */
+    public void recordLogininfor(String tenantId, String username, String status, String message) {
+        LogininforEvent logininforEvent = new LogininforEvent();
+        logininforEvent.setTenantId(tenantId);
+        logininforEvent.setUsername(username);
+        logininforEvent.setStatus(status);
+        logininforEvent.setMessage(message);
+        logininforEvent.setRequest(ServletUtils.getRequest());
+        SpringUtils.context().publishEvent(logininforEvent);
+    }
+
+    /**
+     * 构建登录用户
+     */
+    public LoginUser buildLoginUser(SysUserVo user) {
+        LoginUser loginUser = new LoginUser();
+        loginUser.setTenantId(user.getTenantId());
+        loginUser.setUserId(user.getUserId());
+        loginUser.setDeptId(user.getDeptId());
+        loginUser.setUsername(user.getUserName());
+        loginUser.setNickname(user.getNickName());
+        loginUser.setUserType(user.getUserType());
+        loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
+        loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
+        TenantHelper.dynamic(user.getTenantId(), () -> {
+            SysDeptVo dept = null;
+            if (ObjectUtil.isNotNull(user.getDeptId())) {
+                dept = deptService.selectDeptById(user.getDeptId());
+            }
+            loginUser.setDeptName(ObjectUtil.isNull(dept) ? "" : dept.getDeptName());
+            loginUser.setDeptCategory(ObjectUtil.isNull(dept) ? "" : dept.getDeptCategory());
+            List<SysRoleVo> roles = roleService.selectRolesByUserId(user.getUserId());
+            loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
+        });
+        return loginUser;
+    }
+
+    /**
+     * 记录登录信息
+     *
+     * @param userId 用户ID
+     */
+    public void recordLoginInfo(Long userId, String ip) {
+        SysUser sysUser = new SysUser();
+        sysUser.setUserId(userId);
+        sysUser.setLoginIp(ip);
+        sysUser.setLoginDate(DateUtils.getNowDate());
+        sysUser.setUpdateBy(userId);
+        DataPermissionHelper.ignore(() -> userMapper.updateById(sysUser));
+    }
+
+    /**
+     * 登录校验
+     */
+    public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
+        String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
+        String loginFail = Constants.LOGIN_FAIL;
+
+        // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
+        int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
+        // 锁定时间内登录 则踢出
+        if (errorNumber >= maxRetryCount) {
+            recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+        }
+
+        if (supplier.get()) {
+            // 错误次数递增
+            errorNumber++;
+            RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
+            // 达到规定错误次数 则锁定登录
+            if (errorNumber >= maxRetryCount) {
+                recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+                throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+            } else {
+                // 未达到规定错误次数
+                recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
+                throw new UserException(loginType.getRetryLimitCount(), errorNumber);
+            }
+        }
+
+        // 登录成功 清空错误次数
+        RedisUtils.deleteObject(errorKey);
+    }
+
+    /**
+     * 校验租户
+     *
+     * @param tenantId 租户ID
+     */
+    public void checkTenant(String tenantId) {
+        if (!TenantHelper.isEnable()) {
+            return;
+        }
+        if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
+            return;
+        }
+        if (StringUtils.isBlank(tenantId)) {
+            throw new TenantException("tenant.number.not.blank");
+        }
+        SysTenantVo tenant = tenantService.queryByTenantId(tenantId);
+        if (ObjectUtil.isNull(tenant)) {
+            log.info("登录租户:{} 不存在.", tenantId);
+            throw new TenantException("tenant.not.exists");
+        } else if (TenantStatus.DISABLE.getCode().equals(tenant.getStatus())) {
+            log.info("登录租户:{} 已被停用.", tenantId);
+            throw new TenantException("tenant.blocked");
+        } else if (ObjectUtil.isNotNull(tenant.getExpireTime())
+            && new Date().after(tenant.getExpireTime())) {
+            log.info("登录租户:{} 已超过有效期.", tenantId);
+            throw new TenantException("tenant.expired");
+        }
+    }
+
+}

+ 115 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/SysRegisterService.java

@@ -0,0 +1,115 @@
+package org.dromara.web.service;
+
+import cn.dev33.satoken.secure.BCrypt;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.model.RegisterBody;
+import org.dromara.common.core.enums.UserType;
+import org.dromara.common.core.exception.user.CaptchaException;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.log.event.LogininforEvent;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.common.web.config.properties.CaptchaProperties;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.bo.SysUserBo;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.system.service.ISysUserService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 注册校验方法
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Service
+public class SysRegisterService {
+
+    private final ISysUserService userService;
+    private final SysUserMapper userMapper;
+    private final CaptchaProperties captchaProperties;
+
+    /**
+     * 注册
+     */
+    public void register(RegisterBody registerBody) {
+        String tenantId = registerBody.getTenantId();
+        String username = registerBody.getUsername();
+        String password = registerBody.getPassword();
+        // 校验用户类型是否存在
+        String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
+
+        boolean captchaEnabled = captchaProperties.getEnable();
+        // 验证码开关
+        if (captchaEnabled) {
+            validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
+        }
+        SysUserBo sysUser = new SysUserBo();
+        sysUser.setUserName(username);
+        sysUser.setNickName(username);
+        sysUser.setPassword(BCrypt.hashpw(password));
+        sysUser.setUserType(userType);
+
+        boolean exist = TenantHelper.dynamic(tenantId, () -> {
+            return userMapper.exists(new LambdaQueryWrapper<SysUser>()
+                .eq(SysUser::getUserName, sysUser.getUserName()));
+        });
+        if (exist) {
+            throw new UserException("user.register.save.error", username);
+        }
+        boolean regFlag = userService.registerUser(sysUser, tenantId);
+        if (!regFlag) {
+            throw new UserException("user.register.error");
+        }
+        recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
+    }
+
+    /**
+     * 校验验证码
+     *
+     * @param username 用户名
+     * @param code     验证码
+     * @param uuid     唯一标识
+     */
+    public void validateCaptcha(String tenantId, String username, String code, String uuid) {
+        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
+        String captcha = RedisUtils.getCacheObject(verifyKey);
+        RedisUtils.deleteObject(verifyKey);
+        if (captcha == null) {
+            recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.expire"));
+            throw new CaptchaExpireException();
+        }
+        if (!code.equalsIgnoreCase(captcha)) {
+            recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error"));
+            throw new CaptchaException();
+        }
+    }
+
+    /**
+     * 记录登录信息
+     *
+     * @param tenantId 租户ID
+     * @param username 用户名
+     * @param status   状态
+     * @param message  消息内容
+     * @return
+     */
+    private void recordLogininfor(String tenantId, String username, String status, String message) {
+        LogininforEvent logininforEvent = new LogininforEvent();
+        logininforEvent.setTenantId(tenantId);
+        logininforEvent.setUsername(username);
+        logininforEvent.setStatus(status);
+        logininforEvent.setMessage(message);
+        logininforEvent.setRequest(ServletUtils.getRequest());
+        SpringUtils.context().publishEvent(logininforEvent);
+    }
+
+}

+ 106 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java

@@ -0,0 +1,106 @@
+package org.dromara.web.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.model.EmailLoginBody;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.domain.SysClient;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.web.domain.vo.LoginVo;
+import org.dromara.web.service.IAuthStrategy;
+import org.dromara.web.service.SysLoginService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 邮件认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("email" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class EmailAuthStrategy implements IAuthStrategy {
+
+    private final SysLoginService loginService;
+    private final SysUserMapper userMapper;
+
+    @Override
+    public LoginVo login(String body, SysClientVo client) {
+        EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
+        ValidatorUtils.validate(loginBody);
+        String tenantId = loginBody.getTenantId();
+        String email = loginBody.getEmail();
+        String emailCode = loginBody.getEmailCode();
+
+        // 通过邮箱查找用户
+        SysUserVo user = loadUserByEmail(tenantId, email);
+
+        loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+        LoginUser loginUser = loginService.buildLoginUser(user);
+        loginUser.setClientKey(client.getClientKey());
+        loginUser.setDeviceType(client.getDeviceType());
+        SaLoginModel model = new SaLoginModel();
+        model.setDevice(client.getDeviceType());
+        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+        // 例如: 后台用户30分钟过期 app用户1天过期
+        model.setTimeout(client.getTimeout());
+        model.setActiveTimeout(client.getActiveTimeout());
+        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+        // 生成token
+        LoginHelper.login(loginUser, model);
+
+        LoginVo loginVo = new LoginVo();
+        loginVo.setAccessToken(StpUtil.getTokenValue());
+        loginVo.setExpireIn(StpUtil.getTokenTimeout());
+        loginVo.setClientId(client.getClientId());
+        return loginVo;
+    }
+
+    /**
+     * 校验邮箱验证码
+     */
+    private boolean validateEmailCode(String tenantId, String email, String emailCode) {
+        String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
+        if (StringUtils.isBlank(code)) {
+            loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+            throw new CaptchaExpireException();
+        }
+        return code.equals(emailCode);
+    }
+
+    private SysUserVo loadUserByEmail(String tenantId, String email) {
+        return TenantHelper.dynamic(tenantId, () -> {
+            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().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 user;
+        });
+    }
+
+}

+ 124 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java

@@ -0,0 +1,124 @@
+package org.dromara.web.service.impl;
+
+import cn.dev33.satoken.secure.BCrypt;
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.domain.model.PasswordLoginBody;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.exception.user.CaptchaException;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.common.web.config.properties.CaptchaProperties;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.web.domain.vo.LoginVo;
+import org.dromara.web.service.IAuthStrategy;
+import org.dromara.web.service.SysLoginService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 密码认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("password" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class PasswordAuthStrategy implements IAuthStrategy {
+
+    private final CaptchaProperties captchaProperties;
+    private final SysLoginService loginService;
+    private final SysUserMapper userMapper;
+
+    @Override
+    public LoginVo login(String body, SysClientVo client) {
+        PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
+        ValidatorUtils.validate(loginBody);
+        String tenantId = loginBody.getTenantId();
+        String username = loginBody.getUsername();
+        String password = loginBody.getPassword();
+        String code = loginBody.getCode();
+        String uuid = loginBody.getUuid();
+
+        boolean captchaEnabled = captchaProperties.getEnable();
+        // 验证码开关
+        if (captchaEnabled) {
+            validateCaptcha(tenantId, username, code, uuid);
+        }
+
+        SysUserVo user = loadUserByUsername(tenantId, username);
+        loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
+        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        LoginUser loginUser = loginService.buildLoginUser(user);
+        loginUser.setClientKey(client.getClientKey());
+        loginUser.setDeviceType(client.getDeviceType());
+        SaLoginModel model = new SaLoginModel();
+        model.setDevice(client.getDeviceType());
+        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+        // 例如: 后台用户30分钟过期 app用户1天过期
+        model.setTimeout(client.getTimeout());
+        model.setActiveTimeout(client.getActiveTimeout());
+        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+        // 生成token
+        LoginHelper.login(loginUser, model);
+
+        LoginVo loginVo = new LoginVo();
+        loginVo.setAccessToken(StpUtil.getTokenValue());
+        loginVo.setExpireIn(StpUtil.getTokenTimeout());
+        loginVo.setClientId(client.getClientId());
+        return loginVo;
+    }
+
+    /**
+     * 校验验证码
+     *
+     * @param username 用户名
+     * @param code     验证码
+     * @param uuid     唯一标识
+     */
+    private void validateCaptcha(String tenantId, String username, String code, String uuid) {
+        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
+        String captcha = RedisUtils.getCacheObject(verifyKey);
+        RedisUtils.deleteObject(verifyKey);
+        if (captcha == null) {
+            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+            throw new CaptchaExpireException();
+        }
+        if (!code.equalsIgnoreCase(captcha)) {
+            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
+            throw new CaptchaException();
+        }
+    }
+
+    private SysUserVo loadUserByUsername(String tenantId, String username) {
+        return TenantHelper.dynamic(tenantId, () -> {
+            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
+            if (ObjectUtil.isNull(user)) {
+                log.info("登录用户:{} 不存在.", username);
+                throw new UserException("user.not.exists", username);
+            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+                log.info("登录用户:{} 已被停用.", username);
+                throw new UserException("user.blocked", username);
+            }
+            return user;
+        });
+    }
+
+}

+ 106 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java

@@ -0,0 +1,106 @@
+package org.dromara.web.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.domain.model.SmsLoginBody;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.domain.SysClient;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.web.domain.vo.LoginVo;
+import org.dromara.web.service.IAuthStrategy;
+import org.dromara.web.service.SysLoginService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 短信认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("sms" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class SmsAuthStrategy implements IAuthStrategy {
+
+    private final SysLoginService loginService;
+    private final SysUserMapper userMapper;
+
+    @Override
+    public LoginVo login(String body, SysClientVo client) {
+        SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
+        ValidatorUtils.validate(loginBody);
+        String tenantId = loginBody.getTenantId();
+        String phonenumber = loginBody.getPhonenumber();
+        String smsCode = loginBody.getSmsCode();
+
+        // 通过手机号查找用户
+        SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
+
+        loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+        LoginUser loginUser = loginService.buildLoginUser(user);
+        loginUser.setClientKey(client.getClientKey());
+        loginUser.setDeviceType(client.getDeviceType());
+        SaLoginModel model = new SaLoginModel();
+        model.setDevice(client.getDeviceType());
+        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+        // 例如: 后台用户30分钟过期 app用户1天过期
+        model.setTimeout(client.getTimeout());
+        model.setActiveTimeout(client.getActiveTimeout());
+        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+        // 生成token
+        LoginHelper.login(loginUser, model);
+
+        LoginVo loginVo = new LoginVo();
+        loginVo.setAccessToken(StpUtil.getTokenValue());
+        loginVo.setExpireIn(StpUtil.getTokenTimeout());
+        loginVo.setClientId(client.getClientId());
+        return loginVo;
+    }
+
+    /**
+     * 校验短信验证码
+     */
+    private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
+        String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
+        if (StringUtils.isBlank(code)) {
+            loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+            throw new CaptchaExpireException();
+        }
+        return code.equals(smsCode);
+    }
+
+    private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
+        return TenantHelper.dynamic(tenantId, () -> {
+            SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
+            if (ObjectUtil.isNull(user)) {
+                log.info("登录用户:{} 不存在.", phonenumber);
+                throw new UserException("user.not.exists", phonenumber);
+            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+                log.info("登录用户:{} 已被停用.", phonenumber);
+                throw new UserException("user.blocked", phonenumber);
+            }
+            return user;
+        });
+    }
+
+}

+ 133 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java

@@ -0,0 +1,133 @@
+package org.dromara.web.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.http.Method;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.domain.model.SocialLoginBody;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.utils.SocialUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysSocialVo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.system.service.ISysSocialService;
+import org.dromara.web.domain.vo.LoginVo;
+import org.dromara.web.service.IAuthStrategy;
+import org.dromara.web.service.SysLoginService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 第三方授权策略
+ *
+ * @author thiszhc is 三三
+ */
+@Slf4j
+@Service("social" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class SocialAuthStrategy implements IAuthStrategy {
+
+    private final SocialProperties socialProperties;
+    private final ISysSocialService sysSocialService;
+    private final SysUserMapper userMapper;
+    private final SysLoginService loginService;
+
+    /**
+     * 登录-第三方授权登录
+     *
+     * @param body     登录信息
+     * @param client   客户端信息
+     */
+    @Override
+    public LoginVo login(String body, SysClientVo client) {
+        SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
+        ValidatorUtils.validate(loginBody);
+        AuthResponse<AuthUser> response = SocialUtils.loginAuth(
+                loginBody.getSource(), loginBody.getSocialCode(),
+                loginBody.getSocialState(), socialProperties);
+        if (!response.ok()) {
+            throw new ServiceException(response.getMsg());
+        }
+        AuthUser authUserData = response.getData();
+        if ("GITEE".equals(authUserData.getSource())) {
+            // 如用户使用 gitee 登录顺手 star 给作者一点支持 拒绝白嫖
+            HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Vue-Plus")
+                    .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
+                    .executeAsync();
+            HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Cloud-Plus")
+                    .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
+                    .executeAsync();
+        }
+
+        List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
+        if (CollUtil.isEmpty(list)) {
+            throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
+        }
+        SysSocialVo social;
+        if (TenantHelper.isEnable()) {
+            Optional<SysSocialVo> opt = StreamUtils.findAny(list, x -> x.getTenantId().equals(loginBody.getTenantId()));
+            if (opt.isEmpty()) {
+                throw new ServiceException("对不起,你没有权限登录当前租户!");
+            }
+            social = opt.get();
+        } else {
+            social = list.get(0);
+        }
+        // 查找用户
+        SysUserVo user = loadUser(social.getTenantId(), social.getUserId());
+
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+        LoginUser loginUser = loginService.buildLoginUser(user);
+        loginUser.setClientKey(client.getClientKey());
+        loginUser.setDeviceType(client.getDeviceType());
+        SaLoginModel model = new SaLoginModel();
+        model.setDevice(client.getDeviceType());
+        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+        // 例如: 后台用户30分钟过期 app用户1天过期
+        model.setTimeout(client.getTimeout());
+        model.setActiveTimeout(client.getActiveTimeout());
+        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+        // 生成token
+        LoginHelper.login(loginUser, model);
+
+        LoginVo loginVo = new LoginVo();
+        loginVo.setAccessToken(StpUtil.getTokenValue());
+        loginVo.setExpireIn(StpUtil.getTokenTimeout());
+        loginVo.setClientId(client.getClientId());
+        return loginVo;
+    }
+
+    private SysUserVo loadUser(String tenantId, Long userId) {
+        return TenantHelper.dynamic(tenantId, () -> {
+            SysUserVo user = userMapper.selectVoById(userId);
+            if (ObjectUtil.isNull(user)) {
+                log.info("登录用户:{} 不存在.", "");
+                throw new UserException("user.not.exists", "");
+            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+                log.info("登录用户:{} 已被停用.", "");
+                throw new UserException("user.blocked", "");
+            }
+            return user;
+        });
+    }
+
+}

+ 92 - 0
ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java

@@ -0,0 +1,92 @@
+package org.dromara.web.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.model.XcxLoginBody;
+import org.dromara.common.core.domain.model.XcxLoginUser;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.domain.SysClient;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.web.domain.vo.LoginVo;
+import org.dromara.web.service.IAuthStrategy;
+import org.dromara.web.service.SysLoginService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 小程序认证策略
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("xcx" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class XcxAuthStrategy implements IAuthStrategy {
+
+    private final SysLoginService loginService;
+
+    @Override
+    public LoginVo login(String body, SysClientVo client) {
+        XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
+        ValidatorUtils.validate(loginBody);
+        // xcxCode 为 小程序调用 wx.login 授权后获取
+        String xcxCode = loginBody.getXcxCode();
+        // 多个小程序识别使用
+        String appid = loginBody.getAppid();
+
+        // todo 以下自行实现
+        // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
+        String openid = "";
+        // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
+        SysUserVo user = loadUserByOpenid(openid);
+
+        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
+        XcxLoginUser loginUser = new XcxLoginUser();
+        loginUser.setTenantId(user.getTenantId());
+        loginUser.setUserId(user.getUserId());
+        loginUser.setUsername(user.getUserName());
+        loginUser.setNickname(user.getNickName());
+        loginUser.setUserType(user.getUserType());
+        loginUser.setClientKey(client.getClientKey());
+        loginUser.setDeviceType(client.getDeviceType());
+        loginUser.setOpenid(openid);
+
+        SaLoginModel model = new SaLoginModel();
+        model.setDevice(client.getDeviceType());
+        // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+        // 例如: 后台用户30分钟过期 app用户1天过期
+        model.setTimeout(client.getTimeout());
+        model.setActiveTimeout(client.getActiveTimeout());
+        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+        // 生成token
+        LoginHelper.login(loginUser, model);
+
+        LoginVo loginVo = new LoginVo();
+        loginVo.setAccessToken(StpUtil.getTokenValue());
+        loginVo.setExpireIn(StpUtil.getTokenTimeout());
+        loginVo.setClientId(client.getClientId());
+        loginVo.setOpenid(openid);
+        return loginVo;
+    }
+
+    private SysUserVo loadUserByOpenid(String openid) {
+        // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
+        // todo 自行实现 userService.selectUserByOpenid(openid);
+        SysUserVo user = new SysUserVo();
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", openid);
+            // todo 用户不存在 业务逻辑自行实现
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", openid);
+            // todo 用户已被停用 业务逻辑自行实现
+        }
+        return user;
+    }
+
+}

+ 1946 - 0
ruoyi-admin/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java

@@ -0,0 +1,1946 @@
+package org.flowable.common.engine.impl;
+
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by FernFlower decompiler)
+//
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+import java.util.Set;
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ibatis.builder.xml.XMLConfigBuilder;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.datasource.pooled.PooledDataSource;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;
+import org.apache.ibatis.transaction.TransactionFactory;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
+import org.apache.ibatis.type.ArrayTypeHandler;
+import org.apache.ibatis.type.BigDecimalTypeHandler;
+import org.apache.ibatis.type.BlobInputStreamTypeHandler;
+import org.apache.ibatis.type.BlobTypeHandler;
+import org.apache.ibatis.type.BooleanTypeHandler;
+import org.apache.ibatis.type.ByteTypeHandler;
+import org.apache.ibatis.type.ClobTypeHandler;
+import org.apache.ibatis.type.DateOnlyTypeHandler;
+import org.apache.ibatis.type.DateTypeHandler;
+import org.apache.ibatis.type.DoubleTypeHandler;
+import org.apache.ibatis.type.FloatTypeHandler;
+import org.apache.ibatis.type.IntegerTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.LongTypeHandler;
+import org.apache.ibatis.type.NClobTypeHandler;
+import org.apache.ibatis.type.NStringTypeHandler;
+import org.apache.ibatis.type.ShortTypeHandler;
+import org.apache.ibatis.type.SqlxmlTypeHandler;
+import org.apache.ibatis.type.StringTypeHandler;
+import org.apache.ibatis.type.TimeOnlyTypeHandler;
+import org.apache.ibatis.type.TypeHandlerRegistry;
+import org.flowable.common.engine.api.FlowableException;
+import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
+import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher;
+import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
+import org.flowable.common.engine.api.engine.EngineLifecycleListener;
+import org.flowable.common.engine.impl.agenda.AgendaOperationRunner;
+import org.flowable.common.engine.impl.cfg.CommandExecutorImpl;
+import org.flowable.common.engine.impl.cfg.IdGenerator;
+import org.flowable.common.engine.impl.cfg.TransactionContextFactory;
+import org.flowable.common.engine.impl.cfg.standalone.StandaloneMybatisTransactionContextFactory;
+import org.flowable.common.engine.impl.db.CommonDbSchemaManager;
+import org.flowable.common.engine.impl.db.DbSqlSessionFactory;
+import org.flowable.common.engine.impl.db.LogSqlExecutionTimePlugin;
+import org.flowable.common.engine.impl.db.MybatisTypeAliasConfigurator;
+import org.flowable.common.engine.impl.db.MybatisTypeHandlerConfigurator;
+import org.flowable.common.engine.impl.db.SchemaManager;
+import org.flowable.common.engine.impl.event.EventDispatchAction;
+import org.flowable.common.engine.impl.event.FlowableEventDispatcherImpl;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandConfig;
+import org.flowable.common.engine.impl.interceptor.CommandContextFactory;
+import org.flowable.common.engine.impl.interceptor.CommandContextInterceptor;
+import org.flowable.common.engine.impl.interceptor.CommandExecutor;
+import org.flowable.common.engine.impl.interceptor.CommandInterceptor;
+import org.flowable.common.engine.impl.interceptor.CrDbRetryInterceptor;
+import org.flowable.common.engine.impl.interceptor.DefaultCommandInvoker;
+import org.flowable.common.engine.impl.interceptor.LogInterceptor;
+import org.flowable.common.engine.impl.interceptor.SessionFactory;
+import org.flowable.common.engine.impl.interceptor.TransactionContextInterceptor;
+import org.flowable.common.engine.impl.lock.LockManager;
+import org.flowable.common.engine.impl.lock.LockManagerImpl;
+import org.flowable.common.engine.impl.logging.LoggingListener;
+import org.flowable.common.engine.impl.logging.LoggingSession;
+import org.flowable.common.engine.impl.logging.LoggingSessionFactory;
+import org.flowable.common.engine.impl.persistence.GenericManagerFactory;
+import org.flowable.common.engine.impl.persistence.StrongUuidGenerator;
+import org.flowable.common.engine.impl.persistence.cache.EntityCache;
+import org.flowable.common.engine.impl.persistence.cache.EntityCacheImpl;
+import org.flowable.common.engine.impl.persistence.entity.ByteArrayEntityManager;
+import org.flowable.common.engine.impl.persistence.entity.ByteArrayEntityManagerImpl;
+import org.flowable.common.engine.impl.persistence.entity.Entity;
+import org.flowable.common.engine.impl.persistence.entity.PropertyEntityManager;
+import org.flowable.common.engine.impl.persistence.entity.PropertyEntityManagerImpl;
+import org.flowable.common.engine.impl.persistence.entity.TableDataManager;
+import org.flowable.common.engine.impl.persistence.entity.TableDataManagerImpl;
+import org.flowable.common.engine.impl.persistence.entity.data.ByteArrayDataManager;
+import org.flowable.common.engine.impl.persistence.entity.data.PropertyDataManager;
+import org.flowable.common.engine.impl.persistence.entity.data.impl.MybatisByteArrayDataManager;
+import org.flowable.common.engine.impl.persistence.entity.data.impl.MybatisPropertyDataManager;
+import org.flowable.common.engine.impl.runtime.Clock;
+import org.flowable.common.engine.impl.service.CommonEngineServiceImpl;
+import org.flowable.common.engine.impl.util.DefaultClockImpl;
+import org.flowable.common.engine.impl.util.IoUtil;
+import org.flowable.common.engine.impl.util.ReflectUtil;
+import org.flowable.eventregistry.api.EventRegistryEventConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractEngineConfiguration {
+    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+    public static final String NO_TENANT_ID = "";
+    public static final String DB_SCHEMA_UPDATE_FALSE = "false";
+    public static final String DB_SCHEMA_UPDATE_CREATE = "create";
+    public static final String DB_SCHEMA_UPDATE_CREATE_DROP = "create-drop";
+    public static final String DB_SCHEMA_UPDATE_DROP_CREATE = "drop-create";
+    public static final String DB_SCHEMA_UPDATE_TRUE = "true";
+    protected boolean forceCloseMybatisConnectionPool = true;
+    protected String databaseType;
+    protected String jdbcDriver = "org.h2.Driver";
+    protected String jdbcUrl = "jdbc:h2:tcp://localhost/~/flowable";
+    protected String jdbcUsername = "sa";
+    protected String jdbcPassword = "";
+    protected String dataSourceJndiName;
+    protected int jdbcMaxActiveConnections = 16;
+    protected int jdbcMaxIdleConnections = 8;
+    protected int jdbcMaxCheckoutTime;
+    protected int jdbcMaxWaitTime;
+    protected boolean jdbcPingEnabled;
+    protected String jdbcPingQuery;
+    protected int jdbcPingConnectionNotUsedFor;
+    protected int jdbcDefaultTransactionIsolationLevel;
+    protected DataSource dataSource;
+    protected SchemaManager commonSchemaManager;
+    protected SchemaManager schemaManager;
+    protected Command<Void> schemaManagementCmd;
+    protected String databaseSchemaUpdate = "false";
+    protected boolean useLockForDatabaseSchemaUpdate = false;
+    protected String xmlEncoding = "UTF-8";
+    protected CommandExecutor commandExecutor;
+    protected Collection<? extends CommandInterceptor> defaultCommandInterceptors;
+    protected CommandConfig defaultCommandConfig;
+    protected CommandConfig schemaCommandConfig;
+    protected CommandContextFactory commandContextFactory;
+    protected CommandInterceptor commandInvoker;
+    protected AgendaOperationRunner agendaOperationRunner = (commandContext, runnable) -> {
+        runnable.run();
+    };
+    protected List<CommandInterceptor> customPreCommandInterceptors;
+    protected List<CommandInterceptor> customPostCommandInterceptors;
+    protected List<CommandInterceptor> commandInterceptors;
+    protected Map<String, AbstractEngineConfiguration> engineConfigurations = new HashMap();
+    protected Map<String, AbstractServiceConfiguration> serviceConfigurations = new HashMap();
+    protected ClassLoader classLoader;
+    protected boolean useClassForNameClassLoading = true;
+    protected List<EngineLifecycleListener> engineLifecycleListeners;
+    protected Map<String, EventRegistryEventConsumer> eventRegistryEventConsumers = new HashMap();
+    protected boolean isDbHistoryUsed = true;
+    protected DbSqlSessionFactory dbSqlSessionFactory;
+    protected SqlSessionFactory sqlSessionFactory;
+    protected TransactionFactory transactionFactory;
+    protected TransactionContextFactory transactionContextFactory;
+    protected boolean isBulkInsertEnabled = true;
+    protected int maxNrOfStatementsInBulkInsert = 100;
+    public int DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER = 55;
+    protected String mybatisMappingFile;
+    protected Set<Class<?>> customMybatisMappers;
+    protected Set<String> customMybatisXMLMappers;
+    protected List<Interceptor> customMybatisInterceptors;
+    protected Set<String> dependentEngineMyBatisXmlMappers;
+    protected List<MybatisTypeAliasConfigurator> dependentEngineMybatisTypeAliasConfigs;
+    protected List<MybatisTypeHandlerConfigurator> dependentEngineMybatisTypeHandlerConfigs;
+    protected List<SessionFactory> customSessionFactories;
+    protected Map<Class<?>, SessionFactory> sessionFactories;
+    protected boolean enableEventDispatcher = true;
+    protected FlowableEventDispatcher eventDispatcher;
+    protected List<FlowableEventListener> eventListeners;
+    protected Map<String, List<FlowableEventListener>> typedEventListeners;
+    protected List<EventDispatchAction> additionalEventDispatchActions;
+    protected LoggingListener loggingListener;
+    protected boolean transactionsExternallyManaged;
+    protected boolean usingRelationalDatabase = true;
+    protected boolean usingSchemaMgmt = true;
+    protected String databaseTablePrefix = "";
+    protected String databaseWildcardEscapeCharacter;
+    protected String databaseCatalog = "";
+    protected String databaseSchema;
+    protected boolean tablePrefixIsSchema;
+    protected boolean alwaysLookupLatestDefinitionVersion;
+    protected boolean fallbackToDefaultTenant;
+    protected DefaultTenantProvider defaultTenantProvider = (tenantId, scope, scopeKey) -> {
+        return "";
+    };
+    protected boolean enableLogSqlExecutionTime;
+    protected Properties databaseTypeMappings = getDefaultDatabaseTypeMappings();
+    protected Duration lockPollRate = Duration.ofSeconds(10L);
+    protected Duration schemaLockWaitTime = Duration.ofMinutes(5L);
+    protected PropertyDataManager propertyDataManager;
+    protected ByteArrayDataManager byteArrayDataManager;
+    protected TableDataManager tableDataManager;
+    protected PropertyEntityManager propertyEntityManager;
+    protected ByteArrayEntityManager byteArrayEntityManager;
+    protected List<EngineDeployer> customPreDeployers;
+    protected List<EngineDeployer> customPostDeployers;
+    protected List<EngineDeployer> deployers;
+    protected boolean enableConfiguratorServiceLoader = true;
+    protected List<EngineConfigurator> configurators;
+    protected List<EngineConfigurator> allConfigurators;
+    protected EngineConfigurator idmEngineConfigurator;
+    protected EngineConfigurator eventRegistryConfigurator;
+    public static final String PRODUCT_NAME_POSTGRES = "PostgreSQL";
+    public static final String PRODUCT_NAME_CRDB = "CockroachDB";
+    public static final String DATABASE_TYPE_H2 = "h2";
+    public static final String DATABASE_TYPE_HSQL = "hsql";
+    public static final String DATABASE_TYPE_MYSQL = "mysql";
+    public static final String DATABASE_TYPE_ORACLE = "oracle";
+    public static final String DATABASE_TYPE_POSTGRES = "postgres";
+    public static final String DATABASE_TYPE_MSSQL = "mssql";
+    public static final String DATABASE_TYPE_DB2 = "db2";
+    public static final String DATABASE_TYPE_COCKROACHDB = "cockroachdb";
+    protected Map<Object, Object> beans;
+    protected IdGenerator idGenerator;
+    protected boolean usePrefixId;
+    protected Clock clock;
+    protected ObjectMapper objectMapper;
+    public static final int DEFAULT_GENERIC_MAX_LENGTH_STRING = 4000;
+    public static final int DEFAULT_ORACLE_MAX_LENGTH_STRING = 2000;
+    protected int maxLengthStringVariableType = -1;
+
+    public AbstractEngineConfiguration() {
+    }
+
+    public static Properties getDefaultDatabaseTypeMappings() {
+        Properties databaseTypeMappings = new Properties();
+        databaseTypeMappings.setProperty("H2", "h2");
+        databaseTypeMappings.setProperty("HSQL Database Engine", "hsql");
+        databaseTypeMappings.setProperty("MySQL", "mysql");
+        databaseTypeMappings.setProperty("MariaDB", "mysql");
+        databaseTypeMappings.setProperty("Oracle", "oracle");
+        databaseTypeMappings.setProperty("PostgreSQL", "postgres");
+        databaseTypeMappings.setProperty("Microsoft SQL Server", "mssql");
+        databaseTypeMappings.setProperty("db2", "db2");
+        databaseTypeMappings.setProperty("DB2", "db2");
+        databaseTypeMappings.setProperty("DB2/NT", "db2");
+        databaseTypeMappings.setProperty("DB2/NT64", "db2");
+        databaseTypeMappings.setProperty("DB2 UDP", "db2");
+        databaseTypeMappings.setProperty("DB2/LINUX", "db2");
+        databaseTypeMappings.setProperty("DB2/LINUX390", "db2");
+        databaseTypeMappings.setProperty("DB2/LINUXX8664", "db2");
+        databaseTypeMappings.setProperty("DB2/LINUXZ64", "db2");
+        databaseTypeMappings.setProperty("DB2/LINUXPPC64", "db2");
+        databaseTypeMappings.setProperty("DB2/LINUXPPC64LE", "db2");
+        databaseTypeMappings.setProperty("DB2/400 SQL", "db2");
+        databaseTypeMappings.setProperty("DB2/6000", "db2");
+        databaseTypeMappings.setProperty("DB2 UDB iSeries", "db2");
+        databaseTypeMappings.setProperty("DB2/AIX64", "db2");
+        databaseTypeMappings.setProperty("DB2/HPUX", "db2");
+        databaseTypeMappings.setProperty("DB2/HP64", "db2");
+        databaseTypeMappings.setProperty("DB2/SUN", "db2");
+        databaseTypeMappings.setProperty("DB2/SUN64", "db2");
+        databaseTypeMappings.setProperty("DB2/PTX", "db2");
+        databaseTypeMappings.setProperty("DB2/2", "db2");
+        databaseTypeMappings.setProperty("DB2 UDB AS400", "db2");
+        databaseTypeMappings.setProperty("CockroachDB", "cockroachdb");
+        databaseTypeMappings.setProperty("DM DBMS", DATABASE_TYPE_ORACLE);
+        return databaseTypeMappings;
+    }
+
+    protected void initEngineConfigurations() {
+        this.addEngineConfiguration(this.getEngineCfgKey(), this.getEngineScopeType(), this);
+    }
+
+    protected void initDataSource() {
+        if (this.dataSource == null) {
+            if (this.dataSourceJndiName != null) {
+                try {
+                    this.dataSource = (DataSource)(new InitialContext()).lookup(this.dataSourceJndiName);
+                } catch (Exception var2) {
+                    Exception e = var2;
+                    throw new FlowableException("couldn't lookup datasource from " + this.dataSourceJndiName + ": " + e.getMessage(), e);
+                }
+            } else if (this.jdbcUrl != null) {
+                if (this.jdbcDriver == null || this.jdbcUsername == null) {
+                    throw new FlowableException("DataSource or JDBC properties have to be specified in a process engine configuration");
+                }
+
+                this.logger.debug("initializing datasource to db: {}", this.jdbcUrl);
+                if (this.logger.isInfoEnabled()) {
+                    this.logger.info("Configuring Datasource with following properties (omitted password for security)");
+                    this.logger.info("datasource driver : {}", this.jdbcDriver);
+                    this.logger.info("datasource url : {}", this.jdbcUrl);
+                    this.logger.info("datasource user name : {}", this.jdbcUsername);
+                }
+
+                PooledDataSource pooledDataSource = new PooledDataSource(this.getClass().getClassLoader(), this.jdbcDriver, this.jdbcUrl, this.jdbcUsername, this.jdbcPassword);
+                if (this.jdbcMaxActiveConnections > 0) {
+                    pooledDataSource.setPoolMaximumActiveConnections(this.jdbcMaxActiveConnections);
+                }
+
+                if (this.jdbcMaxIdleConnections > 0) {
+                    pooledDataSource.setPoolMaximumIdleConnections(this.jdbcMaxIdleConnections);
+                }
+
+                if (this.jdbcMaxCheckoutTime > 0) {
+                    pooledDataSource.setPoolMaximumCheckoutTime(this.jdbcMaxCheckoutTime);
+                }
+
+                if (this.jdbcMaxWaitTime > 0) {
+                    pooledDataSource.setPoolTimeToWait(this.jdbcMaxWaitTime);
+                }
+
+                if (this.jdbcPingEnabled) {
+                    pooledDataSource.setPoolPingEnabled(true);
+                    if (this.jdbcPingQuery != null) {
+                        pooledDataSource.setPoolPingQuery(this.jdbcPingQuery);
+                    }
+
+                    pooledDataSource.setPoolPingConnectionsNotUsedFor(this.jdbcPingConnectionNotUsedFor);
+                }
+
+                if (this.jdbcDefaultTransactionIsolationLevel > 0) {
+                    pooledDataSource.setDefaultTransactionIsolationLevel(this.jdbcDefaultTransactionIsolationLevel);
+                }
+
+                this.dataSource = pooledDataSource;
+            }
+        }
+
+        if (this.databaseType == null) {
+            this.initDatabaseType();
+        }
+
+    }
+
+    public void initDatabaseType() {
+        Connection connection = null;
+
+        try {
+            connection = this.dataSource.getConnection();
+            DatabaseMetaData databaseMetaData = connection.getMetaData();
+            String databaseProductName = databaseMetaData.getDatabaseProductName();
+            this.logger.debug("database product name: '{}'", databaseProductName);
+            if ("PostgreSQL".equalsIgnoreCase(databaseProductName)) {
+                PreparedStatement preparedStatement = connection.prepareStatement("select version() as version;");
+
+                try {
+                    ResultSet resultSet = preparedStatement.executeQuery();
+
+                    try {
+                        String version = null;
+                        if (resultSet.next()) {
+                            version = resultSet.getString("version");
+                        }
+
+                        if (StringUtils.isNotEmpty(version) && version.toLowerCase().startsWith("CockroachDB".toLowerCase())) {
+                            databaseProductName = "CockroachDB";
+                            this.logger.info("CockroachDB version '{}' detected", version);
+                        }
+                    } catch (Throwable var21) {
+                        if (resultSet != null) {
+                            try {
+                                resultSet.close();
+                            } catch (Throwable var20) {
+                                var21.addSuppressed(var20);
+                            }
+                        }
+
+                        throw var21;
+                    }
+
+                    if (resultSet != null) {
+                        resultSet.close();
+                    }
+                } catch (Throwable var22) {
+                    if (preparedStatement != null) {
+                        try {
+                            preparedStatement.close();
+                        } catch (Throwable var19) {
+                            var22.addSuppressed(var19);
+                        }
+                    }
+
+                    throw var22;
+                }
+
+                if (preparedStatement != null) {
+                    preparedStatement.close();
+                }
+            }
+
+            this.databaseType = this.databaseTypeMappings.getProperty(databaseProductName);
+            if (this.databaseType == null) {
+                throw new FlowableException("couldn't deduct database type from database product name '" + databaseProductName + "'");
+            }
+
+            this.logger.debug("using database type: {}", this.databaseType);
+        } catch (SQLException var23) {
+            SQLException e = var23;
+            throw new RuntimeException("Exception while initializing Database connection", e);
+        } finally {
+            try {
+                if (connection != null) {
+                    connection.close();
+                }
+            } catch (SQLException var18) {
+                SQLException e = var18;
+                this.logger.error("Exception while closing the Database connection", e);
+            }
+
+        }
+
+        if ("mssql".equals(this.databaseType)) {
+            this.maxNrOfStatementsInBulkInsert = this.DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER;
+        }
+
+    }
+
+    public void initSchemaManager() {
+        if (this.commonSchemaManager == null) {
+            this.commonSchemaManager = new CommonDbSchemaManager();
+        }
+
+    }
+
+    public void addSessionFactory(SessionFactory sessionFactory) {
+        this.sessionFactories.put(sessionFactory.getSessionType(), sessionFactory);
+    }
+
+    public void initCommandContextFactory() {
+        if (this.commandContextFactory == null) {
+            this.commandContextFactory = new CommandContextFactory();
+        }
+
+    }
+
+    public void initTransactionContextFactory() {
+        if (this.transactionContextFactory == null) {
+            this.transactionContextFactory = new StandaloneMybatisTransactionContextFactory();
+        }
+
+    }
+
+    public void initCommandExecutors() {
+        this.initDefaultCommandConfig();
+        this.initSchemaCommandConfig();
+        this.initCommandInvoker();
+        this.initCommandInterceptors();
+        this.initCommandExecutor();
+    }
+
+    public void initDefaultCommandConfig() {
+        if (this.defaultCommandConfig == null) {
+            this.defaultCommandConfig = new CommandConfig();
+        }
+
+    }
+
+    public void initSchemaCommandConfig() {
+        if (this.schemaCommandConfig == null) {
+            this.schemaCommandConfig = new CommandConfig();
+        }
+
+    }
+
+    public void initCommandInvoker() {
+        if (this.commandInvoker == null) {
+            this.commandInvoker = new DefaultCommandInvoker();
+        }
+
+    }
+
+    public void initCommandInterceptors() {
+        if (this.commandInterceptors == null) {
+            this.commandInterceptors = new ArrayList();
+            if (this.customPreCommandInterceptors != null) {
+                this.commandInterceptors.addAll(this.customPreCommandInterceptors);
+            }
+
+            this.commandInterceptors.addAll(this.getDefaultCommandInterceptors());
+            if (this.customPostCommandInterceptors != null) {
+                this.commandInterceptors.addAll(this.customPostCommandInterceptors);
+            }
+
+            this.commandInterceptors.add(this.commandInvoker);
+        }
+
+    }
+
+    public Collection<? extends CommandInterceptor> getDefaultCommandInterceptors() {
+        if (this.defaultCommandInterceptors == null) {
+            List<CommandInterceptor> interceptors = new ArrayList();
+            interceptors.add(new LogInterceptor());
+            if ("cockroachdb".equals(this.databaseType)) {
+                interceptors.add(new CrDbRetryInterceptor());
+            }
+
+            CommandInterceptor transactionInterceptor = this.createTransactionInterceptor();
+            if (transactionInterceptor != null) {
+                interceptors.add(transactionInterceptor);
+            }
+
+            if (this.commandContextFactory != null) {
+                String engineCfgKey = this.getEngineCfgKey();
+                CommandContextInterceptor commandContextInterceptor = new CommandContextInterceptor(this.commandContextFactory, this.classLoader, this.useClassForNameClassLoading, this.clock, this.objectMapper);
+                this.engineConfigurations.put(engineCfgKey, this);
+                commandContextInterceptor.setEngineCfgKey(engineCfgKey);
+                commandContextInterceptor.setEngineConfigurations(this.engineConfigurations);
+                interceptors.add(commandContextInterceptor);
+            }
+
+            if (this.transactionContextFactory != null) {
+                interceptors.add(new TransactionContextInterceptor(this.transactionContextFactory));
+            }
+
+            List<CommandInterceptor> additionalCommandInterceptors = this.getAdditionalDefaultCommandInterceptors();
+            if (additionalCommandInterceptors != null) {
+                interceptors.addAll(additionalCommandInterceptors);
+            }
+
+            this.defaultCommandInterceptors = interceptors;
+        }
+
+        return this.defaultCommandInterceptors;
+    }
+
+    public abstract String getEngineCfgKey();
+
+    public abstract String getEngineScopeType();
+
+    public List<CommandInterceptor> getAdditionalDefaultCommandInterceptors() {
+        return null;
+    }
+
+    public void initCommandExecutor() {
+        if (this.commandExecutor == null) {
+            CommandInterceptor first = this.initInterceptorChain(this.commandInterceptors);
+            this.commandExecutor = new CommandExecutorImpl(this.getDefaultCommandConfig(), first);
+        }
+
+    }
+
+    public CommandInterceptor initInterceptorChain(List<CommandInterceptor> chain) {
+        if (chain != null && !chain.isEmpty()) {
+            for(int i = 0; i < chain.size() - 1; ++i) {
+                ((CommandInterceptor)chain.get(i)).setNext((CommandInterceptor)chain.get(i + 1));
+            }
+
+            return (CommandInterceptor)chain.get(0);
+        } else {
+            throw new FlowableException("invalid command interceptor chain configuration: " + chain);
+        }
+    }
+
+    public abstract CommandInterceptor createTransactionInterceptor();
+
+    public void initBeans() {
+        if (this.beans == null) {
+            this.beans = new HashMap();
+        }
+
+    }
+
+    public void initIdGenerator() {
+        if (this.idGenerator == null) {
+            this.idGenerator = new StrongUuidGenerator();
+        }
+
+    }
+
+    public void initObjectMapper() {
+        if (this.objectMapper == null) {
+            this.objectMapper = new ObjectMapper();
+            this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+        }
+
+    }
+
+    public void initClock() {
+        if (this.clock == null) {
+            this.clock = new DefaultClockImpl();
+        }
+
+    }
+
+    public void initDataManagers() {
+        if (this.propertyDataManager == null) {
+            this.propertyDataManager = new MybatisPropertyDataManager(this.idGenerator);
+        }
+
+        if (this.byteArrayDataManager == null) {
+            this.byteArrayDataManager = new MybatisByteArrayDataManager(this.idGenerator);
+        }
+
+    }
+
+    public void initEntityManagers() {
+        if (this.propertyEntityManager == null) {
+            this.propertyEntityManager = new PropertyEntityManagerImpl(this, this.propertyDataManager);
+        }
+
+        if (this.byteArrayEntityManager == null) {
+            this.byteArrayEntityManager = new ByteArrayEntityManagerImpl(this.byteArrayDataManager, this.getEngineCfgKey(), this::getEventDispatcher);
+        }
+
+        if (this.tableDataManager == null) {
+            this.tableDataManager = new TableDataManagerImpl(this);
+        }
+
+    }
+
+    protected void initService(Object service) {
+        if (service instanceof CommonEngineServiceImpl) {
+            ((CommonEngineServiceImpl)service).setCommandExecutor(this.commandExecutor);
+        }
+
+    }
+
+    public void initSessionFactories() {
+        if (this.sessionFactories == null) {
+            this.sessionFactories = new HashMap();
+            if (this.usingRelationalDatabase) {
+                this.initDbSqlSessionFactory();
+            }
+
+            this.addSessionFactory(new GenericManagerFactory(EntityCache.class, EntityCacheImpl.class));
+            if (this.isLoggingSessionEnabled() && !this.sessionFactories.containsKey(LoggingSession.class)) {
+                LoggingSessionFactory loggingSessionFactory = new LoggingSessionFactory();
+                loggingSessionFactory.setLoggingListener(this.loggingListener);
+                loggingSessionFactory.setObjectMapper(this.objectMapper);
+                this.sessionFactories.put(LoggingSession.class, loggingSessionFactory);
+            }
+
+            this.commandContextFactory.setSessionFactories(this.sessionFactories);
+        } else if (this.usingRelationalDatabase) {
+            this.initDbSqlSessionFactoryEntitySettings();
+        }
+
+        if (this.customSessionFactories != null) {
+            Iterator var3 = this.customSessionFactories.iterator();
+
+            while(var3.hasNext()) {
+                SessionFactory sessionFactory = (SessionFactory)var3.next();
+                this.addSessionFactory(sessionFactory);
+            }
+        }
+
+    }
+
+    public void initDbSqlSessionFactory() {
+        if (this.dbSqlSessionFactory == null) {
+            this.dbSqlSessionFactory = this.createDbSqlSessionFactory();
+        }
+
+        this.dbSqlSessionFactory.setDatabaseType(this.databaseType);
+        this.dbSqlSessionFactory.setSqlSessionFactory(this.sqlSessionFactory);
+        this.dbSqlSessionFactory.setDbHistoryUsed(this.isDbHistoryUsed);
+        this.dbSqlSessionFactory.setDatabaseTablePrefix(this.databaseTablePrefix);
+        this.dbSqlSessionFactory.setTablePrefixIsSchema(this.tablePrefixIsSchema);
+        this.dbSqlSessionFactory.setDatabaseCatalog(this.databaseCatalog);
+        this.dbSqlSessionFactory.setDatabaseSchema(this.databaseSchema);
+        this.dbSqlSessionFactory.setMaxNrOfStatementsInBulkInsert(this.maxNrOfStatementsInBulkInsert);
+        this.initDbSqlSessionFactoryEntitySettings();
+        this.addSessionFactory(this.dbSqlSessionFactory);
+    }
+
+    public DbSqlSessionFactory createDbSqlSessionFactory() {
+        return new DbSqlSessionFactory(this.usePrefixId);
+    }
+
+    protected abstract void initDbSqlSessionFactoryEntitySettings();
+
+    protected void defaultInitDbSqlSessionFactoryEntitySettings(List<Class<? extends Entity>> insertOrder, List<Class<? extends Entity>> deleteOrder) {
+        Iterator var3;
+        Class clazz;
+        if (insertOrder != null) {
+            var3 = insertOrder.iterator();
+
+            while(var3.hasNext()) {
+                clazz = (Class)var3.next();
+                this.dbSqlSessionFactory.getInsertionOrder().add(clazz);
+                if (this.isBulkInsertEnabled) {
+                    this.dbSqlSessionFactory.getBulkInserteableEntityClasses().add(clazz);
+                }
+            }
+        }
+
+        if (deleteOrder != null) {
+            var3 = deleteOrder.iterator();
+
+            while(var3.hasNext()) {
+                clazz = (Class)var3.next();
+                this.dbSqlSessionFactory.getDeletionOrder().add(clazz);
+            }
+        }
+
+    }
+
+    public void initTransactionFactory() {
+        if (this.transactionFactory == null) {
+            if (this.transactionsExternallyManaged) {
+                this.transactionFactory = new ManagedTransactionFactory();
+                Properties properties = new Properties();
+                properties.put("closeConnection", "false");
+                this.transactionFactory.setProperties(properties);
+            } else {
+                this.transactionFactory = new JdbcTransactionFactory();
+            }
+        }
+
+    }
+
+    public void initSqlSessionFactory() {
+        if (this.sqlSessionFactory == null) {
+            InputStream inputStream = null;
+
+            try {
+                inputStream = this.getMyBatisXmlConfigurationStream();
+                Environment environment = new Environment("default", this.transactionFactory, this.dataSource);
+                Reader reader = new InputStreamReader(inputStream);
+                Properties properties = new Properties();
+                properties.put("prefix", this.databaseTablePrefix);
+                String wildcardEscapeClause = "";
+                if (this.databaseWildcardEscapeCharacter != null && this.databaseWildcardEscapeCharacter.length() != 0) {
+                    wildcardEscapeClause = " escape '" + this.databaseWildcardEscapeCharacter + "'";
+                }
+
+                properties.put("wildcardEscapeClause", wildcardEscapeClause);
+                properties.put("limitBefore", "");
+                properties.put("limitAfter", "");
+                properties.put("limitBetween", "");
+                properties.put("limitBeforeNativeQuery", "");
+                properties.put("limitAfterNativeQuery", "");
+                properties.put("blobType", "BLOB");
+                properties.put("boolValue", "TRUE");
+                if (this.databaseType != null) {
+                    properties.load(this.getResourceAsStream(this.pathToEngineDbProperties()));
+                }
+
+                Configuration configuration = this.initMybatisConfiguration(environment, reader, properties);
+                this.sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
+            } catch (Exception var10) {
+                Exception e = var10;
+                throw new FlowableException("Error while building ibatis SqlSessionFactory: " + e.getMessage(), e);
+            } finally {
+                IoUtil.closeSilently(inputStream);
+            }
+        } else {
+            this.applyCustomMybatisCustomizations(this.sqlSessionFactory.getConfiguration());
+        }
+
+    }
+
+    public String pathToEngineDbProperties() {
+        return "org/flowable/common/db/properties/" + this.databaseType + ".properties";
+    }
+
+    public Configuration initMybatisConfiguration(Environment environment, Reader reader, Properties properties) {
+        XMLConfigBuilder parser = new XMLConfigBuilder(reader, "", properties);
+        Configuration configuration = parser.getConfiguration();
+        if (this.databaseType != null) {
+            configuration.setDatabaseId(this.databaseType);
+        }
+
+        configuration.setEnvironment(environment);
+        this.initMybatisTypeHandlers(configuration);
+        this.initCustomMybatisInterceptors(configuration);
+        if (this.isEnableLogSqlExecutionTime()) {
+            this.initMyBatisLogSqlExecutionTimePlugin(configuration);
+        }
+
+        configuration = this.parseMybatisConfiguration(parser);
+        return configuration;
+    }
+
+    public void initCustomMybatisMappers(Configuration configuration) {
+        if (this.getCustomMybatisMappers() != null) {
+            Iterator var2 = this.getCustomMybatisMappers().iterator();
+
+            while(var2.hasNext()) {
+                Class<?> clazz = (Class)var2.next();
+                if (!configuration.hasMapper(clazz)) {
+                    configuration.addMapper(clazz);
+                }
+            }
+        }
+
+    }
+
+    public void initMybatisTypeHandlers(Configuration configuration) {
+        TypeHandlerRegistry handlerRegistry = configuration.getTypeHandlerRegistry();
+        handlerRegistry.register(Object.class, JdbcType.BOOLEAN, new BooleanTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.BIT, new BooleanTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.TINYINT, new ByteTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.SMALLINT, new ShortTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.INTEGER, new IntegerTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.FLOAT, new FloatTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.DOUBLE, new DoubleTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.CHAR, new StringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.CLOB, new ClobTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.VARCHAR, new StringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NVARCHAR, new NStringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NCHAR, new NStringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NCLOB, new NClobTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.BIGINT, new LongTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.REAL, new BigDecimalTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.DECIMAL, new BigDecimalTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NUMERIC, new BigDecimalTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.BLOB, new BlobInputStreamTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.DATE, new DateOnlyTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.TIME, new TimeOnlyTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.TIMESTAMP, new DateTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.SQLXML, new SqlxmlTypeHandler());
+    }
+
+    public void initCustomMybatisInterceptors(Configuration configuration) {
+        if (this.customMybatisInterceptors != null) {
+            Iterator var2 = this.customMybatisInterceptors.iterator();
+
+            while(var2.hasNext()) {
+                Interceptor interceptor = (Interceptor)var2.next();
+                configuration.addInterceptor(interceptor);
+            }
+        }
+
+    }
+
+    public void initMyBatisLogSqlExecutionTimePlugin(Configuration configuration) {
+        configuration.addInterceptor(new LogSqlExecutionTimePlugin());
+    }
+
+    public Configuration parseMybatisConfiguration(XMLConfigBuilder parser) {
+        Configuration configuration = parser.parse();
+        this.applyCustomMybatisCustomizations(configuration);
+        return configuration;
+    }
+
+    protected void applyCustomMybatisCustomizations(Configuration configuration) {
+        this.initCustomMybatisMappers(configuration);
+        Iterator var2;
+        if (this.dependentEngineMybatisTypeAliasConfigs != null) {
+            var2 = this.dependentEngineMybatisTypeAliasConfigs.iterator();
+
+            while(var2.hasNext()) {
+                MybatisTypeAliasConfigurator typeAliasConfig = (MybatisTypeAliasConfigurator)var2.next();
+                typeAliasConfig.configure(configuration.getTypeAliasRegistry());
+            }
+        }
+
+        if (this.dependentEngineMybatisTypeHandlerConfigs != null) {
+            var2 = this.dependentEngineMybatisTypeHandlerConfigs.iterator();
+
+            while(var2.hasNext()) {
+                MybatisTypeHandlerConfigurator typeHandlerConfig = (MybatisTypeHandlerConfigurator)var2.next();
+                typeHandlerConfig.configure(configuration.getTypeHandlerRegistry());
+            }
+        }
+
+        this.parseDependentEngineMybatisXMLMappers(configuration);
+        this.parseCustomMybatisXMLMappers(configuration);
+    }
+
+    public void parseCustomMybatisXMLMappers(Configuration configuration) {
+        if (this.getCustomMybatisXMLMappers() != null) {
+            Iterator var2 = this.getCustomMybatisXMLMappers().iterator();
+
+            while(var2.hasNext()) {
+                String resource = (String)var2.next();
+                this.parseMybatisXmlMapping(configuration, resource);
+            }
+        }
+
+    }
+
+    public void parseDependentEngineMybatisXMLMappers(Configuration configuration) {
+        if (this.getDependentEngineMyBatisXmlMappers() != null) {
+            Iterator var2 = this.getDependentEngineMyBatisXmlMappers().iterator();
+
+            while(var2.hasNext()) {
+                String resource = (String)var2.next();
+                this.parseMybatisXmlMapping(configuration, resource);
+            }
+        }
+
+    }
+
+    protected void parseMybatisXmlMapping(Configuration configuration, String resource) {
+        XMLMapperBuilder mapperParser = new XMLMapperBuilder(this.getResourceAsStream(resource), configuration, resource, configuration.getSqlFragments());
+        mapperParser.parse();
+    }
+
+    protected InputStream getResourceAsStream(String resource) {
+        ClassLoader classLoader = this.getClassLoader();
+        return classLoader != null ? this.getClassLoader().getResourceAsStream(resource) : this.getClass().getClassLoader().getResourceAsStream(resource);
+    }
+
+    public void setMybatisMappingFile(String file) {
+        this.mybatisMappingFile = file;
+    }
+
+    public String getMybatisMappingFile() {
+        return this.mybatisMappingFile;
+    }
+
+    public abstract InputStream getMyBatisXmlConfigurationStream();
+
+    public void initConfigurators() {
+        this.allConfigurators = new ArrayList();
+        this.allConfigurators.addAll(this.getEngineSpecificEngineConfigurators());
+        if (this.configurators != null) {
+            this.allConfigurators.addAll(this.configurators);
+        }
+
+        if (this.enableConfiguratorServiceLoader) {
+            ClassLoader classLoader = this.getClassLoader();
+            if (classLoader == null) {
+                classLoader = ReflectUtil.getClassLoader();
+            }
+
+            ServiceLoader<EngineConfigurator> configuratorServiceLoader = ServiceLoader.load(EngineConfigurator.class, classLoader);
+            int nrOfServiceLoadedConfigurators = 0;
+
+            Iterator var4;
+            EngineConfigurator configurator;
+            for(var4 = configuratorServiceLoader.iterator(); var4.hasNext(); ++nrOfServiceLoadedConfigurators) {
+                configurator = (EngineConfigurator)var4.next();
+                this.allConfigurators.add(configurator);
+            }
+
+            if (nrOfServiceLoadedConfigurators > 0) {
+                this.logger.info("Found {} auto-discoverable Process Engine Configurator{}", nrOfServiceLoadedConfigurators, nrOfServiceLoadedConfigurators > 1 ? "s" : "");
+            }
+
+            if (!this.allConfigurators.isEmpty()) {
+                this.allConfigurators.sort(new Comparator<EngineConfigurator>() {
+                    public int compare(EngineConfigurator configurator1, EngineConfigurator configurator2) {
+                        int priority1 = configurator1.getPriority();
+                        int priority2 = configurator2.getPriority();
+                        if (priority1 < priority2) {
+                            return -1;
+                        } else {
+                            return priority1 > priority2 ? 1 : 0;
+                        }
+                    }
+                });
+                this.logger.info("Found {} Engine Configurators in total:", this.allConfigurators.size());
+                var4 = this.allConfigurators.iterator();
+
+                while(var4.hasNext()) {
+                    configurator = (EngineConfigurator)var4.next();
+                    this.logger.info("{} (priority:{})", configurator.getClass(), configurator.getPriority());
+                }
+            }
+        }
+
+    }
+
+    public void close() {
+        if (this.forceCloseMybatisConnectionPool && this.dataSource instanceof PooledDataSource) {
+            ((PooledDataSource)this.dataSource).forceCloseAll();
+        }
+
+    }
+
+    protected List<EngineConfigurator> getEngineSpecificEngineConfigurators() {
+        return Collections.emptyList();
+    }
+
+    public void configuratorsBeforeInit() {
+        Iterator var1 = this.allConfigurators.iterator();
+
+        while(var1.hasNext()) {
+            EngineConfigurator configurator = (EngineConfigurator)var1.next();
+            this.logger.info("Executing beforeInit() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
+            configurator.beforeInit(this);
+        }
+
+    }
+
+    public void configuratorsAfterInit() {
+        Iterator var1 = this.allConfigurators.iterator();
+
+        while(var1.hasNext()) {
+            EngineConfigurator configurator = (EngineConfigurator)var1.next();
+            this.logger.info("Executing configure() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
+            configurator.configure(this);
+        }
+
+    }
+
+    public LockManager getLockManager(String lockName) {
+        return new LockManagerImpl(this.commandExecutor, lockName, this.getLockPollRate(), this.getEngineCfgKey());
+    }
+
+    public abstract String getEngineName();
+
+    public ClassLoader getClassLoader() {
+        return this.classLoader;
+    }
+
+    public AbstractEngineConfiguration setClassLoader(ClassLoader classLoader) {
+        this.classLoader = classLoader;
+        return this;
+    }
+
+    public boolean isUseClassForNameClassLoading() {
+        return this.useClassForNameClassLoading;
+    }
+
+    public AbstractEngineConfiguration setUseClassForNameClassLoading(boolean useClassForNameClassLoading) {
+        this.useClassForNameClassLoading = useClassForNameClassLoading;
+        return this;
+    }
+
+    public void addEngineLifecycleListener(EngineLifecycleListener engineLifecycleListener) {
+        if (this.engineLifecycleListeners == null) {
+            this.engineLifecycleListeners = new ArrayList();
+        }
+
+        this.engineLifecycleListeners.add(engineLifecycleListener);
+    }
+
+    public List<EngineLifecycleListener> getEngineLifecycleListeners() {
+        return this.engineLifecycleListeners;
+    }
+
+    public AbstractEngineConfiguration setEngineLifecycleListeners(List<EngineLifecycleListener> engineLifecycleListeners) {
+        this.engineLifecycleListeners = engineLifecycleListeners;
+        return this;
+    }
+
+    public String getDatabaseType() {
+        return this.databaseType;
+    }
+
+    public AbstractEngineConfiguration setDatabaseType(String databaseType) {
+        this.databaseType = databaseType;
+        return this;
+    }
+
+    public DataSource getDataSource() {
+        return this.dataSource;
+    }
+
+    public AbstractEngineConfiguration setDataSource(DataSource dataSource) {
+        this.dataSource = dataSource;
+        return this;
+    }
+
+    public SchemaManager getSchemaManager() {
+        return this.schemaManager;
+    }
+
+    public AbstractEngineConfiguration setSchemaManager(SchemaManager schemaManager) {
+        this.schemaManager = schemaManager;
+        return this;
+    }
+
+    public SchemaManager getCommonSchemaManager() {
+        return this.commonSchemaManager;
+    }
+
+    public AbstractEngineConfiguration setCommonSchemaManager(SchemaManager commonSchemaManager) {
+        this.commonSchemaManager = commonSchemaManager;
+        return this;
+    }
+
+    public Command<Void> getSchemaManagementCmd() {
+        return this.schemaManagementCmd;
+    }
+
+    public AbstractEngineConfiguration setSchemaManagementCmd(Command<Void> schemaManagementCmd) {
+        this.schemaManagementCmd = schemaManagementCmd;
+        return this;
+    }
+
+    public String getJdbcDriver() {
+        return this.jdbcDriver;
+    }
+
+    public AbstractEngineConfiguration setJdbcDriver(String jdbcDriver) {
+        this.jdbcDriver = jdbcDriver;
+        return this;
+    }
+
+    public String getJdbcUrl() {
+        return this.jdbcUrl;
+    }
+
+    public AbstractEngineConfiguration setJdbcUrl(String jdbcUrl) {
+        this.jdbcUrl = jdbcUrl;
+        return this;
+    }
+
+    public String getJdbcUsername() {
+        return this.jdbcUsername;
+    }
+
+    public AbstractEngineConfiguration setJdbcUsername(String jdbcUsername) {
+        this.jdbcUsername = jdbcUsername;
+        return this;
+    }
+
+    public String getJdbcPassword() {
+        return this.jdbcPassword;
+    }
+
+    public AbstractEngineConfiguration setJdbcPassword(String jdbcPassword) {
+        this.jdbcPassword = jdbcPassword;
+        return this;
+    }
+
+    public int getJdbcMaxActiveConnections() {
+        return this.jdbcMaxActiveConnections;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxActiveConnections(int jdbcMaxActiveConnections) {
+        this.jdbcMaxActiveConnections = jdbcMaxActiveConnections;
+        return this;
+    }
+
+    public int getJdbcMaxIdleConnections() {
+        return this.jdbcMaxIdleConnections;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxIdleConnections(int jdbcMaxIdleConnections) {
+        this.jdbcMaxIdleConnections = jdbcMaxIdleConnections;
+        return this;
+    }
+
+    public int getJdbcMaxCheckoutTime() {
+        return this.jdbcMaxCheckoutTime;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxCheckoutTime(int jdbcMaxCheckoutTime) {
+        this.jdbcMaxCheckoutTime = jdbcMaxCheckoutTime;
+        return this;
+    }
+
+    public int getJdbcMaxWaitTime() {
+        return this.jdbcMaxWaitTime;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxWaitTime(int jdbcMaxWaitTime) {
+        this.jdbcMaxWaitTime = jdbcMaxWaitTime;
+        return this;
+    }
+
+    public boolean isJdbcPingEnabled() {
+        return this.jdbcPingEnabled;
+    }
+
+    public AbstractEngineConfiguration setJdbcPingEnabled(boolean jdbcPingEnabled) {
+        this.jdbcPingEnabled = jdbcPingEnabled;
+        return this;
+    }
+
+    public int getJdbcPingConnectionNotUsedFor() {
+        return this.jdbcPingConnectionNotUsedFor;
+    }
+
+    public AbstractEngineConfiguration setJdbcPingConnectionNotUsedFor(int jdbcPingConnectionNotUsedFor) {
+        this.jdbcPingConnectionNotUsedFor = jdbcPingConnectionNotUsedFor;
+        return this;
+    }
+
+    public int getJdbcDefaultTransactionIsolationLevel() {
+        return this.jdbcDefaultTransactionIsolationLevel;
+    }
+
+    public AbstractEngineConfiguration setJdbcDefaultTransactionIsolationLevel(int jdbcDefaultTransactionIsolationLevel) {
+        this.jdbcDefaultTransactionIsolationLevel = jdbcDefaultTransactionIsolationLevel;
+        return this;
+    }
+
+    public String getJdbcPingQuery() {
+        return this.jdbcPingQuery;
+    }
+
+    public AbstractEngineConfiguration setJdbcPingQuery(String jdbcPingQuery) {
+        this.jdbcPingQuery = jdbcPingQuery;
+        return this;
+    }
+
+    public String getDataSourceJndiName() {
+        return this.dataSourceJndiName;
+    }
+
+    public AbstractEngineConfiguration setDataSourceJndiName(String dataSourceJndiName) {
+        this.dataSourceJndiName = dataSourceJndiName;
+        return this;
+    }
+
+    public CommandConfig getSchemaCommandConfig() {
+        return this.schemaCommandConfig;
+    }
+
+    public AbstractEngineConfiguration setSchemaCommandConfig(CommandConfig schemaCommandConfig) {
+        this.schemaCommandConfig = schemaCommandConfig;
+        return this;
+    }
+
+    public boolean isTransactionsExternallyManaged() {
+        return this.transactionsExternallyManaged;
+    }
+
+    public AbstractEngineConfiguration setTransactionsExternallyManaged(boolean transactionsExternallyManaged) {
+        this.transactionsExternallyManaged = transactionsExternallyManaged;
+        return this;
+    }
+
+    public Map<Object, Object> getBeans() {
+        return this.beans;
+    }
+
+    public AbstractEngineConfiguration setBeans(Map<Object, Object> beans) {
+        this.beans = beans;
+        return this;
+    }
+
+    public IdGenerator getIdGenerator() {
+        return this.idGenerator;
+    }
+
+    public AbstractEngineConfiguration setIdGenerator(IdGenerator idGenerator) {
+        this.idGenerator = idGenerator;
+        return this;
+    }
+
+    public boolean isUsePrefixId() {
+        return this.usePrefixId;
+    }
+
+    public AbstractEngineConfiguration setUsePrefixId(boolean usePrefixId) {
+        this.usePrefixId = usePrefixId;
+        return this;
+    }
+
+    public String getXmlEncoding() {
+        return this.xmlEncoding;
+    }
+
+    public AbstractEngineConfiguration setXmlEncoding(String xmlEncoding) {
+        this.xmlEncoding = xmlEncoding;
+        return this;
+    }
+
+    public CommandConfig getDefaultCommandConfig() {
+        return this.defaultCommandConfig;
+    }
+
+    public AbstractEngineConfiguration setDefaultCommandConfig(CommandConfig defaultCommandConfig) {
+        this.defaultCommandConfig = defaultCommandConfig;
+        return this;
+    }
+
+    public CommandExecutor getCommandExecutor() {
+        return this.commandExecutor;
+    }
+
+    public AbstractEngineConfiguration setCommandExecutor(CommandExecutor commandExecutor) {
+        this.commandExecutor = commandExecutor;
+        return this;
+    }
+
+    public CommandContextFactory getCommandContextFactory() {
+        return this.commandContextFactory;
+    }
+
+    public AbstractEngineConfiguration setCommandContextFactory(CommandContextFactory commandContextFactory) {
+        this.commandContextFactory = commandContextFactory;
+        return this;
+    }
+
+    public CommandInterceptor getCommandInvoker() {
+        return this.commandInvoker;
+    }
+
+    public AbstractEngineConfiguration setCommandInvoker(CommandInterceptor commandInvoker) {
+        this.commandInvoker = commandInvoker;
+        return this;
+    }
+
+    public AgendaOperationRunner getAgendaOperationRunner() {
+        return this.agendaOperationRunner;
+    }
+
+    public AbstractEngineConfiguration setAgendaOperationRunner(AgendaOperationRunner agendaOperationRunner) {
+        this.agendaOperationRunner = agendaOperationRunner;
+        return this;
+    }
+
+    public List<CommandInterceptor> getCustomPreCommandInterceptors() {
+        return this.customPreCommandInterceptors;
+    }
+
+    public AbstractEngineConfiguration setCustomPreCommandInterceptors(List<CommandInterceptor> customPreCommandInterceptors) {
+        this.customPreCommandInterceptors = customPreCommandInterceptors;
+        return this;
+    }
+
+    public List<CommandInterceptor> getCustomPostCommandInterceptors() {
+        return this.customPostCommandInterceptors;
+    }
+
+    public AbstractEngineConfiguration setCustomPostCommandInterceptors(List<CommandInterceptor> customPostCommandInterceptors) {
+        this.customPostCommandInterceptors = customPostCommandInterceptors;
+        return this;
+    }
+
+    public List<CommandInterceptor> getCommandInterceptors() {
+        return this.commandInterceptors;
+    }
+
+    public AbstractEngineConfiguration setCommandInterceptors(List<CommandInterceptor> commandInterceptors) {
+        this.commandInterceptors = commandInterceptors;
+        return this;
+    }
+
+    public Map<String, AbstractEngineConfiguration> getEngineConfigurations() {
+        return this.engineConfigurations;
+    }
+
+    public AbstractEngineConfiguration setEngineConfigurations(Map<String, AbstractEngineConfiguration> engineConfigurations) {
+        this.engineConfigurations = engineConfigurations;
+        return this;
+    }
+
+    public void addEngineConfiguration(String key, String scopeType, AbstractEngineConfiguration engineConfiguration) {
+        if (this.engineConfigurations == null) {
+            this.engineConfigurations = new HashMap();
+        }
+
+        this.engineConfigurations.put(key, engineConfiguration);
+        this.engineConfigurations.put(scopeType, engineConfiguration);
+    }
+
+    public Map<String, AbstractServiceConfiguration> getServiceConfigurations() {
+        return this.serviceConfigurations;
+    }
+
+    public AbstractEngineConfiguration setServiceConfigurations(Map<String, AbstractServiceConfiguration> serviceConfigurations) {
+        this.serviceConfigurations = serviceConfigurations;
+        return this;
+    }
+
+    public void addServiceConfiguration(String key, AbstractServiceConfiguration serviceConfiguration) {
+        if (this.serviceConfigurations == null) {
+            this.serviceConfigurations = new HashMap();
+        }
+
+        this.serviceConfigurations.put(key, serviceConfiguration);
+    }
+
+    public Map<String, EventRegistryEventConsumer> getEventRegistryEventConsumers() {
+        return this.eventRegistryEventConsumers;
+    }
+
+    public AbstractEngineConfiguration setEventRegistryEventConsumers(Map<String, EventRegistryEventConsumer> eventRegistryEventConsumers) {
+        this.eventRegistryEventConsumers = eventRegistryEventConsumers;
+        return this;
+    }
+
+    public void addEventRegistryEventConsumer(String key, EventRegistryEventConsumer eventRegistryEventConsumer) {
+        if (this.eventRegistryEventConsumers == null) {
+            this.eventRegistryEventConsumers = new HashMap();
+        }
+
+        this.eventRegistryEventConsumers.put(key, eventRegistryEventConsumer);
+    }
+
+    public AbstractEngineConfiguration setDefaultCommandInterceptors(Collection<? extends CommandInterceptor> defaultCommandInterceptors) {
+        this.defaultCommandInterceptors = defaultCommandInterceptors;
+        return this;
+    }
+
+    public SqlSessionFactory getSqlSessionFactory() {
+        return this.sqlSessionFactory;
+    }
+
+    public AbstractEngineConfiguration setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
+        this.sqlSessionFactory = sqlSessionFactory;
+        return this;
+    }
+
+    public boolean isDbHistoryUsed() {
+        return this.isDbHistoryUsed;
+    }
+
+    public AbstractEngineConfiguration setDbHistoryUsed(boolean isDbHistoryUsed) {
+        this.isDbHistoryUsed = isDbHistoryUsed;
+        return this;
+    }
+
+    public DbSqlSessionFactory getDbSqlSessionFactory() {
+        return this.dbSqlSessionFactory;
+    }
+
+    public AbstractEngineConfiguration setDbSqlSessionFactory(DbSqlSessionFactory dbSqlSessionFactory) {
+        this.dbSqlSessionFactory = dbSqlSessionFactory;
+        return this;
+    }
+
+    public TransactionFactory getTransactionFactory() {
+        return this.transactionFactory;
+    }
+
+    public AbstractEngineConfiguration setTransactionFactory(TransactionFactory transactionFactory) {
+        this.transactionFactory = transactionFactory;
+        return this;
+    }
+
+    public TransactionContextFactory getTransactionContextFactory() {
+        return this.transactionContextFactory;
+    }
+
+    public AbstractEngineConfiguration setTransactionContextFactory(TransactionContextFactory transactionContextFactory) {
+        this.transactionContextFactory = transactionContextFactory;
+        return this;
+    }
+
+    public int getMaxNrOfStatementsInBulkInsert() {
+        return this.maxNrOfStatementsInBulkInsert;
+    }
+
+    public AbstractEngineConfiguration setMaxNrOfStatementsInBulkInsert(int maxNrOfStatementsInBulkInsert) {
+        this.maxNrOfStatementsInBulkInsert = maxNrOfStatementsInBulkInsert;
+        return this;
+    }
+
+    public boolean isBulkInsertEnabled() {
+        return this.isBulkInsertEnabled;
+    }
+
+    public AbstractEngineConfiguration setBulkInsertEnabled(boolean isBulkInsertEnabled) {
+        this.isBulkInsertEnabled = isBulkInsertEnabled;
+        return this;
+    }
+
+    public Set<Class<?>> getCustomMybatisMappers() {
+        return this.customMybatisMappers;
+    }
+
+    public AbstractEngineConfiguration setCustomMybatisMappers(Set<Class<?>> customMybatisMappers) {
+        this.customMybatisMappers = customMybatisMappers;
+        return this;
+    }
+
+    public Set<String> getCustomMybatisXMLMappers() {
+        return this.customMybatisXMLMappers;
+    }
+
+    public AbstractEngineConfiguration setCustomMybatisXMLMappers(Set<String> customMybatisXMLMappers) {
+        this.customMybatisXMLMappers = customMybatisXMLMappers;
+        return this;
+    }
+
+    public Set<String> getDependentEngineMyBatisXmlMappers() {
+        return this.dependentEngineMyBatisXmlMappers;
+    }
+
+    public AbstractEngineConfiguration setCustomMybatisInterceptors(List<Interceptor> customMybatisInterceptors) {
+        this.customMybatisInterceptors = customMybatisInterceptors;
+        return this;
+    }
+
+    public List<Interceptor> getCustomMybatisInterceptors() {
+        return this.customMybatisInterceptors;
+    }
+
+    public AbstractEngineConfiguration setDependentEngineMyBatisXmlMappers(Set<String> dependentEngineMyBatisXmlMappers) {
+        this.dependentEngineMyBatisXmlMappers = dependentEngineMyBatisXmlMappers;
+        return this;
+    }
+
+    public List<MybatisTypeAliasConfigurator> getDependentEngineMybatisTypeAliasConfigs() {
+        return this.dependentEngineMybatisTypeAliasConfigs;
+    }
+
+    public AbstractEngineConfiguration setDependentEngineMybatisTypeAliasConfigs(List<MybatisTypeAliasConfigurator> dependentEngineMybatisTypeAliasConfigs) {
+        this.dependentEngineMybatisTypeAliasConfigs = dependentEngineMybatisTypeAliasConfigs;
+        return this;
+    }
+
+    public List<MybatisTypeHandlerConfigurator> getDependentEngineMybatisTypeHandlerConfigs() {
+        return this.dependentEngineMybatisTypeHandlerConfigs;
+    }
+
+    public AbstractEngineConfiguration setDependentEngineMybatisTypeHandlerConfigs(List<MybatisTypeHandlerConfigurator> dependentEngineMybatisTypeHandlerConfigs) {
+        this.dependentEngineMybatisTypeHandlerConfigs = dependentEngineMybatisTypeHandlerConfigs;
+        return this;
+    }
+
+    public List<SessionFactory> getCustomSessionFactories() {
+        return this.customSessionFactories;
+    }
+
+    public AbstractEngineConfiguration addCustomSessionFactory(SessionFactory sessionFactory) {
+        if (this.customSessionFactories == null) {
+            this.customSessionFactories = new ArrayList();
+        }
+
+        this.customSessionFactories.add(sessionFactory);
+        return this;
+    }
+
+    public AbstractEngineConfiguration setCustomSessionFactories(List<SessionFactory> customSessionFactories) {
+        this.customSessionFactories = customSessionFactories;
+        return this;
+    }
+
+    public boolean isUsingRelationalDatabase() {
+        return this.usingRelationalDatabase;
+    }
+
+    public AbstractEngineConfiguration setUsingRelationalDatabase(boolean usingRelationalDatabase) {
+        this.usingRelationalDatabase = usingRelationalDatabase;
+        return this;
+    }
+
+    public boolean isUsingSchemaMgmt() {
+        return this.usingSchemaMgmt;
+    }
+
+    public AbstractEngineConfiguration setUsingSchemaMgmt(boolean usingSchema) {
+        this.usingSchemaMgmt = usingSchema;
+        return this;
+    }
+
+    public String getDatabaseTablePrefix() {
+        return this.databaseTablePrefix;
+    }
+
+    public AbstractEngineConfiguration setDatabaseTablePrefix(String databaseTablePrefix) {
+        this.databaseTablePrefix = databaseTablePrefix;
+        return this;
+    }
+
+    public String getDatabaseWildcardEscapeCharacter() {
+        return this.databaseWildcardEscapeCharacter;
+    }
+
+    public AbstractEngineConfiguration setDatabaseWildcardEscapeCharacter(String databaseWildcardEscapeCharacter) {
+        this.databaseWildcardEscapeCharacter = databaseWildcardEscapeCharacter;
+        return this;
+    }
+
+    public String getDatabaseCatalog() {
+        return this.databaseCatalog;
+    }
+
+    public AbstractEngineConfiguration setDatabaseCatalog(String databaseCatalog) {
+        this.databaseCatalog = databaseCatalog;
+        return this;
+    }
+
+    public String getDatabaseSchema() {
+        return this.databaseSchema;
+    }
+
+    public AbstractEngineConfiguration setDatabaseSchema(String databaseSchema) {
+        this.databaseSchema = databaseSchema;
+        return this;
+    }
+
+    public boolean isTablePrefixIsSchema() {
+        return this.tablePrefixIsSchema;
+    }
+
+    public AbstractEngineConfiguration setTablePrefixIsSchema(boolean tablePrefixIsSchema) {
+        this.tablePrefixIsSchema = tablePrefixIsSchema;
+        return this;
+    }
+
+    public boolean isAlwaysLookupLatestDefinitionVersion() {
+        return this.alwaysLookupLatestDefinitionVersion;
+    }
+
+    public AbstractEngineConfiguration setAlwaysLookupLatestDefinitionVersion(boolean alwaysLookupLatestDefinitionVersion) {
+        this.alwaysLookupLatestDefinitionVersion = alwaysLookupLatestDefinitionVersion;
+        return this;
+    }
+
+    public boolean isFallbackToDefaultTenant() {
+        return this.fallbackToDefaultTenant;
+    }
+
+    public AbstractEngineConfiguration setFallbackToDefaultTenant(boolean fallbackToDefaultTenant) {
+        this.fallbackToDefaultTenant = fallbackToDefaultTenant;
+        return this;
+    }
+
+    public AbstractEngineConfiguration setDefaultTenantValue(String defaultTenantValue) {
+        this.defaultTenantProvider = (tenantId, scope, scopeKey) -> {
+            return defaultTenantValue;
+        };
+        return this;
+    }
+
+    public DefaultTenantProvider getDefaultTenantProvider() {
+        return this.defaultTenantProvider;
+    }
+
+    public AbstractEngineConfiguration setDefaultTenantProvider(DefaultTenantProvider defaultTenantProvider) {
+        this.defaultTenantProvider = defaultTenantProvider;
+        return this;
+    }
+
+    public boolean isEnableLogSqlExecutionTime() {
+        return this.enableLogSqlExecutionTime;
+    }
+
+    public void setEnableLogSqlExecutionTime(boolean enableLogSqlExecutionTime) {
+        this.enableLogSqlExecutionTime = enableLogSqlExecutionTime;
+    }
+
+    public Map<Class<?>, SessionFactory> getSessionFactories() {
+        return this.sessionFactories;
+    }
+
+    public AbstractEngineConfiguration setSessionFactories(Map<Class<?>, SessionFactory> sessionFactories) {
+        this.sessionFactories = sessionFactories;
+        return this;
+    }
+
+    public String getDatabaseSchemaUpdate() {
+        return this.databaseSchemaUpdate;
+    }
+
+    public AbstractEngineConfiguration setDatabaseSchemaUpdate(String databaseSchemaUpdate) {
+        this.databaseSchemaUpdate = databaseSchemaUpdate;
+        return this;
+    }
+
+    public boolean isUseLockForDatabaseSchemaUpdate() {
+        return this.useLockForDatabaseSchemaUpdate;
+    }
+
+    public AbstractEngineConfiguration setUseLockForDatabaseSchemaUpdate(boolean useLockForDatabaseSchemaUpdate) {
+        this.useLockForDatabaseSchemaUpdate = useLockForDatabaseSchemaUpdate;
+        return this;
+    }
+
+    public boolean isEnableEventDispatcher() {
+        return this.enableEventDispatcher;
+    }
+
+    public AbstractEngineConfiguration setEnableEventDispatcher(boolean enableEventDispatcher) {
+        this.enableEventDispatcher = enableEventDispatcher;
+        return this;
+    }
+
+    public FlowableEventDispatcher getEventDispatcher() {
+        return this.eventDispatcher;
+    }
+
+    public AbstractEngineConfiguration setEventDispatcher(FlowableEventDispatcher eventDispatcher) {
+        this.eventDispatcher = eventDispatcher;
+        return this;
+    }
+
+    public List<FlowableEventListener> getEventListeners() {
+        return this.eventListeners;
+    }
+
+    public AbstractEngineConfiguration setEventListeners(List<FlowableEventListener> eventListeners) {
+        this.eventListeners = eventListeners;
+        return this;
+    }
+
+    public Map<String, List<FlowableEventListener>> getTypedEventListeners() {
+        return this.typedEventListeners;
+    }
+
+    public AbstractEngineConfiguration setTypedEventListeners(Map<String, List<FlowableEventListener>> typedEventListeners) {
+        this.typedEventListeners = typedEventListeners;
+        return this;
+    }
+
+    public List<EventDispatchAction> getAdditionalEventDispatchActions() {
+        return this.additionalEventDispatchActions;
+    }
+
+    public AbstractEngineConfiguration setAdditionalEventDispatchActions(List<EventDispatchAction> additionalEventDispatchActions) {
+        this.additionalEventDispatchActions = additionalEventDispatchActions;
+        return this;
+    }
+
+    public void initEventDispatcher() {
+        if (this.eventDispatcher == null) {
+            this.eventDispatcher = new FlowableEventDispatcherImpl();
+        }
+
+        this.initAdditionalEventDispatchActions();
+        this.eventDispatcher.setEnabled(this.enableEventDispatcher);
+        this.initEventListeners();
+        this.initTypedEventListeners();
+    }
+
+    protected void initEventListeners() {
+        if (this.eventListeners != null) {
+            Iterator var1 = this.eventListeners.iterator();
+
+            while(var1.hasNext()) {
+                FlowableEventListener listenerToAdd = (FlowableEventListener)var1.next();
+                this.eventDispatcher.addEventListener(listenerToAdd);
+            }
+        }
+
+    }
+
+    protected void initAdditionalEventDispatchActions() {
+        if (this.additionalEventDispatchActions == null) {
+            this.additionalEventDispatchActions = new ArrayList();
+        }
+
+    }
+
+    protected void initTypedEventListeners() {
+        if (this.typedEventListeners != null) {
+            Iterator var1 = this.typedEventListeners.entrySet().iterator();
+
+            while(var1.hasNext()) {
+                Map.Entry<String, List<FlowableEventListener>> listenersToAdd = (Map.Entry)var1.next();
+                FlowableEngineEventType[] types = FlowableEngineEventType.getTypesFromString((String)listenersToAdd.getKey());
+                Iterator var4 = ((List)listenersToAdd.getValue()).iterator();
+
+                while(var4.hasNext()) {
+                    FlowableEventListener listenerToAdd = (FlowableEventListener)var4.next();
+                    this.eventDispatcher.addEventListener(listenerToAdd, types);
+                }
+            }
+        }
+
+    }
+
+    public boolean isLoggingSessionEnabled() {
+        return this.loggingListener != null;
+    }
+
+    public LoggingListener getLoggingListener() {
+        return this.loggingListener;
+    }
+
+    public void setLoggingListener(LoggingListener loggingListener) {
+        this.loggingListener = loggingListener;
+    }
+
+    public Clock getClock() {
+        return this.clock;
+    }
+
+    public AbstractEngineConfiguration setClock(Clock clock) {
+        this.clock = clock;
+        return this;
+    }
+
+    public ObjectMapper getObjectMapper() {
+        return this.objectMapper;
+    }
+
+    public AbstractEngineConfiguration setObjectMapper(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+        return this;
+    }
+
+    public int getMaxLengthString() {
+        if (this.maxLengthStringVariableType == -1) {
+            return "oracle".equalsIgnoreCase(this.databaseType) ? 2000 : 4000;
+        } else {
+            return this.maxLengthStringVariableType;
+        }
+    }
+
+    public int getMaxLengthStringVariableType() {
+        return this.maxLengthStringVariableType;
+    }
+
+    public AbstractEngineConfiguration setMaxLengthStringVariableType(int maxLengthStringVariableType) {
+        this.maxLengthStringVariableType = maxLengthStringVariableType;
+        return this;
+    }
+
+    public PropertyDataManager getPropertyDataManager() {
+        return this.propertyDataManager;
+    }
+
+    public Duration getLockPollRate() {
+        return this.lockPollRate;
+    }
+
+    public AbstractEngineConfiguration setLockPollRate(Duration lockPollRate) {
+        this.lockPollRate = lockPollRate;
+        return this;
+    }
+
+    public Duration getSchemaLockWaitTime() {
+        return this.schemaLockWaitTime;
+    }
+
+    public void setSchemaLockWaitTime(Duration schemaLockWaitTime) {
+        this.schemaLockWaitTime = schemaLockWaitTime;
+    }
+
+    public AbstractEngineConfiguration setPropertyDataManager(PropertyDataManager propertyDataManager) {
+        this.propertyDataManager = propertyDataManager;
+        return this;
+    }
+
+    public PropertyEntityManager getPropertyEntityManager() {
+        return this.propertyEntityManager;
+    }
+
+    public AbstractEngineConfiguration setPropertyEntityManager(PropertyEntityManager propertyEntityManager) {
+        this.propertyEntityManager = propertyEntityManager;
+        return this;
+    }
+
+    public ByteArrayDataManager getByteArrayDataManager() {
+        return this.byteArrayDataManager;
+    }
+
+    public AbstractEngineConfiguration setByteArrayDataManager(ByteArrayDataManager byteArrayDataManager) {
+        this.byteArrayDataManager = byteArrayDataManager;
+        return this;
+    }
+
+    public ByteArrayEntityManager getByteArrayEntityManager() {
+        return this.byteArrayEntityManager;
+    }
+
+    public AbstractEngineConfiguration setByteArrayEntityManager(ByteArrayEntityManager byteArrayEntityManager) {
+        this.byteArrayEntityManager = byteArrayEntityManager;
+        return this;
+    }
+
+    public TableDataManager getTableDataManager() {
+        return this.tableDataManager;
+    }
+
+    public AbstractEngineConfiguration setTableDataManager(TableDataManager tableDataManager) {
+        this.tableDataManager = tableDataManager;
+        return this;
+    }
+
+    public List<EngineDeployer> getDeployers() {
+        return this.deployers;
+    }
+
+    public AbstractEngineConfiguration setDeployers(List<EngineDeployer> deployers) {
+        this.deployers = deployers;
+        return this;
+    }
+
+    public List<EngineDeployer> getCustomPreDeployers() {
+        return this.customPreDeployers;
+    }
+
+    public AbstractEngineConfiguration setCustomPreDeployers(List<EngineDeployer> customPreDeployers) {
+        this.customPreDeployers = customPreDeployers;
+        return this;
+    }
+
+    public List<EngineDeployer> getCustomPostDeployers() {
+        return this.customPostDeployers;
+    }
+
+    public AbstractEngineConfiguration setCustomPostDeployers(List<EngineDeployer> customPostDeployers) {
+        this.customPostDeployers = customPostDeployers;
+        return this;
+    }
+
+    public boolean isEnableConfiguratorServiceLoader() {
+        return this.enableConfiguratorServiceLoader;
+    }
+
+    public AbstractEngineConfiguration setEnableConfiguratorServiceLoader(boolean enableConfiguratorServiceLoader) {
+        this.enableConfiguratorServiceLoader = enableConfiguratorServiceLoader;
+        return this;
+    }
+
+    public List<EngineConfigurator> getConfigurators() {
+        return this.configurators;
+    }
+
+    public AbstractEngineConfiguration addConfigurator(EngineConfigurator configurator) {
+        if (this.configurators == null) {
+            this.configurators = new ArrayList();
+        }
+
+        this.configurators.add(configurator);
+        return this;
+    }
+
+    public List<EngineConfigurator> getAllConfigurators() {
+        return this.allConfigurators;
+    }
+
+    public AbstractEngineConfiguration setConfigurators(List<EngineConfigurator> configurators) {
+        this.configurators = configurators;
+        return this;
+    }
+
+    public EngineConfigurator getIdmEngineConfigurator() {
+        return this.idmEngineConfigurator;
+    }
+
+    public AbstractEngineConfiguration setIdmEngineConfigurator(EngineConfigurator idmEngineConfigurator) {
+        this.idmEngineConfigurator = idmEngineConfigurator;
+        return this;
+    }
+
+    public EngineConfigurator getEventRegistryConfigurator() {
+        return this.eventRegistryConfigurator;
+    }
+
+    public AbstractEngineConfiguration setEventRegistryConfigurator(EngineConfigurator eventRegistryConfigurator) {
+        this.eventRegistryConfigurator = eventRegistryConfigurator;
+        return this;
+    }
+
+    public AbstractEngineConfiguration setForceCloseMybatisConnectionPool(boolean forceCloseMybatisConnectionPool) {
+        this.forceCloseMybatisConnectionPool = forceCloseMybatisConnectionPool;
+        return this;
+    }
+
+    public boolean isForceCloseMybatisConnectionPool() {
+        return this.forceCloseMybatisConnectionPool;
+    }
+}

+ 264 - 0
ruoyi-admin/src/main/resources/application-dev.yml

@@ -0,0 +1,264 @@
+--- # 监控中心配置
+spring.boot.admin.client:
+  # 增加客户端开关
+  enabled: true
+  url: http://localhost:9090/admin
+  instance:
+    service-host-type: IP
+  username: ruoyi
+  password: 123456
+
+--- # snail-job 配置
+snail-job:
+  enabled: false
+  # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
+  group: "ruoyi_group"
+  # SnailJob 接入验证令牌 详见 script/sql/snail_job.sql `sj_group_config` 表
+  token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
+  server:
+    host: 127.0.0.1
+    port: 17888
+  # 详见 script/sql/snail_job.sql `sj_namespace` 表
+  namespace: ${spring.profiles.active}
+  # 随主应用端口飘逸
+  port: 2${server.port}
+
+--- # 数据源配置
+spring:
+  datasource:
+    type: com.zaxxer.hikari.HikariDataSource
+    # 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
+    dynamic:
+      # 性能分析插件(有性能损耗 不建议生产环境使用)
+      p6spy: true
+      # 设置默认的数据源或者数据源组,默认值即为 master
+      primary: master
+      # 严格模式 匹配不到数据源则报错
+      strict: true
+      datasource:
+        # 主库数据源
+        master:
+          type: ${spring.datasource.type}
+          driverClassName: com.mysql.cj.jdbc.Driver
+          # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
+          # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
+          url: jdbc:mysql://172.192.10.105:30002/little_iot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+          username: root
+          password: root
+#          driverClassName: dm.jdbc.driver.DmDriver
+          # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
+          # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
+#          url: jdbc:dm://127.0.0.1:8533?compatibleMode=oracle&SCHEMA=LITTLE_IOT
+#          username: SYSDBA
+#          password: 123456789
+        # 从库数据源
+#        slave:
+#          lazy: true
+#          type: ${spring.datasource.type}
+#          driverClassName: com.mysql.cj.jdbc.Driver
+#          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+#          username:
+#          password:
+#        oracle:
+#          type: ${spring.datasource.type}
+#          driverClassName: oracle.jdbc.OracleDriver
+#          url: jdbc:oracle:thin:@//localhost:1521/XE
+#          username: ROOT
+#          password: root
+#        postgres:
+#          type: ${spring.datasource.type}
+#          driverClassName: org.postgresql.Driver
+#          url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
+#          username: root
+#          password: root
+#        sqlserver:
+#          type: ${spring.datasource.type}
+#          driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
+#          url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
+#          username: SA
+#          password: root
+      hikari:
+        # 最大连接池数量
+        maxPoolSize: 20
+        # 最小空闲线程数量
+        minIdle: 10
+        # 配置获取连接等待超时的时间
+        connectionTimeout: 30000
+        # 校验超时时间
+        validationTimeout: 5000
+        # 空闲连接存活最大时间,默认10分钟
+        idleTimeout: 600000
+        # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
+        maxLifetime: 1800000
+        # 多久检查一次连接的活性
+        keepaliveTime: 30000
+
+--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
+spring.data:
+  redis:
+    # 地址
+    host: 172.192.10.105
+    # 端口,默认为6379
+    port: 30013
+    # 数据库索引
+    database: 10
+    # redis 密码必须配置
+#    password: ruoyi123
+    # 连接超时时间
+    timeout: 10s
+    # 是否开启ssl
+    ssl.enabled: false
+
+# redisson 配置
+redisson:
+  # redis key前缀
+  keyPrefix:
+  # 线程池数量
+  threads: 4
+  # Netty线程池数量
+  nettyThreads: 8
+  # 单节点配置
+  singleServerConfig:
+    # 客户端名称
+    clientName: ${ruoyi.name}
+    # 最小空闲连接数
+    connectionMinimumIdleSize: 8
+    # 连接池大小
+    connectionPoolSize: 32
+    # 连接空闲超时,单位:毫秒
+    idleConnectionTimeout: 10000
+    # 命令等待超时,单位:毫秒
+    timeout: 3000
+    # 发布和订阅连接池大小
+    subscriptionConnectionPoolSize: 50
+
+--- # mail 邮件发送
+mail:
+  enabled: false
+  host: smtp.163.com
+  port: 465
+  # 是否需要用户名密码验证
+  auth: true
+  # 发送方,遵循RFC-822标准
+  from: xxx@163.com
+  # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
+  user: xxx@163.com
+  # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
+  pass: xxxxxxxxxx
+  # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
+  starttlsEnable: true
+  # 使用SSL安全连接
+  sslEnable: true
+  # SMTP超时时长,单位毫秒,缺省值不超时
+  timeout: 0
+  # Socket连接超时值,单位毫秒,缺省值不超时
+  connectionTimeout: 0
+
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
+sms:
+  # 配置源类型用于标定配置来源(interface,yaml)
+  config-type: yaml
+  # 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
+  restricted: true
+  # 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
+  minute-max: 1
+  # 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
+  account-max: 30
+  # 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
+  blends:
+    # 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
+    # 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
+    config1:
+      # 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: alibaba
+      # 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
+      access-key-id: 您的accessKey
+      # 称为accessSecret有些称之为apiSecret
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
+    config2:
+      # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: tencent
+      access-key-id: 您的accessKey
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
+
+
+--- # 三方授权
+justauth:
+  # 前端外网访问地址
+  address: http://localhost:80
+  type:
+    maxkey:
+      # maxkey 服务器地址
+      # 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
+      server-url: http://sso.maxkey.top
+      client-id: 876892492581044224
+      client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
+      redirect-uri: ${justauth.address}/social-callback?source=maxkey
+    topiam:
+      # topiam 服务器地址
+      server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
+      client-id: 449c4*********937************759
+      client-secret: ac7***********1e0************28d
+      redirect-uri: ${justauth.address}/social-callback?source=topiam
+      scopes: [openid, email, phone, profile]
+    qq:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=qq
+      union-id: false
+    weibo:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=weibo
+    gitee:
+      client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
+      client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
+      redirect-uri: ${justauth.address}/social-callback?source=gitee
+    dingtalk:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=dingtalk
+    baidu:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=baidu
+    csdn:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=csdn
+    coding:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=coding
+      coding-group-name: xx
+    oschina:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=oschina
+    alipay_wallet:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
+      alipay-public-key: MIIB**************DAQAB
+    wechat_open:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=wechat_open
+    wechat_mp:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
+    wechat_enterprise:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
+      agent-id: 1000002
+    gitlab:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=gitlab

+ 260 - 0
ruoyi-admin/src/main/resources/application-prod.yml

@@ -0,0 +1,260 @@
+--- # 临时文件存储位置 避免临时文件被系统清理报错
+spring.servlet.multipart.location: /ruoyi/server/temp
+
+--- # 监控中心配置
+spring.boot.admin.client:
+  # 增加客户端开关
+  enabled: true
+  url: http://localhost:9090/admin
+  instance:
+    service-host-type: IP
+  username: ruoyi
+  password: 123456
+
+--- # snail-job 配置
+snail-job:
+  enabled: true
+  # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
+  group: "ruoyi_group"
+  # SnailJob 接入验证令牌 详见 script/sql/snail_job.sql `sj_group_config` 表
+  token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
+  server:
+    host: 127.0.0.1
+    port: 17888
+  # 详见 script/sql/snail_job.sql `sj_namespace` 表
+  namespace: ${spring.profiles.active}
+  # 随主应用端口飘逸
+  port: 2${server.port}
+
+--- # 数据源配置
+spring:
+  datasource:
+    type: com.zaxxer.hikari.HikariDataSource
+    # 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
+    dynamic:
+      # 性能分析插件(有性能损耗 不建议生产环境使用)
+      p6spy: false
+      # 设置默认的数据源或者数据源组,默认值即为 master
+      primary: master
+      # 严格模式 匹配不到数据源则报错
+      strict: true
+      datasource:
+        # 主库数据源
+        master:
+          type: ${spring.datasource.type}
+          driverClassName: com.mysql.cj.jdbc.Driver
+          # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
+          # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+          username: root
+          password: root
+        # 从库数据源
+        slave:
+          lazy: true
+          type: ${spring.datasource.type}
+          driverClassName: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
+          username:
+          password:
+#        oracle:
+#          type: ${spring.datasource.type}
+#          driverClassName: oracle.jdbc.OracleDriver
+#          url: jdbc:oracle:thin:@//localhost:1521/XE
+#          username: ROOT
+#          password: root
+#        postgres:
+#          type: ${spring.datasource.type}
+#          driverClassName: org.postgresql.Driver
+#          url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
+#          username: root
+#          password: root
+#        sqlserver:
+#          type: ${spring.datasource.type}
+#          driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
+#          url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
+#          username: SA
+#          password: root
+      hikari:
+        # 最大连接池数量
+        maxPoolSize: 20
+        # 最小空闲线程数量
+        minIdle: 10
+        # 配置获取连接等待超时的时间
+        connectionTimeout: 30000
+        # 校验超时时间
+        validationTimeout: 5000
+        # 空闲连接存活最大时间,默认10分钟
+        idleTimeout: 600000
+        # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
+        maxLifetime: 1800000
+        # 多久检查一次连接的活性
+        keepaliveTime: 30000
+
+--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
+spring.data:
+  redis:
+    # 地址
+    host: localhost
+    # 端口,默认为6379
+    port: 6379
+    # 数据库索引
+    database: 0
+    # redis 密码必须配置
+    password: ruoyi123
+    # 连接超时时间
+    timeout: 10s
+    # 是否开启ssl
+    ssl.enabled: false
+
+# redisson 配置
+redisson:
+  # redis key前缀
+  keyPrefix:
+  # 线程池数量
+  threads: 16
+  # Netty线程池数量
+  nettyThreads: 32
+  # 单节点配置
+  singleServerConfig:
+    # 客户端名称
+    clientName: ${ruoyi.name}
+    # 最小空闲连接数
+    connectionMinimumIdleSize: 32
+    # 连接池大小
+    connectionPoolSize: 64
+    # 连接空闲超时,单位:毫秒
+    idleConnectionTimeout: 10000
+    # 命令等待超时,单位:毫秒
+    timeout: 3000
+    # 发布和订阅连接池大小
+    subscriptionConnectionPoolSize: 50
+
+--- # mail 邮件发送
+mail:
+  enabled: false
+  host: smtp.163.com
+  port: 465
+  # 是否需要用户名密码验证
+  auth: true
+  # 发送方,遵循RFC-822标准
+  from: xxx@163.com
+  # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
+  user: xxx@163.com
+  # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
+  pass: xxxxxxxxxx
+  # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
+  starttlsEnable: true
+  # 使用SSL安全连接
+  sslEnable: true
+  # SMTP超时时长,单位毫秒,缺省值不超时
+  timeout: 0
+  # Socket连接超时值,单位毫秒,缺省值不超时
+  connectionTimeout: 0
+
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
+sms:
+  # 配置源类型用于标定配置来源(interface,yaml)
+  config-type: yaml
+  # 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
+  restricted: true
+  # 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
+  minute-max: 1
+  # 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
+  account-max: 30
+  # 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
+  blends:
+    # 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
+    # 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
+    config1:
+      # 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: alibaba
+      # 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
+      access-key-id: 您的accessKey
+      # 称为accessSecret有些称之为apiSecret
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
+    config2:
+      # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
+      supplier: tencent
+      access-key-id: 您的accessKey
+      access-key-secret: 您的accessKeySecret
+      signature: 您的短信签名
+      sdk-app-id: 您的sdkAppId
+
+--- # 三方授权
+justauth:
+  # 前端外网访问地址
+  address: http://localhost:80
+  type:
+    maxkey:
+      # maxkey 服务器地址
+      # 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
+      server-url: http://sso.maxkey.top
+      client-id: 876892492581044224
+      client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
+      redirect-uri: ${justauth.address}/social-callback?source=maxkey
+    topiam:
+      # topiam 服务器地址
+      server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
+      client-id: 449c4*********937************759
+      client-secret: ac7***********1e0************28d
+      redirect-uri: ${justauth.address}/social-callback?source=topiam
+      scopes: [ openid, email, phone, profile ]
+    qq:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=qq
+      union-id: false
+    weibo:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=weibo
+    gitee:
+      client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
+      client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
+      redirect-uri: ${justauth.address}/social-callback?source=gitee
+    dingtalk:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=dingtalk
+    baidu:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=baidu
+    csdn:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=csdn
+    coding:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=coding
+      coding-group-name: xx
+    oschina:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=oschina
+    alipay_wallet:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
+      alipay-public-key: MIIB**************DAQAB
+    wechat_open:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=wechat_open
+    wechat_mp:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
+    wechat_enterprise:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
+      agent-id: 1000002
+    gitlab:
+      client-id: 10**********6
+      client-secret: 1f7d08**********5b7**********29e
+      redirect-uri: ${justauth.address}/social-callback?source=gitlab

+ 321 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -0,0 +1,321 @@
+# 项目相关配置
+ruoyi:
+  # 名称
+  name: RuoYi-Vue-Plus
+  # 版本
+  version: ${revision}
+  # 版权年份
+  copyrightYear: 2024
+
+captcha:
+  enable: true
+  # 页面 <参数设置> 可开启关闭 验证码校验
+  # 验证码类型 math 数组计算 char 字符验证
+  type: MATH
+  # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
+  category: CIRCLE
+  # 数字验证码位数
+  numberLength: 1
+  # 字符验证码长度
+  charLength: 4
+
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 8080
+  servlet:
+    # 应用的访问路径
+    context-path: /
+  # undertow 配置
+  undertow:
+    # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
+    max-http-post-size: -1
+    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
+    # 每块buffer的空间大小,越小的空间被利用越充分
+    buffer-size: 512
+    # 是否分配的直接内存
+    direct-buffers: true
+    threads:
+      # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
+      io: 8
+      # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
+      worker: 256
+
+# 日志配置
+logging:
+  level:
+    org.dromara: @logging.level@
+    org.springframework: warn
+    org.mybatis.spring.mapper: error
+  config: classpath:logback-plus.xml
+
+# 用户配置
+user:
+  password:
+    # 密码最大错误次数
+    maxRetryCount: 5
+    # 密码锁定时间(默认10分钟)
+    lockTime: 10
+
+# Spring配置
+spring:
+  application:
+    name: ${ruoyi.name}
+  threads:
+    # 开启虚拟线程 仅jdk21可用
+    virtual:
+      enabled: false
+  # 资源信息
+  messages:
+    # 国际化资源文件路径
+    basename: i18n/messages
+  profiles:
+    active: @profiles.active@
+  # 文件上传
+  servlet:
+    multipart:
+      # 单个文件大小
+      max-file-size: 10MB
+      # 设置总上传的文件大小
+      max-request-size: 20MB
+  mvc:
+    # 设置静态资源路径 防止所有请求都去查静态资源
+    static-path-pattern: /static/**
+    format:
+      date-time: yyyy-MM-dd HH:mm:ss
+  jackson:
+    # 日期格式化
+    date-format: yyyy-MM-dd HH:mm:ss
+    serialization:
+      # 格式化输出
+      indent_output: false
+      # 忽略无法转换的对象
+      fail_on_empty_beans: false
+    deserialization:
+      # 允许对象忽略json中不存在的属性
+      fail_on_unknown_properties: false
+
+# Sa-Token配置
+sa-token:
+  # token名称 (同时也是cookie名称)
+  token-name: Authorization
+  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
+  is-concurrent: true
+  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
+  is-share: false
+  # jwt秘钥
+  jwt-secret-key: abcdefghijklmnopqrstuvwxyz
+
+# security配置
+security:
+  # 排除路径
+  excludes:
+    # 静态资源
+    - /*.html
+    - /**/*.html
+    - /**/*.css
+    - /**/*.js
+    # 公共路径
+    - /favicon.ico
+    - /error
+    # swagger 文档配置
+    - /*/api-docs
+    - /*/api-docs/**
+    # actuator 监控配置
+    - /actuator
+    - /actuator/**
+
+# 多租户配置
+tenant:
+  # 是否开启
+  enable: false
+  # 排除表
+  excludes:
+    - sys_menu
+    - sys_tenant
+    - sys_tenant_package
+    - sys_role_dept
+    - sys_role_menu
+    - sys_user_post
+    - sys_user_role
+    - sys_client
+    - sys_oss_config
+
+# MyBatisPlus配置
+# https://baomidou.com/config/
+mybatis-plus:
+  # 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
+  mapperPackage: org.dromara.**.mapper
+  # 对应的 XML 文件位置
+  mapperLocations: classpath*:mapper/**/*Mapper.xml
+  # 实体扫描,多个package用逗号或者分号分隔
+  typeAliasesPackage: org.dromara.**.domain
+  global-config:
+    dbConfig:
+      # 主键类型
+      # AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
+      # 如需改为自增 需要将数据库表全部设置为自增
+      idType: ASSIGN_ID
+
+# 数据加密
+mybatis-encryptor:
+  # 是否开启加密
+  enable: false
+  # 默认加密算法
+  algorithm: BASE64
+  # 编码方式 BASE64/HEX。默认BASE64
+  encode: BASE64
+  # 安全秘钥 对称算法的秘钥 如:AES,SM4
+  password:
+  # 公私钥 非对称算法的公私钥 如:SM2,RSA
+  publicKey:
+  privateKey:
+
+# api接口加密
+api-decrypt:
+  # 是否开启全局接口加密
+  enabled: false
+  # AES 加密头标识
+  headerFlag: encrypt-key
+  # 响应加密公钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
+  # 对应前端解密私钥 MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
+  publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJnNwrj4hi/y3CCJu868ghCG5dUj8wZK++RNlTLcXoMmdZWEQ/u02RgD5LyLAXGjLOjbMtC+/J9qofpSGTKSx/MCAwEAAQ==
+  # 请求解密私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
+  # 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
+  privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
+
+springdoc:
+  api-docs:
+    # 是否开启接口文档
+    enabled: true
+#  swagger-ui:
+#    # 持久化认证数据
+#    persistAuthorization: true
+  info:
+    # 标题
+    title: '标题:${ruoyi.name}多租户管理系统_接口文档'
+    # 描述
+    description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
+    # 版本
+    version: '版本号: ${ruoyi.version}'
+    # 作者信息
+    contact:
+      name: Lion Li
+      email: crazylionli@163.com
+      url: https://gitee.com/dromara/RuoYi-Vue-Plus
+  components:
+    # 鉴权方式配置
+    security-schemes:
+      apiKey:
+        type: APIKEY
+        in: HEADER
+        name: ${sa-token.token-name}
+  #这里定义了两个分组,可定义多个,也可以不定义
+  group-configs:
+    - group: 1.演示模块
+      packages-to-scan: org.dromara.demo
+    - group: 2.通用模块
+      packages-to-scan: org.dromara.web
+    - group: 3.系统模块
+      packages-to-scan: org.dromara.system
+    - group: 4.代码生成模块
+      packages-to-scan: org.dromara.generator
+
+# 防止XSS攻击
+xss:
+  # 过滤开关
+  enabled: true
+  # 排除链接(多个用逗号分隔)
+  excludes: /system/notice
+  # 匹配链接
+  urlPatterns: /system/*,/monitor/*,/tool/*
+
+# 全局线程池相关配置
+# 如使用JDK21请直接使用虚拟线程 不要开启此配置
+thread-pool:
+  # 是否开启线程池
+  enabled: false
+  # 队列最大长度
+  queueCapacity: 128
+  # 线程池维护线程所允许的空闲时间
+  keepAliveSeconds: 300
+
+--- # 分布式锁 lock4j 全局配置
+lock4j:
+  # 获取分布式锁超时时间,默认为 3000 毫秒
+  acquire-timeout: 3000
+  # 分布式锁的超时时间,默认为 30 秒
+  expire: 30000
+
+--- # Actuator 监控端点的配置项
+management:
+  endpoints:
+    web:
+      exposure:
+        include: '*'
+  endpoint:
+    health:
+      show-details: ALWAYS
+    logfile:
+      external-file: ./logs/sys-console.log
+
+--- # websocket
+websocket:
+  # 如果关闭 需要和前端开关一起关闭
+  enabled: true
+  # 路径
+  path: /resource/websocket
+  # 设置访问源地址
+  allowedOrigins: '*'
+
+--- #flowable配置
+flowable:
+  async-executor-activate: false #关闭定时任务JOB
+  #  将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
+  database-schema-update: none
+  activity-font-name: 宋体
+  label-font-name: 宋体
+  annotation-font-name: 宋体
+  # 关闭各个模块生成表,目前只使用工作流基础表
+  idm:
+    enabled: false
+  cmmn:
+    enabled: false
+  dmn:
+    enabled: false
+  app:
+    enabled: false
+
+
+mqtt:
+  server:
+    enabled: true               # 是否开启服务端,默认:true
+    #    ip: 0.0.0.0                # 服务端 ip 默认为空,0.0.0.0,建议不要设置
+    port: 1883                  # 端口,默认:1883
+    name: Mica-Mqtt-Server      # 名称,默认:Mica-Mqtt-Server
+    heartbeat-timeout: 120000   # 心跳超时,单位毫秒,默认: 1000 * 120
+    read-buffer-size: 8KB       # 接收数据的 buffer size,默认:8k
+    max-bytes-in-message: 10MB  # 消息解析最大 bytes 长度,默认:10M
+    auth:
+      enable: false             # 是否开启 mqtt 认证
+      username: mica            # mqtt 认证用户名
+      password: mica            # mqtt 认证密码
+    debug: true                 # 如果开启 prometheus 指标收集建议关闭
+    stat-enable: true           # 开启指标收集,debug 和 prometheus 开启时需要打开,默认开启,关闭节省内存
+    web-port: 8083              # http、websocket 端口,默认:8083
+    websocket-enable: false      # 是否开启 websocket,默认: true
+    http-enable: false          # 是否开启 http api,默认: false
+    http-basic-auth:
+      enable: false             # 是否开启 http basic auth,默认: false
+      username: mica            # http basic auth 用户名
+      password: mica            # http basic auth 密码
+    ssl:                        # mqtt tcp ssl 认证
+      enabled: false            # 是否开启 ssl 认证,2.1.0 开始支持双向认证
+      keystore-path:            # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。
+      keystore-pass:            # 必选参数:ssl keystore 密码
+      truststore-path:          # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。
+      truststore-pass:          # 可选参数:ssl 双向认证 truststore 密码
+      client-auth: none         # 是否需要客户端认证(双向认证),默认:NONE(不需要)
+
+record:
+  start: 2024_08

+ 8 - 0
ruoyi-admin/src/main/resources/banner.txt

@@ -0,0 +1,8 @@
+Application Version: ${revision}
+Spring Boot Version: ${spring-boot.version}
+__________            _____.___.__         ____   ____                     __________.__
+\______   \__ __  ____\__  |   |__|        \   \ /   /_ __   ____          \______   \  |  __ __  ______
+ |       _/  |  \/  _ \/   |   |  |  ______ \   Y   /  |  \_/ __ \   ______ |     ___/  | |  |  \/  ___/
+ |    |   \  |  (  <_> )____   |  | /_____/  \     /|  |  /\  ___/  /_____/ |    |   |  |_|  |  /\___ \
+ |____|_  /____/ \____// ______|__|           \___/ |____/  \___  >         |____|   |____/____//____  >
+        \/             \/                                       \/                                   \/

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

@@ -0,0 +1,61 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=对不起, 您的账号:{0} 不存在.
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
+user.password.delete=对不起,您的账号:{0} 已被删除
+user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+length.not.valid=长度必须在{min}到{max}个字符之间
+user.username.not.blank=用户名不能为空
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.username.length.valid=账户长度必须在{min}到{max}个字符之间
+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=登录成功
+user.register.success=注册成功
+user.register.save.error=保存用户 {0} 失败,注册账号已存在
+user.register.error=注册失败,请联系系统管理人员
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+auth.grant.type.error=认证权限类型错误
+auth.grant.type.blocked=认证权限类型已禁用
+auth.grant.type.not.blank=认证权限类型不能为空
+auth.clientid.not.blank=认证客户端id不能为空
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
+repeat.submit.message=不允许重复提交,请稍候再试
+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]不能为空
+social.source.not.blank=第三方登录平台[source]不能为空
+social.code.not.blank=第三方登录平台[code]不能为空
+social.state.not.blank=第三方登录平台[state]不能为空
+##租户
+tenant.number.not.blank=租户编号不能为空
+tenant.not.exists=对不起, 您的租户不存在,请联系管理员
+tenant.blocked=对不起,您的租户已禁用,请联系管理员
+tenant.expired=对不起,您的租户已过期,请联系管理员

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

@@ -0,0 +1,61 @@
+#错误消息
+not.null=* Required fill in
+user.jcaptcha.error=Captcha error
+user.jcaptcha.expire=Captcha invalid
+user.not.exists=Sorry, your account: {0} does not exist
+user.password.not.match=User does not exist/Password error
+user.password.retry.limit.count=Password input error {0} times
+user.password.retry.limit.exceed=Password input error {0} times, account locked for {1} minutes
+user.password.delete=Sorry, your account:{0} has been deleted
+user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator
+role.blocked=Role disabled,please contact administrators
+user.logout.success=Exit successful
+length.not.valid=The length must be between {min} and {max} characters
+user.username.not.blank=Username cannot be blank
+user.username.not.valid=* 2 to 20 chinese characters, letters, numbers or underscores, and must start with a non number
+user.username.length.valid=Account length must be between {min} and {max} characters
+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
+user.register.success=Register successful
+user.register.save.error=Failed to save user {0}, The registered account already exists
+user.register.error=Register failed, please contact system administrator
+user.notfound=Please login again
+user.forcelogout=The administrator is forced to exit,please login again
+user.unknown.error=Unknown error, please login again
+auth.grant.type.error=Auth grant type error
+auth.grant.type.blocked=Auth grant type disabled
+auth.grant.type.not.blank=Auth grant type cannot be blank
+auth.clientid.not.blank=Auth clientid cannot be blank
+##文件上传消息
+upload.exceed.maxSize=The uploaded file size exceeds the limit file size!<br/>the maximum allowed file size is:{0}MB!
+upload.filename.exceed.length=The maximum length of uploaded file name is {0} characters
+##权限
+no.permission=You do not have permission to the data,please contact your administrator to add permissions [{0}]
+no.create.permission=You do not have permission to create data,please contact your administrator to add permissions [{0}]
+no.update.permission=You do not have permission to modify data,please contact your administrator to add permissions [{0}]
+no.delete.permission=You do not have permission to delete data,please contact your administrator to add permissions [{0}]
+no.export.permission=You do not have permission to export data,please contact your administrator to add permissions [{0}]
+no.view.permission=You do not have permission to view data,please contact your administrator to add permissions [{0}]
+repeat.submit.message=Repeat submit is not allowed, please try again later
+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
+social.source.not.blank=Social login platform [source] cannot be blank
+social.code.not.blank=Social login platform [code] cannot be blank
+social.state.not.blank=Social login platform [state] cannot be blank
+##租户
+tenant.number.not.blank=Tenant number cannot be blank
+tenant.not.exists=Sorry, your tenant does not exist. Please contact the administrator
+tenant.blocked=Sorry, your tenant is disabled. Please contact the administrator
+tenant.expired=Sorry, your tenant has expired. Please contact the administrator.

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

@@ -0,0 +1,61 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=对不起, 您的账号:{0} 不存在.
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
+user.password.delete=对不起,您的账号:{0} 已被删除
+user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+length.not.valid=长度必须在{min}到{max}个字符之间
+user.username.not.blank=用户名不能为空
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.username.length.valid=账户长度必须在{min}到{max}个字符之间
+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=登录成功
+user.register.success=注册成功
+user.register.save.error=保存用户 {0} 失败,注册账号已存在
+user.register.error=注册失败,请联系系统管理人员
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+auth.grant.type.error=认证权限类型错误
+auth.grant.type.blocked=认证权限类型已禁用
+auth.grant.type.not.blank=认证权限类型不能为空
+auth.clientid.not.blank=认证客户端id不能为空
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
+repeat.submit.message=不允许重复提交,请稍候再试
+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]不能为空
+social.source.not.blank=第三方登录平台[source]不能为空
+social.code.not.blank=第三方登录平台[code]不能为空
+social.state.not.blank=第三方登录平台[state]不能为空
+##租户
+tenant.number.not.blank=租户编号不能为空
+tenant.not.exists=对不起, 您的租户不存在,请联系管理员
+tenant.blocked=对不起,您的租户已禁用,请联系管理员
+tenant.expired=对不起,您的租户已过期,请联系管理员

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


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

@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <property name="log.path" value="./logs"/>
+    <property name="console.log.pattern"
+              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${console.log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+    </appender>
+
+    <!-- 控制台输出 -->
+    <appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-console.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-console.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大 1天 -->
+            <maxHistory>1</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+        </filter>
+    </appender>
+
+    <!-- 系统日志输出 -->
+    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- info异步输出 -->
+    <appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>512</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_info"/>
+    </appender>
+
+    <!-- error异步输出 -->
+    <appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>512</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_error"/>
+    </appender>
+
+    <!-- 整合 skywalking 控制台输出 tid -->
+<!--    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">-->
+<!--        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
+<!--            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
+<!--                <pattern>[%tid] ${console.log.pattern}</pattern>-->
+<!--            </layout>-->
+<!--            <charset>utf-8</charset>-->
+<!--        </encoder>-->
+<!--    </appender>-->
+
+    <!-- 整合 skywalking 推送采集日志 -->
+<!--    <appender name="sky_log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">-->
+<!--        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
+<!--            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
+<!--                <pattern>[%tid] ${console.log.pattern}</pattern>-->
+<!--            </layout>-->
+<!--            <charset>utf-8</charset>-->
+<!--        </encoder>-->
+<!--    </appender>-->
+
+    <!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="console" />
+        <appender-ref ref="async_info" />
+        <appender-ref ref="async_error" />
+        <appender-ref ref="file_console" />
+<!--        <appender-ref ref="sky_log"/>-->
+    </root>
+
+</configuration>

+ 45 - 0
ruoyi-common/pom.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ruoyi-vue-plus</artifactId>
+        <groupId>org.dromara</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <modules>
+        <module>ruoyi-common-bom</module>
+        <module>ruoyi-common-social</module>
+        <module>ruoyi-common-core</module>
+        <module>ruoyi-common-doc</module>
+        <module>ruoyi-common-excel</module>
+        <module>ruoyi-common-idempotent</module>
+        <module>ruoyi-common-job</module>
+        <module>ruoyi-common-log</module>
+        <module>ruoyi-common-mail</module>
+        <module>ruoyi-common-mybatis</module>
+        <module>ruoyi-common-oss</module>
+        <module>ruoyi-common-ratelimiter</module>
+        <module>ruoyi-common-redis</module>
+        <module>ruoyi-common-satoken</module>
+        <module>ruoyi-common-security</module>
+        <module>ruoyi-common-sms</module>
+        <module>ruoyi-common-web</module>
+        <module>ruoyi-common-translation</module>
+        <module>ruoyi-common-sensitive</module>
+        <module>ruoyi-common-json</module>
+        <module>ruoyi-common-encrypt</module>
+        <module>ruoyi-common-tenant</module>
+        <module>ruoyi-common-websocket</module>
+    </modules>
+
+    <artifactId>ruoyi-common</artifactId>
+    <packaging>pom</packaging>
+
+    <description>
+        common 通用模块
+    </description>
+
+</project>

+ 178 - 0
ruoyi-common/ruoyi-common-bom/pom.xml

@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.dromara</groupId>
+    <artifactId>ruoyi-common-bom</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+
+    <description>
+        ruoyi-common-bom common依赖项
+    </description>
+
+    <properties>
+        <revision>5.2.1</revision>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- 核心模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-core</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 接口模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-doc</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- excel -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-excel</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 幂等 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-idempotent</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 调度模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-job</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 日志记录 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-log</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 邮件服务 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-mail</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 数据库服务 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-mybatis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- OSS -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-oss</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 限流 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-ratelimiter</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 缓存服务 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-redis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- satoken -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-satoken</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 安全模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-security</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 短信模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-sms</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-social</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- web服务 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-web</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 翻译模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-translation</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 脱敏模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-sensitive</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 序列化模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-json</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 数据库加解密模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-encrypt</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 租户模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-tenant</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- WebSocket模块 -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-common-websocket</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+</project>

+ 112 - 0
ruoyi-common/ruoyi-common-core/pom.xml

@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.dromara</groupId>
+        <artifactId>ruoyi-common</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-common-core</artifactId>
+
+    <description>
+        ruoyi-common-core 核心模块
+    </description>
+
+    <dependencies>
+        <!-- Spring框架基本的核心工具 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+        </dependency>
+
+        <!-- SpringWeb模块 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+        </dependency>
+
+        <!-- 自定义验证注解 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!--常用工具类 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <!-- servlet包 -->
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-http</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-extra</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <!--  自动生成YML配置关联JSON文件  -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-properties-migrator</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>io.github.linpeilie</groupId>
+            <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 离线IP地址定位库 -->
+        <dependency>
+            <groupId>org.lionsoul</groupId>
+            <artifactId>ip2region</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>transmittable-thread-local</artifactId>
+        </dependency>
+
+
+
+        <dependency>
+            <groupId>net.dreamlu</groupId>
+            <artifactId>mica-mqtt-server-spring-boot-starter</artifactId>
+            <version>2.3.4</version>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 17 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ApplicationConfig.java

@@ -0,0 +1,17 @@
+package org.dromara.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+/**
+ * 程序注解配置
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@EnableAspectJAutoProxy
+@EnableAsync(proxyTargetClass = true)
+public class ApplicationConfig {
+
+}

+ 52 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/AsyncConfig.java

@@ -0,0 +1,52 @@
+package org.dromara.common.core.config;
+
+import cn.hutool.core.util.ArrayUtil;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.core.task.VirtualThreadTaskExecutor;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+
+/**
+ * 异步配置
+ * <p>
+ * 如果未使用虚拟线程则生效
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class AsyncConfig implements AsyncConfigurer {
+
+    /**
+     * 自定义 @Async 注解使用系统线程池
+     */
+    @Override
+    public Executor getAsyncExecutor() {
+        if(SpringUtils.isVirtual()) {
+            return new VirtualThreadTaskExecutor("async-");
+        }
+        return SpringUtils.getBean("scheduledExecutorService");
+    }
+
+    /**
+     * 异步执行异常处理
+     */
+    @Override
+    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+        return (throwable, method, objects) -> {
+            throwable.printStackTrace();
+            StringBuilder sb = new StringBuilder();
+            sb.append("Exception message - ").append(throwable.getMessage())
+                .append(", Method name - ").append(method.getName());
+            if (ArrayUtil.isNotEmpty(objects)) {
+                sb.append(", Parameter value - ").append(Arrays.toString(objects));
+            }
+            throw new ServiceException(sb.toString());
+        };
+    }
+
+}

+ 33 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/RuoYiConfig.java

@@ -0,0 +1,33 @@
+package org.dromara.common.core.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取项目相关配置
+ *
+ * @author Lion Li
+ */
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "ruoyi")
+public class RuoYiConfig {
+
+    /**
+     * 项目名称
+     */
+    private String name;
+
+    /**
+     * 版本
+     */
+    private String version;
+
+    /**
+     * 版权年份
+     */
+    private String copyrightYear;
+
+}

+ 78 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ThreadPoolConfig.java

@@ -0,0 +1,78 @@
+package org.dromara.common.core.config;
+
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.dromara.common.core.config.properties.ThreadPoolProperties;
+import org.dromara.common.core.utils.Threads;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ * @author Lion Li
+ **/
+@Slf4j
+@AutoConfiguration
+@EnableConfigurationProperties(ThreadPoolProperties.class)
+public class ThreadPoolConfig {
+
+    /**
+     * 核心线程数 = cpu 核心数 + 1
+     */
+    private final int core = Runtime.getRuntime().availableProcessors() + 1;
+
+    private ScheduledExecutorService scheduledExecutorService;
+
+    @Bean(name = "threadPoolTaskExecutor")
+    @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
+    public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(core);
+        executor.setMaxPoolSize(core * 2);
+        executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
+        executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
+    /**
+     * 执行周期性或定时任务
+     */
+    @Bean(name = "scheduledExecutorService")
+    protected ScheduledExecutorService scheduledExecutorService() {
+        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(core,
+            new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
+            new ThreadPoolExecutor.CallerRunsPolicy()) {
+            @Override
+            protected void afterExecute(Runnable r, Throwable t) {
+                super.afterExecute(r, t);
+                Threads.printException(r, t);
+            }
+        };
+        this.scheduledExecutorService = scheduledThreadPoolExecutor;
+        return scheduledThreadPoolExecutor;
+    }
+
+    /**
+     * 销毁事件
+     */
+    @PreDestroy
+    public void destroy() {
+        try {
+            log.info("====关闭后台任务任务线程池====");
+            Threads.shutdownAndAwaitTermination(scheduledExecutorService);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+        }
+    }
+
+}

+ 40 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java

@@ -0,0 +1,40 @@
+package org.dromara.common.core.config;
+
+import jakarta.validation.Validator;
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+import java.util.Properties;
+
+/**
+ * 校验框架配置类
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+public class ValidatorConfig {
+
+    /**
+     * 配置校验框架 快速返回模式
+     */
+    @Bean
+    public Validator validator(MessageSource messageSource) {
+        try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) {
+            // 国际化
+            factoryBean.setValidationMessageSource(messageSource);
+            // 设置使用 HibernateValidator 校验器
+            factoryBean.setProviderClass(HibernateValidator.class);
+            Properties properties = new Properties();
+            // 设置 快速异常返回
+            properties.setProperty("hibernate.validator.fail_fast", "true");
+            factoryBean.setValidationProperties(properties);
+            // 加载配置
+            factoryBean.afterPropertiesSet();
+            return factoryBean.getValidator();
+        }
+    }
+
+}

+ 30 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/properties/ThreadPoolProperties.java

@@ -0,0 +1,30 @@
+package org.dromara.common.core.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 线程池 配置属性
+ *
+ * @author Lion Li
+ */
+@Data
+@ConfigurationProperties(prefix = "thread-pool")
+public class ThreadPoolProperties {
+
+    /**
+     * 是否开启线程池
+     */
+    private boolean enabled;
+
+    /**
+     * 队列最大长度
+     */
+    private int queueCapacity;
+
+    /**
+     * 线程池维护线程所允许的空闲时间
+     */
+    private int keepAliveSeconds;
+
+}

+ 25 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java

@@ -0,0 +1,25 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 缓存的key 常量
+ *
+ * @author Lion Li
+ */
+public interface CacheConstants {
+
+    /**
+     * 在线用户 redis key
+     */
+    String ONLINE_TOKEN_KEY = "online_tokens:";
+
+    /**
+     * 参数管理 cache key
+     */
+    String SYS_CONFIG_KEY = "sys_config:";
+
+    /**
+     * 字典管理 cache key
+     */
+    String SYS_DICT_KEY = "sys_dict:";
+
+}

+ 73 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java

@@ -0,0 +1,73 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 缓存组名称常量
+ * <p>
+ * key 格式为 cacheNames#ttl#maxIdleTime#maxSize
+ * <p>
+ * ttl 过期时间 如果设置为0则不过期 默认为0
+ * maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
+ * maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
+ * <p>
+ * 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500
+ *
+ * @author Lion Li
+ */
+public interface CacheNames {
+
+    /**
+     * 演示案例
+     */
+    String DEMO_CACHE = "demo:cache#60s#10m#20";
+
+    /**
+     * 系统配置
+     */
+    String SYS_CONFIG = "sys_config";
+
+    /**
+     * 数据字典
+     */
+    String SYS_DICT = "sys_dict";
+
+    /**
+     * 租户
+     */
+    String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
+
+    /**
+     * 客户端
+     */
+    String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
+
+    /**
+     * 用户账户
+     */
+    String SYS_USER_NAME = "sys_user_name#30d";
+
+    /**
+     * 用户名称
+     */
+    String SYS_NICKNAME = "sys_nickname#30d";
+
+    /**
+     * 部门
+     */
+    String SYS_DEPT = "sys_dept#30d";
+
+    /**
+     * OSS内容
+     */
+    String SYS_OSS = "sys_oss#30d";
+
+    /**
+     * OSS配置
+     */
+    String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
+
+    /**
+     * 在线用户
+     */
+    String ONLINE_TOKEN = "online_tokens";
+
+}

+ 81 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java

@@ -0,0 +1,81 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 通用常量信息
+ *
+ * @author ruoyi
+ */
+public interface Constants {
+
+    /**
+     * UTF-8 字符集
+     */
+    String UTF8 = "UTF-8";
+
+    /**
+     * GBK 字符集
+     */
+    String GBK = "GBK";
+
+    /**
+     * www主域
+     */
+    String WWW = "www.";
+
+    /**
+     * http请求
+     */
+    String HTTP = "http://";
+
+    /**
+     * https请求
+     */
+    String HTTPS = "https://";
+
+    /**
+     * 通用成功标识
+     */
+    String SUCCESS = "0";
+
+    /**
+     * 通用失败标识
+     */
+    String FAIL = "1";
+
+    /**
+     * 登录成功
+     */
+    String LOGIN_SUCCESS = "Success";
+
+    /**
+     * 注销
+     */
+    String LOGOUT = "Logout";
+
+    /**
+     * 注册
+     */
+    String REGISTER = "Register";
+
+    /**
+     * 登录失败
+     */
+    String LOGIN_FAIL = "Error";
+
+    /**
+     * 验证码有效期(分钟)
+     */
+    Integer CAPTCHA_EXPIRATION = 2;
+
+    /**
+     * 令牌
+     */
+    String TOKEN = "token";
+
+    /**
+     * 顶级部门id
+     */
+    Long TOP_PARENT_ID = 0L;
+
+}
+

+ 39 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/GlobalConstants.java

@@ -0,0 +1,39 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 全局的key常量 (业务无关的key)
+ *
+ * @author Lion Li
+ */
+public interface GlobalConstants {
+
+    /**
+     * 全局 redis key (业务无关的key)
+     */
+    String GLOBAL_REDIS_KEY = "global:";
+
+    /**
+     * 验证码 redis key
+     */
+    String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
+
+    /**
+     * 防重提交 redis key
+     */
+    String REPEAT_SUBMIT_KEY = GLOBAL_REDIS_KEY + "repeat_submit:";
+
+    /**
+     * 限流 redis key
+     */
+    String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
+
+    /**
+     * 登录账户密码错误次数 redis key
+     */
+    String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
+
+    /**
+     * 三方认证 redis key
+     */
+    String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
+}

+ 93 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/HttpStatus.java

@@ -0,0 +1,93 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 返回状态码
+ *
+ * @author Lion Li
+ */
+public interface HttpStatus {
+    /**
+     * 操作成功
+     */
+    int SUCCESS = 200;
+
+    /**
+     * 对象创建成功
+     */
+    int CREATED = 201;
+
+    /**
+     * 请求已经被接受
+     */
+    int ACCEPTED = 202;
+
+    /**
+     * 操作已经执行成功,但是没有返回数据
+     */
+    int NO_CONTENT = 204;
+
+    /**
+     * 资源已被移除
+     */
+    int MOVED_PERM = 301;
+
+    /**
+     * 重定向
+     */
+    int SEE_OTHER = 303;
+
+    /**
+     * 资源没有被修改
+     */
+    int NOT_MODIFIED = 304;
+
+    /**
+     * 参数列表错误(缺少,格式不匹配)
+     */
+    int BAD_REQUEST = 400;
+
+    /**
+     * 未授权
+     */
+    int UNAUTHORIZED = 401;
+
+    /**
+     * 访问受限,授权过期
+     */
+    int FORBIDDEN = 403;
+
+    /**
+     * 资源,服务未找到
+     */
+    int NOT_FOUND = 404;
+
+    /**
+     * 不允许的http方法
+     */
+    int BAD_METHOD = 405;
+
+    /**
+     * 资源冲突,或者资源被锁
+     */
+    int CONFLICT = 409;
+
+    /**
+     * 不支持的数据,媒体类型
+     */
+    int UNSUPPORTED_TYPE = 415;
+
+    /**
+     * 系统内部错误
+     */
+    int ERROR = 500;
+
+    /**
+     * 接口未实现
+     */
+    int NOT_IMPLEMENTED = 501;
+
+    /**
+     * 系统警告消息
+     */
+    int WARN = 601;
+}

+ 54 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java

@@ -0,0 +1,54 @@
+package org.dromara.common.core.constant;
+
+import cn.hutool.core.lang.RegexPool;
+
+/**
+ * 常用正则表达式字符串
+ * <p>
+ * 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/
+ *
+ * @author Feng
+ */
+public interface RegexConstants extends RegexPool {
+
+    /**
+     * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+     */
+    String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";
+
+    /**
+     * 权限标识必须符合 tool:build:list 格式,或者空字符串
+     */
+    String PERMISSION_STRING = "^(|^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+:[a-zA-Z0-9_]+)$";
+
+    /**
+     * 身份证号码(后6位)
+     */
+    String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
+
+    /**
+     * QQ号码
+     */
+    String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$";
+
+    /**
+     * 邮政编码
+     */
+    String POSTAL_CODE = "^[1-9]\\d{5}$";
+
+    /**
+     * 注册账号
+     */
+    String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$";
+
+    /**
+     * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+     */
+    String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
+
+    /**
+     * 通用状态(0表示正常,1表示停用)
+     */
+    String STATUS = "^[01]$";
+
+}

+ 45 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/TenantConstants.java

@@ -0,0 +1,45 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 租户常量信息
+ *
+ * @author Lion Li
+ */
+public interface TenantConstants {
+
+    /**
+     * 租户正常状态
+     */
+    String NORMAL = "0";
+
+    /**
+     * 租户封禁状态
+     */
+    String DISABLE = "1";
+
+    /**
+     * 超级管理员ID
+     */
+    Long SUPER_ADMIN_ID = 1L;
+
+    /**
+     * 超级管理员角色 roleKey
+     */
+    String SUPER_ADMIN_ROLE_KEY = "superadmin";
+
+    /**
+     * 租户管理员角色 roleKey
+     */
+    String TENANT_ADMIN_ROLE_KEY = "admin";
+
+    /**
+     * 租户管理员角色名称
+     */
+    String TENANT_ADMIN_ROLE_NAME = "管理员";
+
+    /**
+     * 默认租户ID
+     */
+    String DEFAULT_TENANT_ID = "000000";
+
+}

+ 142 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/UserConstants.java

@@ -0,0 +1,142 @@
+package org.dromara.common.core.constant;
+
+/**
+ * 用户常量信息
+ *
+ * @author ruoyi
+ */
+public interface UserConstants {
+
+    /**
+     * 平台内系统用户的唯一标志
+     */
+    String SYS_USER = "SYS_USER";
+
+    /**
+     * 正常状态
+     */
+    String NORMAL = "0";
+
+    /**
+     * 异常状态
+     */
+    String EXCEPTION = "1";
+
+    /**
+     * 用户正常状态
+     */
+    String USER_NORMAL = "0";
+
+    /**
+     * 用户封禁状态
+     */
+    String USER_DISABLE = "1";
+
+    /**
+     * 角色正常状态
+     */
+    String ROLE_NORMAL = "0";
+
+    /**
+     * 角色封禁状态
+     */
+    String ROLE_DISABLE = "1";
+
+    /**
+     * 部门正常状态
+     */
+    String DEPT_NORMAL = "0";
+
+    /**
+     * 部门停用状态
+     */
+    String DEPT_DISABLE = "1";
+
+    /**
+     * 岗位正常状态
+     */
+    String POST_NORMAL = "0";
+
+    /**
+     * 岗位停用状态
+     */
+    String POST_DISABLE = "1";
+
+    /**
+     * 字典正常状态
+     */
+    String DICT_NORMAL = "0";
+
+    /**
+     * 是否为系统默认(是)
+     */
+    String YES = "Y";
+
+    /**
+     * 是否菜单外链(是)
+     */
+    String YES_FRAME = "0";
+
+    /**
+     * 是否菜单外链(否)
+     */
+    String NO_FRAME = "1";
+
+    /**
+     * 菜单正常状态
+     */
+    String MENU_NORMAL = "0";
+
+    /**
+     * 菜单停用状态
+     */
+    String MENU_DISABLE = "1";
+
+    /**
+     * 菜单类型(目录)
+     */
+    String TYPE_DIR = "M";
+
+    /**
+     * 菜单类型(菜单)
+     */
+    String TYPE_MENU = "C";
+
+    /**
+     * 菜单类型(按钮)
+     */
+    String TYPE_BUTTON = "F";
+
+    /**
+     * Layout组件标识
+     */
+    String LAYOUT = "Layout";
+
+    /**
+     * ParentView组件标识
+     */
+    String PARENT_VIEW = "ParentView";
+
+    /**
+     * InnerLink组件标识
+     */
+    String INNER_LINK = "InnerLink";
+
+    /**
+     * 用户名长度限制
+     */
+    int USERNAME_MIN_LENGTH = 2;
+    int USERNAME_MAX_LENGTH = 20;
+
+    /**
+     * 密码长度限制
+     */
+    int PASSWORD_MIN_LENGTH = 5;
+    int PASSWORD_MAX_LENGTH = 20;
+
+    /**
+     * 超级管理员ID
+     */
+    Long SUPER_ADMIN_ID = 1L;
+
+}

+ 110 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/R.java

@@ -0,0 +1,110 @@
+package org.dromara.common.core.domain;
+
+import org.dromara.common.core.constant.HttpStatus;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 响应信息主体
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class R<T> implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 成功
+     */
+    public static final int SUCCESS = 200;
+
+    /**
+     * 失败
+     */
+    public static final int FAIL = 500;
+
+    private int code;
+
+    private String msg;
+
+    private T data;
+
+    public static <T> R<T> ok() {
+        return restResult(null, SUCCESS, "操作成功");
+    }
+
+    public static <T> R<T> ok(T data) {
+        return restResult(data, SUCCESS, "操作成功");
+    }
+
+    public static <T> R<T> ok(String msg) {
+        return restResult(null, SUCCESS, msg);
+    }
+
+    public static <T> R<T> ok(String msg, T data) {
+        return restResult(data, SUCCESS, msg);
+    }
+
+    public static <T> R<T> fail() {
+        return restResult(null, FAIL, "操作失败");
+    }
+
+    public static <T> R<T> fail(String msg) {
+        return restResult(null, FAIL, msg);
+    }
+
+    public static <T> R<T> fail(T data) {
+        return restResult(data, FAIL, "操作失败");
+    }
+
+    public static <T> R<T> fail(String msg, T data) {
+        return restResult(data, FAIL, msg);
+    }
+
+    public static <T> R<T> fail(int code, String msg) {
+        return restResult(null, code, msg);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg 返回内容
+     * @return 警告消息
+     */
+    public static <T> R<T> warn(String msg) {
+        return restResult(null, HttpStatus.WARN, msg);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg 返回内容
+     * @param data 数据对象
+     * @return 警告消息
+     */
+    public static <T> R<T> warn(String msg, T data) {
+        return restResult(data, HttpStatus.WARN, msg);
+    }
+
+    private static <T> R<T> restResult(T data, int code, String msg) {
+        R<T> r = new R<>();
+        r.setCode(code);
+        r.setData(data);
+        r.setMsg(msg);
+        return r;
+    }
+
+    public static <T> Boolean isError(R<T> ret) {
+        return !isSuccess(ret);
+    }
+
+    public static <T> Boolean isSuccess(R<T> ret) {
+        return R.SUCCESS == ret.getCode();
+    }
+}

+ 46 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/OssDTO.java

@@ -0,0 +1,46 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * OSS对象
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class OssDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 对象存储主键
+     */
+    private Long ossId;
+
+    /**
+     * 文件名
+     */
+    private String fileName;
+
+    /**
+     * 原名
+     */
+    private String originalName;
+
+    /**
+     * 文件后缀名
+     */
+    private String fileSuffix;
+
+    /**
+     * URL地址
+     */
+    private String url;
+
+}

+ 42 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/RoleDTO.java

@@ -0,0 +1,42 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 角色
+ *
+ * @author Lion Li
+ */
+
+@Data
+@NoArgsConstructor
+public class RoleDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 角色ID
+     */
+    private Long roleId;
+
+    /**
+     * 角色名称
+     */
+    private String roleName;
+
+    /**
+     * 角色权限
+     */
+    private String roleKey;
+
+    /**
+     * 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)
+     */
+    private String dataScope;
+
+}

+ 73 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserDTO.java

@@ -0,0 +1,73 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 用户
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@NoArgsConstructor
+public class UserDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 用户账号
+     */
+    private String userName;
+
+    /**
+     * 用户昵称
+     */
+    private String nickName;
+
+    /**
+     * 用户类型(sys_user系统用户)
+     */
+    private String userType;
+
+    /**
+     * 用户邮箱
+     */
+    private String email;
+
+    /**
+     * 手机号码
+     */
+    private String phonenumber;
+
+    /**
+     * 用户性别(0男 1女 2未知)
+     */
+    private String sex;
+
+    /**
+     * 帐号状态(0正常 1停用)
+     */
+    private String status;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+}

+ 72 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java

@@ -0,0 +1,72 @@
+package org.dromara.common.core.domain.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 当前在线会话
+ *
+ * @author ruoyi
+ */
+
+@Data
+@NoArgsConstructor
+public class UserOnlineDTO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 会话编号
+     */
+    private String tokenId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 用户名称
+     */
+    private String userName;
+
+    /**
+     * 客户端
+     */
+    private String clientKey;
+
+    /**
+     * 设备类型
+     */
+    private String deviceType;
+
+    /**
+     * 登录IP地址
+     */
+    private String ipaddr;
+
+    /**
+     * 登录地址
+     */
+    private String loginLocation;
+
+    /**
+     * 浏览器类型
+     */
+    private String browser;
+
+    /**
+     * 操作系统
+     */
+    private String os;
+
+    /**
+     * 登录时间
+     */
+    private Long loginTime;
+
+}

+ 41 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessEvent.java

@@ -0,0 +1,41 @@
+package org.dromara.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 总体流程监听
+ *
+ * @author may
+ */
+
+@Data
+public class ProcessEvent implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程定义key
+     */
+    private String key;
+
+    /**
+     * 业务id
+     */
+    private String businessKey;
+
+    /**
+     * 状态
+     */
+    private String status;
+
+    /**
+     * 当为true时为申请人节点办理
+     */
+    private boolean submit;
+
+
+}

+ 40 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/event/ProcessTaskEvent.java

@@ -0,0 +1,40 @@
+package org.dromara.common.core.domain.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 流程办理监听
+ *
+ * @author may
+ */
+
+@Data
+public class ProcessTaskEvent implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程定义key
+     */
+    private String key;
+
+    /**
+     * 审批节点key
+     */
+    private String taskDefinitionKey;
+
+    /**
+     * 任务id
+     */
+    private String taskId;
+
+    /**
+     * 业务id
+     */
+    private String businessKey;
+
+}

+ 31 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/EmailLoginBody.java

@@ -0,0 +1,31 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 邮件登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class EmailLoginBody extends LoginBody {
+
+    /**
+     * 邮箱
+     */
+    @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;
+
+}

+ 48 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginBody.java

@@ -0,0 +1,48 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 用户登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class LoginBody implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 客户端id
+     */
+    @NotBlank(message = "{auth.clientid.not.blank}")
+    private String clientId;
+
+    /**
+     * 授权类型
+     */
+    @NotBlank(message = "{auth.grant.type.not.blank}")
+    private String grantType;
+
+    /**
+     * 租户ID
+     */
+    private String tenantId;
+
+    /**
+     * 验证码
+     */
+    private String code;
+
+    /**
+     * 唯一标识
+     */
+    private String uuid;
+
+}

+ 142 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/LoginUser.java

@@ -0,0 +1,142 @@
+package org.dromara.common.core.domain.model;
+
+import org.dromara.common.core.domain.dto.RoleDTO;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class LoginUser implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 租户ID
+     */
+    private String tenantId;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 部门类别编码
+     */
+    private String deptCategory;
+
+    /**
+     * 部门名
+     */
+    private String deptName;
+
+    /**
+     * 用户唯一标识
+     */
+    private String token;
+
+    /**
+     * 用户类型
+     */
+    private String userType;
+
+    /**
+     * 登录时间
+     */
+    private Long loginTime;
+
+    /**
+     * 过期时间
+     */
+    private Long expireTime;
+
+    /**
+     * 登录IP地址
+     */
+    private String ipaddr;
+
+    /**
+     * 登录地点
+     */
+    private String loginLocation;
+
+    /**
+     * 浏览器类型
+     */
+    private String browser;
+
+    /**
+     * 操作系统
+     */
+    private String os;
+
+    /**
+     * 菜单权限
+     */
+    private Set<String> menuPermission;
+
+    /**
+     * 角色权限
+     */
+    private Set<String> rolePermission;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 用户昵称
+     */
+    private String nickname;
+
+    /**
+     * 角色对象
+     */
+    private List<RoleDTO> roles;
+
+    /**
+     * 数据权限 当前角色ID
+     */
+    private Long roleId;
+
+    /**
+     * 客户端
+     */
+    private String clientKey;
+
+    /**
+     * 设备类型
+     */
+    private String deviceType;
+
+    /**
+     * 获取登录id
+     */
+    public String getLoginId() {
+        if (userType == null) {
+            throw new IllegalArgumentException("用户类型不能为空");
+        }
+        if (userId == null) {
+            throw new IllegalArgumentException("用户ID不能为空");
+        }
+        return userType + ":" + userId;
+    }
+
+}

+ 33 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java

@@ -0,0 +1,33 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.validator.constraints.Length;
+
+import static org.dromara.common.core.constant.UserConstants.*;
+
+/**
+ * 密码登录对象
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class PasswordLoginBody extends LoginBody {
+
+    /**
+     * 用户名
+     */
+    @NotBlank(message = "{user.username.not.blank}")
+    @Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+    private String username;
+
+    /**
+     * 用户密码
+     */
+    @NotBlank(message = "{user.password.not.blank}")
+    @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+    private String password;
+
+}

+ 35 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/RegisterBody.java

@@ -0,0 +1,35 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.validator.constraints.Length;
+
+import static org.dromara.common.core.constant.UserConstants.*;
+
+/**
+ * 用户注册对象
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class RegisterBody extends LoginBody {
+
+    /**
+     * 用户名
+     */
+    @NotBlank(message = "{user.username.not.blank}")
+    @Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+    private String username;
+
+    /**
+     * 用户密码
+     */
+    @NotBlank(message = "{user.password.not.blank}")
+    @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+    private String password;
+
+    private String userType;
+
+}

+ 29 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SmsLoginBody.java

@@ -0,0 +1,29 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SmsLoginBody extends LoginBody {
+
+    /**
+     * 手机号
+     */
+    @NotBlank(message = "{user.phonenumber.not.blank}")
+    private String phonenumber;
+
+    /**
+     * 短信code
+     */
+    @NotBlank(message = "{sms.code.not.blank}")
+    private String smsCode;
+
+}

+ 35 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/SocialLoginBody.java

@@ -0,0 +1,35 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 三方登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SocialLoginBody extends LoginBody {
+
+    /**
+     * 第三方登录平台
+     */
+    @NotBlank(message = "{social.source.not.blank}")
+    private String source;
+
+    /**
+     * 第三方登录code
+     */
+    @NotBlank(message = "{social.code.not.blank}")
+    private String socialCode;
+
+    /**
+     * 第三方登录socialState
+     */
+    @NotBlank(message = "{social.state.not.blank}")
+    private String socialState;
+
+}

+ 28 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginBody.java

@@ -0,0 +1,28 @@
+package org.dromara.common.core.domain.model;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 三方登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class XcxLoginBody extends LoginBody {
+
+    /**
+     * 小程序id(多个小程序时使用)
+     */
+    private String appid;
+
+    /**
+     * 小程序code
+     */
+    @NotBlank(message = "{xcx.code.not.blank}")
+    private String xcxCode;
+
+}

+ 27 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/XcxLoginUser.java

@@ -0,0 +1,27 @@
+package org.dromara.common.core.domain.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 小程序登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class XcxLoginUser extends LoginUser {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * openid
+     */
+    private String openid;
+
+}

+ 152 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/BusinessStatusEnum.java

@@ -0,0 +1,152 @@
+package org.dromara.common.core.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 业务状态枚举
+ *
+ * @author may
+ */
+@Getter
+@AllArgsConstructor
+public enum BusinessStatusEnum {
+    /**
+     * 已撤销
+     */
+    CANCEL("cancel", "已撤销"),
+    /**
+     * 草稿
+     */
+    DRAFT("draft", "草稿"),
+    /**
+     * 待审核
+     */
+    WAITING("waiting", "待审核"),
+    /**
+     * 已完成
+     */
+    FINISH("finish", "已完成"),
+    /**
+     * 已作废
+     */
+    INVALID("invalid", "已作废"),
+    /**
+     * 已退回
+     */
+    BACK("back", "已退回"),
+    /**
+     * 已终止
+     */
+    TERMINATION("termination", "已终止");
+
+    /**
+     * 状态
+     */
+    private final String status;
+
+    /**
+     * 描述
+     */
+    private final String desc;
+
+    /**
+     * 获取业务状态
+     *
+     * @param status 状态
+     */
+    public static String findByStatus(String status) {
+        if (StringUtils.isBlank(status)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(BusinessStatusEnum.values())
+            .filter(statusEnum -> statusEnum.getStatus().equals(status))
+            .findFirst()
+            .map(BusinessStatusEnum::getDesc)
+            .orElse(StrUtil.EMPTY);
+    }
+
+    /**
+     * 启动流程校验
+     *
+     * @param status 状态
+     */
+    public static void checkStartStatus(String status) {
+        if (WAITING.getStatus().equals(status)) {
+            throw new ServiceException("该单据已提交过申请,正在审批中!");
+        } else if (FINISH.getStatus().equals(status)) {
+            throw new ServiceException("该单据已完成申请!");
+        } else if (INVALID.getStatus().equals(status)) {
+            throw new ServiceException("该单据已作废!");
+        } else if (TERMINATION.getStatus().equals(status)) {
+            throw new ServiceException("该单据已终止!");
+        } else if (StringUtils.isBlank(status)) {
+            throw new ServiceException("流程状态为空!");
+        }
+    }
+
+    /**
+     * 撤销流程校验
+     *
+     * @param status 状态
+     */
+    public static void checkCancelStatus(String status) {
+        if (CANCEL.getStatus().equals(status)) {
+            throw new ServiceException("该单据已撤销!");
+        } else if (FINISH.getStatus().equals(status)) {
+            throw new ServiceException("该单据已完成申请!");
+        } else if (INVALID.getStatus().equals(status)) {
+            throw new ServiceException("该单据已作废!");
+        } else if (TERMINATION.getStatus().equals(status)) {
+            throw new ServiceException("该单据已终止!");
+        } else if (BACK.getStatus().equals(status)) {
+            throw new ServiceException("该单据已退回!");
+        } else if (StringUtils.isBlank(status)) {
+            throw new ServiceException("流程状态为空!");
+        }
+    }
+
+    /**
+     * 驳回流程校验
+     *
+     * @param status 状态
+     */
+    public static void checkBackStatus(String status) {
+        if (BACK.getStatus().equals(status)) {
+            throw new ServiceException("该单据已退回!");
+        } else if (FINISH.getStatus().equals(status)) {
+            throw new ServiceException("该单据已完成申请!");
+        } else if (INVALID.getStatus().equals(status)) {
+            throw new ServiceException("该单据已作废!");
+        } else if (TERMINATION.getStatus().equals(status)) {
+            throw new ServiceException("该单据已终止!");
+        } else if (CANCEL.getStatus().equals(status)) {
+            throw new ServiceException("该单据已撤销!");
+        } else if (StringUtils.isBlank(status)) {
+            throw new ServiceException("流程状态为空!");
+        }
+    }
+
+    /**
+     * 作废,终止流程校验
+     *
+     * @param status 状态
+     */
+    public static void checkInvalidStatus(String status) {
+        if (FINISH.getStatus().equals(status)) {
+            throw new ServiceException("该单据已完成申请!");
+        } else if (INVALID.getStatus().equals(status)) {
+            throw new ServiceException("该单据已作废!");
+        } else if (TERMINATION.getStatus().equals(status)) {
+            throw new ServiceException("该单据已终止!");
+        } else if (StringUtils.isBlank(status)) {
+            throw new ServiceException("流程状态为空!");
+        }
+    }
+}
+

+ 37 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/DeviceType.java

@@ -0,0 +1,37 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型
+ * 针对一套 用户体系
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum DeviceType {
+
+    /**
+     * pc端
+     */
+    PC("pc"),
+
+    /**
+     * app端
+     */
+    APP("app"),
+
+    /**
+     * 小程序端
+     */
+    XCX("xcx"),
+
+    /**
+     * social第三方端
+     */
+    SOCIAL("social");
+
+    private final String device;
+}

+ 44 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/LoginType.java

@@ -0,0 +1,44 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 登录类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum LoginType {
+
+    /**
+     * 密码登录
+     */
+    PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
+
+    /**
+     * 短信登录
+     */
+    SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
+
+    /**
+     * 邮箱登录
+     */
+    EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
+
+    /**
+     * 小程序登录
+     */
+    XCX("", "");
+
+    /**
+     * 登录重试超出限制提示
+     */
+    final String retryLimitExceed;
+
+    /**
+     * 登录重试限制计数提示
+     */
+    final String retryLimitCount;
+}

+ 30 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TenantStatus.java

@@ -0,0 +1,30 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 用户状态
+ *
+ * @author LionLi
+ */
+@Getter
+@AllArgsConstructor
+public enum TenantStatus {
+    /**
+     * 正常
+     */
+    OK("0", "正常"),
+    /**
+     * 停用
+     */
+    DISABLE("1", "停用"),
+    /**
+     * 删除
+     */
+    DELETED("2", "删除");
+
+    private final String code;
+    private final String info;
+
+}

+ 30 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserStatus.java

@@ -0,0 +1,30 @@
+package org.dromara.common.core.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 用户状态
+ *
+ * @author ruoyi
+ */
+@Getter
+@AllArgsConstructor
+public enum UserStatus {
+    /**
+     * 正常
+     */
+    OK("0", "正常"),
+    /**
+     * 停用
+     */
+    DISABLE("1", "停用"),
+    /**
+     * 删除
+     */
+    DELETED("2", "删除");
+
+    private final String code;
+    private final String info;
+
+}

+ 37 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/UserType.java

@@ -0,0 +1,37 @@
+package org.dromara.common.core.enums;
+
+import org.dromara.common.core.utils.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 设备类型
+ * 针对多套 用户体系
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum UserType {
+
+    /**
+     * pc端
+     */
+    SYS_USER("sys_user"),
+
+    /**
+     * app端
+     */
+    APP_USER("app_user");
+
+    private final String userType;
+
+    public static UserType getUserType(String str) {
+        for (UserType value : values()) {
+            if (StringUtils.contains(str, value.getUserType())) {
+                return value;
+            }
+        }
+        throw new RuntimeException("'UserType' not found By " + str);
+    }
+}

+ 59 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/ServiceException.java

@@ -0,0 +1,59 @@
+package org.dromara.common.core.exception;
+
+import lombok.*;
+
+import java.io.Serial;
+
+/**
+ * 业务异常
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public final class ServiceException extends RuntimeException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误码
+     */
+    private Integer code;
+
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 错误明细,内部调试错误
+     */
+    private String detailMessage;
+
+    public ServiceException(String message) {
+        this.message = message;
+    }
+
+    public ServiceException(String message, Integer code) {
+        this.message = message;
+        this.code = code;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+    public ServiceException setDetailMessage(String detailMessage) {
+        this.detailMessage = detailMessage;
+        return this;
+    }
+}

+ 74 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/base/BaseException.java

@@ -0,0 +1,74 @@
+package org.dromara.common.core.exception.base;
+
+import lombok.AllArgsConstructor;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 基础异常
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public class BaseException extends RuntimeException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 所属模块
+     */
+    private String module;
+
+    /**
+     * 错误码
+     */
+    private String code;
+
+    /**
+     * 错误码对应的参数
+     */
+    private Object[] args;
+
+    /**
+     * 错误消息
+     */
+    private String defaultMessage;
+
+    public BaseException(String module, String code, Object[] args) {
+        this(module, code, args, null);
+    }
+
+    public BaseException(String module, String defaultMessage) {
+        this(module, null, null, defaultMessage);
+    }
+
+    public BaseException(String code, Object[] args) {
+        this(null, code, args, null);
+    }
+
+    public BaseException(String defaultMessage) {
+        this(null, null, null, defaultMessage);
+    }
+
+    @Override
+    public String getMessage() {
+        String message = null;
+        if (!StringUtils.isEmpty(code)) {
+            message = MessageUtils.message(code, args);
+        }
+        if (message == null) {
+            message = defaultMessage;
+        }
+        return message;
+    }
+
+}

+ 21 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileException.java

@@ -0,0 +1,21 @@
+package org.dromara.common.core.exception.file;
+
+import org.dromara.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 文件信息异常类
+ *
+ * @author ruoyi
+ */
+public class FileException extends BaseException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public FileException(String code, Object[] args) {
+        super("file", code, args, null);
+    }
+
+}

+ 18 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileNameLengthLimitExceededException.java

@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 文件名称超长限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileNameLengthLimitExceededException extends FileException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public FileNameLengthLimitExceededException(int defaultFileNameLength) {
+        super("upload.filename.exceed.length", new Object[]{defaultFileNameLength});
+    }
+}

+ 18 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/file/FileSizeLimitExceededException.java

@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.file;
+
+import java.io.Serial;
+
+/**
+ * 文件名大小限制异常类
+ *
+ * @author ruoyi
+ */
+public class FileSizeLimitExceededException extends FileException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public FileSizeLimitExceededException(long defaultMaxSize) {
+        super("upload.exceed.maxSize", new Object[]{defaultMaxSize});
+    }
+}

+ 18 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaException.java

@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码错误异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaException extends UserException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public CaptchaException() {
+        super("user.jcaptcha.error");
+    }
+}

+ 18 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/CaptchaExpireException.java

@@ -0,0 +1,18 @@
+package org.dromara.common.core.exception.user;
+
+import java.io.Serial;
+
+/**
+ * 验证码失效异常类
+ *
+ * @author ruoyi
+ */
+public class CaptchaExpireException extends UserException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public CaptchaExpireException() {
+        super("user.jcaptcha.expire");
+    }
+}

+ 20 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/exception/user/UserException.java

@@ -0,0 +1,20 @@
+package org.dromara.common.core.exception.user;
+
+import org.dromara.common.core.exception.base.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 用户信息异常类
+ *
+ * @author ruoyi
+ */
+public class UserException extends BaseException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public UserException(String code, Object... args) {
+        super("user", code, args, null);
+    }
+}

+ 52 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java

@@ -0,0 +1,52 @@
+package org.dromara.common.core.factory;
+
+import cn.hutool.core.lang.PatternPool;
+import org.dromara.common.core.constant.RegexConstants;
+
+import java.util.regex.Pattern;
+
+/**
+ * 正则表达式模式池工厂
+ * <p>初始化的时候将正则表达式加入缓存池当中</p>
+ * <p>提高正则表达式的性能,避免重复编译相同的正则表达式</p>
+ *
+ * @author 21001
+ */
+public class RegexPatternPoolFactory extends PatternPool {
+
+    /**
+     * 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
+     */
+    public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);
+
+    /**
+     * 身份证号码(后6位)
+     */
+    public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6);
+
+    /**
+     * QQ号码
+     */
+    public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER);
+
+    /**
+     * 邮政编码
+     */
+    public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE);
+
+    /**
+     * 注册账号
+     */
+    public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT);
+
+    /**
+     * 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
+     */
+    public static final Pattern PASSWORD = get(RegexConstants.PASSWORD);
+
+    /**
+     * 通用状态(0表示正常,1表示停用)
+     */
+    public static final Pattern STATUS = get(RegexConstants.STATUS);
+
+}

+ 31 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/YmlPropertySourceFactory.java

@@ -0,0 +1,31 @@
+package org.dromara.common.core.factory;
+
+import org.dromara.common.core.utils.StringUtils;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.support.DefaultPropertySourceFactory;
+import org.springframework.core.io.support.EncodedResource;
+
+import java.io.IOException;
+
+/**
+ * yml 配置源工厂
+ *
+ * @author Lion Li
+ */
+public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
+
+    @Override
+    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
+        String sourceName = resource.getResource().getFilename();
+        if (StringUtils.isNotBlank(sourceName) && StringUtils.endsWithAny(sourceName, ".yml", ".yaml")) {
+            YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
+            factory.setResources(resource.getResource());
+            factory.afterPropertiesSet();
+            return new PropertiesPropertySource(sourceName, factory.getObject());
+        }
+        return super.createPropertySource(name, resource);
+    }
+
+}

+ 18 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/ConfigService.java

@@ -0,0 +1,18 @@
+package org.dromara.common.core.service;
+
+/**
+ * 通用 参数配置服务
+ *
+ * @author Lion Li
+ */
+public interface ConfigService {
+
+    /**
+     * 根据参数 key 获取参数值
+     *
+     * @param configKey 参数 key
+     * @return 参数值
+     */
+    String getConfigValue(String configKey);
+
+}

+ 18 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DeptService.java

@@ -0,0 +1,18 @@
+package org.dromara.common.core.service;
+
+/**
+ * 通用 部门服务
+ *
+ * @author Lion Li
+ */
+public interface DeptService {
+
+    /**
+     * 通过部门ID查询部门名称
+     *
+     * @param deptIds 部门ID串逗号分隔
+     * @return 部门名称串逗号分隔
+     */
+    String selectDeptNameByIds(String deptIds);
+
+}

+ 67 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java

@@ -0,0 +1,67 @@
+package org.dromara.common.core.service;
+
+import java.util.Map;
+
+/**
+ * 通用 字典服务
+ *
+ * @author Lion Li
+ */
+public interface DictService {
+
+    /**
+     * 分隔符
+     */
+    String SEPARATOR = ",";
+
+    /**
+     * 根据字典类型和字典值获取字典标签
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典值
+     * @return 字典标签
+     */
+    default String getDictLabel(String dictType, String dictValue) {
+        return getDictLabel(dictType, dictValue, SEPARATOR);
+    }
+
+    /**
+     * 根据字典类型和字典标签获取字典值
+     *
+     * @param dictType  字典类型
+     * @param dictLabel 字典标签
+     * @return 字典值
+     */
+    default String getDictValue(String dictType, String dictLabel) {
+        return getDictValue(dictType, dictLabel, SEPARATOR);
+    }
+
+    /**
+     * 根据字典类型和字典值获取字典标签
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典值
+     * @param separator 分隔符
+     * @return 字典标签
+     */
+    String getDictLabel(String dictType, String dictValue, String separator);
+
+    /**
+     * 根据字典类型和字典标签获取字典值
+     *
+     * @param dictType  字典类型
+     * @param dictLabel 字典标签
+     * @param separator 分隔符
+     * @return 字典值
+     */
+    String getDictValue(String dictType, String dictLabel, String separator);
+
+    /**
+     * 获取字典下所有的字典值与标签
+     *
+     * @param dictType 字典类型
+     * @return dictValue为key,dictLabel为值组成的Map
+     */
+    Map<String, String> getAllDictByDictType(String dictType);
+
+}

+ 29 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/OssService.java

@@ -0,0 +1,29 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.OssDTO;
+
+import java.util.List;
+
+/**
+ * 通用 OSS服务
+ *
+ * @author Lion Li
+ */
+public interface OssService {
+
+    /**
+     * 通过ossId查询对应的url
+     *
+     * @param ossIds ossId串逗号分隔
+     * @return url串逗号分隔
+     */
+    String selectUrlByIds(String ossIds);
+
+    /**
+     * 通过ossId查询列表
+     *
+     * @param ossIds ossId串逗号分隔
+     * @return 列表
+     */
+    List<OssDTO> selectByIds(String ossIds);
+}

+ 69 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java

@@ -0,0 +1,69 @@
+package org.dromara.common.core.service;
+
+import org.dromara.common.core.domain.dto.UserDTO;
+
+import java.util.List;
+
+/**
+ * 通用 用户服务
+ *
+ * @author Lion Li
+ */
+public interface UserService {
+
+    /**
+     * 通过用户ID查询用户账户
+     *
+     * @param userId 用户ID
+     * @return 用户账户
+     */
+    String selectUserNameById(Long userId);
+
+    /**
+     * 通过用户ID查询用户账户
+     *
+     * @param userId 用户ID
+     * @return 用户名称
+     */
+    String selectNicknameById(Long userId);
+
+    /**
+     * 通过用户ID查询用户账户
+     *
+     * @param userIds 用户ID 多个用逗号隔开
+     * @return 用户名称
+     */
+    String selectNicknameByIds(String userIds);
+
+    /**
+     * 通过用户ID查询用户手机号
+     *
+     * @param userId 用户id
+     * @return 用户手机号
+     */
+    String selectPhonenumberById(Long userId);
+
+    /**
+     * 通过用户ID查询用户邮箱
+     *
+     * @param userId 用户id
+     * @return 用户邮箱
+     */
+    String selectEmailById(Long userId);
+
+    /**
+     * 通过用户ID查询用户列表
+     *
+     * @param userIds 用户ids
+     * @return 用户列表
+     */
+    List<UserDTO> selectListByIds(List<Long> userIds);
+
+    /**
+     * 通过角色ID查询用户ID
+     *
+     * @param roleIds 角色ids
+     * @return 用户ids
+     */
+    List<Long> selectUserIdsByRoleIds(List<Long> roleIds);
+}

+ 76 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/WorkflowService.java

@@ -0,0 +1,76 @@
+package org.dromara.common.core.service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通用 工作流服务
+ *
+ * @author may
+ */
+public interface WorkflowService {
+
+    /**
+     * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param businessKeys 业务id
+     * @return 结果
+     */
+    boolean deleteRunAndHisInstance(List<String> businessKeys);
+
+    /**
+     * 获取当前流程状态
+     *
+     * @param taskId 任务id
+     */
+    String getBusinessStatusByTaskId(String taskId);
+
+    /**
+     * 获取当前流程状态
+     *
+     * @param businessKey 业务id
+     */
+    String getBusinessStatus(String businessKey);
+
+    /**
+     * 设置流程变量(全局变量)
+     *
+     * @param taskId       任务id
+     * @param variableName 变量名称
+     * @param value        变量值
+     */
+    void setVariable(String taskId, String variableName, Object value);
+
+    /**
+     * 设置流程变量(全局变量)
+     *
+     * @param taskId    任务id
+     * @param variables 流程变量
+     */
+    void setVariables(String taskId, Map<String, Object> variables);
+
+    /**
+     * 设置流程变量(本地变量,非全局变量)
+     *
+     * @param taskId       任务id
+     * @param variableName 变量名称
+     * @param value        变量值
+     */
+    void setVariableLocal(String taskId, String variableName, Object value);
+
+    /**
+     * 设置流程变量(本地变量,非全局变量)
+     *
+     * @param taskId    任务id
+     * @param variables 流程变量
+     */
+    void setVariablesLocal(String taskId, Map<String, Object> variables);
+
+    /**
+     * 按照业务id查询流程实例id
+     *
+     * @param businessKey 业务id
+     * @return 结果
+     */
+    String getInstanceIdByBusinessKey(String businessKey);
+}

+ 168 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java

@@ -0,0 +1,168 @@
+package org.dromara.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+
+    public static final String YYYY = "yyyy";
+
+    public static final String YYYY_MM = "yyyy-MM";
+
+    public static final String YYYY_MM_DD = "yyyy-MM-dd";
+
+    public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+    public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+    private static final String[] PARSE_PATTERNS = {
+        "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+        "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+        "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+    /**
+     * 获取当前Date型日期
+     *
+     * @return Date() 当前日期
+     */
+    public static Date getNowDate() {
+        return new Date();
+    }
+
+    /**
+     * 获取当前日期, 默认格式为yyyy-MM-dd
+     *
+     * @return String
+     */
+    public static String getDate() {
+        return dateTimeNow(YYYY_MM_DD);
+    }
+
+    public static String getTime() {
+        return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+    }
+
+    public static String dateTimeNow() {
+        return dateTimeNow(YYYYMMDDHHMMSS);
+    }
+
+    public static String dateTimeNow(final String format) {
+        return parseDateToStr(format, new Date());
+    }
+
+    public static String dateTime(final Date date) {
+        return parseDateToStr(YYYY_MM_DD, date);
+    }
+
+    public static String parseDateToStr(final String format, final Date date) {
+        return new SimpleDateFormat(format).format(date);
+    }
+
+    public static Date dateTime(final String format, final String ts) {
+        try {
+            return new SimpleDateFormat(format).parse(ts);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 日期路径 即年/月/日 如2018/08/08
+     */
+    public static String datePath() {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyy/MM/dd");
+    }
+
+    /**
+     * 日期路径 即年/月/日 如20180808
+     */
+    public static String dateTime() {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyyMMdd");
+    }
+
+    /**
+     * 日期型字符串转化为日期 格式
+     */
+    public static Date parseDate(Object str) {
+        if (str == null) {
+            return null;
+        }
+        try {
+            return parseDate(str.toString(), PARSE_PATTERNS);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取服务器启动时间
+     */
+    public static Date getServerStartDate() {
+        long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+        return new Date(time);
+    }
+
+    /**
+     * 计算相差天数
+     */
+    public static int differentDaysByMillisecond(Date date1, Date date2) {
+        return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
+    }
+
+    /**
+     * 计算两个时间差
+     */
+    public static String getDatePoor(Date endDate, Date nowDate) {
+        long nd = 1000 * 24 * 60 * 60;
+        long nh = 1000 * 60 * 60;
+        long nm = 1000 * 60;
+        // long ns = 1000;
+        // 获得两个时间的毫秒时间差异
+        long diff = endDate.getTime() - nowDate.getTime();
+        // 计算差多少天
+        long day = diff / nd;
+        // 计算差多少小时
+        long hour = diff % nd / nh;
+        // 计算差多少分钟
+        long min = diff % nd % nh / nm;
+        // 计算差多少秒//输出结果
+        // long sec = diff % nd % nh % nm / ns;
+        return day + "天" + hour + "小时" + min + "分钟";
+    }
+
+    /**
+     * 增加 LocalDateTime ==> Date
+     */
+    public static Date toDate(LocalDateTime temporalAccessor) {
+        ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+
+    /**
+     * 增加 LocalDate ==> Date
+     */
+    public static Date toDate(LocalDate temporalAccessor) {
+        LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+        ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+}

+ 93 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MapstructUtils.java

@@ -0,0 +1,93 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import io.github.linpeilie.Converter;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mapstruct 工具类
+ * <p>参考文档:<a href="https://mapstruct.plus/introduction/quick-start.html">mapstruct-plus</a></p>
+ *
+ *
+ * @author Michelle.Chung
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MapstructUtils {
+
+    private final static Converter CONVERTER = SpringUtils.getBean(Converter.class);
+
+    /**
+     * 将 T 类型对象,转换为 desc 类型的对象并返回
+     *
+     * @param source 数据来源实体
+     * @param desc   描述对象 转换后的对象
+     * @return desc
+     */
+    public static <T, V> V convert(T source, Class<V> desc) {
+        if (ObjectUtil.isNull(source)) {
+            return null;
+        }
+        if (ObjectUtil.isNull(desc)) {
+            return null;
+        }
+        return CONVERTER.convert(source, desc);
+    }
+
+    /**
+     * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象
+     *
+     * @param source 数据来源实体
+     * @param desc   转换后的对象
+     * @return desc
+     */
+    public static <T, V> V convert(T source, V desc) {
+        if (ObjectUtil.isNull(source)) {
+            return null;
+        }
+        if (ObjectUtil.isNull(desc)) {
+            return null;
+        }
+        return CONVERTER.convert(source, desc);
+    }
+
+    /**
+     * 将 T 类型的集合,转换为 desc 类型的集合并返回
+     *
+     * @param sourceList 数据来源实体列表
+     * @param desc       描述对象 转换后的对象
+     * @return desc
+     */
+    public static <T, V> List<V> convert(List<T> sourceList, Class<V> desc) {
+        if (ObjectUtil.isNull(sourceList)) {
+            return null;
+        }
+        if (CollUtil.isEmpty(sourceList)) {
+            return CollUtil.newArrayList();
+        }
+        return CONVERTER.convert(sourceList, desc);
+    }
+
+    /**
+     * 将 Map 转换为 beanClass 类型的集合并返回
+     *
+     * @param map       数据来源
+     * @param beanClass bean类
+     * @return bean对象
+     */
+    public static <T> T convert(Map<String, Object> map, Class<T> beanClass) {
+        if (MapUtil.isEmpty(map)) {
+            return null;
+        }
+        if (ObjectUtil.isNull(beanClass)) {
+            return null;
+        }
+        return CONVERTER.convert(map, beanClass);
+    }
+
+}

+ 33 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/MessageUtils.java

@@ -0,0 +1,33 @@
+package org.dromara.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.context.MessageSource;
+import org.springframework.context.NoSuchMessageException;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+/**
+ * 获取i18n资源文件
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MessageUtils {
+
+    private static final MessageSource MESSAGE_SOURCE = SpringUtils.getBean(MessageSource.class);
+
+    /**
+     * 根据消息键和参数 获取消息 委托给spring messageSource
+     *
+     * @param code 消息键
+     * @param args 参数
+     * @return 获取国际化翻译值
+     */
+    public static String message(String code, Object... args) {
+        try {
+            return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
+        } catch (NoSuchMessageException e) {
+            return code;
+        }
+    }
+}

+ 228 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ServletUtils.java

@@ -0,0 +1,228 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import cn.hutool.http.HttpStatus;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author ruoyi
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ServletUtils extends JakartaServletUtil {
+
+    /**
+     * 获取String参数
+     */
+    public static String getParameter(String name) {
+        return getRequest().getParameter(name);
+    }
+
+    /**
+     * 获取String参数
+     */
+    public static String getParameter(String name, String defaultValue) {
+        return Convert.toStr(getRequest().getParameter(name), defaultValue);
+    }
+
+    /**
+     * 获取Integer参数
+     */
+    public static Integer getParameterToInt(String name) {
+        return Convert.toInt(getRequest().getParameter(name));
+    }
+
+    /**
+     * 获取Integer参数
+     */
+    public static Integer getParameterToInt(String name, Integer defaultValue) {
+        return Convert.toInt(getRequest().getParameter(name), defaultValue);
+    }
+
+    /**
+     * 获取Boolean参数
+     */
+    public static Boolean getParameterToBool(String name) {
+        return Convert.toBool(getRequest().getParameter(name));
+    }
+
+    /**
+     * 获取Boolean参数
+     */
+    public static Boolean getParameterToBool(String name, Boolean defaultValue) {
+        return Convert.toBool(getRequest().getParameter(name), defaultValue);
+    }
+
+    /**
+     * 获得所有请求参数
+     *
+     * @param request 请求对象{@link ServletRequest}
+     * @return Map
+     */
+    public static Map<String, String[]> getParams(ServletRequest request) {
+        final Map<String, String[]> map = request.getParameterMap();
+        return Collections.unmodifiableMap(map);
+    }
+
+    /**
+     * 获得所有请求参数
+     *
+     * @param request 请求对象{@link ServletRequest}
+     * @return Map
+     */
+    public static Map<String, String> getParamMap(ServletRequest request) {
+        Map<String, String> params = new HashMap<>();
+        for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
+            params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR));
+        }
+        return params;
+    }
+
+    /**
+     * 获取request
+     */
+    public static HttpServletRequest getRequest() {
+        try {
+            return getRequestAttributes().getRequest();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取response
+     */
+    public static HttpServletResponse getResponse() {
+        try {
+            return getRequestAttributes().getResponse();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取session
+     */
+    public static HttpSession getSession() {
+        return getRequest().getSession();
+    }
+
+    public static ServletRequestAttributes getRequestAttributes() {
+        try {
+            RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+            return (ServletRequestAttributes) attributes;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public static String getHeader(HttpServletRequest request, String name) {
+        String value = request.getHeader(name);
+        if (StringUtils.isEmpty(value)) {
+            return StringUtils.EMPTY;
+        }
+        return urlDecode(value);
+    }
+
+    public static Map<String, String> getHeaders(HttpServletRequest request) {
+        Map<String, String> map = new LinkedCaseInsensitiveMap<>();
+        Enumeration<String> enumeration = request.getHeaderNames();
+        if (enumeration != null) {
+            while (enumeration.hasMoreElements()) {
+                String key = enumeration.nextElement();
+                String value = request.getHeader(key);
+                map.put(key, value);
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 将字符串渲染到客户端
+     *
+     * @param response 渲染对象
+     * @param string   待渲染的字符串
+     */
+    public static void renderString(HttpServletResponse response, String string) {
+        try {
+            response.setStatus(HttpStatus.HTTP_OK);
+            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+            response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
+            response.getWriter().print(string);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 是否是Ajax异步请求
+     *
+     * @param request
+     */
+    public static boolean isAjaxRequest(HttpServletRequest request) {
+
+        String accept = request.getHeader("accept");
+        if (accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE)) {
+            return true;
+        }
+
+        String xRequestedWith = request.getHeader("X-Requested-With");
+        if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
+            return true;
+        }
+
+        String uri = request.getRequestURI();
+        if (StringUtils.equalsAnyIgnoreCase(uri, ".json", ".xml")) {
+            return true;
+        }
+
+        String ajax = request.getParameter("__ajax");
+        return StringUtils.equalsAnyIgnoreCase(ajax, "json", "xml");
+    }
+
+    public static String getClientIP() {
+        return getClientIP(getRequest());
+    }
+
+    /**
+     * 内容编码
+     *
+     * @param str 内容
+     * @return 编码后的内容
+     */
+    public static String urlEncode(String str) {
+        return URLEncoder.encode(str, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 内容解码
+     *
+     * @param str 内容
+     * @return 解码后的内容
+     */
+    public static String urlDecode(String str) {
+        return URLDecoder.decode(str, StandardCharsets.UTF_8);
+    }
+
+}

+ 67 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/SpringUtils.java

@@ -0,0 +1,67 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.extra.spring.SpringUtil;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.boot.autoconfigure.thread.Threading;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring工具类
+ *
+ * @author Lion Li
+ */
+@Component
+public final class SpringUtils extends SpringUtil {
+
+    /**
+     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
+     */
+    public static boolean containsBean(String name) {
+        return getBeanFactory().containsBean(name);
+    }
+
+    /**
+     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
+     * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
+     */
+    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
+        return getBeanFactory().isSingleton(name);
+    }
+
+    /**
+     * @return Class 注册对象的类型
+     */
+    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
+        return getBeanFactory().getType(name);
+    }
+
+    /**
+     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
+     */
+    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
+        return getBeanFactory().getAliases(name);
+    }
+
+    /**
+     * 获取aop代理对象
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T getAopProxy(T invoker) {
+        return (T) getBean(invoker.getClass());
+    }
+
+
+    /**
+     * 获取spring上下文
+     */
+    public static ApplicationContext context() {
+        return getApplicationContext();
+    }
+
+    public static boolean isVirtual() {
+        return Threading.VIRTUAL.isActive(getBean(Environment.class));
+    }
+
+}

+ 283 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StreamUtils.java

@@ -0,0 +1,283 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * stream 流工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StreamUtils {
+
+    /**
+     * 将collection过滤
+     *
+     * @param collection 需要转化的集合
+     * @param function   过滤方法
+     * @return 过滤后的list
+     */
+    public static <E> List<E> filter(Collection<E> collection, Predicate<E> function) {
+        if (CollUtil.isEmpty(collection)) {
+            return CollUtil.newArrayList();
+        }
+        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+        return collection.stream().filter(function).collect(Collectors.toList());
+    }
+
+    /**
+     * 找到流中满足条件的第一个元素
+     *
+     * @param collection 需要查询的集合
+     * @param function   过滤方法
+     * @return 找到符合条件的第一个元素,没有则返回null
+     */
+    public static <E> E findFirst(Collection<E> collection, Predicate<E> function) {
+        if (CollUtil.isEmpty(collection)) {
+            return null;
+        }
+        return collection.stream().filter(function).findFirst().orElse(null);
+    }
+
+    /**
+     * 找到流中任意一个满足条件的元素
+     *
+     * @param collection 需要查询的集合
+     * @param function   过滤方法
+     * @return 找到符合条件的任意一个元素,没有则返回null
+     */
+    public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
+        if (CollUtil.isEmpty(collection)) {
+            return Optional.empty();
+        }
+        return collection.stream().filter(function).findAny();
+    }
+
+    /**
+     * 将collection拼接
+     *
+     * @param collection 需要转化的集合
+     * @param function   拼接方法
+     * @return 拼接后的list
+     */
+    public static <E> String join(Collection<E> collection, Function<E, String> function) {
+        return join(collection, function, StringUtils.SEPARATOR);
+    }
+
+    /**
+     * 将collection拼接
+     *
+     * @param collection 需要转化的集合
+     * @param function   拼接方法
+     * @param delimiter  拼接符
+     * @return 拼接后的list
+     */
+    public static <E> String join(Collection<E> collection, Function<E, String> function, CharSequence delimiter) {
+        if (CollUtil.isEmpty(collection)) {
+            return StringUtils.EMPTY;
+        }
+        return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
+    }
+
+    /**
+     * 将collection排序
+     *
+     * @param collection 需要转化的集合
+     * @param comparing  排序方法
+     * @return 排序后的list
+     */
+    public static <E> List<E> sorted(Collection<E> collection, Comparator<E> comparing) {
+        if (CollUtil.isEmpty(collection)) {
+            return CollUtil.newArrayList();
+        }
+        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+        return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
+    }
+
+    /**
+     * 将collection转化为类型不变的map<br>
+     * <B>{@code Collection<V>  ---->  Map<K,V>}</B>
+     *
+     * @param collection 需要转化的集合
+     * @param key        V类型转化为K类型的lambda方法
+     * @param <V>        collection中的泛型
+     * @param <K>        map中的key类型
+     * @return 转化后的map
+     */
+    public static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> key) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+    }
+
+    /**
+     * 将Collection转化为map(value类型与collection的泛型不同)<br>
+     * <B>{@code Collection<E> -----> Map<K,V>  }</B>
+     *
+     * @param collection 需要转化的集合
+     * @param key        E类型转化为K类型的lambda方法
+     * @param value      E类型转化为V类型的lambda方法
+     * @param <E>        collection中的泛型
+     * @param <K>        map中的key类型
+     * @param <V>        map中的value类型
+     * @return 转化后的map
+     */
+    public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> value) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
+    }
+
+    /**
+     * 将collection按照规则(比如有相同的班级id)分类成map<br>
+     * <B>{@code Collection<E> -------> Map<K,List<E>> } </B>
+     *
+     * @param collection 需要分类的集合
+     * @param key        分类的规则
+     * @param <E>        collection中的泛型
+     * @param <K>        map中的key类型
+     * @return 分类后的map
+     */
+    public static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> key) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection
+            .stream().filter(Objects::nonNull)
+            .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
+    }
+
+    /**
+     * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
+     * <B>{@code Collection<E>  --->  Map<T,Map<U,List<E>>> } </B>
+     *
+     * @param collection 需要分类的集合
+     * @param key1       第一个分类的规则
+     * @param key2       第二个分类的规则
+     * @param <E>        集合元素类型
+     * @param <K>        第一个map中的key类型
+     * @param <U>        第二个map中的key类型
+     * @return 分类后的map
+     */
+    public static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> key2) {
+        if (CollUtil.isEmpty(collection)) {
+            return MapUtil.newHashMap();
+        }
+        return collection
+            .stream().filter(Objects::nonNull)
+            .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
+    }
+
+    /**
+     * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
+     * <B>{@code Collection<E>  --->  Map<T,Map<U,E>> } </B>
+     *
+     * @param collection 需要分类的集合
+     * @param key1       第一个分类的规则
+     * @param key2       第二个分类的规则
+     * @param <T>        第一个map中的key类型
+     * @param <U>        第二个map中的key类型
+     * @param <E>        collection中的泛型
+     * @return 分类后的map
+     */
+    public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
+        if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
+            return MapUtil.newHashMap();
+        }
+        return collection
+            .stream().filter(Objects::nonNull)
+            .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
+    }
+
+    /**
+     * 将collection转化为List集合,但是两者的泛型不同<br>
+     * <B>{@code Collection<E>  ------>  List<T> } </B>
+     *
+     * @param collection 需要转化的集合
+     * @param function   collection中的泛型转化为list泛型的lambda表达式
+     * @param <E>        collection中的泛型
+     * @param <T>        List中的泛型
+     * @return 转化后的list
+     */
+    public static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {
+        if (CollUtil.isEmpty(collection)) {
+            return CollUtil.newArrayList();
+        }
+        return collection
+            .stream()
+            .map(function)
+            .filter(Objects::nonNull)
+            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * 将collection转化为Set集合,但是两者的泛型不同<br>
+     * <B>{@code Collection<E>  ------>  Set<T> } </B>
+     *
+     * @param collection 需要转化的集合
+     * @param function   collection中的泛型转化为set泛型的lambda表达式
+     * @param <E>        collection中的泛型
+     * @param <T>        Set中的泛型
+     * @return 转化后的Set
+     */
+    public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
+        if (CollUtil.isEmpty(collection) || function == null) {
+            return CollUtil.newHashSet();
+        }
+        return collection
+            .stream()
+            .map(function)
+            .filter(Objects::nonNull)
+            .collect(Collectors.toSet());
+    }
+
+
+    /**
+     * 合并两个相同key类型的map
+     *
+     * @param map1  第一个需要合并的 map
+     * @param map2  第二个需要合并的 map
+     * @param merge 合并的lambda,将key  value1 value2合并成最终的类型,注意value可能为空的情况
+     * @param <K>   map中的key类型
+     * @param <X>   第一个 map的value类型
+     * @param <Y>   第二个 map的value类型
+     * @param <V>   最终map的value类型
+     * @return 合并后的map
+     */
+    public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
+        if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
+            return MapUtil.newHashMap();
+        } else if (MapUtil.isEmpty(map1)) {
+            map1 = MapUtil.newHashMap();
+        } else if (MapUtil.isEmpty(map2)) {
+            map2 = MapUtil.newHashMap();
+        }
+        Set<K> key = new HashSet<>();
+        key.addAll(map1.keySet());
+        key.addAll(map2.keySet());
+        Map<K, V> map = new HashMap<>();
+        for (K t : key) {
+            X x = map1.get(t);
+            Y y = map2.get(t);
+            V z = merge.apply(x, y);
+            if (z != null) {
+                map.put(t, z);
+            }
+        }
+        return map;
+    }
+
+}

+ 323 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java

@@ -0,0 +1,323 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+
+    public static final String SEPARATOR = ",";
+
+    public static final String SLASH = "/";
+
+    /**
+     * 获取参数不为空值
+     *
+     * @param str defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static String blankToDefault(String str, String defaultValue) {
+        return StrUtil.blankToDefault(str, defaultValue);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     *
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str) {
+        return StrUtil.isEmpty(str);
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     *
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str) {
+        return !isEmpty(str);
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str) {
+        return StrUtil.trim(str);
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start) {
+        return substring(str, start, str.length());
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @param end   结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end) {
+        return StrUtil.sub(str, start, end);
+    }
+
+    /**
+     * 格式化文本, {} 表示占位符<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     *
+     * @param template 文本模板,被替换的部分用 {} 表示
+     * @param params   参数值
+     * @return 格式化后的文本
+     */
+    public static String format(String template, Object... params) {
+        return StrUtil.format(template, params);
+    }
+
+    /**
+     * 是否为http(s)://开头
+     *
+     * @param link 链接
+     * @return 结果
+     */
+    public static boolean ishttp(String link) {
+        return Validator.isUrl(link);
+    }
+
+    /**
+     * 字符串转set
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static Set<String> str2Set(String str, String sep) {
+        return new HashSet<>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     *
+     * @param str         字符串
+     * @param sep         分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim        去掉首尾空白
+     * @return list集合
+     */
+    public static List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
+        List<String> list = new ArrayList<>();
+        if (isEmpty(str)) {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && isBlank(str)) {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split) {
+            if (filterBlank && isBlank(string)) {
+                continue;
+            }
+            if (trim) {
+                string = trim(string);
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs                  指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
+        return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str) {
+        return StrUtil.toUnderlineCase(str);
+    }
+
+    /**
+     * 是否包含字符串
+     *
+     * @param str  验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs) {
+        return StrUtil.equalsAnyIgnoreCase(str, strs);
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     *
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name) {
+        return StrUtil.upperFirst(StrUtil.toCamelCase(name));
+    }
+
+    /**
+     * 驼峰式命名法 例如:user_name->userName
+     */
+    public static String toCamelCase(String s) {
+        return StrUtil.toCamelCase(s);
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     *
+     * @param str  指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs) {
+        if (isEmpty(str) || CollUtil.isEmpty(strs)) {
+            return false;
+        }
+        for (String pattern : strs) {
+            if (isMatch(pattern, str)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置:
+     * ? 表示单个字符;
+     * * 表示一层路径内的任意字符串,不可跨层级;
+     * ** 表示任意层路径;
+     *
+     * @param pattern 匹配规则
+     * @param url     需要匹配的url
+     */
+    public static boolean isMatch(String pattern, String url) {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     *
+     * @param num  数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static String padl(final Number num, final int size) {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     *
+     * @param s    原始字符串
+     * @param size 字符串指定长度
+     * @param c    用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static String padl(final String s, final int size, final char c) {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null) {
+            final int len = s.length();
+            if (s.length() <= size) {
+                sb.append(String.valueOf(c).repeat(size - len));
+                sb.append(s);
+            } else {
+                return s.substring(len - size, len);
+            }
+        } else {
+            sb.append(String.valueOf(c).repeat(Math.max(0, size)));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 切分字符串(分隔符默认逗号)
+     *
+     * @param str 被切分的字符串
+     * @return 分割后的数据列表
+     */
+    public static List<String> splitList(String str) {
+        return splitTo(str, Convert::toStr);
+    }
+
+    /**
+     * 切分字符串
+     *
+     * @param str       被切分的字符串
+     * @param separator 分隔符
+     * @return 分割后的数据列表
+     */
+    public static List<String> splitList(String str, String separator) {
+        return splitTo(str, separator, Convert::toStr);
+    }
+
+    /**
+     * 切分字符串自定义转换(分隔符默认逗号)
+     *
+     * @param str    被切分的字符串
+     * @param mapper 自定义转换
+     * @return 分割后的数据列表
+     */
+    public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
+        return splitTo(str, SEPARATOR, mapper);
+    }
+
+    /**
+     * 切分字符串自定义转换
+     *
+     * @param str       被切分的字符串
+     * @param separator 分隔符
+     * @param mapper    自定义转换
+     * @return 分割后的数据列表
+     */
+    public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
+        if (isBlank(str)) {
+            return new ArrayList<>(0);
+        }
+        return StrUtil.split(str, separator)
+            .stream()
+            .filter(Objects::nonNull)
+            .map(mapper)
+            .collect(Collectors.toList());
+    }
+
+}

+ 75 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/Threads.java

@@ -0,0 +1,75 @@
+package org.dromara.common.core.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.*;
+
+/**
+ * 线程相关工具类.
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class Threads {
+
+    /**
+     * sleep等待,单位为毫秒
+     */
+    public static void sleep(long milliseconds) {
+        try {
+            Thread.sleep(milliseconds);
+        } catch (InterruptedException e) {
+            return;
+        }
+    }
+
+    /**
+     * 停止线程池
+     * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
+     * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
+     * 如果仍然超時,則強制退出.
+     * 另对在shutdown时线程本身被调用中断做了处理.
+     */
+    public static void shutdownAndAwaitTermination(ExecutorService pool) {
+        if (pool != null && !pool.isShutdown()) {
+            pool.shutdown();
+            try {
+                if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
+                    pool.shutdownNow();
+                    if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
+                        log.info("Pool did not terminate");
+                    }
+                }
+            } catch (InterruptedException ie) {
+                pool.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * 打印线程异常信息
+     */
+    public static void printException(Runnable r, Throwable t) {
+        if (t == null && r instanceof Future<?>) {
+            try {
+                Future<?> future = (Future<?>) r;
+                if (future.isDone()) {
+                    future.get();
+                }
+            } catch (CancellationException ce) {
+                t = ce;
+            } catch (ExecutionException ee) {
+                t = ee.getCause();
+            } catch (InterruptedException ie) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        if (t != null) {
+            log.error(t.getMessage(), t);
+        }
+    }
+}

+ 35 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/TreeBuildUtils.java

@@ -0,0 +1,35 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.tree.Tree;
+import cn.hutool.core.lang.tree.TreeNodeConfig;
+import cn.hutool.core.lang.tree.TreeUtil;
+import cn.hutool.core.lang.tree.parser.NodeParser;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 扩展 hutool TreeUtil 封装系统树构建
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class TreeBuildUtils extends TreeUtil {
+
+    /**
+     * 根据前端定制差异化字段
+     */
+    public static final TreeNodeConfig DEFAULT_CONFIG = TreeNodeConfig.DEFAULT_CONFIG.setNameKey("label");
+
+    public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) {
+        if (CollUtil.isEmpty(list)) {
+            return null;
+        }
+        K k = ReflectUtils.invokeGetter(list.get(0), "parentId");
+        return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
+    }
+
+}

+ 35 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java

@@ -0,0 +1,35 @@
+package org.dromara.common.core.utils;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Set;
+
+/**
+ * Validator 校验框架工具
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ValidatorUtils {
+
+    private static final Validator VALID = SpringUtils.getBean(Validator.class);
+
+    /**
+     * 对给定对象进行参数校验,并根据指定的校验组进行校验
+     *
+     * @param object 要进行校验的对象
+     * @param groups 校验组
+     * @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
+     */
+    public static <T> void validate(T object, Class<?>... groups) {
+        Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
+        if (!validate.isEmpty()) {
+            throw new ConstraintViolationException("参数校验异常", validate);
+        }
+    }
+
+}

+ 43 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java

@@ -0,0 +1,43 @@
+package org.dromara.common.core.utils.file;
+
+import cn.hutool.core.io.FileUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 文件处理工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class FileUtils extends FileUtil {
+
+    /**
+     * 下载文件名重新编码
+     *
+     * @param response     响应对象
+     * @param realFileName 真实文件名
+     */
+    public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
+        String percentEncodedFileName = percentEncode(realFileName);
+        String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
+        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+        response.setHeader("Content-disposition", contentDispositionValue);
+        response.setHeader("download-filename", percentEncodedFileName);
+    }
+
+    /**
+     * 百分号编码工具方法
+     *
+     * @param s 需要百分号编码的字符串
+     * @return 百分号编码后的字符串
+     */
+    public static String percentEncode(String s) {
+        String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
+        return encode.replaceAll("\\+", "%20");
+    }
+}

+ 40 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java

@@ -0,0 +1,40 @@
+package org.dromara.common.core.utils.file;
+
+/**
+ * 媒体类型工具类
+ *
+ * @author ruoyi
+ */
+public class MimeTypeUtils {
+    public static final String IMAGE_PNG = "image/png";
+
+    public static final String IMAGE_JPG = "image/jpg";
+
+    public static final String IMAGE_JPEG = "image/jpeg";
+
+    public static final String IMAGE_BMP = "image/bmp";
+
+    public static final String IMAGE_GIF = "image/gif";
+
+    public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+    public static final String[] FLASH_EXTENSION = {"swf", "flv"};
+
+    public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+        "asf", "rm", "rmvb"};
+
+    public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+    public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+        // 图片
+        "bmp", "gif", "jpg", "jpeg", "png",
+        // word excel powerpoint
+        "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+        // 压缩文件
+        "rar", "zip", "gz", "bz2",
+        // 视频格式
+        "mp4", "avi", "rmvb",
+        // pdf
+        "pdf"};
+
+}

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