Browse Source

初始提交:新大洲车联网CRM系统服务端代码

wangmeng 2 months ago
commit
d061dad0b1
100 changed files with 6642 additions and 0 deletions
  1. 52 0
      .dockerignore
  2. 69 0
      .gitignore
  3. 4 0
      lombok.config
  4. 169 0
      pom.xml
  5. 755 0
      xdz-dependencies/pom.xml
  6. 50 0
      xdz-framework/pom.xml
  7. 156 0
      xdz-framework/xdz-common/pom.xml
  8. 59 0
      xdz-framework/xdz-common/src/main/java/com/fhs/trans/service/AutoTransable.java
  9. 34 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java
  10. 34 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java
  11. 35 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/LoginLogCommonApi.java
  12. 103 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java
  13. 68 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java
  14. 41 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/dto/LoginLogCreateReqDTO.java
  15. 4 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/package-info.java
  16. 4 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/package-info.java
  17. 26 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/dict/DictDataCommonApi.java
  18. 22 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/dict/dto/DictDataRespDTO.java
  19. 34 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/logger/OperateLogCommonApi.java
  20. 50 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java
  21. 57 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java
  22. 33 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java
  23. 32 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java
  24. 28 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java
  25. 4 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/package-info.java
  26. 43 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/permission/PermissionCommonApi.java
  27. 28 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/permission/dto/DeptDataPermissionRespDTO.java
  28. 29 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/tenant/TenantCommonApi.java
  29. 15 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/core/ArrayValuable.java
  30. 22 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/core/KeyValue.java
  31. 46 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/CommonStatusEnum.java
  32. 47 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/DateIntervalEnum.java
  33. 21 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/DocumentEnum.java
  34. 62 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/RpcConstants.java
  35. 40 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/TerminalEnum.java
  36. 39 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/UserTypeEnum.java
  37. 38 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/WebFilterOrderEnum.java
  38. 32 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/ErrorCode.java
  39. 60 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/ServerException.java
  40. 60 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/ServiceException.java
  41. 41 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/enums/GlobalErrorCodeConstants.java
  42. 48 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/enums/ServiceErrorCodeRange.java
  43. 77 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/util/ServiceExceptionUtil.java
  44. 6 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/package-info.java
  45. 121 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/CommonResult.java
  46. 36 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/PageParam.java
  47. 41 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/PageResult.java
  48. 19 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/SortablePageParam.java
  49. 37 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/SortingField.java
  50. 61 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/cache/CacheUtils.java
  51. 58 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/ArrayUtils.java
  52. 352 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/CollectionUtils.java
  53. 68 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/MapUtils.java
  54. 19 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/SetUtils.java
  55. 149 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/date/DateUtils.java
  56. 350 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/date/LocalDateTimeUtils.java
  57. 193 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/http/HttpUtils.java
  58. 61 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/io/FileUtils.java
  59. 28 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/io/IoUtils.java
  60. 232 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/JsonUtils.java
  61. 37 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/databind/NumberSerializer.java
  62. 27 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
  63. 85 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java
  64. 30 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/monitor/TracerUtils.java
  65. 131 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/number/MoneyUtils.java
  66. 78 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/number/NumberUtils.java
  67. 69 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/object/BeanUtils.java
  68. 67 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/object/ObjectUtils.java
  69. 67 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/object/PageUtils.java
  70. 7 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/package-info.java
  71. 105 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/servlet/ServletUtils.java
  72. 123 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/spring/SpringExpressionUtils.java
  73. 24 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/spring/SpringUtils.java
  74. 107 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/string/StrUtils.java
  75. 55 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/validation/ValidationUtils.java
  76. 35 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/InEnum.java
  77. 44 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/InEnumCollectionValidator.java
  78. 43 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/InEnumValidator.java
  79. 28 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/Mobile.java
  80. 25 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/MobileValidator.java
  81. 28 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/Telephone.java
  82. 25 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/TelephoneValidator.java
  83. 4 0
      xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/package-info.java
  84. 52 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/pom.xml
  85. 46 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java
  86. 34 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/config/YudaoDataPermissionRpcAutoConfiguration.java
  87. 45 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java
  88. 35 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/annotation/DataPermission.java
  89. 36 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java
  90. 72 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java
  91. 72 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/aop/DataPermissionContextHolder.java
  92. 64 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/db/DataPermissionRuleHandler.java
  93. 27 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rpc/DataPermissionRequestInterceptor.java
  94. 38 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rpc/DataPermissionRpcWebFilter.java
  95. 36 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/DataPermissionRule.java
  96. 28 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/DataPermissionRuleFactory.java
  97. 84 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java
  98. 207 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
  99. 20 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java
  100. 0 0
      xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/dept/package-info.java

+ 52 - 0
.dockerignore

@@ -0,0 +1,52 @@
+# Maven构建产物
+target/
+*.jar
+*.war
+*.ear
+
+# IDE文件
+.idea/
+.vscode/
+*.iml
+*.ipr
+*.iws
+.classpath
+.project
+.settings/
+
+# Git
+.git/
+.gitignore
+
+# 日志文件
+logs/
+*.log
+
+# 临时文件
+*.tmp
+*.bak
+*.swp
+*~
+
+# 系统文件
+.DS_Store
+Thumbs.db
+
+# 归档文件
+*.zip
+*.tar
+*.tar.gz
+
+# 文档
+doc/
+*.md
+README.md
+
+# 测试文件
+test/
+*Test.java
+*Tests.java
+
+# K8s配置(可选,如果需要单独管理)
+# k8s/
+

+ 69 - 0
.gitignore

@@ -0,0 +1,69 @@
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# IDE - IntelliJ IDEA
+.idea/
+*.iml
+*.iws
+*.ipr
+out/
+
+# IDE - Eclipse
+.classpath
+.project
+.settings/
+bin/
+
+# IDE - VSCode
+.vscode/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Proto 生成代码(不提交)
+**/generated-sources/
+**/generated-test-sources/
+
+# Docker Compose 数据
+deploy/docker-compose/data/
+
+# Kubernetes 敏感配置(实际环境应单独管理)
+deploy/k8s/*-secret.yaml
+!deploy/k8s/secret-example.yaml
+
+# 临时文件
+*.tmp
+*.swp
+*.bak
+
+# 脚本文件(临时脚本不提交,但项目脚本需要提交)
+# 注意:如果需要提交某些脚本,使用 git add -f 强制添加
+# *.sh
+
+# 数据库脚本(不需要提交)
+create_databases.sql
+
+# Maven 其他生成文件
+.flattened-pom.xml
+effective-pom.xml
+**/maven-status/
+**/maven-archiver/
+
+# 测试目录(如果不需要单元测试,可以忽略整个测试目录)
+**/src/test/
+**/test-classes/
+**/target/test-classes/

+ 4 - 0
lombok.config

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

+ 169 - 0
pom.xml

@@ -0,0 +1,169 @@
+<?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>com.xindazhou</groupId>
+    <artifactId>xdz-server</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>新大洲车联网CRM系统服务端架构(基于yudao-cloud-jdk17移植)</description>
+
+    <modules>
+        <!-- 基础模块 -->
+        <module>xdz-dependencies</module>
+        <module>xdz-framework</module>
+        <!-- 业务服务模块 -->
+        <module>xdz-module-user</module>
+        <module>xdz-module-vehicle</module>
+        <module>xdz-module-message</module>
+        <module>xdz-module-business</module>
+        <module>xdz-module-platform</module>
+    </modules>
+
+    <properties>
+        <revision>1.0.0-SNAPSHOT</revision>
+        <!-- Maven 相关 -->
+        <java.version>17</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
+        <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
+        <flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
+        <!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) -->
+        <lombok.version>1.18.42</lombok.version>
+        <spring.boot.version>3.5.9</spring.boot.version>
+        <mapstruct.version>1.6.3</mapstruct.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- 注意:xdz-dependencies 需要先编译安装才能被引用 -->
+            <!-- 如果报错,请先执行: mvn clean install -pl xdz-dependencies -am -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-dependencies</artifactId>
+                <version>${revision}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <!-- maven-surefire-plugin 插件,用于运行单元测试。 -->
+                <!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${maven-surefire-plugin.version}</version>
+                </plugin>
+                <!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>${maven-compiler-plugin.version}</version>
+                    <configuration>
+                        <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring.boot.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok</artifactId>
+                                <version>${lombok.version}</version>
+                            </path>
+                            <path>
+                                <!-- 确保 Lombok 生成的 getter/setter 方法能被 MapStruct 正确识别,
+                                     避免出现 No property named "xxx" exists 的编译错误 -->
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok-mapstruct-binding</artifactId>
+                                <version>0.2.0</version>
+                            </path>
+                            <path>
+                                <groupId>org.mapstruct</groupId>
+                                <artifactId>mapstruct-processor</artifactId>
+                                <version>${mapstruct.version}</version>
+                            </path>
+                        </annotationProcessorPaths>
+                        <!-- 编译参数写在 arg 内,解决 Spring Boot 3.2 的 Parameter Name Discovery 问题 -->
+                        <debug>false</debug>
+                        <compilerArgs>
+                            <arg>-parameters</arg>
+                        </compilerArgs>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+            <!-- 统一 revision 版本 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <flattenMode>oss</flattenMode>
+                    <updatePomFile>true</updatePomFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                    </execution>
+                    <execution>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <!-- 使用 huawei / aliyun 的 Maven 源,提升下载速度 -->
+    <repositories>
+        <repository>
+            <id>huaweicloud</id>
+            <name>huawei</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
+        </repository>
+        <repository>
+            <id>aliyunmaven</id>
+            <name>aliyun</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+        </repository>
+
+        <repository>
+            <id>spring-milestones</id>
+            <name>Spring Milestones</name>
+            <url>https://repo.spring.io/milestone</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>spring-snapshots</id>
+            <name>Spring Snapshots</name>
+            <url>https://repo.spring.io/snapshot</url>
+            <releases>
+                <enabled>false</enabled>
+            </releases>
+        </repository>
+    </repositories>
+
+</project>
+

+ 755 - 0
xdz-dependencies/pom.xml

