Browse Source

空项目

malz 1 year ago
commit
8e5c6147b5
100 changed files with 7791 additions and 0 deletions
  1. 1 0
      .gitattributes
  2. 51 0
      .gitignore
  3. 60 0
      Jenkinsfile
  4. 20 0
      LICENSE
  5. 8 0
      README.md
  6. 160 0
      bin/deploy.sh
  7. 644 0
      cloud-dependencies/pom.xml
  8. 158 0
      cloud-framework/cloud-common/pom.xml
  9. 15 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/core/IntArrayValuable.java
  10. 20 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/core/KeyValue.java
  11. 37 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/CommonStatusEnum.java
  12. 21 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/DocumentEnum.java
  13. 40 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/TerminalEnum.java
  14. 39 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/UserTypeEnum.java
  15. 36 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/WebFilterOrderEnum.java
  16. 32 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/ErrorCode.java
  17. 60 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/ServerException.java
  18. 60 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/ServiceException.java
  19. 51 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/enums/GlobalErrorCodeConstants.java
  20. 39 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/enums/ServiceErrorCodeRange.java
  21. 122 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/util/ServiceExceptionUtil.java
  22. 6 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/package-info.java
  23. 117 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/CommonResult.java
  24. 29 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/PageParam.java
  25. 41 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/PageResult.java
  26. 56 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/SortingField.java
  27. 25 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/cache/CacheUtils.java
  28. 58 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/ArrayUtils.java
  29. 204 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/CollectionUtils.java
  30. 66 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/MapUtils.java
  31. 19 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/SetUtils.java
  32. 882 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/crypt/Sm2Lib.java
  33. 94 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/crypt/Sm2Util.java
  34. 173 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/date/DateUtils.java
  35. 63 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/date/LocalDateTimeUtils.java
  36. 126 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/http/HttpUtils.java
  37. 84 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/io/FileUtils.java
  38. 28 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/io/IoUtils.java
  39. 144 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/json/JsonUtils.java
  40. 30 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/monitor/TracerUtils.java
  41. 16 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/number/NumberUtils.java
  42. 63 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/object/ObjectUtils.java
  43. 16 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/object/PageUtils.java
  44. 7 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/package-info.java
  45. 95 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/servlet/ServletUtils.java
  46. 46 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/spring/SpringAopUtils.java
  47. 82 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/spring/SpringExpressionUtils.java
  48. 48 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/string/StrUtils.java
  49. 47 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/validation/ValidationUtils.java
  50. 35 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/InEnum.java
  51. 44 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/InEnumValidator.java
  52. 28 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/Mobile.java
  53. 25 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/MobileValidator.java
  54. 4 0
      cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/package-info.java
  55. 30 0
      cloud-framework/cloud-spring-boot-starter-banner/pom.xml
  56. 20 0
      cloud-framework/cloud-spring-boot-starter-banner/src/main/java/com/csitc/framework/banner/config/CloudBannerAutoConfiguration.java
  57. 30 0
      cloud-framework/cloud-spring-boot-starter-banner/src/main/java/com/csitc/framework/banner/core/BannerApplicationRunner.java
  58. 6 0
      cloud-framework/cloud-spring-boot-starter-banner/src/main/java/com/csitc/framework/banner/package-info.java
  59. 1 0
      cloud-framework/cloud-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  60. 3 0
      cloud-framework/cloud-spring-boot-starter-banner/src/main/resources/banner.txt
  61. 52 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/pom.xml
  62. 44 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/config/CloudDataPermissionAutoConfiguration.java
  63. 34 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/config/CloudDeptDataPermissionAutoConfiguration.java
  64. 35 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/annotation/DataPermission.java
  65. 36 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java
  66. 72 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java
  67. 72 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/aop/DataPermissionContextHolder.java
  68. 639 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java
  69. 36 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/DataPermissionRule.java
  70. 28 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/DataPermissionRuleFactory.java
  71. 62 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java
  72. 208 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
  73. 20 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java
  74. 6 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/dept/package-info.java
  75. 43 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/util/DataPermissionUtils.java
  76. 4 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/package-info.java
  77. 2 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  78. 108 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java
  79. 66 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/aop/DataPermissionContextHolderTest.java
  80. 190 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java
  81. 533 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java
  82. 145 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java
  83. 235 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java
  84. 15 0
      cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/util/DataPermissionUtilsTest.java
  85. 50 0
      cloud-framework/cloud-spring-boot-starter-biz-dict/pom.xml
  86. 18 0
      cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/config/CloudDictAutoConfiguration.java
  87. 4 0
      cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/core/package-info.java
  88. 70 0
      cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/core/util/DictFrameworkUtils.java
  89. 6 0
      cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/package-info.java
  90. 1 0
      cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  91. 48 0
      cloud-framework/cloud-spring-boot-starter-biz-dict/src/test/java/com/csitc/framework/dict/core/util/DictFrameworkUtilsTest.java
  92. 49 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/pom.xml
  93. 39 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/config/CloudErrorCodeConfiguration.java
  94. 30 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/config/ErrorCodeProperties.java
  95. 15 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java
  96. 104 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java
  97. 24 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/loader/ErrorCodeLoader.java
  98. 73 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java
  99. 10 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/package-info.java
  100. 0 0
      cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

+ 1 - 0
.gitattributes

@@ -0,0 +1 @@
+*.sql linguist-language=java

+ 51 - 0
.gitignore

@@ -0,0 +1,51 @@
+######################################################################
+# Build Tools
+
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+######################################################################
+# IDE
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+nbproject/private/
+build/*
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+
+######################################################################
+# Others
+*.log
+*.xml.versionsBackup
+*.swp
+
+!*/build/*.java
+!*/build/*.html
+!*/build/*.xml
+
+### JRebel ###
+rebel.xml
+.DS_Store
+application-my.yaml
+
+/cloud-ui-app/unpackage/

+ 60 - 0
Jenkinsfile

@@ -0,0 +1,60 @@
+#!groovy
+pipeline {
+
+    agent any
+
+    parameters {
+        string(name: 'TAG_NAME', defaultValue: '', description: '')
+    }
+
+    environment {
+        // DockerHub 凭证 ID(登录您的 DockerHub)
+        DOCKER_CREDENTIAL_ID = 'dockerhub-id'
+        //  GitHub 凭证 ID (推送 tag 到 GitHub 仓库)
+        GITHUB_CREDENTIAL_ID = 'github-id'
+        // kubeconfig 凭证 ID (访问接入正在运行的 Kubernetes 集群)
+        KUBECONFIG_CREDENTIAL_ID = 'demo-kubeconfig'
+        // 镜像的推送
+        REGISTRY = 'docker.io'
+        //  DockerHub 账号名
+        DOCKERHUB_NAMESPACE = 'docker_username'
+        // GitHub 账号名
+        GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/csitc-cloud'
+        // 应用名称
+        APP_NAME = 'cloud-server'
+        // 应用部署路径
+        APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/'
+    }
+
+    stages {
+        stage('检出') {
+            steps {
+                git url: "https://gitee.com/will-we/csitc-cloud.git",
+                        branch: "devops"
+            }
+        }
+
+        stage('构建') {
+            steps {
+                // TODO 解决多环境链接、密码不同配置临时方案
+                sh 'if [ ! -d "' + "${env.HOME}" + '/resources" ];then\n' +
+                        '  echo "配置文件不存在无需修改"\n' +
+                        'else\n' +
+                        '  cp  -rf  ' + "${env.HOME}" + '/resources/*.yaml ' + "${env.APP_NAME}" + '/src/main/resources\n' +
+                        '  echo "配置文件替换"\n' +
+                        'fi'
+                sh 'mvn clean package -Dmaven.test.skip=true'
+            }
+        }
+
+        stage('部署') {
+            steps {
+                sh 'cp -f ' + ' bin/deploy.sh ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}"
+                sh 'cp -f ' + "${env.APP_NAME}" + '/target/*.jar ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" +'/build/'
+                archiveArtifacts "${env.APP_NAME}" + '/target/*.jar'
+                sh 'chmod +x ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh'
+                sh 'bash ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh'
+            }
+        }
+    }
+}

+ 20 - 0
LICENSE

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

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+# 隧道项目
+
+## 依赖框架
+
+1. 项目基于 vue-ruoyi-pro 框架改来的
+
+
+## 

+ 160 - 0
bin/deploy.sh

@@ -0,0 +1,160 @@
+#!/bin/bash
+set -e
+
+DATE=$(date +%Y%m%d%H%M)
+# 基础路径
+BASE_PATH=/work/projects/cloud-server
+# 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下
+SOURCE_PATH=$BASE_PATH/build
+# 服务名称。同时约定部署服务的 jar 包名字也为它。
+SERVER_NAME=cloud-server
+# 环境
+PROFILES_ACTIVE=development
+# 健康检查 URL
+HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/
+
+# heapError 存放路径
+HEAP_ERROR_PATH=$BASE_PATH/heapError
+# JVM 参数
+JAVA_OPS="-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$HEAP_ERROR_PATH"
+
+# SkyWalking Agent 配置
+#export SW_AGENT_NAME=$SERVER_NAME
+#export SW_AGENT_COLLECTOR_BACKEND_SERVICES=192.168.0.84:11800
+#export SW_GRPC_LOG_SERVER_HOST=192.168.0.84
+#export SW_AGENT_TRACE_IGNORE_PATH="Redisson/PING,/actuator/**,/admin/**"
+#export JAVA_AGENT=-javaagent:/work/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar
+
+# 备份
+function backup() {
+    # 如果不存在,则无需备份
+    if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then
+        echo "[backup] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过备份"
+    # 如果存在,则备份到 backup 目录下,使用时间作为后缀
+    else
+        echo "[backup] 开始备份 $SERVER_NAME ..."
+        cp $BASE_PATH/$SERVER_NAME.jar $BASE_PATH/backup/$SERVER_NAME-$DATE.jar
+        echo "[backup] 备份 $SERVER_NAME 完成"
+    fi
+}
+
+# 最新构建代码 移动到项目环境
+function transfer() {
+    echo "[transfer] 开始转移 $SERVER_NAME.jar"
+
+    # 删除原 jar 包
+    if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then
+        echo "[transfer] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过删除"
+    else
+        echo "[transfer] 移除 $BASE_PATH/$SERVER_NAME.jar 完成"
+        rm $BASE_PATH/$SERVER_NAME.jar
+    fi
+
+    # 复制新 jar 包
+    echo "[transfer] 从 $SOURCE_PATH 中获取 $SERVER_NAME.jar 并迁移至 $BASE_PATH ...."
+    cp $SOURCE_PATH/$SERVER_NAME.jar $BASE_PATH
+
+    echo "[transfer] 转移 $SERVER_NAME.jar 完成"
+}
+
+# 停止:优雅关闭之前已经启动的服务
+function stop() {
+    echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME"
+    PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}')
+    # 如果 Java 服务启动中,则进行关闭
+    if [ -n "$PID" ]; then
+        # 正常关闭
+        echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]"
+        kill -15 $PID
+        # 等待最大 120 秒,直到关闭完成。
+        for ((i = 0; i < 120; i++))
+            do
+                sleep 1
+                PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}')
+                if [ -n "$PID" ]; then
+                    echo -e ".\c"
+                else
+                    echo '[stop] 停止 $BASE_PATH/$SERVER_NAME 成功'
+                    break
+                fi
+		    done
+
+        # 如果正常关闭失败,那么进行强制 kill -9 进行关闭
+        if [ -n "$PID" ]; then
+            echo "[stop] $BASE_PATH/$SERVER_NAME 失败,强制 kill -9 $PID"
+            kill -9 $PID
+        fi
+    # 如果 Java 服务未启动,则无需关闭
+    else
+        echo "[stop] $BASE_PATH/$SERVER_NAME 未启动,无需停止"
+    fi
+}
+
+# 启动:启动后端项目
+function start() {
+    # 开启启动前,打印启动参数
+    echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME"
+    echo "[start] JAVA_OPS: $JAVA_OPS"
+    echo "[start] JAVA_AGENT: $JAVA_AGENT"
+    echo "[start] PROFILES: $PROFILES_ACTIVE"
+
+    # 开始启动
+    BUILD_ID=dontKillMe nohup java -server $JAVA_OPS $JAVA_AGENT -jar $BASE_PATH/$SERVER_NAME.jar --spring.profiles.active=$PROFILES_ACTIVE &
+    echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成"
+}
+
+# 健康检查:自动判断后端项目是否正常启动
+function healthCheck() {
+    # 如果配置健康检查,则进行健康检查
+    if [ -n "$HEALTH_CHECK_URL" ]; then
+        # 健康检查最大 120 秒,直到健康检查通过
+        echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查";
+        for ((i = 0; i < 120; i++))
+            do
+                # 请求健康检查地址,只获取状态码。
+                result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"`
+                # 如果状态码为 200,则说明健康检查通过
+                if [ "$result" == "200" ]; then
+                    echo "[healthCheck] 健康检查通过";
+                    break
+                # 如果状态码非 200,则说明未通过。sleep 1 秒后,继续重试
+                else
+                    echo -e ".\c"
+                    sleep 1
+                fi
+            done
+
+        # 健康检查未通过,则异常退出 shell 脚本,不继续部署。
+        if [ ! "$result" == "200" ]; then
+            echo "[healthCheck] 健康检查不通过,可能部署失败。查看日志,自行判断是否启动成功";
+            tail -n 10 nohup.out
+            exit 1;
+        # 健康检查通过,打印最后 10 行日志,可能部署的人想看下日志。
+        else
+            tail -n 10 nohup.out
+        fi
+    # 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。
+    else
+        echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒";
+        sleep 120
+        echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功";
+        tail -n 50 nohup.out
+    fi
+}
+
+# 部署
+function deploy() {
+    cd $BASE_PATH
+    # 备份原 jar
+    backup
+    # 停止 Java 服务
+    stop
+    # 部署新 jar
+    transfer
+    # 启动 Java 服务
+    start
+    # 健康检查
+    healthCheck
+}
+
+deploy

+ 644 - 0
cloud-dependencies/pom.xml

@@ -0,0 +1,644 @@
+<?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.csitc</groupId>
+    <artifactId>cloud-dependencies</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>基础 bom 文件,管理整个项目的依赖版本</description>
+
+
+    <properties>
+        <revision>1.7.1-snapshot</revision>
+        <!-- 统一依赖管理 -->
+        <spring.boot.version>2.7.8</spring.boot.version>
+        <!-- Web 相关 -->
+        <springdoc.version>1.6.14</springdoc.version>
+        <knife4j.version>4.0.0</knife4j.version>
+        <servlet.versoin>2.5</servlet.versoin>
+        <!-- DB 相关 -->
+        <druid.version>1.2.15</druid.version>
+        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
+        <mybatis-plus-generator.version>3.5.3.1</mybatis-plus-generator.version>
+        <dynamic-datasource.version>3.6.1</dynamic-datasource.version>
+        <redisson.version>3.18.0</redisson.version>
+        <!-- 服务保障相关 -->
+        <lock4j.version>2.2.3</lock4j.version>
+        <resilience4j.version>1.7.1</resilience4j.version>
+        <!-- 监控相关 -->
+        <skywalking.version>8.12.0</skywalking.version>
+        <spring-boot-admin.version>2.7.10</spring-boot-admin.version>
+        <opentracing.version>0.33.0</opentracing.version>
+        <!-- Test 测试相关 -->
+        <podam.version>7.2.11.RELEASE</podam.version>
+        <jedis-mock.version>1.0.6</jedis-mock.version>
+        <mockito-inline.version>4.11.0</mockito-inline.version>
+        <!-- Bpm 工作流相关 -->
+        <flowable.version>6.8.0</flowable.version>
+        <!-- 工具类相关 -->
+        <captcha-plus.version>1.0.2</captcha-plus.version>
+        <jsoup.version>1.15.3</jsoup.version>
+        <lombok.version>1.18.24</lombok.version>
+        <mapstruct.version>1.5.3.Final</mapstruct.version>
+        <hutool.version>5.8.11</hutool.version>
+        <easyexcel.verion>3.2.0</easyexcel.verion>
+        <velocity.version>2.3</velocity.version>
+        <screw.version>1.0.5</screw.version>
+        <fastjson.version>1.2.83</fastjson.version>
+        <guava.version>31.1-jre</guava.version>
+        <guice.version>5.1.0</guice.version>
+        <transmittable-thread-local.version>2.14.2</transmittable-thread-local.version>
+        <commons-net.version>3.8.0</commons-net.version>
+        <jsch.version>0.1.55</jsch.version>
+        <tika-core.version>2.6.0</tika-core.version>
+        <netty-all.version>4.1.86.Final</netty-all.version>
+        <ip2region.version>2.6.6</ip2region.version>
+        <!-- 三方云服务相关 -->
+        <okio.version>3.0.0</okio.version>
+        <okhttp3.version>4.10.0</okhttp3.version>
+        <minio.version>8.5.1</minio.version>
+        <aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version>
+        <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
+        <tencentcloud-sdk-java.version>3.1.676</tencentcloud-sdk-java.version>
+        <justauth.version>1.4.0</justauth.version>
+        <jimureport.version>1.5.6</jimureport.version>
+        <xercesImpl.version>2.12.2</xercesImpl.version>
+        <wx-java-mp.version>4.3.0</wx-java-mp.version>
+<!--        <drools.version>7.6.0.Final</drools.version>-->
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- 统一依赖管理 -->
+            <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>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-banner</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-operatelog</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-dict</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-sms</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-pay</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-weixin</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-tenant</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-data-permission</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-social</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-error-code</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-biz-ip</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-captcha</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-desensitize</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>
+
+            <!-- Web 相关 -->
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-web</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-security</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
+                <version>${knife4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springdoc</groupId>
+                <artifactId>springdoc-openapi-ui</artifactId>
+                <version>${springdoc.version}</version>
+            </dependency>
+
+            <!-- DB 相关 -->
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-mybatis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>druid-spring-boot-starter</artifactId>
+                <version>${druid.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-boot-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
+                <version>${mybatis-plus-generator.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
+                <version>${dynamic-datasource.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-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>
+
+            <!-- Job 定时任务相关 -->
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-job</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 消息队列相关 -->
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-mq</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- 服务保障相关 -->
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-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>io.github.resilience4j</groupId>
+                <artifactId>resilience4j-ratelimiter</artifactId>
+                <version>${resilience4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.github.resilience4j</groupId>
+                <artifactId>resilience4j-spring-boot2</artifactId>
+                <version>${resilience4j.version}</version>
+            </dependency>
+
+            <!-- 监控相关 -->
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-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.csitc</groupId>
+                <artifactId>cloud-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>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-flowable</artifactId>
+                <version>${revision}</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.csitc</groupId>
+                <artifactId>cloud-common</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-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.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>easyexcel</artifactId>
+                <version>${easyexcel.verion}</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>cn.smallbun.screw</groupId>
+                <artifactId>screw-core</artifactId> <!-- 实现数据库文档 -->
+                <version>${screw.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.freemarker</groupId>
+                        <artifactId>freemarker</artifactId> <!-- 移除 Freemarker 依赖,采用 Velocity 作为模板引擎 -->
+                    </exclusion>
+                    <exclusion>
+                        <groupId>com.alibaba</groupId>
+                        <artifactId>fastjson</artifactId> <!-- 最新版screw-core1.0.5依赖fastjson1.2.73存在漏洞,移除。 -->
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>${guava.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.google.inject</groupId>
+                <artifactId>guice</artifactId>
+                <version>${guice.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.jcraft</groupId>
+                <artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
+                <version>${jsch.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-all</artifactId>
+                <version>${netty-all.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xingyuv</groupId>
+                <artifactId>spring-boot-starter-captcha-plus</artifactId>
+                <version>${captcha-plus.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>com.squareup.okio</groupId>
+                <artifactId>okio</artifactId>
+                <version>${okio.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>okhttp</artifactId>
+                <version>${okhttp3.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.csitc</groupId>
+                <artifactId>cloud-spring-boot-starter-file</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.minio</groupId>
+                <artifactId>minio</artifactId>
+                <version>${minio.version}</version>
+            </dependency>
+
+            <!-- SMS SDK begin -->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>aliyun-java-sdk-core</artifactId>
+                <version>${aliyun-java-sdk-core.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>com.aliyun</groupId>
+                <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
+                <version>${aliyun-java-sdk-dysmsapi.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.tencentcloudapi</groupId>
+                <artifactId>tencentcloud-sdk-java-sms</artifactId>
+                <version>${tencentcloud-sdk-java.version}</version>
+            </dependency>
+            <!-- SMS SDK end -->
+
+            <dependency>
+                <groupId>com.xkcoding.justauth</groupId>
+                <artifactId>justauth-spring-boot-starter</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
+                <version>${justauth.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>wx-java-mp-spring-boot-starter</artifactId>
+                <version>${wx-java-mp.version}</version>
+            </dependency>
+
+            <!-- 积木报表-->
+            <dependency>
+                <groupId>org.jeecgframework.jimureport</groupId>
+                <artifactId>jimureport-spring-boot-starter</artifactId>
+                <exclusions>
+                    <exclusion>
+                        <groupId>com.alibaba</groupId>
+                        <artifactId>druid</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>xerces</groupId>
+                <artifactId>xercesImpl</artifactId>
+                <version>${xercesImpl.version}</version>
+            </dependency>
+            <!-- SpringBoot Websocket -->
+<!--            <dependency>-->
+<!--                <groupId>org.springframework.boot</groupId>-->
+<!--                <artifactId>spring-boot-starter-websocket</artifactId>-->
+<!--                <version>${spring.boot.version}</version>-->
+<!--            </dependency>-->
+
+<!--            <dependency>-->
+<!--                <groupId>com.csitc</groupId>-->
+<!--                <artifactId>cloud-spring-boot-starter-drools</artifactId>-->
+<!--                <version>${revision}</version>-->
+<!--            </dependency>-->
+
+
+<!--            <dependency>-->
+<!--                <groupId>org.drools</groupId>-->
+<!--                <artifactId>drools-core</artifactId>-->
+<!--                <version>${drools.version}</version>-->
+<!--            </dependency>-->
+<!--            <dependency>-->
+<!--                <groupId>org.drools</groupId>-->
+<!--                <artifactId>drools-compiler</artifactId>-->
+<!--                <version>${drools.version}</version>-->
+<!--            </dependency>-->
+<!--            <dependency>-->
+<!--                <groupId>org.drools</groupId>-->
+<!--                <artifactId>drools-templates</artifactId>-->
+<!--                <version>${drools.version}</version>-->
+<!--            </dependency>-->
+<!--            <dependency>-->
+<!--                <groupId>org.kie</groupId>-->
+<!--                <artifactId>kie-api</artifactId>-->
+<!--                <version>${drools.version}</version>-->
+<!--            </dependency>-->
+<!--            <dependency>-->
+<!--                <groupId>org.kie</groupId>-->
+<!--                <artifactId>kie-spring</artifactId>-->
+<!--                <version>${drools.version}</version>-->
+<!--            </dependency>-->
+
+        </dependencies>
+    </dependencyManagement>
+
+</project>

+ 158 - 0
cloud-framework/cloud-common/pom.xml

@@ -0,0 +1,158 @@
+<?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.csitc</groupId>
+        <artifactId>cloud-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>cloud-common</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>定义基础 pojo 类、枚举、工具类等等</description>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+<!--                <configuration>-->
+<!--                    <source>11</source>-->
+<!--                    <target>11</target>-->
+<!--                </configuration>-->
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <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-ui</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
+        </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>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15to18</artifactId>
+            <version>1.66</version>
+        </dependency>
+
+
+    </dependencies>
+
+</project>

+ 15 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/core/IntArrayValuable.java

@@ -0,0 +1,15 @@
+package com.csitc.framework.common.core;
+
+/**
+ * 可生成 Int 数组的接口
+ *
+ * @author 隧道
+ */
+public interface IntArrayValuable {
+
+    /**
+     * @return int 数组
+     */
+    int[] array();
+
+}

+ 20 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/core/KeyValue.java

@@ -0,0 +1,20 @@
+package com.csitc.framework.common.core;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Key Value 的键值对
+ *
+ * @author 隧道
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class KeyValue<K, V> {
+
+    private K key;
+    private V value;
+
+}

+ 37 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/CommonStatusEnum.java

@@ -0,0 +1,37 @@
+package com.csitc.framework.common.enums;
+
+import com.csitc.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 通用状态枚举
+ *
+ * @author 隧道
+ */
+@Getter
+@AllArgsConstructor
+public enum CommonStatusEnum implements IntArrayValuable {
+
+    ENABLE(0, "开启"),
+    DISABLE(1, "关闭");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray();
+
+    /**
+     * 状态值
+     */
+    private final Integer status;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 21 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/DocumentEnum.java

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

+ 40 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/TerminalEnum.java

@@ -0,0 +1,40 @@
+package com.csitc.framework.common.enums;
+
+import com.csitc.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 终端的枚举
+ *
+ * @author 隧道
+ */
+@RequiredArgsConstructor
+@Getter
+public enum TerminalEnum implements IntArrayValuable {
+
+    WECHAT_MINI_PROGRAM(10, "微信小程序"),
+    WECHAT_WAP(11, "微信公众号"),
+    H5(20, "H5 网页"),
+    IOS(31, "苹果 App"),
+    ANDROID(32, "安卓 App"),
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray();
+
+    /**
+     * 终端
+     */
+    private final Integer terminal;
+    /**
+     * 终端名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 39 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/UserTypeEnum.java

@@ -0,0 +1,39 @@
+package com.csitc.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.csitc.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 全局用户类型枚举
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum implements IntArrayValuable {
+
+    MEMBER(1, "会员"), // 面向 c 端,普通用户
+    ADMIN(2, "管理员"); // 面向 b 端,管理后台
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray();
+
+    /**
+     * 类型
+     */
+    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 int[] array() {
+        return ARRAYS;
+    }
+}

+ 36 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/enums/WebFilterOrderEnum.java

@@ -0,0 +1,36 @@
+package com.csitc.framework.common.enums;
+
+/**
+ * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下
+ *
+ * @author 隧道
+ */
+public interface WebFilterOrderEnum {
+
+    int CORS_FILTER = Integer.MIN_VALUE;
+
+    int TRACE_FILTER = CORS_FILTER + 1;
+
+    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+
+    // 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 ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    int DEMO_FILTER = Integer.MAX_VALUE;
+
+}

+ 32 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/ErrorCode.java

@@ -0,0 +1,32 @@
+package com.csitc.framework.common.exception;
+
+import com.csitc.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.csitc.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
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/ServerException.java

@@ -0,0 +1,60 @@
+package com.csitc.framework.common.exception;
+
+import com.csitc.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
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/ServiceException.java

@@ -0,0 +1,60 @@
+package com.csitc.framework.common.exception;
+
+import com.csitc.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;
+    }
+
+}

+ 51 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/enums/GlobalErrorCodeConstants.java

@@ -0,0 +1,51 @@
+package com.csitc.framework.common.exception.enums;
+
+import com.csitc.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 REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
+    ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
+
+    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
+
+    /**
+     * 是否为服务端错误,参考 HTTP 5XX 错误码段
+     *
+     * @param code 错误码
+     * @return 是否
+     */
+   static boolean isServerErrorCode(Integer code) {
+       return code != null
+               && code >= INTERNAL_SERVER_ERROR.getCode() && code <= INTERNAL_SERVER_ERROR.getCode() + 99;
+   }
+
+}

+ 39 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/enums/ServiceErrorCodeRange.java

@@ -0,0 +1,39 @@
+package com.csitc.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)
+    // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
+    // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
+
+}

+ 122 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/exception/util/ServiceExceptionUtil.java

@@ -0,0 +1,122 @@
+package com.csitc.framework.common.exception.util;
+
+import com.csitc.framework.common.exception.ErrorCode;
+import com.csitc.framework.common.exception.ServiceException;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * {@link ServiceException} 工具类
+ *
+ * 目的在于,格式化异常信息提示。
+ * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
+ *
+ * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式:
+ *
+ * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration
+ * 2. 异常提示信息,写在 .properties 等等配置文件
+ * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新
+ * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新
+ */
+@Slf4j
+public class ServiceExceptionUtil {
+
+    /**
+     * 错误码提示模板
+     */
+    private static final ConcurrentMap<Integer, String> MESSAGES = new ConcurrentHashMap<>();
+
+    public static void putAll(Map<Integer, String> messages) {
+        ServiceExceptionUtil.MESSAGES.putAll(messages);
+    }
+
+    public static void put(Integer code, String message) {
+        ServiceExceptionUtil.MESSAGES.put(code, message);
+    }
+
+    public static void delete(Integer code, String message) {
+        ServiceExceptionUtil.MESSAGES.remove(code, message);
+    }
+
+    // ========== 和 ServiceException 的集成 ==========
+
+    public static ServiceException exception(ErrorCode errorCode) {
+        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
+        return exception0(errorCode.getCode(), messagePattern);
+    }
+
+    public static ServiceException exception(ErrorCode errorCode, Object... params) {
+        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
+        return exception0(errorCode.getCode(), messagePattern, params);
+    }
+
+    /**
+     * 创建指定编号的 ServiceException 的异常
+     *
+     * @param code 编号
+     * @return 异常
+     */
+    public static ServiceException exception(Integer code) {
+        return exception0(code, MESSAGES.get(code));
+    }
+
+    /**
+     * 创建指定编号的 ServiceException 的异常
+     *
+     * @param code 编号
+     * @param params 消息提示的占位符对应的参数
+     * @return 异常
+     */
+    public static ServiceException exception(Integer code, Object... params) {
+        return exception0(code, MESSAGES.get(code), params);
+    }
+
+    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
+        String message = doFormat(code, messagePattern, params);
+        return new ServiceException(code, message);
+    }
+
+    // ========== 格式化方法 ==========
+
+    /**
+     * 将错误编号对应的消息使用 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
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/package-info.java

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

+ 117 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/CommonResult.java

@@ -0,0 +1,117 @@
+package com.csitc.framework.common.pojo;
+
+import com.csitc.framework.common.exception.ErrorCode;
+import com.csitc.framework.common.exception.ServerException;
+import com.csitc.framework.common.exception.ServiceException;
+import com.csitc.framework.common.exception.enums.GlobalErrorCodeConstants;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 通用返回
+ *
+ * @param <T> 数据泛型
+ */
+@Data
+public class CommonResult<T> implements Serializable {
+
+    /**
+     * 错误码
+     *
+     * @see ErrorCode#getCode()
+     */
+    private Integer code;
+    /**
+     * 返回数据
+     */
+    private T data;
+    /**
+     * 错误提示,用户可阅读
+     *
+     * @see ErrorCode#getMsg() ()
+     */
+    private String msg;
+
+    /**
+     * 将传入的 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.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = code;
+        result.msg = message;
+        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;
+        }
+        // 服务端异常
+        if (GlobalErrorCodeConstants.isServerErrorCode(code)) {
+            throw new ServerException(code, msg);
+        }
+        // 业务异常
+        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());
+    }
+
+}

+ 29 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/PageParam.java

@@ -0,0 +1,29 @@
+package com.csitc.framework.common.pojo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.Max;
+import javax.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;
+
+    @Schema(description = "页码,从 1 开始", required = true,example = "1")
+    @NotNull(message = "页码不能为空")
+    @Min(value = 1, message = "页码最小值为 1")
+    private Integer pageNo = PAGE_NO;
+
+    @Schema(description = "每页条数,最大值为 100", required = true, example = "10")
+    @NotNull(message = "每页条数不能为空")
+    @Min(value = 1, message = "每页条数最小值为 1")
+    @Max(value = 100, message = "每页条数最大值为 100")
+    private Integer pageSize = PAGE_SIZE;
+
+}

+ 41 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/PageResult.java

@@ -0,0 +1,41 @@
+package com.csitc.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 = "数据", required = true)
+    private List<T> list;
+
+    @Schema(description = "总量", required = true)
+    private Long total;
+
+    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);
+    }
+
+}

+ 56 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/pojo/SortingField.java

@@ -0,0 +1,56 @@
+package com.csitc.framework.common.pojo;
+
+import java.io.Serializable;
+
+/**
+ * 排序字段 DTO
+ *
+ * 类名加了 ing 的原因是,避免和 ES SortField 重名。
+ */
+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;
+
+    // 空构造方法,解决反序列化
+    public SortingField() {
+    }
+
+    public SortingField(String field, String order) {
+        this.field = field;
+        this.order = order;
+    }
+
+    public String getField() {
+        return field;
+    }
+
+    public SortingField setField(String field) {
+        this.field = field;
+        return this;
+    }
+
+    public String getOrder() {
+        return order;
+    }
+
+    public SortingField setOrder(String order) {
+        this.order = order;
+        return this;
+    }
+}

+ 25 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/cache/CacheUtils.java

@@ -0,0 +1,25 @@
+package com.csitc.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 {
+
+    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder()
+                // 只阻塞当前数据加载线程,其他线程返回旧值
+                .refreshAfterWrite(duration)
+                // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
+                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
+    }
+
+}

+ 58 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/ArrayUtils.java

@@ -0,0 +1,58 @@
+package com.csitc.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.csitc.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];
+    }
+
+}

+ 204 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/CollectionUtils.java