@@ -0,0 +1,755 @@
+<?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>com.xindazhou</groupId>
+    <artifactId>xdz-dependencies</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>基础 bom 文件,管理整个项目的依赖版本</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <properties>
+        <revision>1.0.0-SNAPSHOT</revision>
+        <flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
+        <!-- 统一依赖管理 -->
+        <spring.boot.version>3.5.9</spring.boot.version>
+        <spring.cloud.version>2025.0.0</spring.cloud.version>
+        <spring.cloud.alibaba.version>2023.0.3.3</spring.cloud.alibaba.version>
+        <!-- Web 相关 -->
+        <springdoc.version>2.8.14</springdoc.version>
+        <knife4j.version>4.5.0</knife4j.version>
+        <!-- DB 相关 -->
+        <druid.version>1.2.27</druid.version>
+        <mybatis.version>3.5.19</mybatis.version>
+        <mybatis-plus.version>3.5.15</mybatis-plus.version>
+        <mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
+        <dynamic-datasource.version>4.5.0</dynamic-datasource.version>
+        <easy-trans.version>3.0.6</easy-trans.version>
+        <redisson.version>3.52.0</redisson.version>
+        <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
+        <kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
+        <opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
+        <taos.version>3.7.9</taos.version>
+        <!-- 消息队列 -->
+        <rocketmq-spring.version>2.3.5</rocketmq-spring.version>
+        <!-- RPC 相关 -->
+        <!-- Config 配置中心相关 -->
+        <!-- Job 定时任务相关 -->
+        <xxl-job.version>2.4.0</xxl-job.version>
+        <!-- 服务保障相关 -->
+        <lock4j.version>2.2.7</lock4j.version>
+        <!-- 监控相关 -->
+        <skywalking.version>9.5.0</skywalking.version>
+        <spring-boot-admin.version>3.5.6</spring-boot-admin.version>
+        <opentracing.version>0.33.0</opentracing.version>
+        <!-- Test 测试相关 -->
+        <podam.version>8.0.2.RELEASE</podam.version>
+        <jedis-mock.version>1.1.12</jedis-mock.version>
+        <mockito-inline.version>5.2.0</mockito-inline.version>
+        <!-- Bpm 工作流相关 -->
+        <flowable.version>7.2.0</flowable.version>
+        <!-- 工具类相关 -->
+        <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
+        <jsoup.version>1.21.2</jsoup.version>
+        <lombok.version>1.18.42</lombok.version>
+        <mapstruct.version>1.6.3</mapstruct.version>
+        <hutool-5.version>5.8.42</hutool-5.version>
+        <hutool-6.version>6.0.0-M22</hutool-6.version>
+        <fastexcel.version>1.3.0</fastexcel.version>
+        <velocity.version>2.4.1</velocity.version>
+        <fastjson.version>1.2.83</fastjson.version>
+        <guava.version>33.5.0-jre</guava.version>
+        <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
+        <commons-net.version>3.12.0</commons-net.version>
+        <commons-lang3.version>3.20.0</commons-lang3.version>
+        <jsch.version>2.27.7</jsch.version>
+        <tika-core.version>3.2.3</tika-core.version>
+        <ip2region.version>2.7.0</ip2region.version>
+        <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
+        <reflections.version>0.10.2</reflections.version>
+        <netty.version>4.2.9.Final</netty.version>
+        <netty-socketio.version>2.0.13</netty-socketio.version>
+        <java-jwt.version>4.4.0</java-jwt.version>
+        <mqtt.version>1.2.5</mqtt.version>
+        <vertx.version>4.5.22</vertx.version>
+        <!-- 三方云服务相关 -->
+        <awssdk.version>2.40.15</awssdk.version>
+        <justauth.version>1.16.7</justauth.version>
+        <justauth-starter.version>1.4.0</justauth-starter.version>
+        <jimureport.version>2.1.3</jimureport.version>
+        <jimubi.version>2.3.0</jimubi.version>
+        <weixin-java.version>4.7.9-20251224.161447</weixin-java.version>
+        <alipay-sdk-java.version>4.40.607.ALL</alipay-sdk-java.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- 统一依赖管理 -->
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-bom</artifactId>
+                <version>${netty.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring.boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring.cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring.cloud.alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- 业务组件 -->
+            <dependency>
+                <groupId>io.github.mouzt</groupId>
+                <artifactId>bizlog-sdk</artifactId>
+                <version>${bizlog-sdk.version}</version>
+                <exclusions>
+                    <exclusion> <!-- 排除掉springboot依赖使用项目的 -->
+                        <groupId>org.springframework.boot</groupId>
+                        <artifactId>spring-boot-starter</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-biz-tenant</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-biz-data-permission</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-biz-ip</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- Spring 核心 -->
+            <dependency>
+                <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-configuration-processor</artifactId>
+                <version>${spring.boot.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-env</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- Web 相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-web</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-security</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-websocket</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+                <version>${knife4j.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.springdoc</groupId>
+                        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>org.springdoc</groupId>
+                <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+                <version>${springdoc.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId> <!-- 接口文档 UI:knife4j【网关专属】 -->
+                <artifactId>knife4j-gateway-spring-boot-starter</artifactId>
+                <version>${knife4j.version}</version>
+            </dependency>
+
+            <!-- DB 相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-mybatis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>druid-spring-boot-3-starter</artifactId>
+                <version>${druid.version}</version>
+            </dependency>
+            <dependency>
+                <!-- 注意:必须声明,避免 flowable 和 mybatis-plus 引入的 mybatis 版本不一致!!! -->
+                <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-jsqlparser</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
+                <version>${dynamic-datasource.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.yulichang</groupId>
+                <artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
+                <version>${mybatis-plus-join.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
+                <artifactId>easy-trans-spring-boot-starter</artifactId>
+                <version>${easy-trans.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.springframework</groupId>
+                        <artifactId>spring-context</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.springframework.cloud</groupId>
+                        <artifactId>spring-cloud-commons</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>com.fhs-opensource</groupId>
+                <artifactId>easy-trans-mybatis-plus-extend</artifactId>
+                <version>${easy-trans.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.fhs-opensource</groupId>
+                <artifactId>easy-trans-anno</artifactId>
+                <version>${easy-trans.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-redis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.dameng</groupId>
+                <artifactId>DmJdbcDriver18</artifactId>
+                <version>${dm8.jdbc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.opengauss</groupId>
+                <artifactId>opengauss-jdbc</artifactId>
+                <version>${opengauss.jdbc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.com.kingbase</groupId>
+                <artifactId>kingbase8</artifactId>
+                <version>${kingbase.jdbc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.taosdata.jdbc</groupId>
+                <artifactId>taos-jdbcdriver</artifactId>
+                <version>${taos.version}</version>
+            </dependency>
+
+            <!-- RPC 远程调用相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-rpc</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- Registry 注册中心相关 -->
+
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+                <version>${spring.cloud.alibaba.version}</version>
+                <exclusions>
+                    <!-- 目的:解决 Nacos 启动的 NAMING_LOG_FILE 告警 -->
+                    <exclusion>
+                        <artifactId>logback-adapter</artifactId>
+                        <groupId>com.alibaba.nacos</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!-- Config 配置中心相关 -->
+
+            <!-- Job 定时任务相关 -->
+            <dependency>
+                <groupId>com.xuxueli</groupId>
+                <artifactId>xxl-job-core</artifactId>
+                <version>${xxl-job.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-job</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 消息队列相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-mq</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.rocketmq</groupId>
+                <artifactId>rocketmq-spring-boot-starter</artifactId>
+                <version>${rocketmq-spring.version}</version>
+            </dependency>
+
+            <!-- 服务保障相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-protection</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
+                <version>${lock4j.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>redisson-spring-boot-starter</artifactId>
+                        <groupId>org.redisson</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!-- 监控相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-monitor</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-trace</artifactId>
+                <version>${skywalking.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-logback-1.x</artifactId>
+                <version>${skywalking.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-opentracing</artifactId>
+                <version>${skywalking.version}</version>
+                <!--                <exclusions>-->
+                <!--                    <exclusion>-->
+                <!--                        <artifactId>opentracing-api</artifactId>-->
+                <!--                        <groupId>io.opentracing</groupId>-->
+                <!--                    </exclusion>-->
+                <!--                    <exclusion>-->
+                <!--                        <artifactId>opentracing-util</artifactId>-->
+                <!--                        <groupId>io.opentracing</groupId>-->
+                <!--                    </exclusion>-->
+                <!--                </exclusions>-->
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-api</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-util</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-noop</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>de.codecentric</groupId>
+                <artifactId>spring-boot-admin-starter-server</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
+                <version>${spring-boot-admin.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>de.codecentric</groupId>
+                <artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 -->
+                <version>${spring-boot-admin.version}</version>
+            </dependency>
+
+            <!-- Test 测试相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-test</artifactId>
+                <version>${revision}</version>
+                <scope>test</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.mockito</groupId>
+                <artifactId>mockito-inline</artifactId>
+                <version>${mockito-inline.version}</version> <!-- 支持 Mockito 的 final 类与 static 方法的 mock -->
+            </dependency>
+
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-test</artifactId>
+                <version>${spring.boot.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>asm</artifactId>
+                        <groupId>org.ow2.asm</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.mockito</groupId>
+                        <artifactId>mockito-core</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
+                <artifactId>jedis-mock</artifactId>
+                <version>${jedis-mock.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 -->
+                <artifactId>podam</artifactId>
+                <version>${podam.version}</version>
+            </dependency>
+
+            <!-- 工作流相关 -->
+            <dependency>
+                <groupId>org.flowable</groupId>
+                <artifactId>flowable-spring-boot-starter-process</artifactId>
+                <version>${flowable.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.flowable</groupId>
+                <artifactId>flowable-spring-boot-starter-actuator</artifactId>
+                <version>${flowable.version}</version>
+            </dependency>
+            <!-- 工作流相关结束 -->
+
+            <!-- 工具类相关 -->
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-common</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xindazhou</groupId>
+                <artifactId>xdz-spring-boot-starter-excel</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct-jdk8</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct-processor</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool-5.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.dromara.hutool</groupId>
+                <artifactId>hutool-extra</artifactId>
+                <version>${hutool-6.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.idev.excel</groupId>
+                <artifactId>fastexcel</artifactId>
+                <version>${fastexcel.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.tika</groupId>
+                <artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
+                <version>${tika-core.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.velocity</groupId>
+                <artifactId>velocity-engine-core</artifactId>
+                <version>${velocity.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>fastjson</artifactId>
+                <version>${fastjson.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>${guava.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 -->
+                <version>${transmittable-thread-local.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>commons-net</groupId>
+                <artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
+                <version>${commons-net.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.mwiede</groupId>
+                <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
+                <version>${jsch.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-lang3</artifactId>
+                <version>${commons-lang3.version}</version> <!-- 解决 CVE-2025-48924 漏洞 -->
+            </dependency>
+
+            <dependency>
+                <groupId>com.anji-plus</groupId>
+                <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
+                <version>${anji-plus-captcha.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.lionsoul</groupId>
+                <artifactId>ip2region</artifactId>
+                <version>${ip2region.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.jsoup</groupId>
+                <artifactId>jsoup</artifactId>
+                <version>${jsoup.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.reflections</groupId>
+                <artifactId>reflections</artifactId>
+                <version>${reflections.version}</version>
+            </dependency>
+
+            <!-- 三方云服务相关 -->
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3</artifactId>
+                <version>${awssdk.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alipay.sdk</groupId>
+                <artifactId>alipay-sdk-java</artifactId>
+                <version>${alipay-sdk-java.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcprov-jdk15on</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>weixin-java-pay</artifactId>
+                <version>${weixin-java.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>wx-java-mp-spring-boot-starter</artifactId>
+                <version>${weixin-java.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
+                <version>${weixin-java.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>me.zhyd.oauth</groupId>
+                <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
+                <version>${justauth.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.xkcoding.justauth</groupId>
+                <artifactId>justauth-spring-boot-starter</artifactId>
+                <version>${justauth-starter.version}</version>
+            </dependency>
+
+            <!-- 积木报表-->
+            <dependency>
+                <groupId>org.jeecgframework.jimureport</groupId>
+                <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
+                <version>${jimureport.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.jeecgframework.jimureport</groupId>
+                <artifactId>jimubi-spring-boot3-starter</artifactId>
+                <version>${jimubi.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>com.github.jsqlparser</groupId>
+                        <artifactId>jsqlparser</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>cn.hutool</groupId>
+                        <artifactId>hutool-core</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!-- Vert.x -->
+            <dependency>
+                <groupId>io.vertx</groupId>
+                <artifactId>vertx-core</artifactId>
+                <version>${vertx.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.vertx</groupId>
+                <artifactId>vertx-web</artifactId>
+                <version>${vertx.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.vertx</groupId>
+                <artifactId>vertx-mqtt</artifactId>
+                <version>${vertx.version}</version>
+            </dependency>
+
+            <!-- MQTT -->
+            <dependency>
+                <groupId>org.eclipse.paho</groupId>
+                <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+                <version>${mqtt.version}</version>
+            </dependency>
+
+            <!-- Netty SocketIO -->
+            <dependency>
+                <groupId>com.corundumstudio.socketio</groupId>
+                <artifactId>netty-socketio</artifactId>
+                <version>${netty-socketio.version}</version>
+            </dependency>
+
+            <!-- JWT -->
+            <dependency>
+                <groupId>com.auth0</groupId>
+                <artifactId>java-jwt</artifactId>
+                <version>${java-jwt.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <!-- 统一 revision 版本 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <flattenMode>bom</flattenMode>
+                    <updatePomFile>true</updatePomFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                    </execution>
+                    <execution>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 50 - 0
xdz-framework/pom.xml

@@ -0,0 +1,50 @@
+<?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>
+    <parent>
+        <artifactId>xdz-server</artifactId>
+        <groupId>com.xindazhou</groupId>
+        <version>${revision}</version>
+    </parent>
+    <packaging>pom</packaging>
+    <modules>
+        <module>xdz-common</module>
+        <module>xdz-spring-boot-starter-env</module>
+        <module>xdz-spring-boot-starter-mybatis</module>
+        <module>xdz-spring-boot-starter-redis</module>
+        <module>xdz-spring-boot-starter-web</module>
+        <module>xdz-spring-boot-starter-security</module>
+        <module>xdz-spring-boot-starter-websocket</module>
+
+        <module>xdz-spring-boot-starter-monitor</module>
+        <module>xdz-spring-boot-starter-protection</module>
+<!--        <module>xdz-spring-boot-starter-config</module>-->
+        <module>xdz-spring-boot-starter-job</module>
+        <module>xdz-spring-boot-starter-mq</module>
+        <module>xdz-spring-boot-starter-rpc</module>
+
+        <module>xdz-spring-boot-starter-excel</module>
+        <module>xdz-spring-boot-starter-test</module>
+
+        <module>xdz-spring-boot-starter-biz-tenant</module>
+        <module>xdz-spring-boot-starter-biz-data-permission</module>
+        <module>xdz-spring-boot-starter-biz-ip</module>
+        <module>xdz-spring-boot-starter-biz-social</module>
+    </modules>
+
+    <artifactId>xdz-framework</artifactId>
+    <description>
+        该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
+            1. core 包:是该组件的核心封装
+            2. config 包:是该组件基于 Spring 的配置
+
+        技术组件,也分成两类:
+            1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
+            2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
+        如果是业务组件,Maven 名字会包含 biz
+    </description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+</project>

+ 156 - 0
xdz-framework/xdz-common/pom.xml

@@ -0,0 +1,156 @@
+<?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>com.xindazhou</groupId>
+        <artifactId>xdz-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>xdz-common</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>定义基础 pojo 类、枚举、工具类等等</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <dependencies>
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-expression</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aop</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 -->
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-openfeign-core</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,主要是 api 包使用到 -->
+        </dependency>
+
+        <!-- 监控相关 -->
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-trace</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>transmittable-thread-local</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
+            <artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 59 - 0
xdz-framework/xdz-common/src/main/java/com/fhs/trans/service/AutoTransable.java

@@ -0,0 +1,59 @@
+package com.fhs.trans.service;
+
+import com.fhs.core.trans.vo.VO;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 只有实现了这个接口的才能自动翻译
+ *
+ * 为什么要赋值粘贴到 yudao-common 包下?
+ * 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 yudao-module-xxx-api 模块下使用
+ *
+ * @author jackwang
+ * @since  2020-05-19 10:26:15
+ */
+public interface AutoTransable<V extends VO> {
+
+    /**
+     * 根据 ids 查询数据列表
+     *
+     * 改方法已过期啦,请使用 selectByIds
+     *
+     * @param ids 编号数组
+     * @return 数据列表
+     */
+    @Deprecated
+    default List<V> findByIds(List<? extends Object> ids){
+        return new ArrayList<>();
+    }
+
+    /**
+     * 根据 ids 查询
+     *
+     * @param ids 编号数组
+     * @return 数据列表
+     */
+    default List<V> selectByIds(List<? extends Object> ids){
+        return this.findByIds(ids);
+    }
+
+    /**
+     * 获取 db 中所有的数据
+     *
+     * @return db 中所有的数据
+     */
+    default List<V> select(){
+        return new ArrayList<>();
+    }
+
+    /**
+     * 根据 id 获取 vo
+     *
+     * @param primaryValue id
+     * @return vo
+     */
+    V selectById(Object primaryValue);
+
+}

+ 34 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java

@@ -0,0 +1,34 @@
+package com.xindazhou.framework.common.biz.infra.logger;
+
+import com.xindazhou.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO;
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = RpcConstants.INFRA_NAME) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - API 访问日志")
+public interface ApiAccessLogCommonApi {
+
+    String PREFIX = RpcConstants.INFRA_PREFIX + "/api-access-log";
+
+    @PostMapping(PREFIX + "/create")
+    @Operation(summary = "创建 API 访问日志")
+    CommonResult<Boolean> createApiAccessLog(@Valid @RequestBody ApiAccessLogCreateReqDTO createDTO);
+
+    /**
+     * 【异步】创建 API 访问日志
+     *
+     * @param createDTO 访问日志 DTO
+     */
+    @Async
+    default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
+        createApiAccessLog(createDTO).checkError();
+    }
+
+}

+ 34 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java

@@ -0,0 +1,34 @@
+package com.xindazhou.framework.common.biz.infra.logger;
+
+import com.xindazhou.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = RpcConstants.INFRA_NAME) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - API 异常日志")
+public interface ApiErrorLogCommonApi {
+
+    String PREFIX = RpcConstants.INFRA_PREFIX + "/api-error-log";
+
+    @PostMapping(PREFIX + "/create")
+    @Operation(summary = "创建 API 异常日志")
+    CommonResult<Boolean> createApiErrorLog(@Valid @RequestBody ApiErrorLogCreateReqDTO createDTO);
+
+    /**
+     * 【异步】创建 API 异常日志
+     *
+     * @param createDTO 异常日志 DTO
+     */
+    @Async
+    default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
+        createApiErrorLog(createDTO).checkError();
+    }
+
+}

+ 35 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/LoginLogCommonApi.java

@@ -0,0 +1,35 @@
+package com.xindazhou.framework.common.biz.infra.logger;
+
+import com.xindazhou.framework.common.biz.infra.logger.dto.LoginLogCreateReqDTO;
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = RpcConstants.USER_NAME) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - 登录日志")
+public interface LoginLogCommonApi {
+
+    String PREFIX = RpcConstants.USER_PREFIX + "/login-log";
+
+    @PostMapping(PREFIX + "/create")
+    @Operation(summary = "创建登录日志")
+    CommonResult<Boolean> createLoginLog(@Valid @RequestBody LoginLogCreateReqDTO reqDTO);
+
+    /**
+     * 【异步】创建登录日志
+     *
+     * @param reqDTO 登录日志 DTO
+     */
+    @Async
+    default void createLoginLogAsync(LoginLogCreateReqDTO reqDTO) {
+        createLoginLog(reqDTO).checkError();
+    }
+
+}
+

+ 103 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java

@@ -0,0 +1,103 @@
+package com.xindazhou.framework.common.biz.infra.logger.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * API 访问日志
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ApiAccessLogCreateReqDTO {
+
+    /**
+     * 链路追踪编号
+     */
+    private String traceId;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 应用名
+     */
+    @NotNull(message = "应用名不能为空")
+    private String applicationName;
+
+    /**
+     * 请求方法名
+     */
+    @NotNull(message = "http 请求方法不能为空")
+    private String requestMethod;
+    /**
+     * 访问地址
+     */
+    @NotNull(message = "访问地址不能为空")
+    private String requestUrl;
+    /**
+     * 请求参数
+     */
+    private String requestParams;
+    /**
+     * 响应结果
+     */
+    private String responseBody;
+    /**
+     * 用户 IP
+     */
+    @NotNull(message = "ip 不能为空")
+    private String userIp;
+    /**
+     * 浏览器 UA
+     */
+    @NotNull(message = "User-Agent 不能为空")
+    private String userAgent;
+
+    /**
+     * 操作模块
+     */
+    private String operateModule;
+    /**
+     * 操作名
+     */
+    private String operateName;
+    /**
+     * 操作分类
+     *
+     * 枚举,参见 OperateTypeEnum 类
+     */
+    private Integer operateType;
+
+    /**
+     * 开始请求时间
+     */
+    @NotNull(message = "开始请求时间不能为空")
+    private LocalDateTime beginTime;
+    /**
+     * 结束请求时间
+     */
+    @NotNull(message = "结束请求时间不能为空")
+    private LocalDateTime endTime;
+    /**
+     * 执行时长,单位:毫秒
+     */
+    @NotNull(message = "执行时长不能为空")
+    private Integer duration;
+    /**
+     * 结果码
+     */
+    @NotNull(message = "错误码不能为空")
+    private Integer resultCode;
+    /**
+     * 结果提示
+     */
+    private String resultMsg;
+
+}

+ 68 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java

@@ -0,0 +1,68 @@
+package com.xindazhou.framework.common.biz.infra.logger.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "RPC 服务 - API 错误日志创建 Request DTO")
+@Data
+public class ApiErrorLogCreateReqDTO {
+
+    @Schema(description = "链路追踪编号", example = "89aca178-a370-411c-ae02-3f0d672be4ab")
+    private String traceId;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long userId;
+    @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer userType;
+    @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system-server")
+    @NotNull(message = "应用名不能为空")
+    private String applicationName;
+
+    @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET")
+    @NotNull(message = "http 请求方法不能为空")
+    private String requestMethod;
+    @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy")
+    @NotNull(message = "访问地址不能为空")
+    private String requestUrl;
+    @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "请求参数不能为空")
+    private String requestParams;
+    @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
+    @NotNull(message = "ip 不能为空")
+    private String userIp;
+    @Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0")
+    @NotNull(message = "User-Agent 不能为空")
+    private String userAgent;
+
+    @Schema(description = "异常时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常时间不能为空")
+    private LocalDateTime exceptionTime;
+    @Schema(description = "异常名", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常名不能为空")
+    private String exceptionName;
+    @Schema(description = "异常发生的类全名", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常发生的类全名不能为空")
+    private String exceptionClassName;
+    @Schema(description = "异常发生的类文件", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常发生的类文件不能为空")
+    private String exceptionFileName;
+    @Schema(description = "异常发生的方法名", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常发生的方法名不能为空")
+    private String exceptionMethodName;
+    @Schema(description = "异常发生的方法所在行", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常发生的方法所在行不能为空")
+    private Integer exceptionLineNumber;
+    @Schema(description = "异常的栈轨迹异常的栈轨迹", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常的栈轨迹不能为空")
+    private String exceptionStackTrace;
+    @Schema(description = "异常导致的根消息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常导致的根消息不能为空")
+    private String exceptionRootCauseMessage;
+    @Schema(description = "异常导致的消息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "异常导致的消息不能为空")
+    private String exceptionMessage;
+
+}

+ 41 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/logger/dto/LoginLogCreateReqDTO.java

@@ -0,0 +1,41 @@
+package com.xindazhou.framework.common.biz.infra.logger.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Schema(description = "RPC 服务 - 登录日志创建 Request DTO")
+@Data
+public class LoginLogCreateReqDTO {
+
+    @Schema(description = "日志类型,参见 LoginLogTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1" )
+    @NotNull(message = "日志类型不能为空")
+    private Integer logType;
+
+    @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "89aca178-a370-411c-ae02-3f0d672be4ab")
+    private String traceId;
+
+    @Schema(description = "用户编号", example = "666")
+    private Long userId;
+    @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2" )
+    @NotNull(message = "用户类型不能为空")
+    private Integer userType;
+    @Schema(description = "用户账号", example = "yudao")
+    @Size(max = 30, message = "用户账号长度不能超过30个字符")
+    private String username; // 不再强制校验 username 非空,因为 Member 社交登录时,此时暂时没有 username(mobile)!
+
+    @Schema(description = "登录结果,参见 LoginResultEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "登录结果不能为空")
+    private Integer result;
+
+    @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
+    @NotEmpty(message = "用户 IP 不能为空")
+    private String userIp;
+
+    @Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0")
+    private String userAgent;
+
+}
+

+ 4 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/infra/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 针对 infra 模块的 api 包
+ */
+package com.xindazhou.framework.common.biz.infra;

+ 4 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 特殊:用于 framework 下,starter 需要调用 biz 业务模块的接口定义!
+ */
+package com.xindazhou.framework.common.biz;

+ 26 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/dict/DictDataCommonApi.java

@@ -0,0 +1,26 @@
+package com.xindazhou.framework.common.biz.system.dict;
+
+import com.xindazhou.framework.common.biz.system.dict.dto.DictDataRespDTO;
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = RpcConstants.SYSTEM_NAME, primary = false) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - 字典数据")
+public interface DictDataCommonApi {
+
+    String PREFIX = RpcConstants.SYSTEM_PREFIX + "/dict-data";
+
+    @GetMapping(PREFIX + "/list")
+    @Operation(summary = "获得指定字典类型的字典数据列表")
+    @Parameter(name = "dictType", description = "字典类型", example = "SEX", required = true)
+    CommonResult<List<DictDataRespDTO>> getDictDataList(@RequestParam("dictType") String dictType);
+
+}

+ 22 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/dict/dto/DictDataRespDTO.java

@@ -0,0 +1,22 @@
+package com.xindazhou.framework.common.biz.system.dict.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "RPC 服务 - 字典数据 Response DTO")
+@Data
+public class DictDataRespDTO {
+
+    @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
+    private String label;
+
+    @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "iocoder")
+    private String value;
+
+    @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex")
+    private String dictType;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status; // 参见 CommonStatusEnum 枚举
+
+}

+ 34 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/logger/OperateLogCommonApi.java

@@ -0,0 +1,34 @@
+package com.xindazhou.framework.common.biz.system.logger;
+
+import com.xindazhou.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO;
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = RpcConstants.SYSTEM_NAME, primary = false) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - 操作日志")
+public interface OperateLogCommonApi {
+
+    String PREFIX = RpcConstants.SYSTEM_PREFIX + "/operate-log";
+
+    @PostMapping(PREFIX + "/create")
+    @Operation(summary = "创建操作日志")
+    CommonResult<Boolean> createOperateLog(@Valid @RequestBody OperateLogCreateReqDTO createReqDTO);
+
+    /**
+     * 【异步】创建操作日志
+     *
+     * @param createReqDTO 请求
+     */
+    @Async
+    default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) {
+        createOperateLog(createReqDTO).checkError();
+    }
+
+}

+ 50 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java

@@ -0,0 +1,50 @@
+package com.xindazhou.framework.common.biz.system.logger.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(name = "RPC 服务 - 系统操作日志 Create Request DTO")
+@Data
+public class OperateLogCreateReqDTO {
+
+    @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "89aca178-a370-411c-ae02-3f0d672be4ab")
+    private String traceId;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+    @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2" )
+    @NotNull(message = "用户类型不能为空")
+    private Integer userType;
+    @Schema(description = "操作模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单")
+    @NotEmpty(message = "操作模块类型不能为空")
+    private String type;
+    @Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建订单")
+    @NotEmpty(message = "操作名不能为空")
+    private String subType;
+    @Schema(description = "操作模块业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "188")
+    @NotNull(message = "操作模块业务编号不能为空")
+    private Long bizId;
+    @Schema(description = "操作内容", requiredMode = Schema.RequiredMode.REQUIRED,
+            example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码")
+    @NotEmpty(message = "操作内容不能为空")
+    private String action;
+    @Schema(description = "拓展字段", example = "{\"orderId\": \"1\"}")
+    private String extra;
+
+    @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET")
+    @NotEmpty(message = "请求方法名不能为空")
+    private String requestMethod;
+    @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/order/get")
+    @NotEmpty(message = "请求地址不能为空")
+    private String requestUrl;
+    @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
+    @NotEmpty(message = "用户 IP 不能为空")
+    private String userIp;
+    @Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0")
+    @NotEmpty(message = "浏览器 UA 不能为空")
+    private String userAgent;
+
+}

+ 57 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java

@@ -0,0 +1,57 @@
+package com.xindazhou.framework.common.biz.system.oauth2;
+
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import com.xindazhou.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
+import com.xindazhou.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
+import com.xindazhou.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.Valid;
+
+@FeignClient(name = RpcConstants.SYSTEM_NAME, primary = false) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - OAuth2.0 令牌")
+public interface OAuth2TokenCommonApi {
+
+    String PREFIX = RpcConstants.SYSTEM_PREFIX + "/oauth2/token";
+
+    /**
+     * 校验 Token 的 URL 地址,主要是提供给 Gateway 使用
+     */
+    @SuppressWarnings("HttpUrlsUsage")
+    String URL_CHECK = "http://" + RpcConstants.SYSTEM_NAME + PREFIX + "/check";
+
+    @PostMapping(PREFIX + "/create")
+    @Operation(summary = "创建访问令牌")
+    CommonResult<OAuth2AccessTokenRespDTO> createAccessToken(@Valid @RequestBody OAuth2AccessTokenCreateReqDTO reqDTO);
+
+    @GetMapping(PREFIX + "/check")
+    @Operation(summary = "校验访问令牌")
+    @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou")
+    CommonResult<OAuth2AccessTokenCheckRespDTO> checkAccessToken(@RequestParam("accessToken") String accessToken);
+
+    @DeleteMapping(PREFIX + "/remove")
+    @Operation(summary = "移除访问令牌")
+    @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou")
+    CommonResult<OAuth2AccessTokenRespDTO> removeAccessToken(@RequestParam("accessToken") String accessToken);
+
+    @PutMapping(PREFIX + "/refresh")
+    @Operation(summary = "刷新访问令牌")
+    @Parameters({
+        @Parameter(name = "refreshToken", description = "刷新令牌", required = true, example = "haha"),
+        @Parameter(name = "clientId", description = "客户端编号", required = true, example = "yudaoyuanma")
+    })
+    CommonResult<OAuth2AccessTokenRespDTO> refreshAccessToken(@RequestParam("refreshToken") String refreshToken,
+                                                              @RequestParam("clientId") String clientId);
+
+    @GetMapping(PREFIX + "/is-revoked")
+    @Operation(summary = "检查访问令牌是否已被撤销")
+    @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou")
+    CommonResult<Boolean> isTokenRevoked(@RequestParam("accessToken") String accessToken);
+
+}

+ 33 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java

@@ -0,0 +1,33 @@
+package com.xindazhou.framework.common.biz.system.oauth2.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Schema(description = "RPC 服务 - OAuth2 访问令牌的校验 Response DTO")
+@Data
+public class OAuth2AccessTokenCheckRespDTO implements Serializable {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Long userId;
+
+    @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer userType;
+
+    @Schema(description = "用户信息", example = "{\"nickname\": \"芋道\"}")
+    private Map<String, String> userInfo;
+
+    @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long tenantId;
+
+    @Schema(description = "授权范围的数组", example = "user_info")
+    private List<String> scopes;
+
+    @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime expiresTime;
+
+}

+ 32 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java

@@ -0,0 +1,32 @@
+package com.xindazhou.framework.common.biz.system.oauth2.dto;
+
+import com.xindazhou.framework.common.enums.UserTypeEnum;
+import com.xindazhou.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.List;
+
+@Schema(description = "RPC 服务 - OAuth2 访问令牌创建 Request DTO")
+@Data
+public class OAuth2AccessTokenCreateReqDTO implements Serializable {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "用户类型不能为空")
+    @InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}")
+    private Integer userType;
+
+    @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudaoyuanma")
+    @NotNull(message = "客户端编号不能为空")
+    private String clientId;
+
+    @Schema(description = "授权范围的数组", example = "user_info")
+    private List<String> scopes;
+
+}

+ 28 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java

@@ -0,0 +1,28 @@
+package com.xindazhou.framework.common.biz.system.oauth2.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Schema(description = "RPC 服务 - OAuth2 访问令牌的信息 Response DTO")
+@Data
+public class OAuth2AccessTokenRespDTO implements Serializable {
+
+    @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou")
+    private String accessToken;
+
+    @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "haha")
+    private String refreshToken;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Long userId;
+
+    @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1" )
+    private Integer userType;
+
+    @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime expiresTime;
+
+}

+ 4 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 针对 system 模块的 api 包
+ */
+package com.xindazhou.framework.common.biz.system;

+ 43 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/permission/PermissionCommonApi.java

@@ -0,0 +1,43 @@
+package com.xindazhou.framework.common.biz.system.permission;
+
+import com.xindazhou.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@FeignClient(name = RpcConstants.SYSTEM_NAME, primary = false) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - 权限")
+public interface PermissionCommonApi {
+
+    String PREFIX = RpcConstants.SYSTEM_PREFIX + "/permission";
+
+    @GetMapping(PREFIX + "/has-any-permissions")
+    @Operation(summary = "判断是否有权限,任一一个即可")
+    @Parameters({
+            @Parameter(name = "userId", description = "用户编号", example = "1", required = true),
+            @Parameter(name = "permissions", description = "权限", example = "read,write", required = true)
+    })
+    CommonResult<Boolean> hasAnyPermissions(@RequestParam("userId") Long userId,
+                                            @RequestParam("permissions") String... permissions);
+
+    @GetMapping(PREFIX + "/has-any-roles")
+    @Operation(summary = "判断是否有角色,任一一个即可")
+    @Parameters({
+            @Parameter(name = "userId", description = "用户编号", example = "1", required = true),
+            @Parameter(name = "roles", description = "角色数组", example = "2", required = true)
+    })
+    CommonResult<Boolean> hasAnyRoles(@RequestParam("userId") Long userId,
+                                      @RequestParam("roles") String... roles);
+
+    @GetMapping(PREFIX + "/get-dept-data-permission")
+    @Operation(summary = "获得登陆用户的部门数据权限")
+    @Parameter(name = "userId", description = "用户编号", example = "2", required = true)
+    CommonResult<DeptDataPermissionRespDTO> getDeptDataPermission(@RequestParam("userId") Long userId);
+
+}

+ 28 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/permission/dto/DeptDataPermissionRespDTO.java

@@ -0,0 +1,28 @@
+package com.xindazhou.framework.common.biz.system.permission.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Schema(description = "RPC 服务 - 部门的数据权限 Response DTO")
+@Data
+public class DeptDataPermissionRespDTO {
+
+    @Schema(description = "是否可查看全部数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean all;
+
+    @Schema(description = "是否可查看自己的数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean self;
+
+    @Schema(description = "可查看的部门编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 3]")
+    private Set<Long> deptIds;
+
+    public DeptDataPermissionRespDTO() {
+        this.all = false;
+        this.self = false;
+        this.deptIds = new HashSet<>();
+    }
+
+}

+ 29 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/biz/system/tenant/TenantCommonApi.java

@@ -0,0 +1,29 @@
+package com.xindazhou.framework.common.biz.system.tenant;
+
+import com.xindazhou.framework.common.enums.RpcConstants;
+import com.xindazhou.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = RpcConstants.SYSTEM_NAME) // TODO 芋艿:fallbackFactory =
+@Tag(name = "RPC 服务 - 多租户")
+public interface TenantCommonApi {
+
+    String PREFIX = RpcConstants.SYSTEM_PREFIX + "/tenant";
+
+    @GetMapping(PREFIX + "/id-list")
+    @Operation(summary = "获得所有租户编号")
+    CommonResult<List<Long>> getTenantIdList();
+
+    @GetMapping(PREFIX + "/valid")
+    @Operation(summary = "校验租户是否合法")
+    @Parameter(name = "id", description = "租户编号", required = true, example = "1024")
+    CommonResult<Boolean> validTenant(@RequestParam("id") Long id);
+
+}

+ 15 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/core/ArrayValuable.java

@@ -0,0 +1,15 @@
+package com.xindazhou.framework.common.core;
+
+/**
+ * 可生成 T 数组的接口
+ *
+ * @author HUIHUI
+ */
+public interface ArrayValuable<T> {
+
+    /**
+     * @return 数组
+     */
+    T[] array();
+
+} 

+ 22 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/core/KeyValue.java

@@ -0,0 +1,22 @@
+package com.xindazhou.framework.common.core;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Key Value 的键值对
+ *
+ * @author 芋道源码
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class KeyValue<K, V> implements Serializable {
+
+    private K key;
+    private V value;
+
+}

+ 46 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/CommonStatusEnum.java

@@ -0,0 +1,46 @@
+package com.xindazhou.framework.common.enums;
+
+import cn.hutool.core.util.ObjUtil;
+import com.xindazhou.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 通用状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum CommonStatusEnum implements ArrayValuable<Integer> {
+
+    ENABLE(0, "开启"),
+    DISABLE(1, "关闭");
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(CommonStatusEnum::getStatus).toArray(Integer[]::new);
+
+    /**
+     * 状态值
+     */
+    private final Integer status;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+    public static boolean isEnable(Integer status) {
+        return ObjUtil.equal(ENABLE.status, status);
+    }
+
+    public static boolean isDisable(Integer status) {
+        return ObjUtil.equal(DISABLE.status, status);
+    }
+
+}

+ 47 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/DateIntervalEnum.java

@@ -0,0 +1,47 @@
+package com.xindazhou.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.xindazhou.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 时间间隔的枚举
+ *
+ * @author dhb52
+ */
+@Getter
+@AllArgsConstructor
+public enum DateIntervalEnum implements ArrayValuable<Integer> {
+
+    HOUR(0, "小时"), // 特殊:字典里,暂时不会有这个枚举!!!因为大多数情况下,用不到这个间隔
+    DAY(1, "天"),
+    WEEK(2, "周"),
+    MONTH(3, "月"),
+    QUARTER(4, "季度"),
+    YEAR(5, "年")
+    ;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(DateIntervalEnum::getInterval).toArray(Integer[]::new);
+
+    /**
+     * 类型
+     */
+    private final Integer interval;
+    /**
+     * 名称
+     */
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+
+    public static DateIntervalEnum valueOf(Integer interval) {
+        return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values());
+    }
+
+}

+ 21 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/DocumentEnum.java

@@ -0,0 +1,21 @@
+package com.xindazhou.framework.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 文档地址
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum DocumentEnum {
+
+    REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"),
+    TENANT("https://doc.iocoder.cn", "SaaS 多租户文档");
+
+    private final String url;
+    private final String memo;
+
+}

+ 62 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/RpcConstants.java

@@ -0,0 +1,62 @@
+package com.xindazhou.framework.common.enums;
+
+/**
+ * RPC 相关的枚举
+ *
+ * 虽然放在 yudao-spring-boot-starter-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处
+ *
+ * @author 芋道源码
+ */
+public interface RpcConstants {
+
+    /**
+     * RPC API 的前缀
+     */
+    String RPC_API_PREFIX = "/rpc-api";
+
+    /**
+     * system 服务名
+     *
+     * 注意,需要保证和 spring.application.name 保持一致
+     */
+    String SYSTEM_NAME = "system-server";
+
+    /**
+     * system 服务的前缀
+     */
+    String SYSTEM_PREFIX = RPC_API_PREFIX + "/system";
+
+    /**
+     * infra 服务名
+     *
+     * 注意,需要保证和 spring.application.name 保持一致
+     */
+    String INFRA_NAME = "infra-server";
+    /**
+     * infra 服务的前缀
+     */
+    String INFRA_PREFIX = RPC_API_PREFIX + "/infra";
+
+    /**
+     * business 服务名
+     *
+     * 注意,需要保证和 spring.application.name 保持一致
+     */
+    String BUSINESS_NAME = "xdz-business-server";
+    /**
+     * business 服务的前缀
+     */
+    String BUSINESS_PREFIX = RPC_API_PREFIX + "/business";
+
+    /**
+     * user 服务名
+     *
+     * 注意,需要保证和 spring.application.name 保持一致
+     */
+    String USER_NAME = "xdz-user-server";
+    /**
+     * user 服务的前缀
+     */
+    String USER_PREFIX = RPC_API_PREFIX + "/user";
+
+}

+ 40 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/TerminalEnum.java

@@ -0,0 +1,40 @@
+package com.xindazhou.framework.common.enums;
+
+import com.xindazhou.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 终端的枚举
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Getter
+public enum TerminalEnum implements ArrayValuable<Integer> {
+
+    UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
+    WECHAT_MINI_PROGRAM(10, "微信小程序"),
+    WECHAT_WAP(11, "微信公众号"),
+    H5(20, "H5 网页"),
+    APP(31, "手机 App"),
+    ;
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(TerminalEnum::getTerminal).toArray(Integer[]::new);
+
+    /**
+     * 终端
+     */
+    private final Integer terminal;
+    /**
+     * 终端名
+     */
+    private final String name;
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+}

+ 39 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/UserTypeEnum.java

@@ -0,0 +1,39 @@
+package com.xindazhou.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.xindazhou.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 全局用户类型枚举
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum implements ArrayValuable<Integer> {
+
+    MEMBER(1, "会员"), // 面向 c 端,普通用户
+    ADMIN(2, "管理员"); // 面向 b 端,管理后台
+
+    public static final Integer[] ARRAYS = Arrays.stream(values()).map(UserTypeEnum::getValue).toArray(Integer[]::new);
+
+    /**
+     * 类型
+     */
+    private final Integer value;
+    /**
+     * 类型名
+     */
+    private final String name;
+
+    public static UserTypeEnum valueOf(Integer value) {
+        return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
+    }
+
+    @Override
+    public Integer[] array() {
+        return ARRAYS;
+    }
+}

+ 38 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/enums/WebFilterOrderEnum.java

@@ -0,0 +1,38 @@
+package com.xindazhou.framework.common.enums;
+
+/**
+ * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
+ *
+ * @author 芋道源码
+ */
+public interface WebFilterOrderEnum {
+
+    int CORS_FILTER = Integer.MIN_VALUE;
+
+    int TRACE_FILTER = CORS_FILTER + 1;
+
+    int ENV_TAG_FILTER = TRACE_FILTER + 1;
+
+    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+
+    int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1;
+
+    // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
+
+    int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
+
+    int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
+
+    int XSS_FILTER = -102;  // 需要保证在 RequestBodyCacheFilter 后面
+
+    // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
+
+    int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
+
+    int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    int DEMO_FILTER = Integer.MAX_VALUE;
+
+}

+ 32 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/ErrorCode.java

@@ -0,0 +1,32 @@
+package com.xindazhou.framework.common.exception;
+
+import com.xindazhou.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.xindazhou.framework.common.exception.enums.ServiceErrorCodeRange;
+import lombok.Data;
+
+/**
+ * 错误码对象
+ *
+ * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
+ * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
+ *
+ * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
+ */
+@Data
+public class ErrorCode {
+
+    /**
+     * 错误码
+     */
+    private final Integer code;
+    /**
+     * 错误提示
+     */
+    private final String msg;
+
+    public ErrorCode(Integer code, String message) {
+        this.code = code;
+        this.msg = message;
+    }
+
+}

+ 60 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/ServerException.java

@@ -0,0 +1,60 @@
+package com.xindazhou.framework.common.exception;
+
+import com.xindazhou.framework.common.exception.enums.GlobalErrorCodeConstants;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 服务器异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServerException extends RuntimeException {
+
+    /**
+     * 全局错误码
+     *
+     * @see GlobalErrorCodeConstants
+     */
+    private Integer code;
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServerException() {
+    }
+
+    public ServerException(ErrorCode errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMsg();
+    }
+
+    public ServerException(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public ServerException setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServerException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}

+ 60 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/ServiceException.java

@@ -0,0 +1,60 @@
+package com.xindazhou.framework.common.exception;
+
+import com.xindazhou.framework.common.exception.enums.ServiceErrorCodeRange;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 业务逻辑异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServiceException extends RuntimeException {
+
+    /**
+     * 业务错误码
+     *
+     * @see ServiceErrorCodeRange
+     */
+    private Integer code;
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServiceException() {
+    }
+
+    public ServiceException(ErrorCode errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMsg();
+    }
+
+    public ServiceException(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public ServiceException setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}

+ 41 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/enums/GlobalErrorCodeConstants.java

@@ -0,0 +1,41 @@
+package com.xindazhou.framework.common.exception.enums;
+
+import com.xindazhou.framework.common.exception.ErrorCode;
+
+/**
+ * 全局错误码枚举
+ * 0-999 系统异常编码保留
+ *
+ * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
+ * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
+ * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
+ *
+ * @author 芋道源码
+ */
+public interface GlobalErrorCodeConstants {
+
+    ErrorCode SUCCESS = new ErrorCode(0, "成功");
+
+    // ========== 客户端错误段 ==========
+
+    ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
+    ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
+    ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
+    ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
+    ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
+    ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
+    ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
+
+    // ========== 服务端错误段 ==========
+
+    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
+    ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
+    ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项");
+
+    // ========== 自定义错误段 ==========
+    ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
+    ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
+
+    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
+
+}

+ 48 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/enums/ServiceErrorCodeRange.java

@@ -0,0 +1,48 @@
+package com.xindazhou.framework.common.exception.enums;
+
+/**
+ * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
+ *
+ * 一共 10 位,分成四段
+ *
+ * 第一段,1 位,类型
+ *      1 - 业务级别异常
+ *      x - 预留
+ * 第二段,3 位,系统类型
+ *      001 - 用户系统
+ *      002 - 商品系统
+ *      003 - 订单系统
+ *      004 - 支付系统
+ *      005 - 优惠劵系统
+ *      ... - ...
+ * 第三段,3 位,模块
+ *      不限制规则。
+ *      一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
+ *          001 - OAuth2 模块
+ *          002 - User 模块
+ *          003 - MobileCode 模块
+ * 第四段,3 位,错误码
+ *       不限制规则。
+ *       一般建议,每个模块自增。
+ *
+ * @author 芋道源码
+ */
+public class ServiceErrorCodeRange {
+
+    // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
+    // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
+    // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000)
+    // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
+    // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
+    // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
+    // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
+
+    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
+    // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
+    // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
+
+    // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
+
+    // 模块 ai 错误码区间 [1-022-000-000 ~ 1-023-000-000)
+
+}

+ 77 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/exception/util/ServiceExceptionUtil.java

@@ -0,0 +1,77 @@
+package com.xindazhou.framework.common.exception.util;
+
+import com.xindazhou.framework.common.exception.ErrorCode;
+import com.xindazhou.framework.common.exception.ServiceException;
+import com.xindazhou.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * {@link ServiceException} 工具类
+ *
+ * 目的在于,格式化异常信息提示。
+ * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
+ *
+ */
+@Slf4j
+public class ServiceExceptionUtil {
+
+    // ========== 和 ServiceException 的集成 ==========
+
+    public static ServiceException exception(ErrorCode errorCode) {
+        return exception0(errorCode.getCode(), errorCode.getMsg());
+    }
+
+    public static ServiceException exception(ErrorCode errorCode, Object... params) {
+        return exception0(errorCode.getCode(), errorCode.getMsg(), params);
+    }
+
+    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
+        String message = doFormat(code, messagePattern, params);
+        return new ServiceException(code, message);
+    }
+
+    public static ServiceException invalidParamException(String messagePattern, Object... params) {
+        return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
+    }
+
+    // ========== 格式化方法 ==========
+
+    /**
+     * 将错误编号对应的消息使用 params 进行格式化。
+     *
+     * @param code           错误编号
+     * @param messagePattern 消息模版
+     * @param params         参数
+     * @return 格式化后的提示
+     */
+    @VisibleForTesting
+    public static String doFormat(int code, String messagePattern, Object... params) {
+        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
+        int i = 0;
+        int j;
+        int l;
+        for (l = 0; l < params.length; l++) {
+            j = messagePattern.indexOf("{}", i);
+            if (j == -1) {
+                log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+                if (i == 0) {
+                    return messagePattern;
+                } else {
+                    sbuf.append(messagePattern.substring(i));
+                    return sbuf.toString();
+                }
+            } else {
+                sbuf.append(messagePattern, i, j);
+                sbuf.append(params[l]);
+                i = j + 2;
+            }
+        }
+        if (messagePattern.indexOf("{}", i) != -1) {
+            log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+        }
+        sbuf.append(messagePattern.substring(i));
+        return sbuf.toString();
+    }
+
+}

+ 6 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 基础的通用类,和框架无关
+ *
+ * 例如说,CommonResult 为通用返回
+ */
+package com.xindazhou.framework.common;

+ 121 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/CommonResult.java

@@ -0,0 +1,121 @@
+package com.xindazhou.framework.common.pojo;
+
+import cn.hutool.core.lang.Assert;
+import com.xindazhou.framework.common.exception.ErrorCode;
+import com.xindazhou.framework.common.exception.ServiceException;
+import com.xindazhou.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.xindazhou.framework.common.exception.util.ServiceExceptionUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 通用返回
+ *
+ * @param <T> 数据泛型
+ */
+@Data
+public class CommonResult<T> implements Serializable {
+
+    /**
+     * 错误码
+     *
+     * @see ErrorCode#getCode()
+     */
+    private Integer code;
+    /**
+     * 错误提示,用户可阅读
+     *
+     * @see ErrorCode#getMsg() ()
+     */
+    private String msg;
+    /**
+     * 返回数据
+     */
+    private T data;
+
+    /**
+     * 将传入的 result 对象,转换成另外一个泛型结果的对象
+     *
+     * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
+     *
+     * @param result 传入的 result 对象
+     * @param <T> 返回的泛型
+     * @return 新的 CommonResult 对象
+     */
+    public static <T> CommonResult<T> error(CommonResult<?> result) {
+        return error(result.getCode(), result.getMsg());
+    }
+
+    public static <T> CommonResult<T> error(Integer code, String message) {
+        Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = code;
+        result.msg = message;
+        return result;
+    }
+
+    public static <T> CommonResult<T> error(ErrorCode errorCode, Object... params) {
+        Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = errorCode.getCode();
+        result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params);
+        return result;
+    }
+
+    public static <T> CommonResult<T> error(ErrorCode errorCode) {
+        return error(errorCode.getCode(), errorCode.getMsg());
+    }
+
+    public static <T> CommonResult<T> success(T data) {
+        CommonResult<T> result = new CommonResult<>();
+        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
+        result.data = data;
+        result.msg = "";
+        return result;
+    }
+
+    public static boolean isSuccess(Integer code) {
+        return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
+    }
+
+    @JsonIgnore // 避免 jackson 序列化
+    public boolean isSuccess() {
+        return isSuccess(code);
+    }
+
+    @JsonIgnore // 避免 jackson 序列化
+    public boolean isError() {
+        return !isSuccess();
+    }
+
+    // ========= 和 Exception 异常体系集成 =========
+
+    /**
+     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
+     */
+    public void checkError() throws ServiceException {
+        if (isSuccess()) {
+            return;
+        }
+        // 业务异常
+        throw new ServiceException(code, msg);
+    }
+
+    /**
+     * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
+     * 如果没有,则返回 {@link #data} 数据
+     */
+    @JsonIgnore // 避免 jackson 序列化
+    public T getCheckedData() {
+        checkError();
+        return data;
+    }
+
+    public static <T> CommonResult<T> error(ServiceException serviceException) {
+        return error(serviceException.getCode(), serviceException.getMessage());
+    }
+
+}

+ 36 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/PageParam.java

@@ -0,0 +1,36 @@
+package com.xindazhou.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Schema(description="分页参数")
+@Data
+public class PageParam implements Serializable {
+
+    private static final Integer PAGE_NO = 1;
+    private static final Integer PAGE_SIZE = 10;
+
+    /**
+     * 每页条数 - 不分页
+     *
+     * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
+     */
+    public static final Integer PAGE_SIZE_NONE = -1;
+
+    @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
+    @NotNull(message = "页码不能为空")
+    @Min(value = 1, message = "页码最小值为 1")
+    private Integer pageNo = PAGE_NO;
+
+    @Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "每页条数不能为空")
+    @Min(value = 1, message = "每页条数最小值为 1")
+    @Max(value = 200, message = "每页条数最大值为 200")
+    private Integer pageSize = PAGE_SIZE;
+
+}

+ 41 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/PageResult.java

@@ -0,0 +1,41 @@
+package com.xindazhou.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "分页结果")
+@Data
+public final class PageResult<T> implements Serializable {
+
+    @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long total;
+
+    @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<T> list;
+
+    public PageResult() {
+    }
+
+    public PageResult(List<T> list, Long total) {
+        this.list = list;
+        this.total = total;
+    }
+
+    public PageResult(Long total) {
+        this.list = new ArrayList<>();
+        this.total = total;
+    }
+
+    public static <T> PageResult<T> empty() {
+        return new PageResult<>(0L);
+    }
+
+    public static <T> PageResult<T> empty(Long total) {
+        return new PageResult<>(total);
+    }
+
+}

+ 19 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/SortablePageParam.java

@@ -0,0 +1,19 @@
+package com.xindazhou.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.List;
+
+@Schema(description = "可排序的分页参数")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class SortablePageParam extends PageParam {
+
+    @Schema(description = "排序字段")
+    private List<SortingField> sortingFields;
+
+}

+ 37 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/pojo/SortingField.java

@@ -0,0 +1,37 @@
+package com.xindazhou.framework.common.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 排序字段 DTO
+ *
+ * 类名加了 ing 的原因是,避免和 ES SortField 重名。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SortingField implements Serializable {
+
+    /**
+     * 顺序 - 升序
+     */
+    public static final String ORDER_ASC = "asc";
+    /**
+     * 顺序 - 降序
+     */
+    public static final String ORDER_DESC = "desc";
+
+    /**
+     * 字段
+     */
+    private String field;
+    /**
+     * 顺序
+     */
+    private String order;
+
+}

+ 61 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/cache/CacheUtils.java

@@ -0,0 +1,61 @@
+package com.xindazhou.framework.common.util.cache;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+/**
+ * Cache 工具类
+ *
+ * @author 芋道源码
+ */
+public class CacheUtils {
+
+    /**
+     * 异步刷新的 LoadingCache 最大缓存数量
+     *
+     * @see <a href="">本地缓存 CacheUtils 工具类建议</a>
+     */
+    private static final Integer CACHE_MAX_SIZE = 10000;
+
+    /**
+     * 构建异步刷新的 LoadingCache 对象
+     *
+     * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法
+     *
+     * 或者简单理解:
+     * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法
+     * 2、和“全局”、“系统”相关的,使用当前缓存方法
+     *
+     * @param duration 过期时间
+     * @param loader  CacheLoader 对象
+     * @return LoadingCache 对象
+     */
+    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder()
+                .maximumSize(CACHE_MAX_SIZE)
+                // 只阻塞当前数据加载线程,其他线程返回旧值
+                .refreshAfterWrite(duration)
+                // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
+                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
+    }
+
+    /**
+     * 构建同步刷新的 LoadingCache 对象
+     *
+     * @param duration 过期时间
+     * @param loader  CacheLoader 对象
+     * @return LoadingCache 对象
+     */
+    public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder()
+                .maximumSize(CACHE_MAX_SIZE)
+                // 只阻塞当前数据加载线程,其他线程返回旧值
+                .refreshAfterWrite(duration)
+                .build(loader);
+    }
+
+}

+ 58 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/ArrayUtils.java

@@ -0,0 +1,58 @@
+package com.xindazhou.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.util.ArrayUtil;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static com.xindazhou.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * Array 工具类
+ *
+ * @author 芋道源码
+ */
+public class ArrayUtils {
+
+    /**
+     * 将 object 和 newElements 合并成一个数组
+     *
+     * @param object 对象
+     * @param newElements 数组
+     * @param <T> 泛型
+     * @return 结果数组
+     */
+    @SafeVarargs
+    public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
+        if (object == null) {
+            return newElements;
+        }
+        Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
+        result[0] = object;
+        System.arraycopy(newElements, 0, result, 1, newElements.length);
+        return result;
+    }
+
+    public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) {
+        return toArray(convertList(from, mapper));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T[] toArray(Collection<T> from) {
+        if (CollectionUtil.isEmpty(from)) {
+            return (T[]) (new Object[0]);
+        }
+        return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
+    }
+
+    public static <T> T get(T[] array, int index) {
+        if (null == array || index >= array.length) {
+            return null;
+        }
+        return array[index];
+    }
+
+}

+ 352 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/CollectionUtils.java

@@ -0,0 +1,352 @@
+package com.xindazhou.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.xindazhou.framework.common.pojo.PageResult;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static cn.hutool.core.convert.Convert.toCollection;
+import static java.util.Arrays.asList;
+
+/**
+ * Collection 工具类
+ *
+ * @author 芋道源码
+ */
+public class CollectionUtils {
+
+    public static boolean containsAny(Object source, Object... targets) {
+        return asList(targets).contains(source);
+    }
+
+    public static boolean isAnyEmpty(Collection<?>... collections) {
+        return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
+    }
+
+    public static <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) {
+        return from.stream().anyMatch(predicate);
+    }
+
+    public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(predicate).collect(Collectors.toList());
+    }
+
+    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return distinct(from, keyMapper, (t1, t2) -> t1);
+    }
+
+    public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
+    }
+
+    public static <T, U> List<U> convertList(T[] from, Function<T, U> func) {
+        if (ArrayUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return convertList(Arrays.asList(from), func);
+    }
+
+    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) {
+        if (ArrayUtil.isEmpty(from)) {
+            return new PageResult<>(from.getTotal());
+        }
+        return new PageResult<>(convertList(from.getList(), func), from.getTotal());
+    }
+
+    public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
+                                                      Function<T, ? extends Stream<? extends U>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
+                                                         Function<? super T, ? extends U> mapper,
+                                                         Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
+        return map.values()
+                .stream()
+                .flatMap(List::stream)
+                .collect(Collectors.toList());
+    }
+
+    public static <T> Set<T> convertSet(Collection<T> from) {
+        return convertSet(from, v -> v);
+    }
+
+    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v));
+    }
+
+    public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
+                                                    Function<T, ? extends Stream<? extends U>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
+                                                       Function<? super T, ? extends U> mapper,
+                                                       Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, Function.identity());
+    }
+
+    public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return supplier.get();
+        }
+        return convertMap(from, keyFunc, Function.identity(), supplier);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return supplier.get();
+        }
+        return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
+    }
+
+    public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
+    }
+
+    public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream()
+                .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
+    }
+
+    // 暂时没想好名字,先以 2 结尾噶
+    public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashMap<>();
+        }
+        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
+    }
+
+    public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return Collections.emptyMap();
+        }
+        ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
+        from.forEach(item -> builder.put(keyFunc.apply(item), item));
+        return builder.build();
+    }
+
+    /**
+     * 对比老、新两个列表,找出新增、修改、删除的数据
+     *
+     * @param oldList  老列表
+     * @param newList  新列表
+     * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同
+     *                 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据
+     * @return [新增列表、修改列表、删除列表]
+     */
+    public static <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList,
+                                             BiFunction<T, T, Boolean> sameFunc) {
+        List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
+        List<T> updateList = new ArrayList<>();
+        List<T> deleteList = new ArrayList<>();
+
+        // 通过以 oldList 为主遍历,找出 updateList 和 deleteList
+        for (T oldObj : oldList) {
+            // 1. 寻找是否有匹配的
+            T foundObj = null;
+            for (Iterator<T> iterator = createList.iterator(); iterator.hasNext(); ) {
+                T newObj = iterator.next();
+                // 1.1 不匹配,则直接跳过
+                if (!sameFunc.apply(oldObj, newObj)) {
+                    continue;
+                }
+                // 1.2 匹配,则移除,并结束寻找
+                iterator.remove();
+                foundObj = newObj;
+                break;
+            }
+            // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中
+            if (foundObj != null) {
+                updateList.add(foundObj);
+            } else {
+                deleteList.add(oldObj);
+            }
+        }
+        return asList(createList, updateList, deleteList);
+    }
+
+    public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
+        return org.springframework.util.CollectionUtils.containsAny(source, candidates);
+    }
+
+    public static <T> T getFirst(List<T> from) {
+        return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
+    }
+
+    public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) {
+        return findFirst(from, predicate, Function.identity());
+    }
+
+    public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        return from.stream().filter(predicate).findFirst().map(func).orElse(null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert !from.isEmpty(); // 断言,避免告警
+        T t = from.stream().max(Comparator.comparing(valueFunc)).get();
+        return valueFunc.apply(t);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        T t = from.stream().min(Comparator.comparing(valueFunc)).get();
+        return valueFunc.apply(t);
+    }
+
+    public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        return from.stream().min(Comparator.comparing(valueFunc)).get();
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
+                                                                     BinaryOperator<V> accumulator) {
+        return getSumValue(from, valueFunc, accumulator, null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
+                                                                     BinaryOperator<V> accumulator, V defaultValue) {
+        if (CollUtil.isEmpty(from)) {
+            return defaultValue;
+        }
+        assert !from.isEmpty(); // 断言,避免告警
+        return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue);
+    }
+
+    public static <T> void addIfNotNull(Collection<T> coll, T item) {
+        if (item == null) {
+            return;
+        }
+        coll.add(item);
+    }
+
+    public static <T> Collection<T> singleton(T obj) {
+        return obj == null ? Collections.emptyList() : Collections.singleton(obj);
+    }
+
+    public static <T> List<T> newArrayList(List<List<T>> list) {
+        return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
+    }
+
+    /**
+     * 转换为 LinkedHashSet
+     *
+     * @param <T>         元素类型
+     * @param elementType 集合中元素类型
+     * @param value       被转换的值
+     * @return {@link LinkedHashSet}
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> LinkedHashSet<T> toLinkedHashSet(Class<T> elementType, Object value) {
+        return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
+    }
+
+}

+ 68 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/MapUtils.java

@@ -0,0 +1,68 @@
+package com.xindazhou.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjUtil;
+import com.xindazhou.framework.common.core.KeyValue;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Map 工具类
+ *
+ * @author 芋道源码
+ */
+public class MapUtils {
+
+    /**
+     * 从哈希表表中,获得 keys 对应的所有 value 数组
+     *
+     * @param multimap 哈希表
+     * @param keys keys
+     * @return value 数组
+     */
+    public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) {
+        List<V> result = new ArrayList<>();
+        keys.forEach(k -> {
+            Collection<V> values = multimap.get(k);
+            if (CollectionUtil.isEmpty(values)) {
+                return;
+            }
+            result.addAll(values);
+        });
+        return result;
+    }
+
+    /**
+     * 从哈希表查找到 key 对应的 value,然后进一步处理
+     * key 为 null 时, 不处理
+     * 注意,如果查找到的 value 为 null 时,不进行处理
+     *
+     * @param map 哈希表
+     * @param key key
+     * @param consumer 进一步处理的逻辑
+     */
+    public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) {
+        if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) {
+            return;
+        }
+        V value = map.get(key);
+        if (value == null) {
+            return;
+        }
+        consumer.accept(value);
+    }
+
+    public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) {
+        Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size());
+        keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue()));
+        return map;
+    }
+
+}

+ 19 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/collection/SetUtils.java

@@ -0,0 +1,19 @@
+package com.xindazhou.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.Set;
+
+/**
+ * Set 工具类
+ *
+ * @author 芋道源码
+ */
+public class SetUtils {
+
+    @SafeVarargs
+    public static <T> Set<T> asSet(T... objs) {
+        return CollUtil.newHashSet(objs);
+    }
+
+}

+ 149 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/date/DateUtils.java

@@ -0,0 +1,149 @@
+package com.xindazhou.framework.common.util.date;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+
+import java.time.*;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ *
+ * @author 芋道源码
+ */
+public class DateUtils {
+
+    /**
+     * 时区 - 默认
+     */
+    public static final String TIME_ZONE_DEFAULT = "GMT+8";
+
+    /**
+     * 秒转换成毫秒
+     */
+    public static final long SECOND_MILLIS = 1000;
+
+    public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd";
+
+    public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
+
+    /**
+     * 将 LocalDateTime 转换成 Date
+     *
+     * @param date LocalDateTime
+     * @return LocalDateTime
+     */
+    public static Date of(LocalDateTime date) {
+        if (date == null) {
+            return null;
+        }
+        // 将此日期时间与时区相结合以创建 ZonedDateTime
+        ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault());
+        // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳
+        Instant instant = zonedDateTime.toInstant();
+        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
+        return Date.from(instant);
+    }
+
+    /**
+     * 将 Date 转换成 LocalDateTime
+     *
+     * @param date Date
+     * @return LocalDateTime
+     */
+    public static LocalDateTime of(Date date) {
+        if (date == null) {
+            return null;
+        }
+        // 转为时间戳
+        Instant instant = date.toInstant();
+        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
+        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+    }
+
+    public static Date addTime(Duration duration) {
+        return new Date(System.currentTimeMillis() + duration.toMillis());
+    }
+
+    public static boolean isExpired(LocalDateTime time) {
+        LocalDateTime now = LocalDateTime.now();
+        return now.isAfter(time);
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year  年
+     * @param month 月
+     * @param day   日
+     * @return 指定时间
+     */
+    public static Date buildTime(int year, int month, int day) {
+        return buildTime(year, month, day, 0, 0, 0);
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year   年
+     * @param month  月
+     * @param day    日
+     * @param hour   小时
+     * @param minute 分钟
+     * @param second 秒
+     * @return 指定时间
+     */
+    public static Date buildTime(int year, int month, int day,
+                                 int hour, int minute, int second) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.YEAR, year);
+        calendar.set(Calendar.MONTH, month - 1);
+        calendar.set(Calendar.DAY_OF_MONTH, day);
+        calendar.set(Calendar.HOUR_OF_DAY, hour);
+        calendar.set(Calendar.MINUTE, minute);
+        calendar.set(Calendar.SECOND, second);
+        calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒
+        return calendar.getTime();
+    }
+
+    public static Date max(Date a, Date b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+        return a.compareTo(b) > 0 ? a : b;
+    }
+
+    public static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+        return a.isAfter(b) ? a : b;
+    }
+
+    /**
+     * 是否今天
+     *
+     * @param date 日期
+     * @return 是否
+     */
+    public static boolean isToday(LocalDateTime date) {
+        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now());
+    }
+
+    /**
+     * 是否昨天
+     *
+     * @param date 日期
+     * @return 是否
+     */
+    public static boolean isYesterday(LocalDateTime date) {
+        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1));
+    }
+
+}