@@ -0,0 +1,204 @@
+package com.csitc.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.*;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * Collection 工具类
+ *
+ * @author 隧道
+ */
+public class CollectionUtils {
+
+    public static boolean containsAny(Object source, Object... targets) {
+        return Arrays.asList(targets).contains(source);
+    }
+
+    public static boolean isAnyEmpty(Collection<?>... collections) {
+        return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
+    }
+
+    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(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> 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> 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();
+    }
+
+    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(List<T> from, Predicate<T> predicate) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        return from.stream().filter(predicate).findFirst().orElse(null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getMaxValue(List<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        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>> V getSumValue(List<T> from, Function<T, V> valueFunc, BinaryOperator<V> accumulator) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        return from.stream().map(valueFunc).reduce(accumulator).get();
+    }
+
+    public static <T> void addIfNotNull(Collection<T> coll, T item) {
+        if (item == null) {
+            return;
+        }
+        coll.add(item);
+    }
+
+    public static <T> Collection<T> singleton(T deptId) {
+        return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
+    }
+
+}

+ 66 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/MapUtils.java

@@ -0,0 +1,66 @@
+package com.csitc.framework.common.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import com.csitc.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,然后进一步处理
+     * 注意,如果查找到的 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 (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
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/collection/SetUtils.java

@@ -0,0 +1,19 @@
+package com.csitc.framework.common.util.collection;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Set 工具类
+ *
+ * @author 隧道
+ */
+public class SetUtils {
+
+    @SafeVarargs
+    public static <T> Set<T> asSet(T... objs) {
+        return new HashSet<>(Arrays.asList(objs));
+    }
+
+}

+ 882 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/crypt/Sm2Lib.java

@@ -0,0 +1,882 @@
+package com.csitc.framework.common.util.crypt;
+import java.io.*;
+import java.math.BigInteger;
+import java.security.*;
+import java.security.spec.*;
+import java.util.*;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.asn1.*;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.asn1.x9.X962Parameters;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.asn1.x9.X9ECPoint;
+import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.CipherParameters;
+import org.bouncycastle.crypto.CryptoException;
+import org.bouncycastle.crypto.InvalidCipherTextException;
+import org.bouncycastle.crypto.engines.SM2Engine;
+import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
+import org.bouncycastle.crypto.params.*;
+import org.bouncycastle.crypto.signers.SM2Signer;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
+import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
+import org.bouncycastle.jcajce.spec.SM2ParameterSpec;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.jce.spec.ECNamedCurveSpec;
+import org.bouncycastle.math.ec.ECCurve;
+import org.bouncycastle.math.ec.ECPoint;
+import org.bouncycastle.math.ec.FixedPointCombMultiplier;
+import org.bouncycastle.math.ec.custom.gm.SM2P256V1Curve;
+import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
+
+import org.bouncycastle.jce.spec.ECParameterSpec;
+import org.bouncycastle.util.Strings;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
+import org.bouncycastle.util.io.pem.PemWriter;
+
+import javax.crypto.Cipher;
+
+//SM2 基础库
+public class Sm2Lib
+{
+    //SM2算法默认用户ID,目前开放平台不会使用非默认用户ID
+    public static String DEFAULT_USER_ID = "1234567812345678";
+    public static String DEFAULT_CHARSET = "utf8";
+
+    public static final String SM2_ECC_GY_VAL = "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0";
+    public static final String SM2_ECC_GX_VAL = "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7";
+    public static final SM2P256V1Curve CURVE = new SM2P256V1Curve();
+    public final static BigInteger SM2_ECC_N = CURVE.getOrder();
+    public final static BigInteger SM2_ECC_H = CURVE.getCofactor();
+    public final static BigInteger SM2_ECC_GX = new BigInteger(SM2_ECC_GX_VAL, 16);
+    public final static BigInteger SM2_ECC_GY = new BigInteger(SM2_ECC_GY_VAL, 16);
+    public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY);
+    public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT, SM2_ECC_N, SM2_ECC_H);
+
+    public final static BigInteger SM2_ECC_P = CURVE.getQ();
+    public final static BigInteger SM2_ECC_A = CURVE.getA().toBigInteger();
+    public final static BigInteger SM2_ECC_B = CURVE.getB().toBigInteger();
+
+    //public static final int CURVE_LEN = BCECUtils.getCurveLength(DOMAIN_PARAMS);
+
+    public static final EllipticCurve JDK_CURVE = new EllipticCurve(new ECFieldFp(SM2_ECC_P), SM2_ECC_A, SM2_ECC_B);
+    public static final java.security.spec.ECPoint JDK_G_POINT = new java.security.spec.ECPoint(
+            G_POINT.getAffineXCoord().toBigInteger(), G_POINT.getAffineYCoord().toBigInteger());
+    public static final java.security.spec.ECParameterSpec JDK_EC_SPEC = new java.security.spec.ECParameterSpec(
+            JDK_CURVE, JDK_G_POINT, SM2_ECC_N, SM2_ECC_H.intValue());
+
+    private static BouncyCastleProvider provider;
+
+    static {
+        provider = new BouncyCastleProvider();
+        Security.addProvider(provider);
+    }
+
+    public static String decrypt(String cipherTextBase64, String privateKey) throws Exception {
+        return decrypt(cipherTextBase64, privateKey, DEFAULT_CHARSET);
+    }
+
+    public static String decrypt(String cipherTextBase64, String privateKey, String charset) throws Exception {
+        byte[] cipher = Base64.getDecoder().decode(cipherTextBase64);
+        boolean isMatch = Pattern.matches("^[0-9a-f]+$", privateKey.toLowerCase());
+        if (isMatch) {
+            ECPrivateKeyParameters ecPrivateKeyParameters = BCECUtils.createECPrivateKeyParameters(privateKey, DOMAIN_PARAMS);
+            byte[] buf = decrypt(ecPrivateKeyParameters, cipher);
+            //将解密后的明文按指定字符集编码后返回
+            try {
+                String strContent = new String(buf, charset);
+                return strContent;
+            } catch (UnsupportedEncodingException e) {
+                throw new Exception(e);
+            }
+        } else {
+            byte[] privateKeyByte = Base64.getDecoder().decode(privateKey);
+            // 解析X509格式SM2私钥
+            PrivateKey sm2PrivateKey = parsePKCS8PrivateKey(privateKeyByte);
+            // 使用SM2私钥解密
+            byte[] buf = sm2Decrypt(cipher, sm2PrivateKey);
+            //将解密后的明文按指定字符集编码后返回
+            try {
+                String strContent = new String(buf, charset);
+                return strContent;
+            } catch (UnsupportedEncodingException e) {
+                throw new Exception(e);
+            }
+        }
+    }
+
+    public static String encrypt(String plainText, String publicKey) throws Exception {
+        return encrypt(plainText, publicKey, DEFAULT_CHARSET);
+    }
+
+    public static String encrypt(String plainText, String publicKey, String charset) throws Exception {
+        byte[] plain = plainText.getBytes(charset);
+        boolean isMatch = Pattern.matches("^[0-9a-f]+$", publicKey.toLowerCase());
+        if (isMatch) {
+            ECPoint point = CURVE.decodePoint(ByteUtils.fromHexString(publicKey));
+            ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(point, DOMAIN_PARAMS);
+            byte[] cipher = encrypt(ecPublicKeyParameters, plain);
+            //将密文Base64编码后返回
+            String strContent = Base64.getEncoder().encodeToString(cipher);
+            return strContent;
+        } else {
+            // 解析PKCS8格式SM2私钥
+            byte[] publicKeyByte = Base64.getDecoder().decode(publicKey);
+            PublicKey sm2PublicKey = parseX509PublicKey(publicKeyByte);
+            byte[] cipher = sm2Encrypt(plain, sm2PublicKey);
+            //将密文Base64编码后返回
+            String strContent = Base64.getEncoder().encodeToString(cipher);
+            return strContent;
+        }
+    }
+
+    public static String sign(String content, String privateKey) throws Exception {
+        return sign(content, privateKey, null, DEFAULT_CHARSET);
+    }
+
+    public static String sign(String content, String privateKey, String userId, String charset) throws Exception {
+        if (StringUtils.isEmpty(userId)) {
+            userId = DEFAULT_USER_ID;
+        }
+        byte[] message = content.getBytes(charset);
+        boolean isMatch = Pattern.matches("^[0-9a-f]+$", privateKey.toLowerCase());
+        if (isMatch) {
+            ECPrivateKeyParameters ecPrivateKeyParameters = BCECUtils.createECPrivateKeyParameters(privateKey, DOMAIN_PARAMS);
+            byte[] signature = sign(ecPrivateKeyParameters, Strings.toByteArray(userId), message);
+            try {
+                String sign = Base64.getEncoder().encodeToString(signature);
+                return sign;
+            } catch (Exception e) {
+                throw new Exception(e);
+            }
+        } else {
+            byte[] privateKeyByte = Base64.getDecoder().decode(privateKey);
+            PrivateKey sm2PrivateKey = parsePKCS8PrivateKey(privateKeyByte);
+            byte[] signature = sm2Sign(message, sm2PrivateKey, userId);
+            String sign = Base64.getEncoder().encodeToString(signature);
+            return sign;
+        }
+    }
+
+    public static boolean verify(String content, String publicKey, String sign) throws Exception {
+        return verify(content, publicKey, sign, null, DEFAULT_CHARSET);
+    }
+
+    public static boolean verify(String content, String publicKey, String sign, String userId, String charset) throws Exception {
+        if (StringUtils.isEmpty(userId)) {
+            userId = DEFAULT_USER_ID;
+        }
+
+        byte[] message = content.getBytes(charset);
+        byte[] signature = Base64.getDecoder().decode(sign);
+        boolean isMatch = Pattern.matches("^[0-9a-f]+$", publicKey.toLowerCase());
+        if (isMatch) {
+            ECPoint point = CURVE.decodePoint(ByteUtils.fromHexString(publicKey));
+            ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(point, DOMAIN_PARAMS);
+            boolean valid = verify(ecPublicKeyParameters, Strings.toByteArray(userId), message, signature);
+            return valid;
+        } else {
+            byte[] publicKeyByte = Base64.getDecoder().decode(publicKey);
+            PublicKey sm2PublicKey = parseX509PublicKey(publicKeyByte);
+            boolean valid = sm2Verify(signature, message, sm2PublicKey, userId);
+            return valid;
+        }
+    }
+
+    private static byte[] sm2Encrypt(byte[] plain, PublicKey sm2PublicKey) throws Exception {
+        try {
+            Cipher sm2CipherEngine = Cipher.getInstance("SM2", "BC");
+            sm2CipherEngine.init(Cipher.ENCRYPT_MODE, sm2PublicKey);
+            return sm2CipherEngine.doFinal(plain);
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+
+    private static byte[] sm2Decrypt(byte[] cipher, PrivateKey sm2PrivateKey) throws Exception {
+        try {
+            Cipher sm2CipherEngine = Cipher.getInstance("SM2", "BC");
+            sm2CipherEngine.init(Cipher.DECRYPT_MODE, sm2PrivateKey);
+            return sm2CipherEngine.doFinal(cipher);
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+
+    private static byte[] sm2Sign(byte[] message, PrivateKey sm2PrivateKey, String sm2UserId) throws Exception {
+        try {
+            Signature sm2SignEngine = Signature.getInstance("SM3withSM2");
+            sm2SignEngine.setParameter(new SM2ParameterSpec(
+                    Strings.toByteArray(sm2UserId)));
+            sm2SignEngine.initSign(sm2PrivateKey);
+            sm2SignEngine.update(message);
+            return sm2SignEngine.sign();
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+
+    private static boolean sm2Verify(byte[] signature, byte[] message, PublicKey publicKey, String sm2UserId) {
+        try {
+            Signature sm2SignEngine = Signature.getInstance("SM3withSM2");
+            sm2SignEngine.setParameter(new SM2ParameterSpec(Strings.toByteArray(sm2UserId)));
+            sm2SignEngine.initVerify(publicKey);
+            sm2SignEngine.update(message);
+            return sm2SignEngine.verify(signature);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private static PublicKey parseX509PublicKey(byte[] x509PublicKey) throws Exception {
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("EC");
+            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(x509PublicKey);
+            return keyFactory.generatePublic(keySpec);
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+
+    private static PrivateKey parsePKCS8PrivateKey(byte[] pkcs8PriateKey) throws Exception {
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance("EC");
+            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8PriateKey);
+            return keyFactory.generatePrivate(keySpec);
+        } catch (Exception e) {
+            throw new Exception(e);
+        }
+    }
+
+    public static String getSignContent(Map<String, String> sortedParams) {
+        StringBuffer content = new StringBuffer();
+        List<String> keys = new ArrayList<>(sortedParams.keySet());
+        Collections.sort(keys);
+        int index = 0;
+        for (int i = 0; i < keys.size(); i++) {
+            String key = keys.get(i);
+            String value = sortedParams.get(key);
+            if (areNotEmpty(key, value)) {
+                content.append((index == 0 ? "" : "&") + key + "=" + value);
+                index++;
+            }
+        }
+        return content.toString();
+    }
+
+    private static boolean areNotEmpty(String... values) {
+        boolean result = true;
+        if (values == null || values.length == 0) {
+            result = false;
+        } else {
+            for (String value : values) {
+                result &= !isEmpty(value);
+            }
+        }
+        return result;
+    }
+
+    private static boolean isEmpty(String value) {
+        int strLen;
+        if (value == null || (strLen = value.length()) == 0) {
+            return true;
+        }
+        for (int i = 0; i < strLen; i++) {
+            if ((Character.isWhitespace(value.charAt(i)) == false)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @param pubKeyParams 公钥
+     * @param srcData      原文
+     * @return 默认输出C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
+     * @throws InvalidCipherTextException
+     */
+    private static byte[] encrypt(final ECPublicKeyParameters pubKeyParams, final byte[] srcData) throws InvalidCipherTextException {
+        return encrypt(SM2Engine.Mode.C1C3C2, pubKeyParams, srcData);
+    }
+
+    /**
+     * @param priKeyParams 私钥
+     * @param sm2Cipher    默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
+     * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。
+     * @throws InvalidCipherTextException
+     */
+    private static byte[] decrypt(final ECPrivateKeyParameters priKeyParams, final byte[] sm2Cipher)
+            throws InvalidCipherTextException {
+        return decrypt(SM2Engine.Mode.C1C3C2, priKeyParams, sm2Cipher);
+    }
+
+    /**
+     * @param mode         指定密文结构,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
+     * @param pubKeyParams 公钥
+     * @param srcData      原文
+     * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
+     * @throws InvalidCipherTextException
+     */
+    private static byte[] encrypt(final SM2Engine.Mode mode, final ECPublicKeyParameters pubKeyParams, final byte[] srcData)
+            throws InvalidCipherTextException {
+        final SM2Engine engine = new SM2Engine(mode);
+        final ParametersWithRandom pwr = new ParametersWithRandom(pubKeyParams, new SecureRandom());
+        engine.init(true, pwr);
+        return engine.processBlock(srcData, 0, srcData.length);
+    }
+
+    /**
+     * @param mode         指定密文结构,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2
+     * @param priKeyParams 私钥
+     * @param sm2Cipher    根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。
+     * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。
+     * @throws InvalidCipherTextException
+     */
+    private static byte[] decrypt(final SM2Engine.Mode mode, final ECPrivateKeyParameters priKeyParams, final byte[] sm2Cipher)
+            throws InvalidCipherTextException {
+        final SM2Engine engine = new SM2Engine(mode);
+        engine.init(false, priKeyParams);
+        return engine.processBlock(sm2Cipher, 0, sm2Cipher.length);
+    }
+
+    /**
+     * 签名
+     *
+     * @param priKeyParams 私钥
+     * @param withId       可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes()
+     * @param srcData      源数据
+     * @return DER编码后的签名值
+     * @throws CryptoException
+     */
+    private static byte[] sign(final ECPrivateKeyParameters priKeyParams, final byte[] withId, final byte[] srcData)
+            throws CryptoException {
+        SM2Signer signer = new SM2Signer();
+        CipherParameters param = null;
+        ParametersWithRandom pwr = new ParametersWithRandom(priKeyParams, new SecureRandom());
+        if (withId != null) {
+            param = new ParametersWithID(pwr, withId);
+        } else {
+            param = pwr;
+        }
+        signer.init(true, param);
+        signer.update(srcData, 0, srcData.length);
+        return signer.generateSignature();
+    }
+
+    /**
+     * 验签
+     *
+     * @param pubKeyParams 公钥
+     * @param withId       可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes()
+     * @param srcData      原文
+     * @param sign         DER编码的签名值
+     * @return 验签成功返回true,失败返回false
+     */
+    private static boolean verify(final ECPublicKeyParameters pubKeyParams, final byte[] withId, final byte[] srcData,
+                                  final byte[] sign) {
+        final SM2Signer signer = new SM2Signer();
+        CipherParameters param;
+        if (withId != null) {
+            param = new ParametersWithID(pubKeyParams, withId);
+        } else {
+            param = pubKeyParams;
+        }
+        signer.init(false, param);
+        signer.update(srcData, 0, srcData.length);
+        return signer.verifySignature(sign);
+    }
+
+    /**
+     * 生成秘钥对(工具函数)
+     * @return 0 公钥, 1私钥
+     */
+    public static String[] genKey()
+    {
+        AsymmetricCipherKeyPair kPair = BCECUtils.generateKeyPairParameter(DOMAIN_PARAMS, new SecureRandom());
+        ECPrivateKeyParameters ecPriv = (ECPrivateKeyParameters)kPair.getPrivate();
+        ECPublicKeyParameters ecPub = (ECPublicKeyParameters)kPair.getPublic();
+        BigInteger privateKey = ecPriv.getD();
+        ECPoint publicKey = ecPub.getQ();
+
+        byte[] priv = privateKey.toByteArray();
+        byte[] pub = publicKey.getEncoded(false);
+        if (priv.length == 33)
+        {
+            byte[] newPriv = new byte[32];
+            System.arraycopy(priv, 1, newPriv, 0, 32);
+            priv = newPriv;
+        }
+        String[] keyPairs = new String[2];
+        keyPairs[0] = ByteUtils.toHexString(pub);
+        keyPairs[1] = ByteUtils.toHexString(priv);
+
+        return keyPairs;
+    }
+}
+
+
+/**
+ * 这个工具类的方法,也适用于其他基于BC库的ECC算法
+ */
+class BCECUtils {
+    private static final String ALGO_NAME_EC = "EC";
+    private static final String PEM_STRING_PUBLIC = "PUBLIC KEY";
+    private static final String PEM_STRING_ECPRIVATEKEY = "EC PRIVATE KEY";
+
+    /**
+     * 生成ECC密钥对
+     *
+     * @return ECC密钥对
+     */
+    public static AsymmetricCipherKeyPair generateKeyPairParameter(
+            ECDomainParameters domainParameters, SecureRandom random) {
+        ECKeyGenerationParameters keyGenerationParams = new ECKeyGenerationParameters(domainParameters,
+                random);
+        ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
+        keyGen.init(keyGenerationParams);
+        return keyGen.generateKeyPair();
+    }
+
+
+    public static KeyPair generateKeyPair(ECDomainParameters domainParameters, SecureRandom random)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException {
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME);
+        ECParameterSpec parameterSpec = new ECParameterSpec(domainParameters.getCurve(), domainParameters.getG(),
+                domainParameters.getN(), domainParameters.getH());
+        kpg.initialize(parameterSpec, random);
+        return kpg.generateKeyPair();
+    }
+
+    public static int getCurveLength(ECKeyParameters ecKey) {
+        return getCurveLength(ecKey.getParameters());
+    }
+
+    public static int getCurveLength(ECDomainParameters domainParams) {
+        return (domainParams.getCurve().getFieldSize() + 7) / 8;
+    }
+
+    public static byte[] fixToCurveLengthBytes(int curveLength, byte[] src) {
+        if (src.length == curveLength) {
+            return src;
+        }
+
+        byte[] result = new byte[curveLength];
+        if (src.length > curveLength) {
+            System.arraycopy(src, src.length - result.length, result, 0, result.length);
+        } else {
+            System.arraycopy(src, 0, result, result.length - src.length, src.length);
+        }
+        return result;
+    }
+
+    /**
+     * @param dHex             十六进制字符串形式的私钥d值,如果是SM2算法,Hex字符串长度应该是64(即32字节)
+     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#DOMAIN_PARAMS}
+     * @return
+     */
+    public static ECPrivateKeyParameters createECPrivateKeyParameters(
+            String dHex, ECDomainParameters domainParameters) {
+        return createECPrivateKeyParameters(ByteUtils.fromHexString(dHex), domainParameters);
+    }
+
+    /**
+     * @param dBytes           字节数组形式的私钥d值,如果是SM2算法,应该是32字节
+     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#DOMAIN_PARAMS}
+     * @return
+     */
+    public static ECPrivateKeyParameters createECPrivateKeyParameters(
+            byte[] dBytes, ECDomainParameters domainParameters) {
+        return createECPrivateKeyParameters(new BigInteger(1, dBytes), domainParameters);
+    }
+
+    /**
+     * @param d                大数形式的私钥d值
+     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#DOMAIN_PARAMS}
+     * @return
+     */
+    public static ECPrivateKeyParameters createECPrivateKeyParameters(
+            BigInteger d, ECDomainParameters domainParameters) {
+        return new ECPrivateKeyParameters(d, domainParameters);
+    }
+
+    /**
+     * 根据EC私钥构造EC公钥
+     *
+     * @param priKey ECC私钥参数对象
+     * @return
+     */
+    public static ECPublicKeyParameters buildECPublicKeyByPrivateKey(ECPrivateKeyParameters priKey) {
+        ECDomainParameters domainParameters = priKey.getParameters();
+        ECPoint q = new FixedPointCombMultiplier().multiply(domainParameters.getG(), priKey.getD());
+        return new ECPublicKeyParameters(q, domainParameters);
+    }
+
+    /**
+     * @param x                大数形式的公钥x分量
+     * @param y                大数形式的公钥y分量
+     * @param curve            EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#CURVE}
+     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#DOMAIN_PARAMS}
+     * @return
+     */
+    public static ECPublicKeyParameters createECPublicKeyParameters(
+            BigInteger x, BigInteger y, ECCurve curve, ECDomainParameters domainParameters) {
+        return createECPublicKeyParameters(x.toByteArray(), y.toByteArray(), curve, domainParameters);
+    }
+
+    /**
+     * @param xHex             十六进制形式的公钥x分量,如果是SM2算法,Hex字符串长度应该是64(即32字节)
+     * @param yHex             十六进制形式的公钥y分量,如果是SM2算法,Hex字符串长度应该是64(即32字节)
+     * @param curve            EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#CURVE}
+     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#DOMAIN_PARAMS}
+     * @return
+     */
+    public static ECPublicKeyParameters createECPublicKeyParameters(
+            String xHex, String yHex, ECCurve curve, ECDomainParameters domainParameters) {
+        return createECPublicKeyParameters(ByteUtils.fromHexString(xHex), ByteUtils.fromHexString(yHex),
+                curve, domainParameters);
+    }
+
+    /**
+     * @param xBytes           十六进制形式的公钥x分量,如果是SM2算法,应该是32字节
+     * @param yBytes           十六进制形式的公钥y分量,如果是SM2算法,应该是32字节
+     * @param curve            EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#CURVE}
+     * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link Sm2Lib#DOMAIN_PARAMS}
+     * @return
+     */
+    public static ECPublicKeyParameters createECPublicKeyParameters(
+            byte[] xBytes, byte[] yBytes, ECCurve curve, ECDomainParameters domainParameters) {
+        final byte uncompressedFlag = 0x04;
+        int curveLength = getCurveLength(domainParameters);
+        xBytes = fixToCurveLengthBytes(curveLength, xBytes);
+        yBytes = fixToCurveLengthBytes(curveLength, yBytes);
+        byte[] encodedPubKey = new byte[1 + xBytes.length + yBytes.length];
+        encodedPubKey[0] = uncompressedFlag;
+        System.arraycopy(xBytes, 0, encodedPubKey, 1, xBytes.length);
+        System.arraycopy(yBytes, 0, encodedPubKey, 1 + xBytes.length, yBytes.length);
+        return new ECPublicKeyParameters(curve.decodePoint(encodedPubKey), domainParameters);
+    }
+
+    public static ECPrivateKeyParameters convertPrivateKeyToParameters(BCECPrivateKey ecPriKey) {
+        ECParameterSpec parameterSpec = ecPriKey.getParameters();
+        ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(),
+                parameterSpec.getN(), parameterSpec.getH());
+        return new ECPrivateKeyParameters(ecPriKey.getD(), domainParameters);
+    }
+
+    public static ECPublicKeyParameters convertPublicKeyToParameters(BCECPublicKey ecPubKey) {
+        ECParameterSpec parameterSpec = ecPubKey.getParameters();
+        ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(),
+                parameterSpec.getN(), parameterSpec.getH());
+        return new ECPublicKeyParameters(ecPubKey.getQ(), domainParameters);
+    }
+
+    public static BCECPublicKey createPublicKeyFromSubjectPublicKeyInfo(SubjectPublicKeyInfo subPubInfo)
+            throws NoSuchProviderException,
+            NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+        return BCECUtils.convertX509ToECPublicKey(subPubInfo.toASN1Primitive().getEncoded(ASN1Encoding.DER));
+    }
+
+    /**
+     * 将ECC私钥转换为PKCS8标准的字节流
+     *
+     * @param priKey
+     * @param pubKey 可以为空,但是如果为空的话得到的结果OpenSSL可能解析不了
+     * @return
+     */
+    public static byte[] convertECPrivateKeyToPKCS8(
+            ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) {
+        ECDomainParameters domainParams = priKey.getParameters();
+        ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(),
+                domainParams.getN(), domainParams.getH());
+        BCECPublicKey publicKey = null;
+        if (pubKey != null) {
+            publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec,
+                    BouncyCastleProvider.CONFIGURATION);
+        }
+        BCECPrivateKey privateKey = new BCECPrivateKey(ALGO_NAME_EC, priKey, publicKey,
+                spec, BouncyCastleProvider.CONFIGURATION);
+        return privateKey.getEncoded();
+    }
+
+    /**
+     * 将PKCS8标准的私钥字节流转换为私钥对象
+     *
+     * @param pkcs8Key
+     * @return
+     * @throws NoSuchAlgorithmException
+     * @throws NoSuchProviderException
+     * @throws InvalidKeySpecException
+     */
+    public static BCECPrivateKey convertPKCS8ToECPrivateKey(byte[] pkcs8Key)
+            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+        PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(pkcs8Key);
+        KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME);
+        return (BCECPrivateKey) kf.generatePrivate(peks);
+    }
+
+    /**
+     * 将PKCS8标准的私钥字节流转换为PEM
+     *
+     * @param encodedKey
+     * @return
+     * @throws IOException
+     */
+    public static String convertECPrivateKeyPKCS8ToPEM(byte[] encodedKey) throws IOException {
+        return convertEncodedDataToPEM(PEM_STRING_ECPRIVATEKEY, encodedKey);
+    }
+
+    /**
+     * 将PEM格式的私钥转换为PKCS8标准字节流
+     *
+     * @param pemString
+     * @return
+     * @throws IOException
+     */
+    public static byte[] convertECPrivateKeyPEMToPKCS8(String pemString) throws IOException {
+        return convertPEMToEncodedData(pemString);
+    }
+
+    /**
+     * 将ECC私钥转换为SEC1标准的字节流
+     * openssl d2i_ECPrivateKey函数要求的DER编码的私钥也是SEC1标准的,
+     * 这个工具函数的主要目的就是为了能生成一个openssl可以直接“识别”的ECC私钥.
+     * 相对RSA私钥的PKCS1标准,ECC私钥的标准为SEC1
+     *
+     * @param priKey
+     * @param pubKey
+     * @return
+     * @throws IOException
+     */
+    public static byte[] convertECPrivateKeyToSEC1(
+            ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) throws IOException {
+        byte[] pkcs8Bytes = convertECPrivateKeyToPKCS8(priKey, pubKey);
+        PrivateKeyInfo pki = PrivateKeyInfo.getInstance(pkcs8Bytes);
+        ASN1Encodable encodable = pki.parsePrivateKey();
+        ASN1Primitive primitive = encodable.toASN1Primitive();
+        byte[] sec1Bytes = primitive.getEncoded();
+        return sec1Bytes;
+    }
+
+    /**
+     * 将SEC1标准的私钥字节流恢复为PKCS8标准的字节流
+     *
+     * @param sec1Key
+     * @return
+     * @throws IOException
+     */
+    public static byte[] convertECPrivateKeySEC1ToPKCS8(byte[] sec1Key) throws IOException {
+        /**
+         * 参考org.bouncycastle.asn1.pkcs.PrivateKeyInfo和
+         * org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey,逆向拼装
+         */
+        X962Parameters params = getDomainParametersFromName(Sm2Lib.JDK_EC_SPEC, false);
+        ASN1OctetString privKey = new DEROctetString(sec1Key);
+        ASN1EncodableVector v = new ASN1EncodableVector();
+        v.add(new ASN1Integer(0)); //版本号
+        v.add(new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, params)); //算法标识
+        v.add(privKey);
+        DERSequence ds = new DERSequence(v);
+        return ds.getEncoded(ASN1Encoding.DER);
+    }
+
+    /**
+     * 将SEC1标准的私钥字节流转为BCECPrivateKey对象
+     *
+     * @param sec1Key
+     * @return
+     * @throws NoSuchAlgorithmException
+     * @throws NoSuchProviderException
+     * @throws InvalidKeySpecException
+     * @throws IOException
+     */
+    public static BCECPrivateKey convertSEC1ToBCECPrivateKey(byte[] sec1Key)
+            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException {
+        PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(convertECPrivateKeySEC1ToPKCS8(sec1Key));
+        KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME);
+        return (BCECPrivateKey) kf.generatePrivate(peks);
+    }
+
+    /**
+     * 将SEC1标准的私钥字节流转为ECPrivateKeyParameters对象
+     * openssl i2d_ECPrivateKey函数生成的DER编码的ecc私钥是:SEC1标准的、带有EC_GROUP、带有公钥的,
+     * 这个工具函数的主要目的就是为了使Java程序能够“识别”openssl生成的ECC私钥
+     *
+     * @param sec1Key
+     * @return
+     * @throws NoSuchAlgorithmException
+     * @throws NoSuchProviderException
+     * @throws InvalidKeySpecException
+     */
+    public static ECPrivateKeyParameters convertSEC1ToECPrivateKey(byte[] sec1Key)
+            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException {
+        BCECPrivateKey privateKey = convertSEC1ToBCECPrivateKey(sec1Key);
+        return convertPrivateKeyToParameters(privateKey);
+    }
+
+    /**
+     * 将ECC公钥对象转换为X509标准的字节流
+     *
+     * @param pubKey
+     * @return
+     */
+    public static byte[] convertECPublicKeyToX509(ECPublicKeyParameters pubKey) {
+        ECDomainParameters domainParams = pubKey.getParameters();
+        ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(),
+                domainParams.getN(), domainParams.getH());
+        BCECPublicKey publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec,
+                BouncyCastleProvider.CONFIGURATION);
+        return publicKey.getEncoded();
+    }
+
+    /**
+     * 将X509标准的公钥字节流转为公钥对象
+     *
+     * @param x509Bytes
+     * @return
+     * @throws NoSuchProviderException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeySpecException
+     */
+    public static BCECPublicKey convertX509ToECPublicKey(byte[] x509Bytes) throws NoSuchProviderException,
+            NoSuchAlgorithmException, InvalidKeySpecException {
+        X509EncodedKeySpec eks = new X509EncodedKeySpec(x509Bytes);
+        KeyFactory kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME);
+        return (BCECPublicKey) kf.generatePublic(eks);
+    }
+
+    /**
+     * 将X509标准的公钥字节流转为PEM
+     *
+     * @param encodedKey
+     * @return
+     * @throws IOException
+     */
+    public static String convertECPublicKeyX509ToPEM(byte[] encodedKey) throws IOException {
+        return convertEncodedDataToPEM(PEM_STRING_PUBLIC, encodedKey);
+    }
+
+    /**
+     * 将PEM格式的公钥转为X509标准的字节流
+     *
+     * @param pemString
+     * @return
+     * @throws IOException
+     */
+    public static byte[] convertECPublicKeyPEMToX509(String pemString) throws IOException {
+        return convertPEMToEncodedData(pemString);
+    }
+
+    /**
+     * copy from BC
+     *
+     * @param genSpec
+     * @return
+     */
+    public static X9ECParameters getDomainParametersFromGenSpec(ECGenParameterSpec genSpec) {
+        return getDomainParametersFromName(genSpec.getName());
+    }
+
+    /**
+     * copy from BC
+     *
+     * @param curveName
+     * @return
+     */
+    public static X9ECParameters getDomainParametersFromName(String curveName) {
+        X9ECParameters domainParameters;
+        try {
+            if (curveName.charAt(0) >= '0' && curveName.charAt(0) <= '2') {
+                ASN1ObjectIdentifier oidID = new ASN1ObjectIdentifier(curveName);
+                domainParameters = ECUtil.getNamedCurveByOid(oidID);
+            } else {
+                if (curveName.indexOf(' ') > 0) {
+                    curveName = curveName.substring(curveName.indexOf(' ') + 1);
+                    domainParameters = ECUtil.getNamedCurveByName(curveName);
+                } else {
+                    domainParameters = ECUtil.getNamedCurveByName(curveName);
+                }
+            }
+        } catch (IllegalArgumentException ex) {
+            domainParameters = ECUtil.getNamedCurveByName(curveName);
+        }
+        return domainParameters;
+    }
+
+    /**
+     * copy from BC
+     *
+     * @param ecSpec
+     * @param withCompression
+     * @return
+     */
+    public static X962Parameters getDomainParametersFromName(
+            java.security.spec.ECParameterSpec ecSpec, boolean withCompression) {
+        X962Parameters params;
+
+        if (ecSpec instanceof ECNamedCurveSpec) {
+            ASN1ObjectIdentifier curveOid = ECUtil.getNamedCurveOid(((ECNamedCurveSpec) ecSpec).getName());
+            if (curveOid == null) {
+                curveOid = new ASN1ObjectIdentifier(((ECNamedCurveSpec) ecSpec).getName());
+            }
+            params = new X962Parameters(curveOid);
+        } else if (ecSpec == null) {
+            params = new X962Parameters(DERNull.INSTANCE);
+        } else {
+            ECCurve curve = EC5Util.convertCurve(ecSpec.getCurve());
+
+            X9ECParameters ecP = new X9ECParameters(
+                    curve,
+                    new X9ECPoint(EC5Util.convertPoint(curve, ecSpec.getGenerator()), withCompression),
+                    ecSpec.getOrder(),
+                    BigInteger.valueOf(ecSpec.getCofactor()),
+                    ecSpec.getCurve().getSeed());
+
+            //// 如果是1.62或更低版本的bcprov-jdk15on应该使用以下这段代码,因为高版本的EC5Util.convertPoint没有向下兼容
+            /*
+            X9ECParameters ecP = new X9ECParameters(
+                curve,
+                EC5Util.convertPoint(curve, ecSpec.getGenerator(), withCompression),
+                ecSpec.getOrder(),
+                BigInteger.valueOf(ecSpec.getCofactor()),
+                ecSpec.getCurve().getSeed());
+            */
+
+            params = new X962Parameters(ecP);
+        }
+
+        return params;
+    }
+
+    private static String convertEncodedDataToPEM(String type, byte[] encodedData) throws IOException {
+        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
+        PemWriter pWrt = new PemWriter(new OutputStreamWriter(bOut));
+        try {
+            PemObject pemObj = new PemObject(type, encodedData);
+            pWrt.writeObject(pemObj);
+        } finally {
+            pWrt.close();
+        }
+        return new String(bOut.toByteArray());
+    }
+
+    private static byte[] convertPEMToEncodedData(String pemString) throws IOException {
+        ByteArrayInputStream bIn = new ByteArrayInputStream(pemString.getBytes());
+        PemReader pRdr = new PemReader(new InputStreamReader(bIn));
+        try {
+            PemObject pemObject = pRdr.readPemObject();
+            return pemObject.getContent();
+        } finally {
+            pRdr.close();
+        }
+    }
+}
+