+ 350 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/date/LocalDateTimeUtils.java

@@ -0,0 +1,350 @@
+package com.xindazhou.framework.common.util.date;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.date.TemporalAccessorUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import com.xindazhou.framework.common.enums.DateIntervalEnum;
+
+import java.sql.Timestamp;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+import static cn.hutool.core.date.DatePattern.*;
+
+/**
+ * 时间工具类,用于 {@link LocalDateTime}
+ *
+ * @author 芋道源码
+ */
+public class LocalDateTimeUtils {
+
+    /**
+     * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
+     */
+    public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
+
+    public static DateTimeFormatter UTC_MS_WITH_XXX_OFFSET_FORMATTER = createFormatter(UTC_MS_WITH_XXX_OFFSET_PATTERN);
+
+    /**
+     * 解析时间
+     *
+     * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功
+     *
+     * @param time 时间
+     * @return 时间字符串
+     */
+    public static LocalDateTime parse(String time) {
+        try {
+            return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN);
+        } catch (DateTimeParseException e) {
+            return LocalDateTimeUtil.parse(time);
+        }
+    }
+
+    public static LocalDateTime addTime(Duration duration) {
+        return LocalDateTime.now().plus(duration);
+    }
+
+    public static LocalDateTime minusTime(Duration duration) {
+        return LocalDateTime.now().minus(duration);
+    }
+
+    public static boolean beforeNow(LocalDateTime date) {
+        return date.isBefore(LocalDateTime.now());
+    }
+
+    public static boolean afterNow(LocalDateTime date) {
+        return date.isAfter(LocalDateTime.now());
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year  年
+     * @param month 月
+     * @param day   日
+     * @return 指定时间
+     */
+    public static LocalDateTime buildTime(int year, int month, int day) {
+        return LocalDateTime.of(year, month, day, 0, 0, 0);
+    }
+
+    public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1,
+                                                   int year2, int month2, int day2) {
+        return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)};
+    }
+
+    /**
+     * 判指定断时间,是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @param time 指定时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, Timestamp time) {
+        if (startTime == null || endTime == null || time == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(LocalDateTimeUtil.of(time), startTime, endTime);
+    }
+
+    /**
+     * 判指定断时间,是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @param time 指定时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) {
+        if (startTime == null || endTime == null || time == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(parse(time), startTime, endTime);
+    }
+
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) {
+        if (startTime == null || endTime == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime);
+    }
+
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 是否
+     */
+    public static boolean isBetween(String startTime, String endTime) {
+        if (startTime == null || endTime == null) {
+            return false;
+        }
+        LocalDate nowDate = LocalDate.now();
+        return LocalDateTimeUtil.isIn(LocalDateTime.now(),
+                LocalDateTime.of(nowDate, LocalTime.parse(startTime)),
+                LocalDateTime.of(nowDate, LocalTime.parse(endTime)));
+    }
+
+    /**
+     * 判断时间段是否重叠
+     *
+     * @param startTime1 开始 time1
+     * @param endTime1   结束 time1
+     * @param startTime2 开始 time2
+     * @param endTime2   结束 time2
+     * @return 重叠:true 不重叠:false
+     */
+    public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) {
+        LocalDate nowDate = LocalDate.now();
+        return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1),
+                LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
+    }
+
+    /**
+     * 获取指定日期所在的月份的开始时间
+     * 例如:2023-09-30 00:00:00,000
+     *
+     * @param date 日期
+     * @return 月份的开始时间
+     */
+    public static LocalDateTime beginOfMonth(LocalDateTime date) {
+        return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
+    }
+
+    /**
+     * 获取指定日期所在的月份的最后时间
+     * 例如:2023-09-30 23:59:59,999
+     *
+     * @param date 日期
+     * @return 月份的结束时间
+     */
+    public static LocalDateTime endOfMonth(LocalDateTime date) {
+        return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
+    }
+
+    /**
+     * 获得指定日期所在季度
+     *
+     * @param date 日期
+     * @return 所在季度
+     */
+    public static int getQuarterOfYear(LocalDateTime date) {
+        return (date.getMonthValue() - 1) / 3 + 1;
+    }
+
+    /**
+     * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负
+     *
+     * @param dateTime 日期
+     * @return 相差天数
+     */
+    public static Long between(LocalDateTime dateTime) {
+        return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS);
+    }
+
+    /**
+     * 获取今天的开始时间
+     *
+     * @return 今天
+     */
+    public static LocalDateTime getToday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
+    }
+
+    /**
+     * 获取昨天的开始时间
+     *
+     * @return 昨天
+     */
+    public static LocalDateTime getYesterday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1));
+    }
+
+    /**
+     * 获取本月的开始时间
+     *
+     * @return 本月
+     */
+    public static LocalDateTime getMonth() {
+        return beginOfMonth(LocalDateTime.now());
+    }
+
+    /**
+     * 获取本年的开始时间
+     *
+     * @return 本年
+     */
+    public static LocalDateTime getYear() {
+        return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
+    }
+
+    public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
+                                                         LocalDateTime endTime,
+                                                         Integer interval) {
+        // 1.1 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+        // 1.2 将时间对齐
+        startTime = LocalDateTimeUtil.beginOfDay(startTime);
+        endTime = LocalDateTimeUtil.endOfDay(endTime);
+
+        // 2. 循环,生成时间范围
+        List<LocalDateTime[]> timeRanges = new ArrayList<>();
+        switch (intervalEnum) {
+            case HOUR:
+                while (startTime.isBefore(endTime)) {
+                    timeRanges.add(new LocalDateTime[]{startTime, startTime.plusHours(1).minusNanos(1)});
+                    startTime = startTime.plusHours(1);
+                }
+            case DAY:
+                while (startTime.isBefore(endTime)) {
+                    timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
+                    startTime = startTime.plusDays(1);
+                }
+                break;
+            case WEEK:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfWeek});
+                    startTime = endOfWeek.plusNanos(1);
+                }
+                break;
+            case MONTH:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfMonth});
+                    startTime = endOfMonth.plusNanos(1);
+                }
+                break;
+            case QUARTER:
+                while (startTime.isBefore(endTime)) {
+                    int quarterOfYear = getQuarterOfYear(startTime);
+                    LocalDateTime quarterEnd = quarterOfYear == 4
+                            ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1)
+                            : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, quarterEnd});
+                    startTime = quarterEnd.plusNanos(1);
+                }
+                break;
+            case YEAR:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfYear});
+                    startTime = endOfYear.plusNanos(1);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+        // 3. 兜底,最后一个时间,需要保持在 endTime 之前
+        LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges);
+        if (lastTimeRange != null) {
+            lastTimeRange[1] = endTime;
+        }
+        return timeRanges;
+    }
+
+    /**
+     * 格式化时间范围
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @param interval  时间间隔
+     * @return 时间范围
+     */
+    public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) {
+        // 1. 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+
+        // 2. 循环,生成时间范围
+        switch (intervalEnum) {
+            case HOUR:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATETIME_MINUTE_PATTERN);
+            case DAY:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
+            case WEEK:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN)
+                        + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime));
+            case MONTH:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN);
+            case QUARTER:
+                return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime));
+            case YEAR:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN);
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+    }
+
+    /**
+     * 将给定的 {@link LocalDateTime} 转换为自 Unix 纪元时间(1970-01-01T00:00:00Z)以来的秒数。
+     *
+     * @param sourceDateTime 需要转换的本地日期时间,不能为空
+     * @return 自 1970-01-01T00:00:00Z 起的秒数(epoch second)
+     * @throws NullPointerException 如果 {@code sourceDateTime} 为 {@code null}
+     * @throws DateTimeException 如果转换过程中发生时间超出范围或其他时间处理异常
+     */
+    public static Long toEpochSecond(LocalDateTime sourceDateTime) {
+        return TemporalAccessorUtil.toInstant(sourceDateTime).getEpochSecond();
+    }
+
+}