+ 94 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/crypt/Sm2Util.java

@@ -0,0 +1,94 @@
+package com.csitc.framework.common.util.crypt;
+/**
+ *  国密2 加签/验签, 加密解密接口
+ */
+public class Sm2Util {
+
+    //单例模式
+    private static Sm2Util instance;
+    public static Sm2Util getInstance() {
+        if (instance == null) {
+            instance = new Sm2Util();
+        }
+        return instance;
+    }
+
+    /**
+     * 加签: 加密签名 (使用 签名私钥)
+     * @param raw 待加签的数据
+     * @param privateKey 签名私钥
+     * @return 加签后的数据, hex
+     */
+    public String sign(String raw, String privateKey) {
+        try {
+            return Sm2Lib.sign(raw, privateKey);
+        }catch (Exception e){
+            log("Sm2Util.sign Exception:"+e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 验签: 签名校验 (使用 公钥)
+     * @param raw  明文数据
+     * @param sign 密文数据,hex 格式
+     * @param publicKey 验签公钥
+     * @return 验签结果 true/false
+     */
+    public boolean verify(String raw, String sign, String publicKey) {
+        try {
+            return Sm2Lib.verify(raw, publicKey, sign);
+        } catch (Exception e) {
+            log("Sm2Util.verify Exception:"+e.getMessage());
+        }
+        return false;
+    }
+
+
+
+    /**
+     * 生成秘钥对(工具函数)
+     * @return 0 公钥, 1私钥
+     */
+    public String[] genKey() {
+        return Sm2Lib.genKey();
+    }
+
+
+    /**
+     * 数据加密(使用 公钥)
+     * @param data 待加密数据
+     * @param publicKey 数据加密公钥
+     * @return 加密后的密文数据
+     */
+    public String encrypt(String data, String publicKey) {
+        try{
+            return  Sm2Lib.encrypt(data, publicKey);
+        }catch(Exception e){
+            log("Sm2Util.encrypt Exception: "+ e);
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
+    /**
+     * 数据解密 (默认  私钥)
+     * @param data 密文数据
+     * @param privateKey 数据解密私钥
+     * @return 解密后的明文数据
+     */
+    public String decrypt(String data, String privateKey)  {
+        try {
+            return Sm2Lib.decrypt(data, privateKey);
+        }catch(Exception e){
+            log("Sm2Util.decrypt Exception: "+ e);
+            return null;
+        }
+    }
+
+    private void log(String msg){
+        System.out.println(msg);
+    }
+
+}

+ 173 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/date/DateUtils.java

@@ -0,0 +1,173 @@
+package com.csitc.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_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
+
+    public static final String FORMAT_HOUR_MINUTE_SECOND = "HH:mm:ss";
+
+    /**
+     * 将 LocalDateTime 转换成 Date
+     *
+     * @param date LocalDateTime
+     * @return LocalDateTime
+     */
+    public static Date of(LocalDateTime date) {
+        // 将此日期时间与时区相结合以创建 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) {
+        // 转为时间戳
+        Instant instant = date.toInstant();
+        // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
+        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+    }
+
+    @Deprecated
+    public static Date addTime(Duration duration) {
+        return new Date(System.currentTimeMillis() + duration.toMillis());
+    }
+
+    public static boolean isExpired(Date time) {
+        return System.currentTimeMillis() > time.getTime();
+    }
+
+    public static boolean isExpired(LocalDateTime time) {
+        LocalDateTime now = LocalDateTime.now();
+        return now.isAfter(time);
+    }
+
+    public static long diff(Date endTime, Date startTime) {
+        return endTime.getTime() - startTime.getTime();
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year  年
+     * @param mouth 月
+     * @param day   日
+     * @return 指定时间
+     */
+    public static Date buildTime(int year, int mouth, int day) {
+        return buildTime(year, mouth, day, 0, 0, 0);
+    }
+
+    /**
+     * 创建指定时间
+     *
+     * @param year   年
+     * @param mouth  月
+     * @param day    日
+     * @param hour   小时
+     * @param minute 分钟
+     * @param second 秒
+     * @return 指定时间
+     */
+    public static Date buildTime(int year, int mouth, int day,
+                                 int hour, int minute, int second) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.YEAR, year);
+        calendar.set(Calendar.MONTH, mouth - 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 field  日历字段.<br/>eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,<br/>Calendar.HOUR_OF_DAY等.
+     * @param amount 相差的数值
+     * @return 计算后的日志
+     */
+    public static Date addDate(int field, int amount) {
+        return addDate(null, field, amount);
+    }
+
+    /**
+     * 计算当期时间相差的日期
+     *
+     * @param date   设置时间
+     * @param field  日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等
+     * @param amount 相差的数值
+     * @return 计算后的日志
+     */
+    public static Date addDate(Date date, int field, int amount) {
+        if (amount == 0) {
+            return date;
+        }
+        Calendar c = Calendar.getInstance();
+        if (date != null) {
+            c.setTime(date);
+        }
+        c.add(field, amount);
+        return c.getTime();
+    }
+
+    /**
+     * 是否今天
+     *
+     * @param date 日期
+     * @return 是否
+     */
+    public static boolean isToday(LocalDateTime date) {
+        return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now());
+    }
+
+}

+ 63 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/date/LocalDateTimeUtils.java

@@ -0,0 +1,63 @@
+package com.csitc.framework.common.util.date;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * 时间工具类,用于 {@link java.time.LocalDateTime}
+ *
+ * @author 隧道
+ */
+public class LocalDateTimeUtils {
+
+    /**
+     * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
+     */
+    public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
+
+    public static LocalDateTime addTime(Duration duration) {
+        return LocalDateTime.now().plus(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 mouth 月
+     * @param day   日
+     * @return 指定时间
+     */
+    public static LocalDateTime buildTime(int year, int mouth, int day) {
+        return LocalDateTime.of(year, mouth, day, 0, 0, 0);
+    }
+
+    public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1,
+                                                   int year2, int mouth2, int day2) {
+        return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)};
+    }
+
+    /**
+     * 判断当前时间是否在该时间范围内
+     *
+     * @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);
+    }
+
+}

+ 126 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/http/HttpUtils.java

@@ -0,0 +1,126 @@
+package com.csitc.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 org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.Map;
+
+/**
+ * HTTP 工具类
+ *
+ * @author 隧道
+ */
+public class HttpUtils {
+
+    @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();
+    }
+
+    private String append(String base, Map<String, ?> query, boolean fragment) {
+        return append(base, query, null, fragment);
+    }
+
+    /**
+     * 拼接 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;
+    }
+
+
+}

+ 84 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/io/FileUtils.java

@@ -0,0 +1,84 @@
+package com.csitc.framework.common.util.io;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import lombok.SneakyThrows;
+
+import java.io.ByteArrayInputStream;
+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;
+    }
+
+    /**
+     * 生成文件路径
+     *
+     * @param content      文件内容
+     * @param originalName 原始文件名
+     * @return path,唯一不可重复
+     */
+    public static String generatePath(byte[] content, String originalName) {
+        String sha256Hex = DigestUtil.sha256Hex(content);
+        // 情况一:如果存在 name,则优先使用 name 的后缀
+        if (StrUtil.isNotBlank(originalName)) {
+            String extName = FileNameUtil.extName(originalName);
+            return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
+        }
+        // 情况二:基于 content 计算
+        return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
+    }
+
+}

+ 28 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/io/IoUtils.java

@@ -0,0 +1,28 @@
+package com.csitc.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));
+    }
+
+}

+ 144 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/json/JsonUtils.java

@@ -0,0 +1,144 @@
+package com.csitc.framework.common.util.json;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * @author 隧道
+ */
+@UtilityClass
+@Slf4j
+public class JsonUtils {
+
+    private static ObjectMapper objectMapper = new ObjectMapper();
+
+    static {
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
+    }
+
+    /**
+     * 初始化 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);
+        }
+    }
+
+    /**
+     * 将字符串解析成指定类型的对象
+     * 使用 {@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);
+        }
+    }
+
+    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 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);
+    }
+
+}

+ 30 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/monitor/TracerUtils.java

@@ -0,0 +1,30 @@
+package com.csitc.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();
+    }
+
+}

+ 16 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/number/NumberUtils.java

@@ -0,0 +1,16 @@
+package com.csitc.framework.common.util.number;
+
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
+ *
+ * @author 隧道
+ */
+public class NumberUtils {
+
+    public static Long parseLong(String str) {
+        return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
+    }
+
+}

+ 63 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/object/ObjectUtils.java

@@ -0,0 +1,63 @@
+package com.csitc.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);
+    }
+
+}

+ 16 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/object/PageUtils.java

@@ -0,0 +1,16 @@
+package com.csitc.framework.common.util.object;
+
+import com.csitc.framework.common.pojo.PageParam;
+
+/**
+ * {@link com.csitc.framework.common.pojo.PageParam} 工具类
+ *
+ * @author 隧道
+ */
+public class PageUtils {
+
+    public static int getStart(PageParam pageParam) {
+        return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
+    }
+
+}

+ 7 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/package-info.java

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

+ 95 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/servlet/ServletUtils.java

@@ -0,0 +1,95 @@
+package com.csitc.framework.common.util.servlet;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import com.csitc.framework.common.util.json.JsonUtils;
+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 javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+
+/**
+ * 客户端工具类
+ *
+ * @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);
+        ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
+    }
+
+    /**
+     * 返回附件
+     *
+     * @param response 响应
+     * @param filename 文件名
+     * @param content 附件内容
+     * @throws IOException
+     */
+    public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
+        // 设置 header 和 contentType
+        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+        // 输出附件
+        IoUtil.write(response.getOutputStream(), false, content);
+    }
+
+    /**
+     * @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 ServletUtil.getClientIP(request);
+    }
+
+    public static boolean isJsonRequest(ServletRequest request) {
+        return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+    }
+
+}

+ 46 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/spring/SpringAopUtils.java

@@ -0,0 +1,46 @@
+package com.csitc.framework.common.util.spring;
+
+import cn.hutool.core.bean.BeanUtil;
+import org.springframework.aop.framework.AdvisedSupport;
+import org.springframework.aop.framework.AopProxy;
+import org.springframework.aop.support.AopUtils;
+
+/**
+ * Spring AOP 工具类
+ *
+ * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现
+ */
+public class SpringAopUtils {
+
+    /**
+     * 获取代理的目标对象
+     *
+     * @param proxy 代理对象
+     * @return 目标对象
+     */
+    public static Object getTarget(Object proxy) throws Exception {
+        // 不是代理对象
+        if (!AopUtils.isAopProxy(proxy)) {
+            return proxy;
+        }
+        // Jdk 代理
+        if (AopUtils.isJdkDynamicProxy(proxy)) {
+            return getJdkDynamicProxyTargetObject(proxy);
+        }
+        // Cglib 代理
+        return getCglibProxyTargetObject(proxy);
+    }
+
+    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
+        Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0");
+        AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised");
+        return advisedSupport.getTargetSource().getTarget();
+    }
+
+    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
+        AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h");
+        AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised");
+        return advisedSupport.getTargetSource().getTarget();
+    }
+
+}

+ 82 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/spring/SpringExpressionUtils.java

@@ -0,0 +1,82 @@
+package com.csitc.framework.common.util.spring;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+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 {
+
+    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(ProceedingJoinPoint 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(ProceedingJoinPoint 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;
+    }
+}

+ 48 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/string/StrUtils.java

@@ -0,0 +1,48 @@
+package com.csitc.framework.common.util.string;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+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());
+    }
+
+}

+ 47 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/util/validation/ValidationUtils.java

@@ -0,0 +1,47 @@
+package com.csitc.framework.common.util.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import org.springframework.util.StringUtils;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.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[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[189]))\\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(Validator validator, Object object, Class<?>... groups) {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+        if (CollUtil.isNotEmpty(constraintViolations)) {
+            throw new ConstraintViolationException(constraintViolations);
+        }
+    }
+
+}

+ 35 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/InEnum.java

@@ -0,0 +1,35 @@
+package com.csitc.framework.common.validation;
+
+import com.csitc.framework.common.core.IntArrayValuable;
+
+import javax.validation.Constraint;
+import javax.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
+)
+public @interface InEnum {
+
+    /**
+     * @return 实现 EnumValuable 接口的
+     */
+    Class<? extends IntArrayValuable> value();
+
+    String message() default "必须在指定范围 {value}";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 44 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/InEnumValidator.java

@@ -0,0 +1,44 @@
+package com.csitc.framework.common.validation;
+
+import com.csitc.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {
+
+    private List<Integer> values;
+
+    @Override
+    public void initialize(InEnum annotation) {
+        IntArrayValuable[] values = annotation.value().getEnumConstants();
+        if (values.length == 0) {
+            this.values = Collections.emptyList();
+        } else {
+            this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList());
+        }
+    }
+
+    @Override
+    public boolean isValid(Integer value, ConstraintValidatorContext context) {
+        // 为空时,默认不校验,即认为通过
+        if (value == null) {
+            return true;
+        }
+        // 校验通过
+        if (values.contains(value)) {
+            return true;
+        }
+        // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+                .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
+        return false;
+    }
+
+}
+

+ 28 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/Mobile.java

@@ -0,0 +1,28 @@
+package com.csitc.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.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
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/MobileValidator.java

@@ -0,0 +1,25 @@
+package com.csitc.framework.common.validation;
+
+import cn.hutool.core.util.StrUtil;
+import com.csitc.framework.common.util.validation.ValidationUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.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);
+    }
+
+}

+ 4 - 0
cloud-framework/cloud-common/src/main/java/com/csitc/framework/common/validation/package-info.java

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

+ 30 - 0
cloud-framework/cloud-spring-boot-starter-banner/pom.xml

@@ -0,0 +1,30 @@
+<?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>cloud-framework</artifactId>
+        <groupId>com.csitc</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>cloud-spring-boot-starter-banner</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>Banner 用于在 console 控制台,打印开发文档、接口文档等</description>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 20 - 0
cloud-framework/cloud-spring-boot-starter-banner/src/main/java/com/csitc/framework/banner/config/CloudBannerAutoConfiguration.java

@@ -0,0 +1,20 @@
+package com.csitc.framework.banner.config;
+
+import com.csitc.framework.banner.core.BannerApplicationRunner;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * Banner 的自动配置类
+ *
+ * @author 隧道
+ */
+@AutoConfiguration
+public class CloudBannerAutoConfiguration {
+
+    @Bean
+    public BannerApplicationRunner bannerApplicationRunner() {
+        return new BannerApplicationRunner();
+    }
+
+}

+ 30 - 0
cloud-framework/cloud-spring-boot-starter-banner/src/main/java/com/csitc/framework/banner/core/BannerApplicationRunner.java

@@ -0,0 +1,30 @@
+package com.csitc.framework.banner.core;
+
+import cn.hutool.core.thread.ThreadUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 项目启动成功后,提供文档相关的地址
+ *
+ * @author 隧道
+ */
+@Slf4j
+public class BannerApplicationRunner implements ApplicationRunner {
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        ThreadUtil.execute(() -> {
+            ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾
+            log.info("\n----------------------------------------------------------\n\t" +
+                    "隧道项目启动成功!\n\t" +
+                    "http://127.0.0.1:48080/\n\t" +
+                    "http://127.0.0.1:48080/doc.html\n" +
+                    "----------------------------------------------------------");
+        });
+    }
+
+}

+ 6 - 0
cloud-framework/cloud-spring-boot-starter-banner/src/main/java/com/csitc/framework/banner/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * Banner 用于在 console 控制台,打印开发文档、接口文档等
+ *
+ * @author 隧道
+ */
+package com.csitc.framework.banner;

+ 1 - 0
cloud-framework/cloud-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.csitc.framework.banner.config.CloudBannerAutoConfiguration

+ 3 - 0
cloud-framework/cloud-spring-boot-starter-banner/src/main/resources/banner.txt

@@ -0,0 +1,3 @@
+隧道 http://www.cloud.csitc.com
+Application Version: ${cloud.info.version}
+Spring Boot Version: ${spring-boot.version}

+ 52 - 0
cloud-framework/cloud-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>cloud-framework</artifactId>
+        <groupId>com.csitc</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>cloud-spring-boot-starter-biz-data-permission</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>数据权限</description>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-spring-boot-starter-security</artifactId>
+            <optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 -->
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 44 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/config/CloudDataPermissionAutoConfiguration.java

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

+ 34 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/config/CloudDeptDataPermissionAutoConfiguration.java

@@ -0,0 +1,34 @@
+package com.csitc.framework.datapermission.config;
+
+import com.csitc.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
+import com.csitc.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
+import com.csitc.framework.security.core.LoginUser;
+import com.csitc.module.system.api.permission.PermissionApi;
+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 = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
+public class CloudDeptDataPermissionAutoConfiguration {
+
+    @Bean
+    public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
+                                                         List<DeptDataPermissionRuleCustomizer> customizers) {
+        // 创建 DeptDataPermissionRule 对象
+        DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
+        // 补全表配置
+        customizers.forEach(customizer -> customizer.customize(rule));
+        return rule;
+    }
+
+}

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

@@ -0,0 +1,35 @@
+package com.csitc.framework.datapermission.core.annotation;
+
+import com.csitc.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
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java

@@ -0,0 +1,36 @@
+package com.csitc.framework.datapermission.core.aop;
+
+import com.csitc.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.csitc.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
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java

@@ -0,0 +1,72 @@
+package com.csitc.framework.datapermission.core.aop;
+
+import com.csitc.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
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/aop/DataPermissionContextHolder.java

@@ -0,0 +1,72 @@
+package com.csitc.framework.datapermission.core.aop;
+
+import com.csitc.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();
+    }
+
+}

+ 639 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java