+ 193 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/http/HttpUtils.java

@@ -0,0 +1,193 @@
+package com.xindazhou.framework.common.util.http;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.map.TableMap;
+import cn.hutool.core.net.url.UrlBuilder;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * HTTP 工具类
+ *
+ * @author 芋道源码
+ */
+public class HttpUtils {
+
+    /**
+     * 编码 URL 参数
+     *
+     * @param value 参数
+     * @return 编码后的参数
+     */
+    public static String encodeUtf8(String value) {
+        return URLEncoder.encode(value, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 解码 URL 参数
+     *
+     * @param value 参数
+     * @return 解码后的参数
+     */
+    public static String decodeUtf8(String value) {
+        return URLDecoder.decode(value, StandardCharsets.UTF_8);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static String replaceUrlQuery(String url, String key, String value) {
+        UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
+        // 先移除
+        TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
+                ReflectUtil.getFieldValue(builder.getQuery(), "query");
+        query.remove(key);
+        // 后添加
+        builder.addQuery(key, value);
+        return builder.build();
+    }
+
+    public static String removeUrlQuery(String url) {
+        if (!StrUtil.contains(url, '?')) {
+            return url;
+        }
+        UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
+        // 移除 query、fragment
+        builder.setQuery(null);
+        builder.setFragment(null);
+        return builder.build();
+    }
+
+    /**
+     * 拼接 URL
+     *
+     * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
+     *
+     * @param base 基础 URL
+     * @param query 查询参数
+     * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
+     * @param fragment URL 的 fragment,即拼接到 # 中
+     * @return 拼接后的 URL
+     */
+    public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
+        UriComponentsBuilder template = UriComponentsBuilder.newInstance();
+        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
+        URI redirectUri;
+        try {
+            // assume it's encoded to start with (if it came in over the wire)
+            redirectUri = builder.build(true).toUri();
+        } catch (Exception e) {
+            // ... but allow client registrations to contain hard-coded non-encoded values
+            redirectUri = builder.build().toUri();
+            builder = UriComponentsBuilder.fromUri(redirectUri);
+        }
+        template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
+                .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
+
+        if (fragment) {
+            StringBuilder values = new StringBuilder();
+            if (redirectUri.getFragment() != null) {
+                String append = redirectUri.getFragment();
+                values.append(append);
+            }
+            for (String key : query.keySet()) {
+                if (values.length() > 0) {
+                    values.append("&");
+                }
+                String name = key;
+                if (keys != null && keys.containsKey(key)) {
+                    name = keys.get(key);
+                }
+                values.append(name).append("={").append(key).append("}");
+            }
+            if (values.length() > 0) {
+                template.fragment(values.toString());
+            }
+            UriComponents encoded = template.build().expand(query).encode();
+            builder.fragment(encoded.getFragment());
+        } else {
+            for (String key : query.keySet()) {
+                String name = key;
+                if (keys != null && keys.containsKey(key)) {
+                    name = keys.get(key);
+                }
+                template.queryParam(name, "{" + key + "}");
+            }
+            template.fragment(redirectUri.getFragment());
+            UriComponents encoded = template.build().expand(query).encode();
+            builder.query(encoded.getQuery());
+        }
+        return builder.build().toUriString();
+    }
+
+    public static String[] obtainBasicAuthorization(HttpServletRequest request) {
+        String clientId;
+        String clientSecret;
+        // 先从 Header 中获取
+        String authorization = request.getHeader("Authorization");
+        authorization = StrUtil.subAfter(authorization, "Basic ", true);
+        if (StringUtils.hasText(authorization)) {
+            authorization = Base64.decodeStr(authorization);
+            clientId = StrUtil.subBefore(authorization, ":", false);
+            clientSecret = StrUtil.subAfter(authorization, ":", false);
+            // 再从 Param 中获取
+        } else {
+            clientId = request.getParameter("client_id");
+            clientSecret = request.getParameter("client_secret");
+        }
+
+        // 如果两者非空,则返回
+        if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
+            return new String[]{clientId, clientSecret};
+        }
+        return null;
+    }
+
+    /**
+     * HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
+     *
+     * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
+     *
+     * @param url URL
+     * @param headers 请求头
+     * @param requestBody 请求体
+     * @return 请求结果
+     */
+    public static String post(String url, Map<String, String> headers, String requestBody) {
+        try (HttpResponse response = HttpRequest.post(url)
+                .addHeaders(headers)
+                .body(requestBody)
+                .execute()) {
+            return response.body();
+        }
+    }
+
+    /**
+     * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
+     *
+     * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
+     *
+     * @param url URL
+     * @param headers 请求头
+     * @return 请求结果
+     */
+    public static String get(String url, Map<String, String> headers) {
+        try (HttpResponse response = HttpRequest.get(url)
+                .addHeaders(headers)
+                .execute()) {
+            return response.body();
+        }
+    }
+
+}

+ 61 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/io/FileUtils.java

@@ -0,0 +1,61 @@
+package com.xindazhou.framework.common.util.io;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import lombok.SneakyThrows;
+
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author 芋道源码
+ */
+public class FileUtils {
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(String data) {
+        File file = createTempFile();
+        // 写入内容
+        FileUtil.writeUtf8String(data, file);
+        return file;
+    }
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(byte[] data) {
+        File file = createTempFile();
+        // 写入内容
+        FileUtil.writeBytes(data, file);
+        return file;
+    }
+
+    /**
+     * 创建临时文件,无内容
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile() {
+        // 创建文件,通过 UUID 保证唯一
+        File file = File.createTempFile(IdUtil.simpleUUID(), null);
+        // 标记 JVM 退出时,自动删除
+        file.deleteOnExit();
+        return file;
+    }
+
+}

+ 28 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/io/IoUtils.java

@@ -0,0 +1,28 @@
+package com.xindazhou.framework.common.util.io;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.InputStream;
+
+/**
+ * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法
+ *
+ * @author 芋道源码
+ */
+public class IoUtils {
+
+    /**
+     * 从流中读取 UTF8 编码的内容
+     *
+     * @param in 输入流
+     * @param isClose 是否关闭
+     * @return 内容
+     * @throws IORuntimeException IO 异常
+     */
+    public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
+        return StrUtil.utf8Str(IoUtil.read(in, isClose));
+    }
+
+}

+ 232 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/JsonUtils.java

@@ -0,0 +1,232 @@
+package com.xindazhou.framework.common.util.json;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.xindazhou.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
+import com.xindazhou.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class JsonUtils {
+
+    @Getter
+    private static ObjectMapper objectMapper = new ObjectMapper();
+
+    static {
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
+        // 解决 LocalDateTime 的序列化
+        SimpleModule simpleModule = new JavaTimeModule()
+                .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
+                .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
+        objectMapper.registerModules(simpleModule);
+    }
+
+    /**
+     * 初始化 objectMapper 属性
+     * <p>
+     * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+     *
+     * @param objectMapper ObjectMapper 对象
+     */
+    public static void init(ObjectMapper objectMapper) {
+        JsonUtils.objectMapper = objectMapper;
+    }
+
+    @SneakyThrows
+    public static String toJsonString(Object object) {
+        return objectMapper.writeValueAsString(object);
+    }
+
+    @SneakyThrows
+    public static byte[] toJsonByte(Object object) {
+        return objectMapper.writeValueAsBytes(object);
+    }
+
+    @SneakyThrows
+    public static String toJsonPrettyString(Object object) {
+        return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+    }
+
+    public static <T> T parseObject(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, Type type) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(byte[] text, Type type) {
+        if (ArrayUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 将字符串解析成指定类型的对象
+     * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
+     * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
+     *
+     * @param text 字符串
+     * @param clazz 类型
+     * @return 对象
+     */
+    public static <T> T parseObject2(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        return JSONUtil.toBean(text, clazz);
+    }
+
+    public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
+        if (ArrayUtil.isEmpty(bytes)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(bytes, clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", bytes, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> T parseObject(String text, TypeReference<T> typeReference) {
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
+     *
+     * @param text 字符串
+     * @param typeReference 类型引用
+     * @return 指定类型的对象
+     */
+    public static <T> T parseObjectQuietly(String text, TypeReference<T> typeReference) {
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    public static <T> List<T> parseArray(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return new ArrayList<>();
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static <T> List<T> parseArray(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static JsonNode parseTree(String text) {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static JsonNode parseTree(byte[] text) {
+        try {
+            return objectMapper.readTree(text);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static boolean isJson(String text) {
+        return JSONUtil.isTypeJSON(text);
+    }
+
+    /**
+     * 判断字符串是否为 JSON 类型的字符串
+     * @param str 字符串
+     */
+    public static boolean isJsonObject(String str) {
+        return JSONUtil.isTypeJSONObject(str);
+    }
+
+}

+ 37 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/databind/NumberSerializer.java

@@ -0,0 +1,37 @@
+package com.xindazhou.framework.common.util.json.databind;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
+
+import java.io.IOException;
+
+/**
+ * Long 序列化规则
+ *
+ * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
+ *
+ * @author 星语
+ */
+@JacksonStdImpl
+public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
+
+    private static final long MAX_SAFE_INTEGER = 9007199254740991L;
+    private static final long MIN_SAFE_INTEGER = -9007199254740991L;
+
+    public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
+
+    public NumberSerializer(Class<? extends Number> rawType) {
+        super(rawType);
+    }
+
+    @Override
+    public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        // 超出范围 序列化位字符串
+        if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
+            super.serialize(value, gen, serializers);
+        } else {
+            gen.writeString(value.toString());
+        }
+    }
+}

+ 27 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java

@@ -0,0 +1,27 @@
+package com.xindazhou.framework.common.util.json.databind;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+/**
+ * 基于时间戳的 LocalDateTime 反序列化器
+ *
+ * @author 老五
+ */
+public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
+
+    public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
+
+    @Override
+    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        // 将 Long 时间戳,转换为 LocalDateTime 对象
+        return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
+    }
+
+}

+ 85 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java

@@ -0,0 +1,85 @@
+package com.xindazhou.framework.common.util.json.databind;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 基于时间戳的 LocalDateTime 序列化器
+ *
+ * @author 老五
+ */
+@Slf4j
+public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
+
+    public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
+
+    private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
+
+    @Override
+    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        // 情况一:有 JsonFormat 自定义注解,则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
+        String fieldName = gen.getOutputContext().getCurrentName();
+        if (fieldName != null) {
+            Object currentValue = gen.getOutputContext().getCurrentValue();
+            if (currentValue != null) {
+                Class<?> clazz = currentValue.getClass();
+                Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);
+                Field field = fieldMap.get(fieldName);
+                // 进一步修复:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1480
+                if (field != null && field.isAnnotationPresent(JsonFormat.class)) {
+                    JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class);
+                    try {
+                        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern());
+                        gen.writeString(formatter.format(value));
+                        return;
+                    } catch (Exception ex) {
+                        log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]",
+                                clazz.getName(), fieldName, ex);
+                    }
+                }
+            }
+        }
+
+        // 情况二:默认将 LocalDateTime 对象,转换为 Long 时间戳
+        gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+    }
+
+    /**
+     * 构建字段映射(缓存)
+     *
+     * @param clazz 类
+     * @return 字段映射
+     */
+    private Map<String, Field> buildFieldMap(Class<?> clazz) {
+        Map<String, Field> fieldMap = new HashMap<>();
+        for (Field field : ReflectUtil.getFields(clazz)) {
+            String fieldName = field.getName();
+            JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
+            if (jsonProperty != null) {
+                String value = jsonProperty.value();
+                if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) {
+                    fieldName = value;
+                }
+            }
+            fieldMap.put(fieldName, field);
+        }
+        return fieldMap;
+    }
+
+}