@@ -0,0 +1,639 @@
+package com.csitc.framework.datapermission.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import com.csitc.framework.common.util.collection.SetUtils;
+import com.csitc.framework.datapermission.core.rule.DataPermissionRule;
+import com.csitc.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import com.csitc.framework.mybatis.core.util.MyBatisUtils;
+import com.alibaba.ttl.TransmittableThreadLocal;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import net.sf.jsqlparser.expression.*;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.select.*;
+import net.sf.jsqlparser.statement.update.Update;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.SqlCommandType;
+import org.apache.ibatis.session.ResultHandler;
+import org.apache.ibatis.session.RowBounds;
+
+import java.sql.Connection;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现
+ * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法
+ *
+ * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。
+ * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更!
+ *
+ * @author 隧道
+ */
+@RequiredArgsConstructor
+public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor {
+
+    private final DataPermissionRuleFactory ruleFactory;
+
+    @Getter
+    private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
+
+    @Override // SELECT 场景
+    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+        // 获得 Mapper 对应的数据权限的规则
+        List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
+        if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
+            return;
+        }
+
+        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
+        try {
+            // 初始化上下文
+            ContextHolder.init(rules);
+            // 处理 SQL
+            mpBs.sql(parserSingle(mpBs.sql(), null));
+        } finally {
+            // 添加是否需要重写的缓存
+            addMappedStatementCache(ms);
+            // 清空上下文
+            ContextHolder.clear();
+        }
+    }
+
+    @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限)
+    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
+        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
+        MappedStatement ms = mpSh.mappedStatement();
+        SqlCommandType sct = ms.getSqlCommandType();
+        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
+            // 获得 Mapper 对应的数据权限的规则
+            List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
+            if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
+                return;
+            }
+
+            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
+            try {
+                // 初始化上下文
+                ContextHolder.init(rules);
+                // 处理 SQL
+                mpBs.sql(parserMulti(mpBs.sql(), null));
+            } finally {
+                // 添加是否需要重写的缓存
+                addMappedStatementCache(ms);
+                // 清空上下文
+                ContextHolder.clear();
+            }
+        }
+    }
+
+    @Override
+    protected void processSelect(Select select, int index, String sql, Object obj) {
+        processSelectBody(select.getSelectBody());
+        List<WithItem> withItemsList = select.getWithItemsList();
+        if (!CollectionUtils.isEmpty(withItemsList)) {
+            withItemsList.forEach(this::processSelectBody);
+        }
+    }
+
+    /**
+     * update 语句处理
+     */
+    @Override
+    protected void processUpdate(Update update, int index, String sql, Object obj) {
+        final Table table = update.getTable();
+        update.setWhere(this.builderExpression(update.getWhere(), table));
+    }
+
+    /**
+     * delete 语句处理
+     */
+    @Override
+    protected void processDelete(Delete delete, int index, String sql, Object obj) {
+        delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
+    }
+
+    // ========== 和 TenantLineInnerInterceptor 一致的逻辑 ==========
+
+    protected void processSelectBody(SelectBody selectBody) {
+        if (selectBody == null) {
+            return;
+        }
+        if (selectBody instanceof PlainSelect) {
+            processPlainSelect((PlainSelect) selectBody);
+        } else if (selectBody instanceof WithItem) {
+            WithItem withItem = (WithItem) selectBody;
+            processSelectBody(withItem.getSubSelect().getSelectBody());
+        } else {
+            SetOperationList operationList = (SetOperationList) selectBody;
+            List<SelectBody> selectBodyList = operationList.getSelects();
+            if (CollectionUtils.isNotEmpty(selectBodyList)) {
+                selectBodyList.forEach(this::processSelectBody);
+            }
+        }
+    }
+
+    /**
+     * 处理 PlainSelect
+     */
+    protected void processPlainSelect(PlainSelect plainSelect) {
+        //#3087 github
+        List<SelectItem> selectItems = plainSelect.getSelectItems();
+        if (CollectionUtils.isNotEmpty(selectItems)) {
+            selectItems.forEach(this::processSelectItem);
+        }
+
+        // 处理 where 中的子查询
+        Expression where = plainSelect.getWhere();
+        processWhereSubSelect(where);
+
+        // 处理 fromItem
+        FromItem fromItem = plainSelect.getFromItem();
+        List<Table> list = processFromItem(fromItem);
+        List<Table> mainTables = new ArrayList<>(list);
+
+        // 处理 join
+        List<Join> joins = plainSelect.getJoins();
+        if (CollectionUtils.isNotEmpty(joins)) {
+            mainTables = processJoins(mainTables, joins);
+        }
+
+        // 当有 mainTable 时,进行 where 条件追加
+        if (CollectionUtils.isNotEmpty(mainTables)) {
+            plainSelect.setWhere(builderExpression(where, mainTables));
+        }
+    }
+
+    private List<Table> processFromItem(FromItem fromItem) {
+        // 处理括号括起来的表达式
+        while (fromItem instanceof ParenthesisFromItem) {
+            fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
+        }
+
+        List<Table> mainTables = new ArrayList<>();
+        // 无 join 时的处理逻辑
+        if (fromItem instanceof Table) {
+            Table fromTable = (Table) fromItem;
+            mainTables.add(fromTable);
+        } else if (fromItem instanceof SubJoin) {
+            // SubJoin 类型则还需要添加上 where 条件
+            List<Table> tables = processSubJoin((SubJoin) fromItem);
+            mainTables.addAll(tables);
+        } else {
+            // 处理下 fromItem
+            processOtherFromItem(fromItem);
+        }
+        return mainTables;
+    }
+
+    /**
+     * 处理where条件内的子查询
+     * <p>
+     * 支持如下:
+     * 1. in
+     * 2. =
+     * 3. >
+     * 4. <
+     * 5. >=
+     * 6. <=
+     * 7. <>
+     * 8. EXISTS
+     * 9. NOT EXISTS
+     * <p>
+     * 前提条件:
+     * 1. 子查询必须放在小括号中
+     * 2. 子查询一般放在比较操作符的右边
+     *
+     * @param where where 条件
+     */
+    protected void processWhereSubSelect(Expression where) {
+        if (where == null) {
+            return;
+        }
+        if (where instanceof FromItem) {
+            processOtherFromItem((FromItem) where);
+            return;
+        }
+        if (where.toString().indexOf("SELECT") > 0) {
+            // 有子查询
+            if (where instanceof BinaryExpression) {
+                // 比较符号 , and , or , 等等
+                BinaryExpression expression = (BinaryExpression) where;
+                processWhereSubSelect(expression.getLeftExpression());
+                processWhereSubSelect(expression.getRightExpression());
+            } else if (where instanceof InExpression) {
+                // in
+                InExpression expression = (InExpression) where;
+                Expression inExpression = expression.getRightExpression();
+                if (inExpression instanceof SubSelect) {
+                    processSelectBody(((SubSelect) inExpression).getSelectBody());
+                }
+            } else if (where instanceof ExistsExpression) {
+                // exists
+                ExistsExpression expression = (ExistsExpression) where;
+                processWhereSubSelect(expression.getRightExpression());
+            } else if (where instanceof NotExpression) {
+                // not exists
+                NotExpression expression = (NotExpression) where;
+                processWhereSubSelect(expression.getExpression());
+            } else if (where instanceof Parenthesis) {
+                Parenthesis expression = (Parenthesis) where;
+                processWhereSubSelect(expression.getExpression());
+            }
+        }
+    }
+
+    protected void processSelectItem(SelectItem selectItem) {
+        if (selectItem instanceof SelectExpressionItem) {
+            SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
+            if (selectExpressionItem.getExpression() instanceof SubSelect) {
+                processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
+            } else if (selectExpressionItem.getExpression() instanceof Function) {
+                processFunction((Function) selectExpressionItem.getExpression());
+            }
+        }
+    }
+
+    /**
+     * 处理函数
+     * <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p>
+     * <p> fixed gitee pulls/141</p>
+     *
+     * @param function
+     */
+    protected void processFunction(Function function) {
+        ExpressionList parameters = function.getParameters();
+        if (parameters != null) {
+            parameters.getExpressions().forEach(expression -> {
+                if (expression instanceof SubSelect) {
+                    processSelectBody(((SubSelect) expression).getSelectBody());
+                } else if (expression instanceof Function) {
+                    processFunction((Function) expression);
+                }
+            });
+        }
+    }
+
+    /**
+     * 处理子查询等
+     */
+    protected void processOtherFromItem(FromItem fromItem) {
+        // 去除括号
+        while (fromItem instanceof ParenthesisFromItem) {
+            fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
+        }
+
+        if (fromItem instanceof SubSelect) {
+            SubSelect subSelect = (SubSelect) fromItem;
+            if (subSelect.getSelectBody() != null) {
+                processSelectBody(subSelect.getSelectBody());
+            }
+        } else if (fromItem instanceof ValuesList) {
+            logger.debug("Perform a subQuery, if you do not give us feedback");
+        } else if (fromItem instanceof LateralSubSelect) {
+            LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
+            if (lateralSubSelect.getSubSelect() != null) {
+                SubSelect subSelect = lateralSubSelect.getSubSelect();
+                if (subSelect.getSelectBody() != null) {
+                    processSelectBody(subSelect.getSelectBody());
+                }
+            }
+        }
+    }
+
+    /**
+     * 处理 sub join
+     *
+     * @param subJoin subJoin
+     * @return Table subJoin 中的主表
+     */
+    private List<Table> processSubJoin(SubJoin subJoin) {
+        List<Table> mainTables = new ArrayList<>();
+        if (subJoin.getJoinList() != null) {
+            List<Table> list = processFromItem(subJoin.getLeft());
+            mainTables.addAll(list);
+            mainTables = processJoins(mainTables, subJoin.getJoinList());
+        }
+        return mainTables;
+    }
+
+    /**
+     * 处理 joins
+     *
+     * @param mainTables 可以为 null
+     * @param joins      join 集合
+     * @return List<Table> 右连接查询的 Table 列表
+     */
+    private List<Table> processJoins(List<Table> mainTables, List<Join> joins) {
+        // join 表达式中最终的主表
+        Table mainTable = null;
+        // 当前 join 的左表
+        Table leftTable = null;
+
+        if (mainTables == null) {
+            mainTables = new ArrayList<>();
+        } else if (mainTables.size() == 1) {
+            mainTable = mainTables.get(0);
+            leftTable = mainTable;
+        }
+
+        //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
+        Deque<List<Table>> onTableDeque = new LinkedList<>();
+        for (Join join : joins) {
+            // 处理 on 表达式
+            FromItem joinItem = join.getRightItem();
+
+            // 获取当前 join 的表,subJoint 可以看作是一张表
+            List<Table> joinTables = null;
+            if (joinItem instanceof Table) {
+                joinTables = new ArrayList<>();
+                joinTables.add((Table) joinItem);
+            } else if (joinItem instanceof SubJoin) {
+                joinTables = processSubJoin((SubJoin) joinItem);
+            }
+
+            if (joinTables != null) {
+
+                // 如果是隐式内连接
+                if (join.isSimple()) {
+                    mainTables.addAll(joinTables);
+                    continue;
+                }
+
+                // 当前表是否忽略
+                Table joinTable = joinTables.get(0);
+
+                List<Table> onTables = null;
+                // 如果不要忽略,且是右连接,则记录下当前表
+                if (join.isRight()) {
+                    mainTable = joinTable;
+                    if (leftTable != null) {
+                        onTables = Collections.singletonList(leftTable);
+                    }
+                } else if (join.isLeft()) {
+                    onTables = Collections.singletonList(joinTable);
+                } else if (join.isInner()) {
+                    if (mainTable == null) {
+                        onTables = Collections.singletonList(joinTable);
+                    } else {
+                        onTables = Arrays.asList(mainTable, joinTable);
+                    }
+                    mainTable = null;
+                }
+
+                mainTables = new ArrayList<>();
+                if (mainTable != null) {
+                    mainTables.add(mainTable);
+                }
+
+                // 获取 join 尾缀的 on 表达式列表
+                Collection<Expression> originOnExpressions = join.getOnExpressions();
+                // 正常 join on 表达式只有一个,立刻处理
+                if (originOnExpressions.size() == 1 && onTables != null) {
+                    List<Expression> onExpressions = new LinkedList<>();
+                    onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables));
+                    join.setOnExpressions(onExpressions);
+                    leftTable = joinTable;
+                    continue;
+                }
+                // 表名压栈,忽略的表压入 null,以便后续不处理
+                onTableDeque.push(onTables);
+                // 尾缀多个 on 表达式的时候统一处理
+                if (originOnExpressions.size() > 1) {
+                    Collection<Expression> onExpressions = new LinkedList<>();
+                    for (Expression originOnExpression : originOnExpressions) {
+                        List<Table> currentTableList = onTableDeque.poll();
+                        if (CollectionUtils.isEmpty(currentTableList)) {
+                            onExpressions.add(originOnExpression);
+                        } else {
+                            onExpressions.add(builderExpression(originOnExpression, currentTableList));
+                        }
+                    }
+                    join.setOnExpressions(onExpressions);
+                }
+                leftTable = joinTable;
+            } else {
+                processOtherFromItem(joinItem);
+                leftTable = null;
+            }
+        }
+
+        return mainTables;
+    }
+
+    // ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ==========
+
+    /**
+     * 处理条件
+     *
+     * @param currentExpression 当前 where 条件
+     * @param table             单个表
+     */
+    protected Expression builderExpression(Expression currentExpression, Table table) {
+        return this.builderExpression(currentExpression, Collections.singletonList(table));
+    }
+
+    /**
+     * 处理条件
+     *
+     * @param currentExpression 当前 where 条件
+     * @param tables 多个表
+     */
+    protected Expression builderExpression(Expression currentExpression, List<Table> tables) {
+        // 没有表需要处理直接返回
+        if (CollectionUtils.isEmpty(tables)) {
+            return currentExpression;
+        }
+
+        // 第一步,获得 Table 对应的数据权限条件
+        Expression dataPermissionExpression = null;
+        for (Table table : tables) {
+            // 构建每个表的权限 Expression 条件
+            Expression expression = buildDataPermissionExpression(table);
+            if (expression == null) {
+                continue;
+            }
+            // 合并到 dataPermissionExpression 中
+            dataPermissionExpression = dataPermissionExpression == null ? expression
+                    : new AndExpression(dataPermissionExpression, expression);
+        }
+
+        // 第二步,合并多个 Expression 条件
+        if (dataPermissionExpression == null) {
+            return currentExpression;
+        }
+        if (currentExpression == null) {
+            return dataPermissionExpression;
+        }
+        // ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression
+        if (currentExpression instanceof OrExpression) {
+            return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);
+        }
+        // ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression
+        return new AndExpression(currentExpression, dataPermissionExpression);
+    }
+
+    /**
+     * 构建指定表的数据权限的 Expression 过滤条件
+     *
+     * @param table 表
+     * @return Expression 过滤条件
+     */
+    private Expression buildDataPermissionExpression(Table table) {
+        // 生成条件
+        Expression allExpression = null;
+        for (DataPermissionRule rule : ContextHolder.getRules()) {
+            // 判断表名是否匹配
+            if (!rule.getTableNames().contains(table.getName())) {
+                continue;
+            }
+            // 如果有匹配的规则,说明可重写。
+            // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
+            // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
+            ContextHolder.setRewrite(true);
+
+            // 单条规则的条件
+            String tableName = MyBatisUtils.getTableName(table);
+            Expression oneExpress = rule.getExpression(tableName, table.getAlias());
+            // 拼接到 allExpression 中
+            allExpression = allExpression == null ? oneExpress
+                    : new AndExpression(allExpression, oneExpress);
+        }
+
+        return allExpression;
+    }
+
+    /**
+     * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中
+     *
+     * @param ms MappedStatement
+     */
+    private void addMappedStatementCache(MappedStatement ms) {
+        if (ContextHolder.getRewrite()) {
+            return;
+        }
+        // 无重写,进行添加
+        mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules());
+    }
+
+    /**
+     * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则
+     *
+     * @author 隧道
+     */
+    static final class ContextHolder {
+
+        /**
+         * 该 {@link MappedStatement} 对应的规则
+         */
+        private static final ThreadLocal<List<DataPermissionRule>> RULES = new TransmittableThreadLocal<>();
+        /**
+         * SQL 是否进行重写
+         */
+        private static final ThreadLocal<Boolean> REWRITE = new TransmittableThreadLocal<>();
+
+        public static void init(List<DataPermissionRule> rules) {
+            RULES.set(rules);
+            REWRITE.set(false);
+        }
+
+        public static void clear() {
+            RULES.remove();
+            REWRITE.remove();
+        }
+
+        public static boolean getRewrite() {
+            return REWRITE.get();
+        }
+
+        public static void setRewrite(boolean rewrite) {
+            REWRITE.set(rewrite);
+        }
+
+        public static List<DataPermissionRule> getRules() {
+            return RULES.get();
+        }
+
+    }
+
+    /**
+     * {@link MappedStatement} 缓存
+     * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效
+     * 如果无效,则可以避免 SQL 的解析,加快速度
+     *
+     * @author 隧道
+     */
+    static final class MappedStatementCache {
+
+        /**
+         * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存
+         *
+         * value:{@link MappedStatement#getId()} 编号
+         */
+        @Getter
+        private final Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>();
+
+        /**
+         * 判断是否无需重写
+         * ps:虽然有点中文式英语,但是容易读懂即可
+         *
+         * @param ms MappedStatement
+         * @param rules 数据权限规则数组
+         * @return 是否无需重写
+         */
+        public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
+            // 如果规则为空,说明无需重写
+            if (CollUtil.isEmpty(rules)) {
+                return true;
+            }
+            // 任一规则不在 noRewritableMap 中,则说明可能需要重写
+            for (DataPermissionRule rule : rules) {
+                Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
+                if (!CollUtil.contains(mappedStatementIds, ms.getId())) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * 添加无需重写的 MappedStatement
+         *
+         * @param ms MappedStatement
+         * @param rules 数据权限规则数组
+         */
+        public void addNoRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
+            for (DataPermissionRule rule : rules) {
+                Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
+                if (CollUtil.isNotEmpty(mappedStatementIds)) {
+                    mappedStatementIds.add(ms.getId());
+                } else {
+                    noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId()));
+                }
+            }
+        }
+
+        /**
+         * 清空缓存
+         * 目前主要提供给单元测试
+         */
+        public void clear() {
+            noRewritableMappedStatements.clear();
+        }
+
+    }
+
+}

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

@@ -0,0 +1,36 @@
+package com.csitc.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
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/DataPermissionRuleFactory.java

@@ -0,0 +1,28 @@
+package com.csitc.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);
+
+}

+ 62 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java

@@ -0,0 +1,62 @@
+package com.csitc.framework.datapermission.core.rule;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.csitc.framework.datapermission.core.annotation.DataPermission;
+import com.csitc.framework.datapermission.core.aop.DataPermissionContextHolder;
+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. 无数据权限
+        if (CollUtil.isEmpty(rules)) {
+            return Collections.emptyList();
+        }
+        // 2. 未配置,则默认开启
+        DataPermission dataPermission = DataPermissionContextHolder.get();
+        if (dataPermission == null) {
+            return rules;
+        }
+        // 3. 已配置,但禁用
+        if (!dataPermission.enable()) {
+            return Collections.emptyList();
+        }
+
+        // 4. 已配置,只选择部分规则
+        if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
+            return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
+                    .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
+        }
+        // 5. 已配置,只排除部分规则
+        if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
+            return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
+                    .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
+        }
+        // 6. 已配置,全部规则
+        return rules;
+    }
+
+}

+ 208 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java

@@ -0,0 +1,208 @@
+package com.csitc.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.csitc.framework.common.enums.UserTypeEnum;
+import com.csitc.framework.common.util.collection.CollectionUtils;
+import com.csitc.framework.common.util.json.JsonUtils;
+import com.csitc.framework.datapermission.core.rule.DataPermissionRule;
+import com.csitc.framework.expression.OrExpressionX;
+import com.csitc.framework.mybatis.core.dataobject.BaseDO;
+import com.csitc.framework.mybatis.core.util.MyBatisUtils;
+import com.csitc.framework.security.core.LoginUser;
+import com.csitc.framework.security.core.util.SecurityFrameworkUtils;
+import com.csitc.module.system.api.permission.PermissionApi;
+import com.csitc.module.system.api.permission.dto.DeptDataPermissionRespDTO;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.NullValue;
+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 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 不进行修改,则会导致用户看到之前的数据。【cloud-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 PermissionApi 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());
+            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 OrExpressionX(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),
+                new ExpressionList(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
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java

@@ -0,0 +1,20 @@
+package com.csitc.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);
+
+}

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

@@ -0,0 +1,6 @@
+/**
+ * 基于部门的数据权限规则
+ *
+ * @author 隧道
+ */
+package com.csitc.framework.datapermission.core.rule.dept;

+ 43 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/core/util/DataPermissionUtils.java

@@ -0,0 +1,43 @@
+package com.csitc.framework.datapermission.core.util;
+
+import com.csitc.framework.datapermission.core.annotation.DataPermission;
+import com.csitc.framework.datapermission.core.aop.DataPermissionContextHolder;
+import lombok.SneakyThrows;
+
+/**
+ * 数据权限 Util
+ *
+ * @author 隧道
+ */
+public class DataPermissionUtils {
+
+    private static DataPermission DATA_PERMISSION_DISABLE;
+
+    @DataPermission(enable = false)
+    @SneakyThrows
+    private static DataPermission getDisableDataPermissionDisable() {
+        if (DATA_PERMISSION_DISABLE == null) {
+            DATA_PERMISSION_DISABLE = DataPermissionUtils.class
+                    .getDeclaredMethod("getDisableDataPermissionDisable")
+                    .getAnnotation(DataPermission.class);
+        }
+        return DATA_PERMISSION_DISABLE;
+    }
+
+    /**
+     * 忽略数据权限,执行对应的逻辑
+     *
+     * @param runnable 逻辑
+     */
+    public static void executeIgnore(Runnable runnable) {
+        DataPermission dataPermission = getDisableDataPermissionDisable();
+        DataPermissionContextHolder.add(dataPermission);
+        try {
+            // 执行 runnable
+            runnable.run();
+        } finally {
+            DataPermissionContextHolder.remove();
+        }
+    }
+
+}

+ 4 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/java/com/csitc/framework/datapermission/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件
+ */
+package com.csitc.framework.datapermission;

+ 2 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,2 @@
+com.csitc.framework.datapermission.config.CloudDataPermissionAutoConfiguration
+com.csitc.framework.datapermission.config.CloudDeptDataPermissionAutoConfiguration

+ 108 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java

@@ -0,0 +1,108 @@
+package com.csitc.framework.datapermission.core.aop;
+
+import cn.hutool.core.collection.CollUtil;
+import com.csitc.framework.datapermission.core.annotation.DataPermission;
+import com.csitc.framework.test.core.ut.BaseMockitoUnitTest;
+import org.aopalliance.intercept.MethodInvocation;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link DataPermissionAnnotationInterceptor} 的单元测试
+ *
+ * @author 隧道
+ */
+public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private DataPermissionAnnotationInterceptor interceptor;
+
+    @Mock
+    private MethodInvocation methodInvocation;
+
+    @BeforeEach
+    public void setUp() {
+        interceptor.getDataPermissionCache().clear();
+    }
+
+    @Test // 无 @DataPermission 注解
+    public void testInvoke_none() throws Throwable {
+        // 参数
+        mockMethodInvocation(TestNone.class);
+
+        // 调用
+        Object result = interceptor.invoke(methodInvocation);
+        // 断言
+        assertEquals("none", result);
+        assertEquals(1, interceptor.getDataPermissionCache().size());
+        assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
+    }
+
+    @Test // 在 Method 上有 @DataPermission 注解
+    public void testInvoke_method() throws Throwable {
+        // 参数
+        mockMethodInvocation(TestMethod.class);
+
+        // 调用
+        Object result = interceptor.invoke(methodInvocation);
+        // 断言
+        assertEquals("method", result);
+        assertEquals(1, interceptor.getDataPermissionCache().size());
+        assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
+    }
+
+    @Test // 在 Class 上有 @DataPermission 注解
+    public void testInvoke_class() throws Throwable {
+        // 参数
+        mockMethodInvocation(TestClass.class);
+
+        // 调用
+        Object result = interceptor.invoke(methodInvocation);
+        // 断言
+        assertEquals("class", result);
+        assertEquals(1, interceptor.getDataPermissionCache().size());
+        assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
+    }
+
+    private void mockMethodInvocation(Class<?> clazz) throws Throwable {
+        Object targetObject = clazz.newInstance();
+        Method method = targetObject.getClass().getMethod("echo");
+        when(methodInvocation.getThis()).thenReturn(targetObject);
+        when(methodInvocation.getMethod()).thenReturn(method);
+        when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject));
+    }
+
+    static class TestMethod {
+
+        @DataPermission(enable = false)
+        public String echo() {
+            return "method";
+        }
+
+    }
+
+    @DataPermission(enable = false)
+    static class TestClass {
+
+        public String echo() {
+            return "class";
+        }
+
+    }
+
+    static class TestNone {
+
+        public String echo() {
+            return "none";
+        }
+
+    }
+
+}

+ 66 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/aop/DataPermissionContextHolderTest.java

@@ -0,0 +1,66 @@
+package com.csitc.framework.datapermission.core.aop;
+
+import com.csitc.framework.datapermission.core.annotation.DataPermission;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+
+/**
+ * {@link DataPermissionContextHolder} 的单元测试
+ *
+ * @author 隧道
+ */
+class DataPermissionContextHolderTest {
+
+    @BeforeEach
+    public void setUp() {
+        DataPermissionContextHolder.clear();
+    }
+
+    @Test
+    public void testGet() {
+        // mock 方法
+        DataPermission dataPermission01 = mock(DataPermission.class);
+        DataPermissionContextHolder.add(dataPermission01);
+        DataPermission dataPermission02 = mock(DataPermission.class);
+        DataPermissionContextHolder.add(dataPermission02);
+
+        // 调用
+        DataPermission result = DataPermissionContextHolder.get();
+        // 断言
+        assertSame(result, dataPermission02);
+    }
+
+    @Test
+    public void testPush() {
+        // 调用
+        DataPermission dataPermission01 = mock(DataPermission.class);
+        DataPermissionContextHolder.add(dataPermission01);
+        DataPermission dataPermission02 = mock(DataPermission.class);
+        DataPermissionContextHolder.add(dataPermission02);
+        // 断言
+        DataPermission first = DataPermissionContextHolder.getAll().get(0);
+        DataPermission second = DataPermissionContextHolder.getAll().get(1);
+        assertSame(dataPermission01, first);
+        assertSame(dataPermission02, second);
+    }
+
+    @Test
+    public void testRemove() {
+        // mock 方法
+        DataPermission dataPermission01 = mock(DataPermission.class);
+        DataPermissionContextHolder.add(dataPermission01);
+        DataPermission dataPermission02 = mock(DataPermission.class);
+        DataPermissionContextHolder.add(dataPermission02);
+
+        // 调用
+        DataPermission result = DataPermissionContextHolder.remove();
+        // 断言
+        assertSame(result, dataPermission02);
+        assertEquals(1, DataPermissionContextHolder.getAll().size());
+    }
+
+}

+ 190 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java

@@ -0,0 +1,190 @@
+package com.csitc.framework.datapermission.core.db;
+
+import com.csitc.framework.common.util.collection.SetUtils;
+import com.csitc.framework.datapermission.core.rule.DataPermissionRule;
+import com.csitc.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import com.csitc.framework.mybatis.core.util.MyBatisUtils;
+import com.csitc.framework.test.core.ut.BaseMockitoUnitTest;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.schema.Column;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+
+import java.sql.Connection;
+import java.util.*;
+
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link DataPermissionDatabaseInterceptor} 的单元测试
+ * 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)}
+ * 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)}
+ * 以及在这个过程中,ContextHolder 和 MappedStatementCache
+ *
+ * @author 隧道
+ */
+public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private DataPermissionDatabaseInterceptor interceptor;
+
+    @Mock
+    private DataPermissionRuleFactory ruleFactory;
+
+    @BeforeEach
+    public void setUp() {
+        // 清理上下文
+        DataPermissionDatabaseInterceptor.ContextHolder.clear();
+        // 清空缓存
+        interceptor.getMappedStatementCache().clear();
+    }
+
+    @Test // 不存在规则,且不匹配
+    public void testBeforeQuery_withoutRule() {
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+            // 准备参数
+            MappedStatement mappedStatement = mock(MappedStatement.class);
+            BoundSql boundSql = mock(BoundSql.class);
+
+            // 调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+            // 断言
+            pluginUtilsMock.verify(() -> PluginUtils.mpBoundSql(boundSql), never());
+        }
+    }
+
+    @Test // 存在规则,且不匹配
+    public void testBeforeQuery_withMatchRule() {
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+            // 准备参数
+            MappedStatement mappedStatement = mock(MappedStatement.class);
+            BoundSql boundSql = mock(BoundSql.class);
+            // mock 方法(数据权限)
+            when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
+                    .thenReturn(singletonList(new DeptDataPermissionRule()));
+            // mock 方法(MPBoundSql)
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+            // mock 方法(SQL)
+            String sql = "select * from t_user where id = 1";
+            when(mpBs.sql()).thenReturn(sql);
+            // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
+
+            // 调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+            // 断言
+            verify(mpBs, times(1)).sql(
+                    eq("SELECT * FROM t_user WHERE id = 1 AND t_user.dept_id = 100"));
+            // 断言缓存
+            assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
+        }
+    }
+
+    @Test // 存在规则,但不匹配
+    public void testBeforeQuery_withoutMatchRule() {
+        try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) {
+            // 准备参数
+            MappedStatement mappedStatement = mock(MappedStatement.class);
+            BoundSql boundSql = mock(BoundSql.class);
+            // mock 方法(数据权限)
+            when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
+                    .thenReturn(singletonList(new DeptDataPermissionRule()));
+            // mock 方法(MPBoundSql)
+            PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+            pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+            // mock 方法(SQL)
+            String sql = "select * from t_role where id = 1";
+            when(mpBs.sql()).thenReturn(sql);
+            // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
+
+            // 调用
+            interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+            // 断言
+            verify(mpBs, times(1)).sql(
+                    eq("SELECT * FROM t_role WHERE id = 1"));
+            // 断言缓存
+            assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
+        }
+    }
+
+    @Test
+    public void testAddNoRewritable() {
+        // 准备参数
+        MappedStatement ms = mock(MappedStatement.class);
+        List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule());
+        // mock 方法
+        when(ms.getId()).thenReturn("selectById");
+
+        // 调用
+        interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
+        // 断言
+        Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements =
+                interceptor.getMappedStatementCache().getNoRewritableMappedStatements();
+        assertEquals(1, noRewritableMappedStatements.size());
+        assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class));
+    }
+
+    @Test
+    public void testNoRewritable() {
+        // 准备参数
+        MappedStatement ms = mock(MappedStatement.class);
+        // mock 方法
+        when(ms.getId()).thenReturn("selectById");
+        // mock 数据
+        List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule());
+        interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
+
+        // 场景一,rules 为空
+        assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null));
+        // 场景二,rules 非空,可重写
+        assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule())));
+        // 场景三,rule 非空,不可重写
+        assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules));
+    }
+
+    private static class DeptDataPermissionRule implements DataPermissionRule {
+
+        private static final String COLUMN = "dept_id";
+
+        @Override
+        public Set<String> getTableNames() {
+            return SetUtils.asSet("t_user");
+        }
+
+        @Override
+        public Expression getExpression(String tableName, Alias tableAlias) {
+            Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
+            LongValue value = new LongValue(100L);
+            return new EqualsTo(column, value);
+        }
+
+    }
+
+    private static class EmptyDataPermissionRule implements DataPermissionRule {
+
+        @Override
+        public Set<String> getTableNames() {
+            return Collections.emptySet();
+        }
+
+        @Override
+        public Expression getExpression(String tableName, Alias tableAlias) {
+            return null;
+        }
+
+    }
+
+}

+ 533 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java

@@ -0,0 +1,533 @@
+package com.csitc.framework.datapermission.core.db;
+
+import com.csitc.framework.datapermission.core.rule.DataPermissionRule;
+import com.csitc.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import com.csitc.framework.mybatis.core.util.MyBatisUtils;
+import com.csitc.framework.test.core.ut.BaseMockitoUnitTest;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+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.schema.Column;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.Set;
+
+import static com.csitc.framework.common.util.collection.SetUtils.asSet;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * {@link DataPermissionDatabaseInterceptor} 的单元测试
+ * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试
+ * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~
+ *
+ * @author 隧道
+ */
+public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private DataPermissionDatabaseInterceptor interceptor;
+
+    @Mock
+    private DataPermissionRuleFactory ruleFactory;
+
+    @BeforeEach
+    public void setUp() {
+        // 租户的数据权限规则
+        DataPermissionRule tenantRule = new DataPermissionRule() {
+
+            private static final String COLUMN = "tenant_id";
+
+            @Override
+            public Set<String> getTableNames() {
+                return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试
+                        "t_user", "t_role"); // 满足自己的单元测试
+            }
+
+            @Override
+            public Expression getExpression(String tableName, Alias tableAlias) {
+                Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
+                LongValue value = new LongValue(1L);
+                return new EqualsTo(column, value);
+            }
+
+        };
+        // 部门的数据权限规则
+        DataPermissionRule deptRule = new DataPermissionRule() {
+
+            private static final String COLUMN = "dept_id";
+
+            @Override
+            public Set<String> getTableNames() {
+                return asSet("t_user");  // 满足自己的单元测试
+            }
+
+            @Override
+            public Expression getExpression(String tableName, Alias tableAlias) {
+                Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
+                ExpressionList values = new ExpressionList(new LongValue(10L),
+                        new LongValue(20L));
+                return new InExpression(column, values);
+            }
+
+        };
+        // 设置到上下文,保证
+        DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule));
+    }
+
+    @Test
+    void delete() {
+        assertSql("delete from entity where id = ?",
+                "DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1");
+    }
+
+    @Test
+    void update() {
+        assertSql("update entity set name = ? where id = ?",
+                "UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1");
+    }
+
+    @Test
+    void selectSingle() {
+        // 单表
+        assertSql("select * from entity where id = ?",
+                "SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1");
+
+        assertSql("select * from entity where id = ? or name = ?",
+                "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)",
+                "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
+
+        /* not */
+        assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)",
+                "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1");
+    }
+
+    @Test
+    void selectSubSelectIn() {
+        /* in */
+        assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+        // 在最前
+        assertSql("SELECT * FROM entity e WHERE e.id IN " +
+                        "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
+                "SELECT * FROM entity e WHERE e.id IN " +
+                        "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
+        // 在最后
+        assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
+                        "(select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
+                        "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+        // 在中间
+        assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
+                        "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
+                "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
+                        "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
+    }
+
+    @Test
+    void selectSubSelectEq() {
+        /* = */
+        assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+    }
+
+    @Test
+    void selectSubSelectInnerNotEq() {
+        /* inner not = */
+        assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))",
+                "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)",
+                "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1");
+    }
+
+    @Test
+    void selectSubSelectExists() {
+        /* EXISTS */
+        assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+
+
+        /* NOT EXISTS */
+        assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+    }
+
+    @Test
+    void selectSubSelect() {
+        /* >= */
+        assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+
+
+        /* <= */
+        assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+
+
+        /* <> */
+        assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)",
+                "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+    }
+
+    @Test
+    void selectFromSelect() {
+        assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))",
+                "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)");
+    }
+
+    @Test
+    void selectBodySubSelect() {
+        assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1",
+                "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1");
+    }
+
+    @Test
+    void selectLeftJoin() {
+        // left join
+        assertSql("SELECT * FROM entity e " +
+                        "left join entity1 e1 on e1.id = e.id " +
+                        "WHERE e.id = ? OR e.name = ?",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e " +
+                        "left join entity1 e1 on e1.id = e.id " +
+                        "WHERE (e.id = ? OR e.name = ?)",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e " +
+                        "left join entity1 e1 on e1.id = e.id " +
+                        "left join entity2 e2 on e1.id = e2.id",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
+                        "WHERE e.tenant_id = 1");
+    }
+
+    @Test
+    void selectRightJoin() {
+        // right join
+        assertSql("SELECT * FROM entity e " +
+                        "right join entity1 e1 on e1.id = e.id",
+                "SELECT * FROM entity e " +
+                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
+                        "WHERE e1.tenant_id = 1");
+
+        assertSql("SELECT * FROM with_as_1 e " +
+                        "right join entity1 e1 on e1.id = e.id",
+                "SELECT * FROM with_as_1 e " +
+                        "RIGHT JOIN entity1 e1 ON e1.id = e.id " +
+                        "WHERE e1.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e " +
+                        "right join entity1 e1 on e1.id = e.id " +
+                        "WHERE e.id = ? OR e.name = ?",
+                "SELECT * FROM entity e " +
+                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e " +
+                        "right join entity1 e1 on e1.id = e.id " +
+                        "right join entity2 e2 on e1.id = e2.id ",
+                "SELECT * FROM entity e " +
+                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
+                        "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " +
+                        "WHERE e2.tenant_id = 1");
+    }
+
+    @Test
+    void selectMixJoin() {
+        assertSql("SELECT * FROM entity e " +
+                        "right join entity1 e1 on e1.id = e.id " +
+                        "left join entity2 e2 on e1.id = e2.id",
+                "SELECT * FROM entity e " +
+                        "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
+                        "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
+                        "WHERE e1.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e " +
+                        "left join entity1 e1 on e1.id = e.id " +
+                        "right join entity2 e2 on e1.id = e2.id",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " +
+                        "WHERE e2.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e " +
+                        "left join entity1 e1 on e1.id = e.id " +
+                        "inner join entity2 e2 on e1.id = e2.id",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1");
+    }
+
+
+    @Test
+    void selectJoinSubSelect() {
+        assertSql("select * from (select * from entity) e1 " +
+                        "left join entity2 e2 on e1.id = e2.id",
+                "SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " +
+                        "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1");
+
+        assertSql("select * from entity1 e1 " +
+                        "left join (select * from entity2) e2 " +
+                        "on e1.id = e2.id",
+                "SELECT * FROM entity1 e1 " +
+                        "LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " +
+                        "ON e1.id = e2.id " +
+                        "WHERE e1.tenant_id = 1");
+    }
+
+    @Test
+    void selectSubJoin() {
+
+        assertSql("select * FROM " +
+                        "(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)",
+                "SELECT * FROM " +
+                        "(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
+                        "WHERE e2.tenant_id = 1");
+
+        assertSql("select * FROM " +
+                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)",
+                "SELECT * FROM " +
+                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
+                        "WHERE e1.tenant_id = 1");
+
+
+        assertSql("select * FROM " +
+                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " +
+                        "right join entity3 e3 on e1.id = e3.id",
+                "SELECT * FROM " +
+                        "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
+                        "RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " +
+                        "WHERE e3.tenant_id = 1");
+
+
+        assertSql("select * FROM entity e " +
+                        "LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " +
+                        "on e.id = e2.id",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
+                        "ON e.id = e2.id AND e2.tenant_id = 1 " +
+                        "WHERE e.tenant_id = 1");
+
+        assertSql("select * FROM entity e " +
+                        "LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
+                        "on e.id = e2.id",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
+                        "ON e.id = e2.id AND e1.tenant_id = 1 " +
+                        "WHERE e.tenant_id = 1");
+
+        assertSql("select * FROM entity e " +
+                        "RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
+                        "on e.id = e2.id",
+                "SELECT * FROM entity e " +
+                        "RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
+                        "ON e.id = e2.id AND e.tenant_id = 1 " +
+                        "WHERE e1.tenant_id = 1");
+    }
+
+
+    @Test
+    void selectLeftJoinMultipleTrailingOn() {
+        // 多个 on 尾缀的
+        assertSql("SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 " +
+                        "LEFT JOIN entity2 e2 ON e2.id = e1.id " +
+                        "ON e1.id = e.id " +
+                        "WHERE (e.id = ? OR e.NAME = ?)",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 " +
+                        "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " +
+                        "ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
+
+        assertSql("SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 " +
+                        "LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
+                        "ON e1.id = e.id " +
+                        "WHERE (e.id = ? OR e.NAME = ?)",
+                "SELECT * FROM entity e " +
+                        "LEFT JOIN entity1 e1 " +
+                        "LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
+                        "ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
+    }
+
+    @Test
+    void selectInnerJoin() {
+        // inner join
+        assertSql("SELECT * FROM entity e " +
+                        "inner join entity1 e1 on e1.id = e.id " +
+                        "WHERE e.id = ? OR e.name = ?",
+                "SELECT * FROM entity e " +
+                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
+                        "WHERE e.id = ? OR e.name = ?");
+
+        assertSql("SELECT * FROM entity e " +
+                        "inner join entity1 e1 on e1.id = e.id " +
+                        "WHERE (e.id = ? OR e.name = ?)",
+                "SELECT * FROM entity e " +
+                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.name = ?)");
+
+        // 隐式内连接
+        assertSql("SELECT * FROM entity,entity1 " +
+                        "WHERE entity.id = entity1.id",
+                "SELECT * FROM entity, entity1 " +
+                        "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
+
+        // 隐式内连接
+        assertSql("SELECT * FROM entity a, with_as_entity1 b " +
+                        "WHERE a.id = b.id",
+                "SELECT * FROM entity a, with_as_entity1 b " +
+                        "WHERE a.id = b.id AND a.tenant_id = 1");
+
+        assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " +
+                        "WHERE a.id = b.id",
+                "SELECT * FROM with_as_entity a, with_as_entity1 b " +
+                        "WHERE a.id = b.id");
+
+        // SubJoin with 隐式内连接
+        assertSql("SELECT * FROM (entity,entity1) " +
+                        "WHERE entity.id = entity1.id",
+                "SELECT * FROM (entity, entity1) " +
+                        "WHERE entity.id = entity1.id " +
+                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
+
+        assertSql("SELECT * FROM ((entity,entity1),entity2) " +
+                        "WHERE entity.id = entity1.id and entity.id = entity2.id",
+                "SELECT * FROM ((entity, entity1), entity2) " +
+                        "WHERE entity.id = entity1.id AND entity.id = entity2.id " +
+                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
+
+        assertSql("SELECT * FROM (entity,(entity1,entity2)) " +
+                        "WHERE entity.id = entity1.id and entity.id = entity2.id",
+                "SELECT * FROM (entity, (entity1, entity2)) " +
+                        "WHERE entity.id = entity1.id AND entity.id = entity2.id " +
+                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
+
+        // 沙雕的括号写法
+        assertSql("SELECT * FROM (((entity,entity1))) " +
+                        "WHERE entity.id = entity1.id",
+                "SELECT * FROM (((entity, entity1))) " +
+                        "WHERE entity.id = entity1.id " +
+                        "AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
+
+    }
+
+
+    @Test
+    void selectWithAs() {
+        assertSql("with with_as_A as (select * from entity) select * from with_as_A",
+                "WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A");
+    }
+
+
+    @Test
+    void selectIgnoreTable() {
+        assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)",
+                "SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)");
+    }
+
+    private void assertSql(String sql, String targetSql) {
+        assertEquals(targetSql, interceptor.parserSingle(sql, null));
+    }
+
+
+    // ========== 额外的测试 ==========
+
+    @Test
+    public void testSelectSingle() {
+        // 单表
+        assertSql("select * from t_user where id = ?",
+                "SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
+
+        assertSql("select * from t_user where id = ? or name = ?",
+                "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
+
+        assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)",
+                "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
+
+        /* not */
+        assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)",
+                "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
+    }
+
+    @Test
+    public void testSelectLeftJoin() {
+        // left join
+        assertSql("SELECT * FROM t_user e " +
+                        "left join t_role e1 on e1.id = e.id " +
+                        "WHERE e.id = ? OR e.name = ?",
+                "SELECT * FROM t_user e " +
+                        "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+
+        // 条件 e.id = ? OR e.name = ? 带括号
+        assertSql("SELECT * FROM t_user e " +
+                        "left join t_role e1 on e1.id = e.id " +
+                        "WHERE (e.id = ? OR e.name = ?)",
+                "SELECT * FROM t_user e " +
+                        "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+    }
+
+    @Test
+    public void testSelectRightJoin() {
+        // right join
+        assertSql("SELECT * FROM t_user e " +
+                        "right join t_role e1 on e1.id = e.id " +
+                        "WHERE e.id = ? OR e.name = ?",
+                "SELECT * FROM t_user e " +
+                        "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
+                        "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
+
+        // 条件 e.id = ? OR e.name = ? 带括号
+        assertSql("SELECT * FROM t_user e " +
+                        "right join t_role e1 on e1.id = e.id " +
+                        "WHERE (e.id = ? OR e.name = ?)",
+                "SELECT * FROM t_user e " +
+                        "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
+                        "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
+    }
+
+    @Test
+    public void testSelectInnerJoin() {
+        // inner join
+        assertSql("SELECT * FROM t_user e " +
+                        "inner join entity1 e1 on e1.id = e.id " +
+                        "WHERE e.id = ? OR e.name = ?",
+                "SELECT * FROM t_user e " +
+                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
+                        "WHERE e.id = ? OR e.name = ?");
+
+        // 条件 e.id = ? OR e.name = ? 带括号
+        assertSql("SELECT * FROM t_user e " +
+                        "inner join entity1 e1 on e1.id = e.id " +
+                        "WHERE (e.id = ? OR e.name = ?)",
+                "SELECT * FROM t_user e " +
+                        "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
+                        "WHERE (e.id = ? OR e.name = ?)");
+
+        // 没有 On 的 inner join
+        assertSql("SELECT * FROM entity,entity1 " +
+                "WHERE entity.id = entity1.id",
+            "SELECT * FROM entity, entity1 " +
+                    "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
+    }
+
+}

+ 145 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java

@@ -0,0 +1,145 @@
+package com.csitc.framework.datapermission.core.rule;
+
+import com.csitc.framework.datapermission.core.annotation.DataPermission;
+import com.csitc.framework.datapermission.core.aop.DataPermissionContextHolder;
+import com.csitc.framework.test.core.ut.BaseMockitoUnitTest;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Spy;
+import org.springframework.core.annotation.AnnotationUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import static com.csitc.framework.test.core.util.RandomUtils.randomString;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link DataPermissionRuleFactoryImpl} 单元测试
+ *
+ * @author 隧道
+ */
+class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private DataPermissionRuleFactoryImpl dataPermissionRuleFactory;
+
+    @Spy
+    private List<DataPermissionRule> rules = Arrays.asList(new DataPermissionRule01(),
+            new DataPermissionRule02());
+
+    @BeforeEach
+    public void setUp() {
+        DataPermissionContextHolder.clear();
+    }
+
+    @Test
+    public void testGetDataPermissionRule_02() {
+        // 准备参数
+        String mappedStatementId = randomString();
+
+        // 调用
+        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+        // 断言
+        assertSame(rules, result);
+    }
+
+    @Test
+    public void testGetDataPermissionRule_03() {
+        // 准备参数
+        String mappedStatementId = randomString();
+        // mock 方法
+        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class));
+
+        // 调用
+        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+        // 断言
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    public void testGetDataPermissionRule_04() {
+        // 准备参数
+        String mappedStatementId = randomString();
+        // mock 方法
+        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class));
+
+        // 调用
+        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+        // 断言
+        assertEquals(1, result.size());
+        assertEquals(DataPermissionRule01.class, result.get(0).getClass());
+    }
+
+    @Test
+    public void testGetDataPermissionRule_05() {
+        // 准备参数
+        String mappedStatementId = randomString();
+        // mock 方法
+        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class));
+
+        // 调用
+        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+        // 断言
+        assertEquals(1, result.size());
+        assertEquals(DataPermissionRule02.class, result.get(0).getClass());
+    }
+
+    @Test
+    public void testGetDataPermissionRule_06() {
+        // 准备参数
+        String mappedStatementId = randomString();
+        // mock 方法
+        DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class));
+
+        // 调用
+        List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+        // 断言
+        assertSame(rules, result);
+    }
+
+    @DataPermission(enable = false)
+    static class TestClass03 {}
+
+    @DataPermission(includeRules = DataPermissionRule01.class)
+    static class TestClass04 {}
+
+    @DataPermission(excludeRules = DataPermissionRule01.class)
+    static class TestClass05 {}
+
+    @DataPermission
+    static class TestClass06 {}
+
+    static class DataPermissionRule01 implements DataPermissionRule {
+
+        @Override
+        public Set<String> getTableNames() {
+            return null;
+        }
+
+        @Override
+        public Expression getExpression(String tableName, Alias tableAlias) {
+            return null;
+        }
+
+    }
+
+    static class DataPermissionRule02 implements DataPermissionRule {
+
+        @Override
+        public Set<String> getTableNames() {
+            return null;
+        }
+
+        @Override
+        public Expression getExpression(String tableName, Alias tableAlias) {
+            return null;
+        }
+
+    }
+
+}