+ 30 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/monitor/TracerUtils.java

@@ -0,0 +1,30 @@
+package com.xindazhou.framework.common.util.monitor;
+
+import org.apache.skywalking.apm.toolkit.trace.TraceContext;
+
+/**
+ * 链路追踪工具类
+ *
+ * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
+ *
+ * @author 芋道源码
+ */
+public class TracerUtils {
+
+    /**
+     * 私有化构造方法
+     */
+    private TracerUtils() {
+    }
+
+    /**
+     * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
+     * 如果不存在的话为空字符串!!!
+     *
+     * @return 链路追踪编号
+     */
+    public static String getTraceId() {
+        return TraceContext.traceId();
+    }
+
+}

+ 131 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/number/MoneyUtils.java

@@ -0,0 +1,131 @@
+package com.xindazhou.framework.common.util.number;
+
+import cn.hutool.core.math.Money;
+import cn.hutool.core.util.NumberUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 金额工具类
+ *
+ * @author 芋道源码
+ */
+public class MoneyUtils {
+
+    /**
+     * 金额的小数位数
+     */
+    private static final int PRICE_SCALE = 2;
+
+    /**
+     * 百分比对应的 BigDecimal 对象
+     */
+    public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100);
+
+    /**
+     * 计算百分比金额,四舍五入
+     *
+     * @param price 金额
+     * @param rate  百分比,例如说 56.77% 则传入 56.77
+     * @return 百分比金额
+     */
+    public static Integer calculateRatePrice(Integer price, Double rate) {
+        return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue();
+    }
+
+    /**
+     * 计算百分比金额,向下传入
+     *
+     * @param price 金额
+     * @param rate  百分比,例如说 56.77% 则传入 56.77
+     * @return 百分比金额
+     */
+    public static Integer calculateRatePriceFloor(Integer price, Double rate) {
+        return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
+    }
+
+    /**
+     * 计算百分比金额
+     *
+     * @param price   金额(单位分)
+     * @param count   数量
+     * @param percent 折扣(单位分),列如 60.2%,则传入 6020
+     * @return 商品总价
+     */
+    public static Integer calculator(Integer price, Integer count, Integer percent) {
+        price = price * count;
+        if (percent == null) {
+            return price;
+        }
+        return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100));
+    }
+
+    /**
+     * 计算百分比金额
+     *
+     * @param price        金额
+     * @param rate         百分比,例如说 56.77% 则传入 56.77
+     * @param scale        保留小数位数
+     * @param roundingMode 舍入模式
+     */
+    public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
+        return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
+                .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
+    }
+
+    /**
+     * 分转元
+     *
+     * @param fen 分
+     * @return 元
+     */
+    public static BigDecimal fenToYuan(int fen) {
+        return new Money(0, fen).getAmount();
+    }
+
+    /**
+     * 分转元(字符串)
+     *
+     * 例如说 fen 为 1 时,则结果为 0.01
+     *
+     * @param fen 分
+     * @return 元
+     */
+    public static String fenToYuanStr(int fen) {
+        return new Money(0, fen).toString();
+    }
+
+    /**
+     * 金额相乘,默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price 金额
+     * @param count 数量
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) {
+        if (price == null || count == null) {
+            return null;
+        }
+        return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 金额相乘(百分比),默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price  金额
+     * @param percent 百分比
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) {
+        if (price == null || percent == null) {
+            return null;
+        }
+        return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
+}

+ 78 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/number/NumberUtils.java

@@ -0,0 +1,78 @@
+package com.xindazhou.framework.common.util.number;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
+ *
+ * @author 芋道源码
+ */
+public class NumberUtils {
+
+    public static Long parseLong(String str) {
+        return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
+    }
+
+    public static Integer parseInt(String str) {
+        return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null;
+    }
+
+    public static boolean isAllNumber(List<String> values) {
+        if (CollUtil.isEmpty(values)) {
+            return false;
+        }
+        for (String value : values) {
+            if (!NumberUtil.isNumber(value)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 通过经纬度获取地球上两点之间的距离
+     *
+     * 参考 <<a href="https://gitee.com/dromara/hutool/blob/1caabb586b1f95aec66a21d039c5695df5e0f4c1/hutool-core/src/main/java/cn/hutool/core/util/DistanceUtil.java">DistanceUtil</a>> 实现,目前它已经被 hutool 删除
+     *
+     * @param lat1 经度1
+     * @param lng1 纬度1
+     * @param lat2 经度2
+     * @param lng2 纬度2
+     * @return 距离,单位:千米
+     */
+    public static double getDistance(double lat1, double lng1, double lat2, double lng2) {
+        double radLat1 = lat1 * Math.PI / 180.0;
+        double radLat2 = lat2 * Math.PI / 180.0;
+        double a = radLat1 - radLat2;
+        double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
+        double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+                + Math.cos(radLat1) * Math.cos(radLat2)
+                * Math.pow(Math.sin(b / 2), 2)));
+        distance = distance * 6378.137;
+        distance = Math.round(distance * 10000d) / 10000d;
+        return distance;
+    }
+
+    /**
+     * 提供精确的乘法运算
+     *
+     * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null
+     *
+     * @param values 多个被乘值
+     * @return 积
+     */
+    public static BigDecimal mul(BigDecimal... values) {
+        for (BigDecimal value : values) {
+            if (value == null) {
+                return null;
+            }
+        }
+        return NumberUtil.mul(values);
+    }
+
+}

+ 69 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/object/BeanUtils.java

@@ -0,0 +1,69 @@
+package com.xindazhou.framework.common.util.object;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.xindazhou.framework.common.pojo.PageResult;
+import com.xindazhou.framework.common.util.collection.CollectionUtils;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Bean 工具类
+ *
+ * 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
+ * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
+ *
+ * @author 芋道源码
+ */
+public class BeanUtils {
+
+    public static <T> T toBean(Object source, Class<T> targetClass) {
+        return BeanUtil.toBean(source, targetClass);
+    }
+
+    public static <T> T toBean(Object source, Class<T> targetClass, Consumer<T> peek) {
+        T target = toBean(source, targetClass);
+        if (target != null) {
+            peek.accept(target);
+        }
+        return target;
+    }
+
+    public static <S, T> List<T> toBean(List<S> source, Class<T> targetType) {
+        if (source == null) {
+            return null;
+        }
+        return CollectionUtils.convertList(source, s -> toBean(s, targetType));
+    }
+
+    public static <S, T> List<T> toBean(List<S> source, Class<T> targetType, Consumer<T> peek) {
+        List<T> list = toBean(source, targetType);
+        if (list != null) {
+            list.forEach(peek);
+        }
+        return list;
+    }
+
+    public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
+        return toBean(source, targetType, null);
+    }
+
+    public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType, Consumer<T> peek) {
+        if (source == null) {
+            return null;
+        }
+        List<T> list = toBean(source.getList(), targetType);
+        if (peek != null) {
+            list.forEach(peek);
+        }
+        return new PageResult<>(list, source.getTotal());
+    }
+
+    public static void copyProperties(Object source, Object target) {
+        if (source == null || target == null) {
+            return;
+        }
+        BeanUtil.copyProperties(source, target, false);
+    }
+
+}