+ 235 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java

@@ -0,0 +1,235 @@
+package com.csitc.framework.datapermission.core.rule.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ReflectUtil;
+import com.csitc.framework.common.enums.UserTypeEnum;
+import com.csitc.framework.common.util.collection.SetUtils;
+import com.csitc.module.system.api.permission.PermissionApi;
+import com.csitc.module.system.api.permission.dto.DeptDataPermissionRespDTO;
+import com.csitc.framework.security.core.LoginUser;
+import com.csitc.framework.security.core.util.SecurityFrameworkUtils;
+import com.csitc.framework.test.core.ut.BaseMockitoUnitTest;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+
+import java.util.Map;
+
+import static com.csitc.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL;
+import static com.csitc.framework.test.core.util.RandomUtils.randomPojo;
+import static com.csitc.framework.test.core.util.RandomUtils.randomString;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link DeptDataPermissionRule} 的单元测试
+ *
+ * @author 隧道
+ */
+class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private DeptDataPermissionRule rule;
+
+    @Mock
+    private PermissionApi permissionApi;
+
+    @BeforeEach
+    @SuppressWarnings("unchecked")
+    public void setUp() {
+        // 清空 rule
+        rule.getTableNames().clear();
+        ((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
+        ((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
+    }
+
+    @Test // 无 LoginUser
+    public void testGetExpression_noLoginUser() {
+        // 准备参数
+        String tableName = randomString();
+        Alias tableAlias = new Alias(randomString());
+        // mock 方法
+
+        // 调用
+        Expression expression = rule.getExpression(tableName, tableAlias);
+        // 断言
+        assertNull(expression);
+    }
+
+    @Test // 无数据权限时
+    public void testGetExpression_noDeptDataPermission() {
+        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
+                     = mockStatic(SecurityFrameworkUtils.class)) {
+            // 准备参数
+            String tableName = "t_user";
+            Alias tableAlias = new Alias("u");
+            // mock 方法
+            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+                    .setUserType(UserTypeEnum.ADMIN.getValue()));
+            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+
+            // 调用
+            NullPointerException exception = assertThrows(NullPointerException.class,
+                    () -> rule.getExpression(tableName, tableAlias));
+            // 断言
+            assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage());
+        }
+    }
+
+    @Test // 全部数据权限
+    public void testGetExpression_allDeptDataPermission() {
+        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
+                     = mockStatic(SecurityFrameworkUtils.class)) {
+            // 准备参数
+            String tableName = "t_user";
+            Alias tableAlias = new Alias("u");
+            // mock 方法(LoginUser)
+            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+                    .setUserType(UserTypeEnum.ADMIN.getValue()));
+            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+            // mock 方法(DeptDataPermissionRespDTO)
+            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true);
+            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+
+            // 调用
+            Expression expression = rule.getExpression(tableName, tableAlias);
+            // 断言
+            assertNull(expression);
+            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+        }
+    }
+
+    @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限
+    public void testGetExpression_noDept_noSelf() {
+        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
+                     = mockStatic(SecurityFrameworkUtils.class)) {
+            // 准备参数
+            String tableName = "t_user";
+            Alias tableAlias = new Alias("u");
+            // mock 方法(LoginUser)
+            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+                    .setUserType(UserTypeEnum.ADMIN.getValue()));
+            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+            // mock 方法(DeptDataPermissionRespDTO)
+            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO();
+            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+
+            // 调用
+            Expression expression = rule.getExpression(tableName, tableAlias);
+            // 断言
+            assertEquals("null = null", expression.toString());
+            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+        }
+    }
+
+    @Test // 拼接 Dept 和 User 的条件(字段都不符合)
+    public void testGetExpression_noDeptColumn_noSelfColumn() {
+        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
+                     = mockStatic(SecurityFrameworkUtils.class)) {
+            // 准备参数
+            String tableName = "t_user";
+            Alias tableAlias = new Alias("u");
+            // mock 方法(LoginUser)
+            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+                    .setUserType(UserTypeEnum.ADMIN.getValue()));
+            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+            // mock 方法(DeptDataPermissionRespDTO)
+            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+                    .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true);
+            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+
+            // 调用
+            Expression expression = rule.getExpression(tableName, tableAlias);
+            // 断言
+            assertSame(EXPRESSION_NULL, expression);
+            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+        }
+    }
+
+    @Test // 拼接 Dept 和 User 的条件(self 符合)
+    public void testGetExpression_noDeptColumn_yesSelfColumn() {
+        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
+                     = mockStatic(SecurityFrameworkUtils.class)) {
+            // 准备参数
+            String tableName = "t_user";
+            Alias tableAlias = new Alias("u");
+            // mock 方法(LoginUser)
+            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+                    .setUserType(UserTypeEnum.ADMIN.getValue()));
+            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+            // mock 方法(DeptDataPermissionRespDTO)
+            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+                    .setSelf(true);
+            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+            // 添加 user 字段配置
+            rule.addUserColumn("t_user", "id");
+
+            // 调用
+            Expression expression = rule.getExpression(tableName, tableAlias);
+            // 断言
+            assertEquals("u.id = 1", expression.toString());
+            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+        }
+    }
+
+    @Test // 拼接 Dept 和 User 的条件(dept 符合)
+    public void testGetExpression_yesDeptColumn_noSelfColumn() {
+        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
+                     = mockStatic(SecurityFrameworkUtils.class)) {
+            // 准备参数
+            String tableName = "t_user";
+            Alias tableAlias = new Alias("u");
+            // mock 方法(LoginUser)
+            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+                    .setUserType(UserTypeEnum.ADMIN.getValue()));
+            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+            // mock 方法(DeptDataPermissionRespDTO)
+            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+                    .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L));
+            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+            // 添加 dept 字段配置
+            rule.addDeptColumn("t_user", "dept_id");
+
+            // 调用
+            Expression expression = rule.getExpression(tableName, tableAlias);
+            // 断言
+            assertEquals("u.dept_id IN (10, 20)", expression.toString());
+            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+        }
+    }
+
+    @Test // 拼接 Dept 和 User 的条件(dept + self 符合)
+    public void testGetExpression_yesDeptColumn_yesSelfColumn() {
+        try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock
+                     = mockStatic(SecurityFrameworkUtils.class)) {
+            // 准备参数
+            String tableName = "t_user";
+            Alias tableAlias = new Alias("u");
+            // mock 方法(LoginUser)
+            LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+                    .setUserType(UserTypeEnum.ADMIN.getValue()));
+            securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+            // mock 方法(DeptDataPermissionRespDTO)
+            DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+                    .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true);
+            when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+            // 添加 user 字段配置
+            rule.addUserColumn("t_user", "id");
+            // 添加 dept 字段配置
+            rule.addDeptColumn("t_user", "dept_id");
+
+            // 调用
+            Expression expression = rule.getExpression(tableName, tableAlias);
+            // 断言
+            assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString());
+            assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+        }
+    }
+
+}

+ 15 - 0
cloud-framework/cloud-spring-boot-starter-biz-data-permission/src/test/java/com/csitc/framework/datapermission/core/util/DataPermissionUtilsTest.java

@@ -0,0 +1,15 @@
+package com.csitc.framework.datapermission.core.util;
+
+import com.csitc.framework.datapermission.core.aop.DataPermissionContextHolder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DataPermissionUtilsTest {
+
+    @Test
+    public void testExecuteIgnore() {
+        DataPermissionUtils.executeIgnore(() -> assertFalse(DataPermissionContextHolder.get().enable()));
+    }
+
+}

+ 50 - 0
cloud-framework/cloud-spring-boot-starter-biz-dict/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">
+    <parent>
+        <groupId>com.csitc</groupId>
+        <artifactId>cloud-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>cloud-spring-boot-starter-biz-dict</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>字典类型、数据</description>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 18 - 0
cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/config/CloudDictAutoConfiguration.java

@@ -0,0 +1,18 @@
+package com.csitc.framework.dict.config;
+
+import com.csitc.framework.dict.core.util.DictFrameworkUtils;
+import com.csitc.module.system.api.dict.DictDataApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+public class CloudDictAutoConfiguration {
+
+    @Bean
+    @SuppressWarnings("InstantiationOfUtilityClass")
+    public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) {
+        DictFrameworkUtils.init(dictDataApi);
+        return new DictFrameworkUtils();
+    }
+
+}

+ 4 - 0
cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/core/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 占位
+ */
+package com.csitc.framework.dict.core;

+ 70 - 0
cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/core/util/DictFrameworkUtils.java

@@ -0,0 +1,70 @@
+package com.csitc.framework.dict.core.util;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.csitc.framework.common.core.KeyValue;
+import com.csitc.framework.common.util.cache.CacheUtils;
+import com.csitc.module.system.api.dict.DictDataApi;
+import com.csitc.module.system.api.dict.dto.DictDataRespDTO;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.Duration;
+
+/**
+ * 字典工具类
+ *
+ * @author 隧道
+ */
+@Slf4j
+public class DictFrameworkUtils {
+
+    private static DictDataApi dictDataApi;
+
+    private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();
+
+    /**
+     * 针对 {@link #getDictDataLabel(String, String)} 的缓存
+     */
+    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
+
+                @Override
+                public DictDataRespDTO load(KeyValue<String, String> key) {
+                    return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
+                }
+
+            });
+
+    /**
+     * 针对 {@link #parseDictDataValue(String, String)} 的缓存
+     */
+    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
+
+                @Override
+                public DictDataRespDTO load(KeyValue<String, String> key) {
+                    return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
+                }
+
+            });
+
+    public static void init(DictDataApi dictDataApi) {
+        DictFrameworkUtils.dictDataApi = dictDataApi;
+        log.info("[init][初始化 DictFrameworkUtils 成功]");
+    }
+
+    @SneakyThrows
+    public static String getDictDataLabel(String dictType, String value) {
+        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
+    }
+
+    @SneakyThrows
+    public static String parseDictDataValue(String dictType, String label) {
+        return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
+    }
+
+}

+ 6 - 0
cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/java/com/csitc/framework/dict/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 字典数据模块,提供 {@link com.csitc.framework.dict.core.util.DictFrameworkUtils} 工具类
+ *
+ * 通过将字典缓存在内存中,保证性能
+ */
+package com.csitc.framework.dict;

+ 1 - 0
cloud-framework/cloud-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.csitc.framework.dict.config.CloudDictAutoConfiguration

+ 48 - 0
cloud-framework/cloud-spring-boot-starter-biz-dict/src/test/java/com/csitc/framework/dict/core/util/DictFrameworkUtilsTest.java

@@ -0,0 +1,48 @@
+package com.csitc.framework.dict.core.util;
+
+import com.csitc.framework.common.enums.CommonStatusEnum;
+import com.csitc.framework.test.core.ut.BaseMockitoUnitTest;
+import com.csitc.module.system.api.dict.DictDataApi;
+import com.csitc.module.system.api.dict.dto.DictDataRespDTO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import static com.csitc.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link DictFrameworkUtils} 的单元测试
+ */
+public class DictFrameworkUtilsTest extends BaseMockitoUnitTest {
+
+    @Mock
+    private DictDataApi dictDataApi;
+
+    @BeforeEach
+    public void setUp() {
+        DictFrameworkUtils.init(dictDataApi);
+    }
+
+    @Test
+    public void testGetDictDataLabel() {
+        // mock 数据
+        DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        // mock 方法
+        when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(dataRespDTO);
+        // 断言返回值
+        assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue()));
+    }
+
+    @Test
+    public void testParseDictDataValue() {
+        // mock 数据
+        DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        // mock 方法
+        when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(resp);
+        // 断言返回值
+        assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel()));
+    }
+
+}

+ 49 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/pom.xml

@@ -0,0 +1,49 @@
+<?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>cloud-framework</artifactId>
+        <groupId>com.csitc</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>cloud-spring-boot-starter-biz-error-code</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        错误码 ErrorCode 的自动配置功能,提供如下功能:
+        1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置;
+        2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-server 服务加载最新的 ErrorCode 错误码;
+        3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
+    </description>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>com.csitc</groupId>
+            <artifactId>cloud-module-system-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
+            <version>${revision}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+            <scope>provided</scope> <!-- 设置为 provided,主要是 ErrorCodeProperties 使用到 -->
+        </dependency>
+    </dependencies>
+
+</project>

+ 39 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/config/CloudErrorCodeConfiguration.java

@@ -0,0 +1,39 @@
+package com.csitc.framework.errorcode.config;
+
+import com.csitc.framework.errorcode.core.generator.ErrorCodeAutoGenerator;
+import com.csitc.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl;
+import com.csitc.framework.errorcode.core.loader.ErrorCodeLoader;
+import com.csitc.framework.errorcode.core.loader.ErrorCodeLoaderImpl;
+import com.csitc.module.system.api.errorcode.ErrorCodeApi;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 错误码配置类
+ *
+ * @author 隧道
+ */
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "cloud.error-code", value = "enable", matchIfMissing = true) // 允许使用 cloud.error-code.enable=false 禁用访问日志
+@EnableConfigurationProperties(ErrorCodeProperties.class)
+@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码
+public class CloudErrorCodeConfiguration {
+
+    @Bean
+    public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName,
+                                                         ErrorCodeProperties errorCodeProperties,
+                                                         ErrorCodeApi errorCodeApi) {
+        return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), errorCodeApi);
+    }
+
+    @Bean
+    public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName,
+                                           ErrorCodeApi errorCodeApi) {
+        return new ErrorCodeLoaderImpl(applicationName, errorCodeApi);
+    }
+
+}

+ 30 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/config/ErrorCodeProperties.java

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

+ 15 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java

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

+ 104 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java

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

+ 24 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/loader/ErrorCodeLoader.java

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

+ 73 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java

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

+ 10 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/java/com/csitc/framework/errorcode/package-info.java

@@ -0,0 +1,10 @@
+/**
+ * 错误码 ErrorCode 的自动配置功能,提供如下功能:
+ *
+ * 1. 远程读取:项目启动时,从 system-service 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置;
+ * 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-service 服务加载最新的 ErrorCode 错误码;
+ * 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
+ *
+ * @author 隧道
+ */
+package com.csitc.framework.errorcode;

+ 0 - 0
cloud-framework/cloud-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports


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