+ 67 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/object/ObjectUtils.java

@@ -0,0 +1,67 @@
+package com.xindazhou.framework.common.util.object;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+/**
+ * Object 工具类
+ *
+ * @author 芋道源码
+ */
+public class ObjectUtils {
+
+    /**
+     * 复制对象,并忽略 Id 编号
+     *
+     * @param object 被复制对象
+     * @param consumer 消费者,可以二次编辑被复制对象
+     * @return 复制后的对象
+     */
+    public static <T> T cloneIgnoreId(T object, Consumer<T> consumer) {
+        T result = ObjectUtil.clone(object);
+        // 忽略 id 编号
+        Field field = ReflectUtil.getField(object.getClass(), "id");
+        if (field != null) {
+            ReflectUtil.setFieldValue(result, field, null);
+        }
+        // 二次编辑
+        if (result != null) {
+            consumer.accept(result);
+        }
+        return result;
+    }
+
+    public static <T extends Comparable<T>> T max(T obj1, T obj2) {
+        if (obj1 == null) {
+            return obj2;
+        }
+        if (obj2 == null) {
+            return obj1;
+        }
+        return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+    }
+
+    @SafeVarargs
+    public static <T> T defaultIfNull(T... array) {
+        for (T item : array) {
+            if (item != null) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    @SafeVarargs
+    public static <T> boolean equalsAny(T obj, T... array) {
+        return Arrays.asList(array).contains(obj);
+    }
+
+    public static boolean isNotAllEmpty(Object... objs) {
+        return !ObjectUtil.isAllEmpty(objs);
+    }
+
+}

+ 67 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/object/PageUtils.java

@@ -0,0 +1,67 @@
+package com.xindazhou.framework.common.util.object;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.func.Func1;
+import cn.hutool.core.lang.func.LambdaUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.xindazhou.framework.common.pojo.PageParam;
+import com.xindazhou.framework.common.pojo.SortablePageParam;
+import com.xindazhou.framework.common.pojo.SortingField;
+import org.springframework.util.Assert;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * {@link com.xindazhou.framework.common.pojo.PageParam} 工具类
+ *
+ * @author 芋道源码
+ */
+public class PageUtils {
+
+    private static final Object[] ORDER_TYPES = new String[]{SortingField.ORDER_ASC, SortingField.ORDER_DESC};
+
+    public static int getStart(PageParam pageParam) {
+        return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
+    }
+
+    /**
+     * 构建排序字段(默认倒序)
+     *
+     * @param func 排序字段的 Lambda 表达式
+     * @param <T>  排序字段所属的类型
+     * @return 排序字段
+     */
+    public static <T> SortingField buildSortingField(Func1<T, ?> func) {
+        return buildSortingField(func, SortingField.ORDER_DESC);
+    }
+
+    /**
+     * 构建排序字段
+     *
+     * @param func  排序字段的 Lambda 表达式
+     * @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC}
+     * @param <T>   排序字段所属的类型
+     * @return 排序字段
+     */
+    public static <T> SortingField buildSortingField(Func1<T, ?> func, String order) {
+        Assert.isTrue(ArrayUtil.contains(ORDER_TYPES, order), String.format("字段的排序类型只能是 %s/%s", ORDER_TYPES));
+
+        String fieldName = LambdaUtil.getFieldName(func);
+        return new SortingField(fieldName, order);
+    }
+
+    /**
+     * 构建默认的排序字段
+     * 如果排序字段为空,则设置排序字段;否则忽略
+     *
+     * @param sortablePageParam 排序分页查询参数
+     * @param func              排序字段的 Lambda 表达式
+     * @param <T>               排序字段所属的类型
+     */
+    public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) {
+        if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) {
+            sortablePageParam.setSortingFields(singletonList(buildSortingField(func)));
+        }
+    }
+
+}

+ 7 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * 对于工具类的选择,优先查找 Hutool 中有没对应的方法
+ * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分
+ *
+ * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。
+ */
+package com.xindazhou.framework.common.util;

+ 105 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/servlet/ServletUtils.java

@@ -0,0 +1,105 @@
+package com.xindazhou.framework.common.util.servlet;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import com.xindazhou.framework.common.util.json.JsonUtils;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author 芋道源码
+ */
+public class ServletUtils {
+
+    /**
+     * 返回 JSON 字符串
+     *
+     * @param response 响应
+     * @param object   对象,会序列化成 JSON 字符串
+     */
+    @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
+    public static void writeJSON(HttpServletResponse response, Object object) {
+        String content = JsonUtils.toJsonString(object);
+        JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
+    }
+
+    /**
+     * @param request 请求
+     * @return ua
+     */
+    public static String getUserAgent(HttpServletRequest request) {
+        String ua = request.getHeader("User-Agent");
+        return ua != null ? ua : "";
+    }
+
+    /**
+     * 获得请求
+     *
+     * @return HttpServletRequest
+     */
+    public static HttpServletRequest getRequest() {
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (!(requestAttributes instanceof ServletRequestAttributes)) {
+            return null;
+        }
+        return ((ServletRequestAttributes) requestAttributes).getRequest();
+    }
+
+    public static String getUserAgent() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return getUserAgent(request);
+    }
+
+    public static String getClientIP() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return JakartaServletUtil.getClientIP(request);
+    }
+
+    public static boolean isJsonRequest(ServletRequest request) {
+        return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+    }
+
+    public static String getBody(HttpServletRequest request) {
+        // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
+        if (isJsonRequest(request)) {
+            return JakartaServletUtil.getBody(request);
+        }
+        return null;
+    }
+
+    public static byte[] getBodyBytes(HttpServletRequest request) {
+        // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
+        if (isJsonRequest(request)) {
+            return JakartaServletUtil.getBodyBytes(request);
+        }
+        return null;
+    }
+
+    public static String getClientIP(HttpServletRequest request) {
+        return JakartaServletUtil.getClientIP(request);
+    }
+
+    public static Map<String, String> getParamMap(HttpServletRequest request) {
+        return JakartaServletUtil.getParamMap(request);
+    }
+
+    public static Map<String, String> getHeaderMap(HttpServletRequest request) {
+        return JakartaServletUtil.getHeaderMap(request);
+    }
+
+}

+ 123 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/spring/SpringExpressionUtils.java

@@ -0,0 +1,123 @@
+package com.xindazhou.framework.common.util.spring;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.context.expression.BeanFactoryResolver;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Spring EL 表达式的工具类
+ *
+ * @author mashu
+ */
+public class SpringExpressionUtils {
+
+    /**
+     * Spring EL 表达式解析器
+     */
+    private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
+    /**
+     * 参数名发现器
+     */
+    private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+    private SpringExpressionUtils() {
+    }
+
+    /**
+     * 从切面中,单个解析 EL 表达式的结果
+     *
+     * @param joinPoint        切面点
+     * @param expressionString EL 表达式数组
+     * @return 执行界面
+     */
+    public static Object parseExpression(JoinPoint joinPoint, String expressionString) {
+        Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
+        return result.get(expressionString);
+    }
+
+    /**
+     * 从切面中,批量解析 EL 表达式的结果
+     *
+     * @param joinPoint         切面点
+     * @param expressionStrings EL 表达式数组
+     * @return 结果,key 为表达式,value 为对应值
+     */
+    public static Map<String, Object> parseExpressions(JoinPoint joinPoint, List<String> expressionStrings) {
+        // 如果为空,则不进行解析
+        if (CollUtil.isEmpty(expressionStrings)) {
+            return MapUtil.newHashMap();
+        }
+
+        // 第一步,构建解析的上下文 EvaluationContext
+        // 通过 joinPoint 获取被注解方法
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        Method method = methodSignature.getMethod();
+        // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
+        String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
+        // Spring 的表达式上下文对象
+        EvaluationContext context = new StandardEvaluationContext();
+        // 给上下文赋值
+        if (ArrayUtil.isNotEmpty(paramNames)) {
+            Object[] args = joinPoint.getArgs();
+            for (int i = 0; i < paramNames.length; i++) {
+                context.setVariable(paramNames[i], args[i]);
+            }
+        }
+
+        // 第二步,逐个参数解析
+        Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true);
+        expressionStrings.forEach(key -> {
+            Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
+            result.put(key, value);
+        });
+        return result;
+    }
+
+    /**
+     * 从 Bean 工厂,解析 EL 表达式的结果
+     *
+     * @param expressionString EL 表达式
+     * @return 执行界面
+     */
+    public static Object parseExpression(String expressionString) {
+        return parseExpression(expressionString, null);
+    }
+
+    /**
+     * 从 Bean 工厂,解析 EL 表达式的结果
+     *
+     * @param expressionString EL 表达式
+     * @param variables        变量
+     * @return 执行界面
+     */
+    public static Object parseExpression(String expressionString, Map<String, Object> variables) {
+        if (StrUtil.isBlank(expressionString)) {
+            return null;
+        }
+        Expression expression = EXPRESSION_PARSER.parseExpression(expressionString);
+        StandardEvaluationContext context = new StandardEvaluationContext();
+        context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext()));
+        if (MapUtil.isNotEmpty(variables)) {
+            context.setVariables(variables);
+        }
+        return expression.getValue(context);
+    }
+
+}

+ 24 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/spring/SpringUtils.java

@@ -0,0 +1,24 @@
+package com.xindazhou.framework.common.util.spring;
+
+import cn.hutool.extra.spring.SpringUtil;
+
+import java.util.Objects;
+
+/**
+ * Spring 工具类
+ *
+ * @author 芋道源码
+ */
+public class SpringUtils extends SpringUtil {
+
+    /**
+     * 是否为生产环境
+     *
+     * @return 是否生产环境
+     */
+    public static boolean isProd() {
+        String activeProfile = getActiveProfile();
+        return Objects.equals("prod", activeProfile);
+    }
+
+}

+ 107 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/string/StrUtils.java

@@ -0,0 +1,107 @@
+package com.xindazhou.framework.common.util.string;
+
+import cn.hutool.core.text.StrPool;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import org.aspectj.lang.JoinPoint;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author 芋道源码
+ */
+public class StrUtils {
+
+    public static String maxLength(CharSequence str, int maxLength) {
+        return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
+    }
+
+    /**
+     * 给定字符串是否以任何一个字符串开始
+     * 给定字符串和数组为空都返回 false
+     *
+     * @param str      给定字符串
+     * @param prefixes 需要检测的开始字符串
+     * @since 3.0.6
+     */
+    public static boolean startWithAny(String str, Collection<String> prefixes) {
+        if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
+            return false;
+        }
+
+        for (CharSequence suffix : prefixes) {
+            if (StrUtil.startWith(str, suffix, false)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static List<Long> splitToLong(String value, CharSequence separator) {
+        long[] longs = StrUtil.splitToLong(value, separator);
+        return Arrays.stream(longs).boxed().collect(Collectors.toList());
+    }
+
+    public static Set<Long> splitToLongSet(String value) {
+        return splitToLongSet(value, StrPool.COMMA);
+    }
+
+    public static Set<Long> splitToLongSet(String value, CharSequence separator) {
+        long[] longs = StrUtil.splitToLong(value, separator);
+        return Arrays.stream(longs).boxed().collect(Collectors.toSet());
+    }
+
+    public static List<Integer> splitToInteger(String value, CharSequence separator) {
+        int[] integers = StrUtil.splitToInt(value, separator);
+        return Arrays.stream(integers).boxed().collect(Collectors.toList());
+    }
+
+    /**
+     * 移除字符串中,包含指定字符串的行
+     *
+     * @param content 字符串
+     * @param sequence 包含的字符串
+     * @return 移除后的字符串
+     */
+    public static String removeLineContains(String content, String sequence) {
+        if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
+            return content;
+        }
+        return Arrays.stream(content.split("\n"))
+                .filter(line -> !line.contains(sequence))
+                .collect(Collectors.joining("\n"));
+    }
+
+    /**
+     * 拼接方法的参数
+     *
+     * 特殊:排除一些无法序列化的参数,如 ServletRequest、ServletResponse、MultipartFile
+     *
+     * @param joinPoint 连接点
+     * @return 拼接后的参数
+     */
+    public static String joinMethodArgs(JoinPoint joinPoint) {
+        Object[] args = joinPoint.getArgs();
+        if (ArrayUtil.isEmpty(args)) {
+            return "";
+        }
+        return ArrayUtil.join(args, ",", item -> {
+            if (item == null) {
+                return "";
+            }
+            // 讨论可见:https://t.zsxq.com/XUJVk、https://t.zsxq.com/MnKcL
+            String clazzName = item.getClass().getName();
+            if (StrUtil.startWithAny(clazzName, "javax.servlet", "jakarta.servlet", "org.springframework.web")) {
+                return "";
+            }
+            return item;
+        });
+    }
+
+}

+ 55 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/util/validation/ValidationUtils.java

@@ -0,0 +1,55 @@
+package com.xindazhou.framework.common.util.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import org.springframework.util.StringUtils;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 校验工具类
+ *
+ * @author 芋道源码
+ */
+public class ValidationUtils {
+
+    private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$");
+
+    private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
+
+    private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
+
+    public static boolean isMobile(String mobile) {
+        return StringUtils.hasText(mobile)
+                && PATTERN_MOBILE.matcher(mobile).matches();
+    }
+
+    public static boolean isURL(String url) {
+        return StringUtils.hasText(url)
+                && PATTERN_URL.matcher(url).matches();
+    }
+
+    public static boolean isXmlNCName(String str) {
+        return StringUtils.hasText(str)
+                && PATTERN_XML_NCNAME.matcher(str).matches();
+    }
+
+    public static void validate(Object object, Class<?>... groups) {
+        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+        Assert.notNull(validator);
+        validate(validator, object, groups);
+    }
+
+    public static void validate(Validator validator, Object object, Class<?>... groups) {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+        if (CollUtil.isNotEmpty(constraintViolations)) {
+            throw new ConstraintViolationException(constraintViolations);
+        }
+    }
+
+}

+ 35 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/InEnum.java

@@ -0,0 +1,35 @@
+package com.xindazhou.framework.common.validation;
+
+import com.xindazhou.framework.common.core.ArrayValuable;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class}
+)
+public @interface InEnum {
+
+    /**
+     * @return 实现 ArrayValuable 接口的类
+     */
+    Class<? extends ArrayValuable<?>> value();
+
+    String message() default "必须在指定范围 {value}";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 44 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/InEnumCollectionValidator.java

@@ -0,0 +1,44 @@
+package com.xindazhou.framework.common.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import com.xindazhou.framework.common.core.ArrayValuable;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class InEnumCollectionValidator implements ConstraintValidator<InEnum, Collection<?>> {
+
+    private List<?> values;
+
+    @Override
+    public void initialize(InEnum annotation) {
+        ArrayValuable<?>[] values = annotation.value().getEnumConstants();
+        if (values.length == 0) {
+            this.values = Collections.emptyList();
+        } else {
+            this.values = Arrays.asList(values[0].array());
+        }
+    }
+
+    @Override
+    public boolean isValid(Collection<?> list, ConstraintValidatorContext context) {
+        if (list == null) {
+            return true;
+        }
+        // 校验通过
+        if (CollUtil.containsAll(values, list)) {
+            return true;
+        }
+        // 校验不通过,自定义提示语句
+        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+                .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
+        return false;
+    }
+
+}
+

+ 43 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/InEnumValidator.java

@@ -0,0 +1,43 @@
+package com.xindazhou.framework.common.validation;
+
+import com.xindazhou.framework.common.core.ArrayValuable;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class InEnumValidator implements ConstraintValidator<InEnum, Object> {
+
+    private List<?> values;
+
+    @Override
+    public void initialize(InEnum annotation) {
+        ArrayValuable<?>[] values = annotation.value().getEnumConstants();
+        if (values.length == 0) {
+            this.values = Collections.emptyList();
+        } else {
+            this.values = Arrays.asList(values[0].array());
+        }
+    }
+
+    @Override
+    public boolean isValid(Object value, ConstraintValidatorContext context) {
+        // 为空时,默认不校验,即认为通过
+        if (value == null) {
+            return true;
+        }
+        // 校验通过
+        if (values.contains(value)) {
+            return true;
+        }
+        // 校验不通过,自定义提示语句
+        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+                .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
+        return false;
+    }
+
+}
+

+ 28 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/Mobile.java

@@ -0,0 +1,28 @@
+package com.xindazhou.framework.common.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = MobileValidator.class
+)
+public @interface Mobile {
+
+    String message() default "手机号格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 25 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/MobileValidator.java

@@ -0,0 +1,25 @@
+package com.xindazhou.framework.common.validation;
+
+import cn.hutool.core.util.StrUtil;
+import com.xindazhou.framework.common.util.validation.ValidationUtils;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+public class MobileValidator implements ConstraintValidator<Mobile, String> {
+
+    @Override
+    public void initialize(Mobile annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (StrUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return ValidationUtils.isMobile(value);
+    }
+
+}

+ 28 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/Telephone.java

@@ -0,0 +1,28 @@
+package com.xindazhou.framework.common.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = TelephoneValidator.class
+)
+public @interface Telephone {
+
+    String message() default "电话格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 25 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/TelephoneValidator.java

@@ -0,0 +1,25 @@
+package com.xindazhou.framework.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.PhoneUtil;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
+
+    @Override
+    public void initialize(Telephone annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (CharSequenceUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value);
+    }
+
+}

+ 4 - 0
xdz-framework/xdz-common/src/main/java/com/xindazhou/framework/common/validation/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 使用 Hibernate Validator 实现参数校验
+ */
+package com.xindazhou.framework.common.validation;

+ 52 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/pom.xml

@@ -0,0 +1,52 @@
+<?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>xdz-framework</artifactId>
+        <groupId>com.xindazhou</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>xdz-spring-boot-starter-biz-data-permission</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>数据权限</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xindazhou</groupId>
+            <artifactId>xdz-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>com.xindazhou</groupId>
+            <artifactId>xdz-spring-boot-starter-security</artifactId>
+            <optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>com.xindazhou</groupId>
+            <artifactId>xdz-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>com.xindazhou</groupId>
+            <artifactId>xdz-spring-boot-starter-rpc</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>com.xindazhou</groupId>
+            <artifactId>xdz-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 46 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java

@@ -0,0 +1,46 @@
+package com.xindazhou.framework.datapermission.config;
+
+import com.xindazhou.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
+import com.xindazhou.framework.datapermission.core.db.DataPermissionRuleHandler;
+import com.xindazhou.framework.datapermission.core.rule.DataPermissionRule;
+import com.xindazhou.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import com.xindazhou.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
+import com.xindazhou.framework.mybatis.core.util.MyBatisUtils;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+import java.util.List;
+
+/**
+ * 数据权限的自动配置类
+ *
+ * @author 芋道源码
+ */
+@AutoConfiguration
+public class YudaoDataPermissionAutoConfiguration {
+
+    @Bean
+    public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
+        return new DataPermissionRuleFactoryImpl(rules);
+    }
+
+    @Bean
+    public DataPermissionRuleHandler dataPermissionRuleHandler(MybatisPlusInterceptor interceptor,
+                                                               DataPermissionRuleFactory ruleFactory) {
+        // 创建 DataPermissionInterceptor 拦截器
+        DataPermissionRuleHandler handler = new DataPermissionRuleHandler(ruleFactory);
+        DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
+        // 添加到 interceptor 中
+        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
+        MyBatisUtils.addInterceptor(interceptor, inner, 0);
+        return handler;
+    }
+
+    @Bean
+    public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
+        return new DataPermissionAnnotationAdvisor();
+    }
+
+}

+ 34 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/config/YudaoDataPermissionRpcAutoConfiguration.java

@@ -0,0 +1,34 @@
+package com.xindazhou.framework.datapermission.config;
+
+import com.xindazhou.framework.datapermission.core.rpc.DataPermissionRequestInterceptor;
+import com.xindazhou.framework.datapermission.core.rpc.DataPermissionRpcWebFilter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+import static com.xindazhou.framework.common.enums.WebFilterOrderEnum.TENANT_CONTEXT_FILTER;
+
+/**
+ * 数据权限针对 RPC 的自动配置类
+ *
+ * @author 芋道源码
+ */
+@AutoConfiguration
+@ConditionalOnClass(name = "feign.RequestInterceptor")
+public class YudaoDataPermissionRpcAutoConfiguration {
+
+    @Bean
+    public DataPermissionRequestInterceptor dataPermissionRequestInterceptor() {
+        return new DataPermissionRequestInterceptor();
+    }
+
+    @Bean
+    public FilterRegistrationBean<DataPermissionRpcWebFilter> dataPermissionRpcFilter() {
+        FilterRegistrationBean<DataPermissionRpcWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new DataPermissionRpcWebFilter());
+        registrationBean.setOrder(TENANT_CONTEXT_FILTER - 1); // 顺序没有绝对的要求,在租户 Filter 前面稳妥点
+        return registrationBean;
+    }
+
+}

+ 45 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java

@@ -0,0 +1,45 @@
+package com.xindazhou.framework.datapermission.config;
+
+import cn.hutool.extra.spring.SpringUtil;
+import com.xindazhou.framework.common.biz.system.permission.PermissionCommonApi;
+import com.xindazhou.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
+import com.xindazhou.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
+import com.xindazhou.framework.security.core.LoginUser;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+
+import java.util.List;
+
+/**
+ * 基于部门的数据权限 AutoConfiguration
+ *
+ * @author 芋道源码
+ */
+@AutoConfiguration
+@ConditionalOnClass(LoginUser.class)
+@ConditionalOnBean(value = {DeptDataPermissionRuleCustomizer.class})
+public class YudaoDeptDataPermissionAutoConfiguration {
+
+    @Bean
+    public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi,
+                                                         List<DeptDataPermissionRuleCustomizer> customizers) {
+        // Cloud 专属逻辑:优先使用本地的 PermissionApi 实现类,而不是 Feign 调用
+        // 原因:在创建租户时,租户还没创建好,导致 Feign 调用获取数据权限时,报“租户不存在”的错误
+        try {
+            PermissionCommonApi permissionApiImpl = SpringUtil.getBean("permissionApiImpl", PermissionCommonApi.class);
+            if (permissionApiImpl != null) {
+                permissionApi = permissionApiImpl;
+            }
+        } catch (Exception ignored) {}
+
+        // 创建 DeptDataPermissionRule 对象
+        DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
+        // 补全表配置
+        customizers.forEach(customizer -> customizer.customize(rule));
+        return rule;
+    }
+
+}

+ 35 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/annotation/DataPermission.java

@@ -0,0 +1,35 @@
+package com.xindazhou.framework.datapermission.core.annotation;
+
+import com.xindazhou.framework.datapermission.core.rule.DataPermissionRule;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限注解
+ * 可声明在类或者方法上,标识使用的数据权限规则
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataPermission {
+
+    /**
+     * 当前类或方法是否开启数据权限
+     * 即使不添加 @DataPermission 注解,默认是开启状态
+     * 可通过设置 enable 为 false 禁用
+     */
+    boolean enable() default true;
+
+    /**
+     * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()}
+     */
+    Class<? extends DataPermissionRule>[] includeRules() default {};
+
+    /**
+     * 排除的数据权限规则数组,优先级最低
+     */
+    Class<? extends DataPermissionRule>[] excludeRules() default {};
+
+}

+ 36 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java

@@ -0,0 +1,36 @@
+package com.xindazhou.framework.datapermission.core.aop;
+
+import com.xindazhou.framework.datapermission.core.annotation.DataPermission;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import org.aopalliance.aop.Advice;
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.AbstractPointcutAdvisor;
+import org.springframework.aop.support.ComposablePointcut;
+import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
+
+/**
+ * {@link com.xindazhou.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类
+ *
+ * @author 芋道源码
+ */
+@Getter
+@EqualsAndHashCode(callSuper = true)
+public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
+
+    private final Advice advice;
+
+    private final Pointcut pointcut;
+
+    public DataPermissionAnnotationAdvisor() {
+        this.advice = new DataPermissionAnnotationInterceptor();
+        this.pointcut = this.buildPointcut();
+    }
+
+    protected Pointcut buildPointcut() {
+        Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
+        Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
+        return new ComposablePointcut(classPointcut).union(methodPointcut);
+    }
+
+}

+ 72 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java

@@ -0,0 +1,72 @@
+package com.xindazhou.framework.datapermission.core.aop;
+
+import com.xindazhou.framework.datapermission.core.annotation.DataPermission;
+import lombok.Getter;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.core.MethodClassKey;
+import org.springframework.core.annotation.AnnotationUtils;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link DataPermission} 注解的拦截器
+ * 1. 在执行方法前,将 @DataPermission 注解入栈
+ * 2. 在执行方法后,将 @DataPermission 注解出栈
+ *
+ * @author 芋道源码
+ */
+@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
+public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
+
+    /**
+     * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位
+     */
+    static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
+
+    @Getter
+    private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();
+
+    @Override
+    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
+        // 入栈
+        DataPermission dataPermission = this.findAnnotation(methodInvocation);
+        if (dataPermission != null) {
+            DataPermissionContextHolder.add(dataPermission);
+        }
+        try {
+            // 执行逻辑
+            return methodInvocation.proceed();
+        } finally {
+            // 出栈
+            if (dataPermission != null) {
+                DataPermissionContextHolder.remove();
+            }
+        }
+    }
+
+    private DataPermission findAnnotation(MethodInvocation methodInvocation) {
+        // 1. 从缓存中获取
+        Method method = methodInvocation.getMethod();
+        Object targetObject = methodInvocation.getThis();
+        Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
+        MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
+        DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
+        if (dataPermission != null) {
+            return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
+        }
+
+        // 2.1 从方法中获取
+        dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
+        // 2.2 从类上获取
+        if (dataPermission == null) {
+            dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
+        }
+        // 2.3 添加到缓存中
+        dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
+        return dataPermission;
+    }
+
+}

+ 72 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/aop/DataPermissionContextHolder.java

@@ -0,0 +1,72 @@
+package com.xindazhou.framework.datapermission.core.aop;
+
+import com.xindazhou.framework.datapermission.core.annotation.DataPermission;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * {@link DataPermission} 注解的 Context 上下文
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionContextHolder {
+
+    /**
+     * 使用 List 的原因,可能存在方法的嵌套调用
+     */
+    private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
+            TransmittableThreadLocal.withInitial(LinkedList::new);
+
+    /**
+     * 获得当前的 DataPermission 注解
+     *
+     * @return DataPermission 注解
+     */
+    public static DataPermission get() {
+        return DATA_PERMISSIONS.get().peekLast();
+    }
+
+    /**
+     * 入栈 DataPermission 注解
+     *
+     * @param dataPermission DataPermission 注解
+     */
+    public static void add(DataPermission dataPermission) {
+        DATA_PERMISSIONS.get().addLast(dataPermission);
+    }
+
+    /**
+     * 出栈 DataPermission 注解
+     *
+     * @return DataPermission 注解
+     */
+    public static DataPermission remove() {
+        DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
+        // 无元素时,清空 ThreadLocal
+        if (DATA_PERMISSIONS.get().isEmpty()) {
+            DATA_PERMISSIONS.remove();
+        }
+        return dataPermission;
+    }
+
+    /**
+     * 获得所有 DataPermission
+     *
+     * @return DataPermission 队列
+     */
+    public static List<DataPermission> getAll() {
+        return DATA_PERMISSIONS.get();
+    }
+
+    /**
+     * 清空上下文
+     *
+     * 目前仅仅用于单测
+     */
+    public static void clear() {
+        DATA_PERMISSIONS.remove();
+    }
+
+}

+ 64 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/db/DataPermissionRuleHandler.java

@@ -0,0 +1,64 @@
+package com.xindazhou.framework.datapermission.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import com.xindazhou.framework.datapermission.core.rule.DataPermissionRule;
+import com.xindazhou.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import com.xindazhou.framework.mybatis.core.util.MyBatisUtils;
+import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
+import lombok.RequiredArgsConstructor;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.schema.Table;
+
+import java.util.List;
+
+import static com.xindazhou.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
+
+/**
+ * 基于 {@link DataPermissionRule} 的数据权限处理器
+ *
+ * 它的底层,是基于 MyBatis Plus 的 <a href="https://baomidou.com/plugins/data-permission/">数据权限插件</a>
+ * 核心原理:它会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
+
+    private final DataPermissionRuleFactory ruleFactory;
+
+    @Override
+    public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
+        // 特殊:跨租户访问
+        if (skipPermissionCheck()) {
+            return null;
+        }
+
+        // 获得 Mapper 对应的数据权限的规则
+        List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
+        if (CollUtil.isEmpty(rules)) {
+            return null;
+        }
+
+        // 生成条件
+        Expression allExpression = null;
+        for (DataPermissionRule rule : rules) {
+            // 判断表名是否匹配
+            String tableName = MyBatisUtils.getTableName(table);
+            if (!rule.getTableNames().contains(tableName)) {
+                continue;
+            }
+
+            // 单条规则的条件
+            Expression oneExpress = rule.getExpression(tableName, table.getAlias());
+            if (oneExpress == null) {
+                continue;
+            }
+            // 拼接到 allExpression 中
+            allExpression = allExpression == null ? oneExpress
+                    : new AndExpression(allExpression, oneExpress);
+        }
+        return allExpression;
+    }
+
+}

+ 27 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rpc/DataPermissionRequestInterceptor.java

@@ -0,0 +1,27 @@
+package com.xindazhou.framework.datapermission.core.rpc;
+
+import com.xindazhou.framework.datapermission.core.annotation.DataPermission;
+import com.xindazhou.framework.datapermission.core.aop.DataPermissionContextHolder;
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+
+/**
+ * DataPermission 的 RequestInterceptor 实现类:Feign 请求时,将 {@link DataPermission} 设置到 header 中,继续透传给被调用的服务
+ *
+ * 注意:由于 {@link DataPermission} 不支持序列化和反序列化,所以暂时只能传递它的 enable 属性
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionRequestInterceptor implements RequestInterceptor {
+
+    public static final String ENABLE_HEADER_NAME = "data-permission-enable";
+
+    @Override
+    public void apply(RequestTemplate requestTemplate) {
+        DataPermission dataPermission = DataPermissionContextHolder.get();
+        if (dataPermission != null && Boolean.FALSE.equals(dataPermission.enable())) {
+            requestTemplate.header(ENABLE_HEADER_NAME, "false");
+        }
+    }
+
+}

+ 38 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rpc/DataPermissionRpcWebFilter.java

@@ -0,0 +1,38 @@
+package com.xindazhou.framework.datapermission.core.rpc;
+
+import com.xindazhou.framework.datapermission.core.aop.DataPermissionContextHolder;
+import com.xindazhou.framework.datapermission.core.util.DataPermissionUtils;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * 针对 {@link DataPermissionRequestInterceptor} 的 RPC 调用,设置 {@link DataPermissionContextHolder} 的上下文
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionRpcWebFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        String enable = request.getHeader(DataPermissionRequestInterceptor.ENABLE_HEADER_NAME);
+        if (Objects.equals(enable, Boolean.FALSE.toString())) {
+            DataPermissionUtils.executeIgnore(() -> {
+                try {
+                    chain.doFilter(request, response);
+                } catch (IOException | ServletException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+
+}

+ 36 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/DataPermissionRule.java

@@ -0,0 +1,36 @@
+package com.xindazhou.framework.datapermission.core.rule;
+
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+
+import java.util.Set;
+
+/**
+ * 数据权限规则接口
+ * 通过实现接口,自定义数据规则。例如说,
+ *
+ * @author 芋道源码
+ */
+public interface DataPermissionRule {
+
+    /**
+     * 返回需要生效的表名数组
+     * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
+     *
+     * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得
+     *
+     * @return 表名数组
+     */
+    Set<String> getTableNames();
+
+    /**
+     * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
+     *
+     * @param tableName 表名
+     * @param tableAlias 别名,可能为空
+     * @return 过滤条件 Expression 表达式
+     */
+    Expression getExpression(String tableName, Alias tableAlias);
+
+}

+ 28 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/DataPermissionRuleFactory.java

@@ -0,0 +1,28 @@
+package com.xindazhou.framework.datapermission.core.rule;
+
+import java.util.List;
+
+/**
+ * {@link DataPermissionRule} 工厂接口
+ * 作为 {@link DataPermissionRule} 的容器,提供管理能力
+ *
+ * @author 芋道源码
+ */
+public interface DataPermissionRuleFactory {
+
+    /**
+     * 获得所有数据权限规则数组
+     *
+     * @return 数据权限规则数组
+     */
+    List<DataPermissionRule> getDataPermissionRules();
+
+    /**
+     * 获得指定 Mapper 的数据权限规则数组
+     *
+     * @param mappedStatementId 指定 Mapper 的编号
+     * @return 数据权限规则数组
+     */
+    List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);
+
+}

+ 84 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java

@@ -0,0 +1,84 @@
+package com.xindazhou.framework.datapermission.core.rule;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.xindazhou.framework.datapermission.core.annotation.DataPermission;
+import com.xindazhou.framework.datapermission.core.aop.DataPermissionContextHolder;
+import com.fhs.trans.service.impl.SimpleTransService;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 默认的 DataPermissionRuleFactoryImpl 实现类
+ * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {
+
+    /**
+     * 数据权限规则数组
+     */
+    private final List<DataPermissionRule> rules;
+
+    @Override
+    public List<DataPermissionRule> getDataPermissionRules() {
+        return rules;
+    }
+
+    @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
+    public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
+        // 1.1 无数据权限
+        if (CollUtil.isEmpty(rules)) {
+            return Collections.emptyList();
+        }
+        // 1.2 未配置,则默认开启
+        DataPermission dataPermission = DataPermissionContextHolder.get();
+        if (dataPermission == null) {
+            return rules;
+        }
+        // 1.3 已配置,但禁用
+        if (!dataPermission.enable()) {
+            return Collections.emptyList();
+        }
+        // 1.4 特殊:数据翻译时,强制忽略数据权限 https://github.com/YunaiV/ruoyi-vue-pro/issues/1007
+        if (isTranslateCall()) {
+            return Collections.emptyList();
+        }
+
+        // 2.1 情况一:已配置,只选择部分规则
+        if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
+            return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
+                    .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
+        }
+        // 2.2 已配置,只排除部分规则
+        if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
+            return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
+                    .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
+        }
+        // 2.3 已配置,全部规则
+        return rules;
+    }
+
+    /**
+     * 判断是否为数据翻译 {@link com.fhs.core.trans.anno.Trans} 的调用
+     *
+     * 目前暂时只有这个办法,已经和 easy-trans 做过沟通
+     *
+     * @return 是否
+     */
+    private boolean isTranslateCall() {
+        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+        for (StackTraceElement e : stack) {
+            if (SimpleTransService.class.getName().equals(e.getClassName())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

+ 207 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java

@@ -0,0 +1,207 @@
+package com.xindazhou.framework.datapermission.core.rule.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.xindazhou.framework.common.biz.system.permission.PermissionCommonApi;
+import com.xindazhou.framework.common.enums.UserTypeEnum;
+import com.xindazhou.framework.common.util.collection.CollectionUtils;
+import com.xindazhou.framework.common.util.json.JsonUtils;
+import com.xindazhou.framework.datapermission.core.rule.DataPermissionRule;
+import com.xindazhou.framework.mybatis.core.dataobject.BaseDO;
+import com.xindazhou.framework.mybatis.core.util.MyBatisUtils;
+import com.xindazhou.framework.security.core.LoginUser;
+import com.xindazhou.framework.security.core.util.SecurityFrameworkUtils;
+import com.xindazhou.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.*;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 基于部门的 {@link DataPermissionRule} 数据权限规则实现
+ *
+ * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
+ *
+ * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
+ * 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【yudao-server 采用该方案】
+ * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
+ *  1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】
+ *      最终过滤条件是 WHERE dept_id = ?
+ *  2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号;
+ *      最终过滤条件是 WHERE user_id IN (?, ?, ? ...)
+ *  3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤;
+ *      最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...)
+ *
+ * @author 芋道源码
+ */
+@AllArgsConstructor
+@Slf4j
+public class DeptDataPermissionRule implements DataPermissionRule {
+
+    /**
+     * LoginUser 的 Context 缓存 Key
+     */
+    protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName();
+
+    private static final String DEPT_COLUMN_NAME = "dept_id";
+    private static final String USER_COLUMN_NAME = "user_id";
+
+    static final Expression EXPRESSION_NULL = new NullValue();
+
+    private final PermissionCommonApi permissionApi;
+
+    /**
+     * 基于部门的表字段配置
+     * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
+     *
+     * key:表名
+     * value:字段名
+     */
+    private final Map<String, String> deptColumns = new HashMap<>();
+    /**
+     * 基于用户的表字段配置
+     * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
+     *
+     * key:表名
+     * value:字段名
+     */
+    private final Map<String, String> userColumns = new HashMap<>();
+    /**
+     * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集
+     */
+    private final Set<String> TABLE_NAMES = new HashSet<>();
+
+    @Override
+    public Set<String> getTableNames() {
+        return TABLE_NAMES;
+    }
+
+    @Override
+    public Expression getExpression(String tableName, Alias tableAlias) {
+        // 只有有登陆用户的情况下,才进行数据权限的处理
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        if (loginUser == null) {
+            return null;
+        }
+        // 只有管理员类型的用户,才进行数据权限的处理
+        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
+            return null;
+        }
+
+        // 获得数据权限
+        DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
+        // 从上下文中拿不到,则调用逻辑进行获取
+        if (deptDataPermission == null) {
+            deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()).getCheckedData();
+            if (deptDataPermission == null) {
+                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
+                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
+                        loginUser.getId(), tableName, tableAlias.getName()));
+            }
+            // 添加到上下文中,避免重复计算
+            loginUser.setContext(CONTEXT_KEY, deptDataPermission);
+        }
+
+        // 情况一,如果是 ALL 可查看全部,则无需拼接条件
+        if (deptDataPermission.getAll()) {
+            return null;
+        }
+
+        // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
+        if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
+                && Boolean.FALSE.equals(deptDataPermission.getSelf())) {
+            return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
+        }
+
+        // 情况三,拼接 Dept 和 User 的条件,最后组合
+        Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
+        Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
+        if (deptExpression == null && userExpression == null) {
+            // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
+            log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
+                    JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
+//            throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
+//                    loginUser.getId(), tableName, tableAlias.getName()));
+            return EXPRESSION_NULL;
+        }
+        if (deptExpression == null) {
+            return userExpression;
+        }
+        if (userExpression == null) {
+            return deptExpression;
+        }
+        // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
+        return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression));
+    }
+
+    private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
+        // 如果不存在配置,则无需作为条件
+        String columnName = deptColumns.get(tableName);
+        if (StrUtil.isEmpty(columnName)) {
+            return null;
+        }
+        // 如果为空,则无条件
+        if (CollUtil.isEmpty(deptIds)) {
+            return null;
+        }
+        // 拼接条件
+        return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
+                // Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号
+                new ParenthesedExpressionList(new ExpressionList<LongValue>(CollectionUtils.convertList(deptIds, LongValue::new))));
+    }
+
+    private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {
+        // 如果不查看自己,则无需作为条件
+        if (Boolean.FALSE.equals(self)) {
+            return null;
+        }
+        String columnName = userColumns.get(tableName);
+        if (StrUtil.isEmpty(columnName)) {
+            return null;
+        }
+        // 拼接条件
+        return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));
+    }
+
+    // ==================== 添加配置 ====================
+
+    public void addDeptColumn(Class<? extends BaseDO> entityClass) {
+        addDeptColumn(entityClass, DEPT_COLUMN_NAME);
+    }
+
+    public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) {
+        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
+        addDeptColumn(tableName, columnName);
+    }
+
+    public void addDeptColumn(String tableName, String columnName) {
+        deptColumns.put(tableName, columnName);
+        TABLE_NAMES.add(tableName);
+    }
+
+    public void addUserColumn(Class<? extends BaseDO> entityClass) {
+        addUserColumn(entityClass, USER_COLUMN_NAME);
+    }
+
+    public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) {
+        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
+        addUserColumn(tableName, columnName);
+    }
+
+    public void addUserColumn(String tableName, String columnName) {
+        userColumns.put(tableName, columnName);
+        TABLE_NAMES.add(tableName);
+    }
+
+}

+ 20 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java

@@ -0,0 +1,20 @@
+package com.xindazhou.framework.datapermission.core.rule.dept;
+
+/**
+ * {@link DeptDataPermissionRule} 的自定义配置接口
+ *
+ * @author 芋道源码
+ */
+@FunctionalInterface
+public interface DeptDataPermissionRuleCustomizer {
+
+    /**
+     * 自定义该权限规则
+     * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则
+     * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则
+     *
+     * @param rule 权限规则
+     */
+    void customize(DeptDataPermissionRule rule);
+
+}

+ 0 - 0
xdz-framework/xdz-spring-boot-starter-biz-data-permission/src/main/java/com/xindazhou/framework/datapermission/core/rule/dept/package-info.java


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