Pārlūkot izejas kodu

Merge branch 'master' into develop_zqc

DESKTOP-VAEGFGM\zqc 6 mēneši atpakaļ
vecāks
revīzija
2445682c23
100 mainītis faili ar 1406 papildinājumiem un 552 dzēšanām
  1. 3 3
      README.md
  2. 51 36
      citu-dependencies/pom.xml
  3. 18 2
      citu-framework/citu-common/src/main/java/com/citu/framework/common/util/collection/CollectionUtils.java
  4. 2 0
      citu-framework/citu-common/src/main/java/com/citu/framework/common/util/date/DateUtils.java
  5. 26 9
      citu-framework/citu-common/src/main/java/com/citu/framework/common/util/http/HttpUtils.java
  6. 1 1
      citu-framework/citu-common/src/main/java/com/citu/framework/common/util/json/databind/NumberSerializer.java
  7. 1 1
      citu-framework/citu-common/src/main/java/com/citu/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
  8. 1 1
      citu-framework/citu-common/src/main/java/com/citu/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java
  9. 7 0
      citu-framework/citu-common/src/main/java/com/citu/framework/common/util/object/BeanUtils.java
  10. 3 2
      citu-framework/citu-spring-boot-starter-biz-data-permission/src/main/java/com/citu/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
  11. 2 1
      citu-framework/citu-spring-boot-starter-biz-data-permission/src/test/java/com/citu/framework/datapermission/core/db/DataPermissionRuleHandlerTest.java
  12. 2 1
      citu-framework/citu-spring-boot-starter-biz-tenant/src/main/java/com/citu/framework/tenant/core/db/TenantDatabaseInterceptor.java
  13. 13 3
      citu-framework/citu-spring-boot-starter-biz-tenant/src/main/java/com/citu/framework/tenant/core/job/TenantJobAspect.java
  14. 5 0
      citu-framework/citu-spring-boot-starter-excel/pom.xml
  15. 12 0
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/config/CituMybatisAutoConfiguration.java
  16. 5 5
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java
  17. 95 0
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/enums/DbTypeEnum.java
  18. 0 21
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/enums/SqlConstants.java
  19. 6 14
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/mapper/BaseMapperX.java
  20. 10 10
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/query/QueryWrapperX.java
  21. 49 3
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/util/JdbcUtils.java
  22. 21 3
      citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/util/MyBatisUtils.java
  23. 6 0
      citu-framework/citu-spring-boot-starter-protection/pom.xml
  24. 7 1
      citu-framework/citu-spring-boot-starter-protection/src/main/java/com/citu/framework/signature/config/CituApiSignatureAutoConfiguration.java
  25. 37 2
      citu-framework/citu-spring-boot-starter-protection/src/main/java/com/citu/framework/signature/core/aop/ApiSignatureAspect.java
  26. 15 10
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/operatelog/core/service/LogRecordServiceImpl.java
  27. 2 1
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/config/AuthorizeRequestsCustomizer.java
  28. 0 9
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/config/CituSecurityAutoConfiguration.java
  29. 18 19
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/config/CituWebSecurityConfigurerAdapter.java
  30. 5 0
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/LoginUser.java
  31. 2 2
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/annotations/PreAuthenticated.java
  32. 2 0
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/aop/PreAuthenticatedAspect.java
  33. 2 1
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/filter/TokenAuthenticationFilter.java
  34. 10 2
      citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/service/SecurityFrameworkServiceImpl.java
  35. 4 0
      citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/ut/BaseDbAndRedisUnitTest.java
  36. 4 0
      citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/ut/BaseDbUnitTest.java
  37. 4 0
      citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/ut/BaseRedisUnitTest.java
  38. 9 0
      citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/util/RandomUtils.java
  39. 10 25
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/config/CituApiLogAutoConfiguration.java
  40. 1 2
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/config/CituApiLogRpcAutoConfiguration.java
  41. 99 98
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/filter/ApiAccessLogFilter.java
  42. 1 1
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java
  43. 0 19
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiAccessLogFrameworkService.java
  44. 0 33
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
  45. 0 19
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiErrorLogFrameworkService.java
  46. 0 26
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
  47. 1 1
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java
  48. 9 1
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/i18n/config/CituI18nAutoConfiguration.java
  49. 3 4
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/jackson/config/CituJacksonAutoConfiguration.java
  50. 1 1
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/swagger/config/CituSwaggerAutoConfiguration.java
  51. 3 3
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/web/config/CituWebAutoConfiguration.java
  52. 49 17
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/web/core/handler/GlobalExceptionHandler.java
  53. 11 5
      citu-framework/citu-spring-boot-starter-websocket/src/main/java/com/citu/framework/websocket/config/CituWebSocketAutoConfiguration.java
  54. 3 2
      citu-framework/citu-spring-boot-starter-websocket/src/main/java/com/citu/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
  55. 4 2
      citu-framework/citu-spring-boot-starter-websocket/src/main/java/com/citu/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
  56. 1 4
      citu-gateway/Dockerfile
  57. 0 1
      citu-gateway/pom.xml
  58. 5 1
      citu-gateway/src/main/java/com/citu/gateway/filter/security/LoginUser.java
  59. 9 5
      citu-gateway/src/main/java/com/citu/gateway/filter/security/TokenAuthenticationFilter.java
  60. 51 0
      citu-gateway/src/main/java/com/citu/gateway/jackson/JacksonAutoConfiguration.java
  61. 14 0
      citu-gateway/src/main/resources/application-dev.yaml
  62. 4 2
      citu-gateway/src/main/resources/bootstrap-local.yaml
  63. 2 2
      citu-gateway/src/main/resources/logback-spring.xml
  64. 1 1
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/ApiConstants.java
  65. 1 4
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/DictTypeConstants.java
  66. 5 3
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/ErrorCodeConstants.java
  67. 25 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmBoundaryEventType.java
  68. 33 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmFieldPermissionEnum.java
  69. 1 1
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmModelFormTypeEnum.java
  70. 31 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmModelTypeEnum.java
  71. 1 1
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmProcessListenerType.java
  72. 1 1
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmProcessListenerValueType.java
  73. 36 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmSimpleModeConditionType.java
  74. 62 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmSimpleModelNodeType.java
  75. 47 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java
  76. 31 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java
  77. 33 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java
  78. 31 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java
  79. 35 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java
  80. 32 0
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java
  81. 3 2
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/message/BpmMessageEnum.java
  82. 13 2
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java
  83. 13 9
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/task/BpmReasonEnum.java
  84. 10 1
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/task/BpmTaskStatusEnum.java
  85. 1 1
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/event/BpmProcessInstanceStatusEvent.java
  86. 1 1
      citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/event/BpmProcessInstanceStatusEventListener.java
  87. 1 4
      citu-module-bpm/citu-module-bpm-biz/Dockerfile
  88. 1 1
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/BpmServerApplication.java
  89. 1 1
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/api/task/BpmProcessInstanceApiImpl.java
  90. 4 0
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/base/package-info.java
  91. 22 0
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java
  92. 9 0
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/BpmCategoryController.java
  93. 71 39
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/BpmModelController.java
  94. 14 8
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
  95. 1 1
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java
  96. 1 1
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSaveReqVO.java
  97. 0 17
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java
  98. 20 0
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java
  99. 65 0
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
  100. 8 16
      citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java

+ 3 - 3
README.md

@@ -1,8 +1,8 @@
 <p align="center">
- <img src="https://img.shields.io/badge/Spring%20Cloud-2021-blue.svg" alt="Coverage Status">
- <img src="https://img.shields.io/badge/Spring%20Boot-2.7.18-blue.svg" alt="Downloads">
+ <img src="https://img.shields.io/badge/Spring%20Cloud-2023-blue.svg" alt="Coverage Status">
+ <img src="https://img.shields.io/badge/Spring%20Boot-3.3.4-blue.svg" alt="Downloads">
  <img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
- <img src="https://img.shields.io/github/license/YunaiV/citu-cloud" alt="Downloads" />
+ <img src="https://img.shields.io/github/license/YunaiV/yudao-cloud" alt="Downloads" />
 </p>
 
 **严肃声明:现在、未来都不会有商业版本,所有代码全部开源!**

+ 51 - 36
citu-dependencies/pom.xml

@@ -14,9 +14,11 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>2.2.0-snapshot</revision>
+        <revision>2.3.0-snapshot</revision>
         <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
         <!-- 统一依赖管理 -->
+        <spring.framework.version>5.3.39</spring.framework.version>
+        <spring.security.version>5.8.14</spring.security.version>
         <spring.boot.version>2.7.18</spring.boot.version>
         <spring.cloud.version>2021.0.9</spring.cloud.version>
         <spring.cloud.alibaba.version>2021.0.6.1</spring.cloud.alibaba.version>
@@ -28,14 +30,15 @@
         <druid.version>1.2.23</druid.version>
         <mybatis.version>3.5.16</mybatis.version>
         <mybatis-plus.version>3.5.7</mybatis-plus.version>
-        <mybatis-plus-generator.version>3.5.7</mybatis-plus-generator.version>
         <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
         <mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
-        <easy-trans.version>3.0.5</easy-trans.version>
-        <redisson.version>3.32.0</redisson.version>
-        <dm8.jdbc.version>8.1.3.62</dm8.jdbc.version>
+        <easy-trans.version>3.0.6</easy-trans.version>
+        <redisson.version>3.36.0</redisson.version>
+        <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
+        <kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
+        <opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
         <!-- 消息队列 -->
-        <rocketmq-spring.version>2.3.0</rocketmq-spring.version>
+        <rocketmq-spring.version>2.3.1</rocketmq-spring.version>
         <!-- RPC 相关 -->
         <!-- Config 配置中心相关 -->
         <!-- Job 定时任务相关 -->
@@ -48,7 +51,7 @@
         <opentracing.version>0.33.0</opentracing.version>
         <!-- Test 测试相关 -->
         <podam.version>7.2.11.RELEASE</podam.version> <!-- Spring Boot 2.X 最多使用 7.2.11 版本 -->
-        <jedis-mock.version>1.1.2</jedis-mock.version>
+        <jedis-mock.version>1.1.4</jedis-mock.version>
         <mockito-inline.version>4.11.0</mockito-inline.version>
         <!-- Bpm 工作流相关 -->
         <flowable.version>6.8.0</flowable.version>
@@ -56,10 +59,10 @@
         <captcha-plus.version>1.0.8</captcha-plus.version>
         <jsoup.version>1.18.1</jsoup.version>
         <lombok.version>1.18.34</lombok.version>
-        <mapstruct.version>1.5.5.Final</mapstruct.version>
-        <hutool.version>5.8.29</hutool.version>
-        <easyexcel.verion>3.3.4</easyexcel.verion>
-        <velocity.version>2.3</velocity.version>
+        <mapstruct.version>1.6.2</mapstruct.version>
+        <hutool.version>5.8.32</hutool.version>
+        <easyexcel.verion>4.0.3</easyexcel.verion>
+        <velocity.version>2.4</velocity.version>
         <screw.version>1.0.5</screw.version>
         <fastjson.version>2.0.53</fastjson.version>
         <guava.version>33.2.1-jre</guava.version>
@@ -70,23 +73,45 @@
         <ip2region.version>2.7.0</ip2region.version>
         <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
         <reflections.version>0.10.2</reflections.version>
+        <netty.version>4.1.113.Final</netty.version>
         <!-- 三方云服务相关 -->
-        <okio.version>3.5.0</okio.version>
-        <okhttp3.version>4.11.0</okhttp3.version>
-        <commons-io.version>2.15.1</commons-io.version>
-        <minio.version>8.5.7</minio.version>
+        <commons-io.version>2.17.0</commons-io.version>
+        <commons-compress.version>1.27.1</commons-compress.version>
+        <aws-java-sdk-s3.version>1.12.777</aws-java-sdk-s3.version>
         <justauth.version>1.0.8</justauth.version>
         <jimureport.version>1.7.8</jimureport.version>
-        <xercesImpl.version>2.12.2</xercesImpl.version>
         <weixin-java.version>4.6.0</weixin-java.version>
         <baidu.aip.version>4.16.19</baidu.aip.version>
         <!-- 规则引擎 -->
         <easy.rules.version>4.1.0</easy.rules.version>
+        <!-- 专属于 JDK8 安全漏洞升级 -->
+        <logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
     </properties>
 
     <dependencyManagement>
         <dependencies>
             <!-- 统一依赖管理 -->
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-bom</artifactId>
+                <version>${netty.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework</groupId>
+                <artifactId>spring-framework-bom</artifactId> <!-- JDK8 版本独有:保证 Spring Framework 尽量高 -->
+                <version>${spring.framework.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.security</groupId>
+                <artifactId>spring-security-bom</artifactId> <!-- JDK8 版本独有:保证 Spring Security 尽量高 -->
+                <version>${spring.security.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-dependencies</artifactId>
@@ -216,7 +241,7 @@
             <dependency>
                 <groupId>com.baomidou</groupId>
                 <artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
-                <version>${mybatis-plus-generator.version}</version>
+                <version>${mybatis-plus.version}</version>
             </dependency>
             <dependency>
                 <groupId>com.baomidou</groupId>
@@ -506,6 +531,11 @@
                 <artifactId>commons-io</artifactId>
                 <version>${commons-io.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-compress</artifactId>
+                <version>${commons-compress.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>org.apache.tika</groupId>
@@ -596,19 +626,9 @@
 
             <!-- 三方云服务相关 -->
             <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>io.minio</groupId>
-                <artifactId>minio</artifactId>
-                <version>${minio.version}</version>
+                <groupId>com.amazonaws</groupId>
+                <artifactId>aws-java-sdk-s3</artifactId>
+                <version>${aws-java-sdk-s3.version}</version>
             </dependency>
 
             <dependency>
@@ -651,11 +671,6 @@
                     </exclusion>
                 </exclusions>
             </dependency>
-            <dependency>
-                <groupId>xerces</groupId>
-                <artifactId>xercesImpl</artifactId>
-                <version>${xercesImpl.version}</version>
-            </dependency>
 
             <dependency>
                 <groupId>org.jeasy</groupId>
@@ -685,7 +700,7 @@
                 <artifactId>flatten-maven-plugin</artifactId>
                 <version>${flatten-maven-plugin.version}</version>
                 <configuration>
-                    <flattenMode>resolveCiFriendliesOnly</flattenMode>
+                    <flattenMode>bom</flattenMode>
                     <updatePomFile>true</updatePomFile>
                 </configuration>
                 <executions>

+ 18 - 2
citu-framework/citu-common/src/main/java/com/citu/framework/common/util/collection/CollectionUtils.java

@@ -3,6 +3,7 @@ package com.citu.framework.common.util.collection;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.ArrayUtil;
+import com.citu.framework.common.pojo.PageResult;
 import com.google.common.collect.ImmutableMap;
 
 import java.util.*;
@@ -73,6 +74,13 @@ public class CollectionUtils {
         return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
     }
 
+    public static <T, U> PageResult<U> convertPage(PageResult<T> from, Function<T, U> func) {
+        if (ArrayUtil.isEmpty(from)) {
+            return new PageResult<>(from.getTotal());
+        }
+        return new PageResult<>(convertList(from.getList(), func), from.getTotal());
+    }
+
     public static <T, U> List<U> convertListByFlatMap(Collection<T> from,
                                                       Function<T, ? extends Stream<? extends U>> func) {
         if (CollUtil.isEmpty(from)) {
@@ -290,7 +298,15 @@ public class CollectionUtils {
         return valueFunc.apply(t);
     }
 
-    public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc,
+    public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) {
+        if (CollUtil.isEmpty(from)) {
+            return null;
+        }
+        assert from.size() > 0; // 断言,避免告警
+        return from.stream().min(Comparator.comparing(valueFunc)).get();
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
                                                                      BinaryOperator<V> accumulator) {
         return getSumValue(from, valueFunc, accumulator, null);
     }
@@ -316,7 +332,7 @@ public class CollectionUtils {
     }
 
     public static <T> List<T> newArrayList(List<List<T>> list) {
-        return list.stream().flatMap(Collection::stream).collect(Collectors.toList());
+        return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
     }
 
 }

+ 2 - 0
citu-framework/citu-common/src/main/java/com/citu/framework/common/util/date/DateUtils.java

@@ -28,6 +28,8 @@ public class DateUtils {
 
     public static final String FORMAT_YEAR_MONTH_DAY2= "yyyyMMdd";
 
+    public static final String FORMAT_YEAR_MONTH_DAY3= "yyyy-MM-dd HH:mm";
+
     public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
 
     public static final String FORMAT_YEAR_MONTH_DAY_CHINESE = "yyyy年MM月dd日";

+ 26 - 9
citu-framework/citu-common/src/main/java/com/citu/framework/common/util/http/HttpUtils.java

@@ -35,14 +35,18 @@ public class HttpUtils {
         return builder.build();
     }
 
+    private String append(String base, Map<String, ?> query, boolean fragment) {
+        return append(base, query, null, fragment);
+    }
+
     /**
      * 拼接 URL
-     * <p>
+     *
      * 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 base 基础 URL
+     * @param query 查询参数
+     * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
      * @param fragment URL 的 fragment,即拼接到 # 中
      * @return 拼接后的 URL
      */
@@ -122,11 +126,11 @@ public class HttpUtils {
 
     /**
      * HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
-     * <p>
+     *
      * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
      *
-     * @param url         URL
-     * @param headers     请求头
+     * @param url URL
+     * @param headers 请求头
      * @param requestBody 请求体
      * @return 请求结果
      */
@@ -139,8 +143,21 @@ public class HttpUtils {
         }
     }
 
-    private String append(String base, Map<String, ?> query, boolean fragment) {
-        return append(base, query, null, fragment);
+    /**
+     * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
+     *
+     * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
+     *
+     * @param url URL
+     * @param headers 请求头
+     * @return 请求结果
+     */
+    public static String get(String url, Map<String, String> headers) {
+        try (HttpResponse response = HttpRequest.get(url)
+                .addHeaders(headers)
+                .execute()) {
+            return response.body();
+        }
     }
 
 }

+ 1 - 1
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/jackson/core/databind/NumberSerializer.java → citu-framework/citu-common/src/main/java/com/citu/framework/common/util/json/databind/NumberSerializer.java

@@ -1,4 +1,4 @@
-package com.citu.framework.jackson.core.databind;
+package com.citu.framework.common.util.json.databind;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.databind.SerializerProvider;

+ 1 - 1
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/jackson/core/databind/TimestampLocalDateTimeDeserializer.java → citu-framework/citu-common/src/main/java/com/citu/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java

@@ -1,4 +1,4 @@
-package com.citu.framework.jackson.core.databind;
+package com.citu.framework.common.util.json.databind;
 
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.DeserializationContext;

+ 1 - 1
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/jackson/core/databind/TimestampLocalDateTimeSerializer.java → citu-framework/citu-common/src/main/java/com/citu/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java

@@ -1,4 +1,4 @@
-package com.citu.framework.jackson.core.databind;
+package com.citu.framework.common.util.json.databind;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.databind.JsonSerializer;

+ 7 - 0
citu-framework/citu-common/src/main/java/com/citu/framework/common/util/object/BeanUtils.java

@@ -59,4 +59,11 @@ public class BeanUtils {
         return new PageResult<>(list, source.getTotal());
     }
 
+    public static void copyProperties(Object source, Object target) {
+        if (source == null || target == null) {
+            return;
+        }
+        BeanUtil.copyProperties(source, target, false);
+    }
+
 }

+ 3 - 2
citu-framework/citu-spring-boot-starter-biz-data-permission/src/main/java/com/citu/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java

@@ -21,6 +21,7 @@ import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
 import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
 import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
 import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -141,7 +142,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
             return deptExpression;
         }
         // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
-        return new Parenthesis(new OrExpression(deptExpression, userExpression));
+        return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression));
     }
 
     private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
@@ -156,7 +157,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
         }
         // 拼接条件
         return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
-                new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new)));
+                new ParenthesedExpressionList(CollectionUtils.convertList(deptIds, LongValue::new)));
     }
 
     private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {

+ 2 - 1
citu-framework/citu-spring-boot-starter-biz-data-permission/src/test/java/com/citu/framework/datapermission/core/db/DataPermissionRuleHandlerTest.java

@@ -12,6 +12,7 @@ import net.sf.jsqlparser.expression.Parenthesis;
 import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
 import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
 import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
 import net.sf.jsqlparser.schema.Column;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -81,7 +82,7 @@ public class DataPermissionRuleHandlerTest extends BaseMockitoUnitTest {
                 Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
                 ExpressionList<LongValue> values = new ExpressionList<>(new LongValue(10L),
                         new LongValue(20L));
-                return new InExpression(column, new Parenthesis((values)));
+                return new InExpression(column, new ParenthesedExpressionList((values)));
             }
 
         };

+ 2 - 1
citu-framework/citu-spring-boot-starter-biz-tenant/src/main/java/com/citu/framework/tenant/core/db/TenantDatabaseInterceptor.java

@@ -1,6 +1,7 @@
 package com.citu.framework.tenant.core.db;
 
 import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils;
 import com.citu.framework.tenant.config.TenantProperties;
 import com.citu.framework.tenant.core.context.TenantContextHolder;
 import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
@@ -37,7 +38,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
     @Override
     public boolean ignoreTable(String tableName) {
         return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
-            || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表
+                || CollUtil.contains(ignoreTables, SqlParserUtils.removeWrapperSymbol(tableName)); // 情况二,忽略多租户的表
     }
 
 }

+ 13 - 3
citu-framework/citu-spring-boot-starter-biz-tenant/src/main/java/com/citu/framework/tenant/core/job/TenantJobAspect.java

@@ -6,6 +6,7 @@ import cn.hutool.core.util.StrUtil;
 import com.citu.framework.common.util.json.JsonUtils;
 import com.citu.framework.tenant.core.service.TenantFrameworkService;
 import com.citu.framework.tenant.core.util.TenantUtils;
+import com.xxl.job.core.context.XxlJobContext;
 import com.xxl.job.core.context.XxlJobHelper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -17,6 +18,7 @@ import org.aspectj.lang.annotation.Aspect;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * 多租户 JobHandler AOP
@@ -43,21 +45,29 @@ public class TenantJobAspect {
 
         // 逐个租户,执行 Job
         Map<Long, String> results = new ConcurrentHashMap<>();
+        AtomicBoolean success = new AtomicBoolean(true); // 标记,是否存在失败的情况
+        XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); // XXL-Job 上下文
         tenantIds.parallelStream().forEach(tenantId -> {
             // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
             TenantUtils.execute(tenantId, () -> {
                 try {
-                    joinPoint.proceed();
+                    XxlJobContext.setXxlJobContext(xxlJobContext);
+                    // 执行 Job
+                    Object result = joinPoint.proceed();
+                    results.put(tenantId, StrUtil.toStringOrEmpty(result));
                 } catch (Throwable e) {
                     results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
+                    success.set(false);
                     // 打印异常
                     XxlJobHelper.log(StrUtil.format("[多租户({}) 执行任务({}),发生异常:{}]",
                             tenantId, joinPoint.getSignature(), ExceptionUtils.getStackTrace(e)));
                 }
             });
         });
-        // 如果 results 非空,说明发生了异常,标记 XXL-Job 执行失败
-        if (CollUtil.isNotEmpty(results)) {
+        // 记录执行结果
+        if (success.get()) {
+            XxlJobHelper.handleSuccess(JsonUtils.toJsonString(results));
+        } else {
             XxlJobHelper.handleFail(JsonUtils.toJsonString(results));
         }
     }

+ 5 - 0
citu-framework/citu-spring-boot-starter-excel/pom.xml

@@ -65,6 +65,11 @@
             <artifactId>guava</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId> <!-- 解决 https://github.com/alibaba/easyexcel/issues/3954 问题 -->
+        </dependency>
+
         <dependency>
             <groupId>com.citu</groupId>
             <artifactId>citu-spring-boot-starter-biz-ip</artifactId>

+ 12 - 0
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/config/CituMybatisAutoConfiguration.java

@@ -6,6 +6,8 @@ import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
 import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
 import com.baomidou.mybatisplus.extension.incrementer.*;
+import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
+import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
 import com.citu.framework.mybatis.core.handler.DefaultDBFieldHandler;
@@ -16,6 +18,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.core.env.ConfigurableEnvironment;
 
+import java.util.concurrent.TimeUnit;
+
 /**
  * MyBaits 配置类
  *
@@ -27,6 +31,14 @@ import org.springframework.core.env.ConfigurableEnvironment;
         lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
 public class CituMybatisAutoConfiguration {
 
+    static {
+        // 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存
+        JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(
+                (cache) -> cache.maximumSize(1024)
+                        .expireAfterWrite(5, TimeUnit.SECONDS))
+        );
+    }
+
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();

+ 5 - 5
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java

@@ -2,7 +2,6 @@ package com.citu.framework.mybatis.config;
 
 import cn.hutool.core.util.StrUtil;
 import com.citu.framework.common.util.collection.SetUtils;
-import com.citu.framework.mybatis.core.enums.SqlConstants;
 import com.citu.framework.mybatis.core.util.JdbcUtils;
 import com.baomidou.mybatisplus.annotation.DbType;
 import com.baomidou.mybatisplus.annotation.IdType;
@@ -42,9 +41,6 @@ public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor
         // TODO 芋艿:暂时没有找到特别合适的地方,先放在这里
         setJobStoreDriverIfPresent(environment, dbType);
 
-        // 初始化 SQL 静态变量
-        SqlConstants.init(dbType);
-
         // 如果非 NONE,则不进行处理
         IdType idType = getIdType(environment);
         if (idType != IdType.NONE) {
@@ -55,7 +51,7 @@ public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor
             setIdType(environment, IdType.INPUT);
             return;
         }
-        // 情况二,自增 ID,适合 MySQL 等直接自增的数据库
+        // 情况二,自增 ID,适合 MySQL、DM 达梦等直接自增的数据库
         setIdType(environment, IdType.AUTO);
     }
 
@@ -86,6 +82,10 @@ public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor
             case SQL_SERVER2005:
                 driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
                 break;
+            case DM:
+            case KINGBASE_ES:
+                driverClass = "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
+                break;
         }
         // 设置 driverClass 变量
         if (StrUtil.isNotEmpty(driverClass)) {

+ 95 - 0
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/enums/DbTypeEnum.java

@@ -0,0 +1,95 @@
+package com.citu.framework.mybatis.core.enums;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息
+ */
+@Getter
+@AllArgsConstructor
+public enum DbTypeEnum {
+
+    /**
+     * H2
+     *
+     * 注意:H2 不支持 find_in_set 函数
+     */
+    H2(DbType.H2, "H2", ""),
+
+    /**
+     * MySQL
+     */
+    MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
+
+    /**
+     * Oracle
+     */
+    ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
+
+    /**
+     * PostgreSQL
+     *
+     * 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
+     */
+    POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
+
+    /**
+     * SQL Server
+     */
+    SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
+    /**
+     * SQL Server 2005
+     */
+    SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
+
+    /**
+     * 达梦
+     */
+    DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
+
+    /**
+     * 人大金仓
+     */
+    KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
+    ;
+
+    public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())
+            .collect(Collectors.toMap(DbTypeEnum::getProductName, Function.identity()));
+
+    public static final Map<DbType, DbTypeEnum> MAP_BY_MP = Arrays.stream(values())
+            .collect(Collectors.toMap(DbTypeEnum::getMpDbType, Function.identity()));
+
+    /**
+     * MyBatis Plus 类型
+     */
+    private final DbType mpDbType;
+    /**
+     * 数据库产品名
+     */
+    private final String productName;
+    /**
+     * SQL FIND_IN_SET 模板
+     */
+    private final String findInSetTemplate;
+
+    public static DbType find(String databaseProductName) {
+        if (StrUtil.isBlank(databaseProductName)) {
+            return null;
+        }
+        return MAP_BY_NAME.get(databaseProductName).getMpDbType();
+    }
+
+    public static String getFindInSetTemplate(DbType dbType) {
+        return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
+                .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
+    }
+}

+ 0 - 21
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/enums/SqlConstants.java

@@ -1,21 +0,0 @@
-package com.citu.framework.mybatis.core.enums;
-
-import com.baomidou.mybatisplus.annotation.DbType;
-
-/**
- * SQL相关常量类
- *
- * @author Rayson
- */
-public class SqlConstants {
-
-    /**
-     * 数据库的类型
-     */
-    public static DbType DB_TYPE;
-
-    public static void init(DbType dbType) {
-        DB_TYPE = dbType;
-    }
-
-}

+ 6 - 14
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/mapper/BaseMapperX.java

@@ -13,7 +13,7 @@ import com.citu.framework.common.pojo.PageParam;
 import com.citu.framework.common.pojo.PageResult;
 import com.citu.framework.common.pojo.SortablePageParam;
 import com.citu.framework.common.pojo.SortingField;
-import com.citu.framework.mybatis.core.enums.SqlConstants;
+import com.citu.framework.mybatis.core.util.JdbcUtils;
 import com.citu.framework.mybatis.core.util.MyBatisUtils;
 import com.github.yulichang.base.MPJBaseMapper;
 import com.github.yulichang.interfaces.MPJBaseJoin;
@@ -56,7 +56,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
 
     default <D> PageResult<D> selectJoinPage(PageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) {
         // 特殊:不分页,直接查询全部
-        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageNo())) {
+        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
             List<D> list = selectJoinList(clazz, lambdaWrapper);
             return new PageResult<>(list, (long) list.size());
         }
@@ -135,11 +135,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         return selectList(new LambdaQueryWrapper<T>().in(field, values));
     }
 
-    @Deprecated
-    default List<T> selectList(SFunction<T, ?> leField, SFunction<T, ?> geField, Object value) {
-        return selectList(new LambdaQueryWrapper<T>().le(leField, value).ge(geField, value));
-    }
-
     default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
         return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
     }
@@ -151,7 +146,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
      */
     default Boolean insertBatch(Collection<T> entities) {
         // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
-        if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) {
+        DbType dbType = JdbcUtils.getDbType();
+        if (JdbcUtils.isSQLServer(dbType)) {
             entities.forEach(this::insert);
             return CollUtil.isNotEmpty(entities);
         }
@@ -166,7 +162,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
      */
     default Boolean insertBatch(Collection<T> entities, int size) {
         // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
-        if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) {
+        DbType dbType = JdbcUtils.getDbType();
+        if (JdbcUtils.isSQLServer(dbType)) {
             entities.forEach(this::insert);
             return CollUtil.isNotEmpty(entities);
         }
@@ -185,10 +182,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         return Db.updateBatchById(entities, size);
     }
 
-    default Boolean insertOrUpdateBatch(Collection<T> collection) {
-        return Db.saveOrUpdateBatch(collection);
-    }
-
     default int delete(String field, String value) {
         return delete(new QueryWrapper<T>().eq(field, value));
     }
@@ -198,4 +191,3 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
     }
 
 }
-

+ 10 - 10
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/query/QueryWrapperX.java

@@ -1,17 +1,17 @@
 package com.citu.framework.mybatis.core.query;
 
-import cn.hutool.core.lang.Assert;
-import com.citu.framework.mybatis.core.enums.SqlConstants;
+import com.baomidou.mybatisplus.annotation.DbType;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.citu.framework.mybatis.core.util.JdbcUtils;
 import org.springframework.util.StringUtils;
 
 import java.util.Collection;
 
 /**
  * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
- *
+ * <p>
  * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
  *
  * @param <T> 数据类型
@@ -95,13 +95,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
     }
 
     public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
-        if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
+        if (values != null && values.length != 0 && values[0] != null && values[1] != null) {
             return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
         }
-        if (values!= null && values.length != 0 && values[0] != null) {
+        if (values != null && values.length != 0 && values[0] != null) {
             return (QueryWrapperX<T>) ge(column, values[0]);
         }
-        if (values!= null && values.length != 0 && values[1] != null) {
+        if (values != null && values.length != 0 && values[1] != null) {
             return (QueryWrapperX<T>) le(column, values[1]);
         }
         return this;
@@ -141,14 +141,14 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
 
     /**
      * 设置只返回最后一条
-     *
+     * <p>
      * TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同
      *
      * @return this
      */
     public QueryWrapperX<T> limitN(int n) {
-        Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型");
-        switch (SqlConstants.DB_TYPE) {
+        DbType dbType = JdbcUtils.getDbType();
+        switch (dbType) {
             case ORACLE:
             case ORACLE_12C:
                 super.le("ROWNUM", n);
@@ -157,7 +157,7 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
             case SQL_SERVER2005:
                 super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段
                 break;
-            default:
+            default: // MySQL、PostgreSQL、DM 达梦、KingbaseES 大金都是采用 LIMIT 实现
                 super.last("LIMIT " + n);
         }
         return this;

+ 49 - 3
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/util/JdbcUtils.java

@@ -1,9 +1,15 @@
 package com.citu.framework.mybatis.core.util;
 
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
 import com.baomidou.mybatisplus.annotation.DbType;
-
+import com.citu.framework.common.util.spring.SpringUtils;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import com.citu.framework.common.util.object.ObjectUtils;
+import com.citu.framework.mybatis.core.enums.DbTypeEnum;
+import javax.sql.DataSource;
 import java.sql.Connection;
 import java.sql.DriverManager;
+import java.sql.SQLException;
 
 /**
  * JDBC 工具类
@@ -35,8 +41,48 @@ public class JdbcUtils {
      * @return DB 类型
      */
     public static DbType getDbType(String url) {
-        String name = com.alibaba.druid.util.JdbcUtils.getDbType(url, null);
-        return DbType.getDbType(name);
+        return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url);
+    }
+
+    /**
+     * 通过当前数据库连接获得对应的 DB 类型
+     *
+     * @return DB 类型
+     */
+    public static DbType getDbType() {
+        DataSource dataSource;
+        try {
+            DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
+            dataSource = dynamicRoutingDataSource.determineDataSource();
+        } catch (NoSuchBeanDefinitionException e) {
+            dataSource = SpringUtils.getBean(DataSource.class);
+        }
+        try (Connection conn = dataSource.getConnection()) {
+            return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName());
+        } catch (SQLException e) {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+
+    /**
+     * 判断 JDBC 连接是否为 SQLServer 数据库
+     *
+     * @param url JDBC 连接
+     * @return 是否为 SQLServer 数据库
+     */
+    public static boolean isSQLServer(String url) {
+        DbType dbType = getDbType(url);
+        return isSQLServer(dbType);
+    }
+
+    /**
+     * 判断 JDBC 连接是否为 SQLServer 数据库
+     *
+     * @param dbType DB 类型
+     * @return 是否为 SQLServer 数据库
+     */
+    public static boolean isSQLServer(DbType dbType) {
+        return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
     }
 
 }

+ 21 - 3
citu-framework/citu-spring-boot-starter-mybatis/src/main/java/com/citu/framework/mybatis/core/util/MyBatisUtils.java

@@ -1,6 +1,8 @@
 package com.citu.framework.mybatis.core.util;
 
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
 import com.citu.framework.common.pojo.PageParam;
 import com.citu.framework.common.pojo.SortingField;
 import com.baomidou.mybatisplus.core.metadata.OrderItem;
@@ -8,6 +10,7 @@ import com.baomidou.mybatisplus.core.toolkit.StringPool;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.citu.framework.mybatis.core.enums.DbTypeEnum;
 import net.sf.jsqlparser.expression.Alias;
 import net.sf.jsqlparser.schema.Column;
 import net.sf.jsqlparser.schema.Table;
@@ -33,8 +36,9 @@ public class MyBatisUtils {
         Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
         // 排序字段
         if (!CollectionUtil.isEmpty(sortingFields)) {
-            page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ?
-                            OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField()))
+            page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder())
+                            ? OrderItem.asc(StrUtil.toUnderlineCase(sortingField.getField()))
+                            : OrderItem.desc(StrUtil.toUnderlineCase(sortingField.getField())))
                     .collect(Collectors.toList()));
         }
         return page;
@@ -56,7 +60,7 @@ public class MyBatisUtils {
 
     /**
      * 获得 Table 对应的表名
-     *
+     * <p>
      * 兼容 MySQL 转义表名 `t_xxx`
      *
      * @param table 表
@@ -85,4 +89,18 @@ public class MyBatisUtils {
         return new Column(tableName + StringPool.DOT + column);
     }
 
+    /**
+     * 跨数据库的 find_in_set 实现
+     *
+     * @param column 字段名称
+     * @param value  查询值(不带单引号)
+     * @return sql
+     */
+    public static String findInSet(String column, Object value) {
+        DbType dbType = JdbcUtils.getDbType();
+        return DbTypeEnum.getFindInSetTemplate(dbType)
+                .replace("#{column}", column)
+                .replace("#{value}", StrUtil.toString(value));
+    }
+
 }

+ 6 - 0
citu-framework/citu-spring-boot-starter-protection/pom.xml

@@ -42,6 +42,12 @@
             <artifactId>citu-spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>com.citu</groupId>
+            <artifactId>menduner-system-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 7 - 1
citu-framework/citu-spring-boot-starter-protection/src/main/java/com/citu/framework/signature/config/CituApiSignatureAutoConfiguration.java

@@ -3,6 +3,7 @@ package com.citu.framework.signature.config;
 import com.citu.framework.redis.config.CituRedisAutoConfiguration;
 import com.citu.framework.signature.core.aop.ApiSignatureAspect;
 import com.citu.framework.signature.core.redis.ApiSignatureRedisDAO;
+import com.citu.module.menduner.system.api.error.ErrorRecordApi;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.context.annotation.Bean;
 import org.springframework.data.redis.core.StringRedisTemplate;
@@ -15,9 +16,14 @@ import org.springframework.data.redis.core.StringRedisTemplate;
 @AutoConfiguration(after = CituRedisAutoConfiguration.class)
 public class CituApiSignatureAutoConfiguration {
 
+    @Bean
+    public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO, ErrorRecordApi errorRecordApi) {
+        return new ApiSignatureAspect(signatureRedisDAO,errorRecordApi);
+    }
+
     @Bean
     public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
-        return new ApiSignatureAspect(signatureRedisDAO);
+        return new ApiSignatureAspect(signatureRedisDAO,null);
     }
 
     @Bean

+ 37 - 2
citu-framework/citu-spring-boot-starter-protection/src/main/java/com/citu/framework/signature/core/aop/ApiSignatureAspect.java

@@ -6,10 +6,13 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.digest.DigestUtil;
+import com.alibaba.fastjson.JSON;
 import com.citu.framework.common.exception.ServiceException;
 import com.citu.framework.common.util.servlet.ServletUtils;
 import com.citu.framework.signature.core.annotation.ApiSignature;
 import com.citu.framework.signature.core.redis.ApiSignatureRedisDAO;
+import com.citu.module.menduner.system.api.error.ErrorRecordApi;
+import com.citu.module.menduner.system.api.error.ErrorRecordReqDTO;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.JoinPoint;
@@ -32,7 +35,9 @@ import static com.citu.framework.common.exception.enums.GlobalErrorCodeConstants
 @AllArgsConstructor
 public class ApiSignatureAspect {
 
+    private static final ThreadLocal<Map<String, Object>> recordThreadLocal = new ThreadLocal<>();
     private final ApiSignatureRedisDAO signatureRedisDAO;
+    private final ErrorRecordApi errorRecordApi;
 
     /**
      * 获取请求头加签参数 Map
@@ -56,7 +61,7 @@ public class ApiSignatureAspect {
      * @return queryParams
      */
     private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
-         SortedMap<String, String> sortedMap = new TreeMap<>();
+        SortedMap<String, String> sortedMap = new TreeMap<>();
         for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
             sortedMap.put(entry.getKey(), entry.getValue()[0]);
         }
@@ -74,9 +79,18 @@ public class ApiSignatureAspect {
     @Before("@annotation(signature)")
     public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
         // 1. 验证通过,直接结束
+        recordThreadLocal.set(new HashMap<>());
         if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
+            recordThreadLocal.remove();
             return;
         }
+        if (null != errorRecordApi) {
+            errorRecordApi.create(ErrorRecordReqDTO.builder()
+                    .mark(ServletUtils.getRequest().getHeader(signature.timestamp()))
+                    .content(JSON.toJSONString(recordThreadLocal.get()))
+                    .build());
+            recordThreadLocal.remove();
+        }
 
         // 2. 验证不通过,抛出异常
         log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
@@ -93,13 +107,18 @@ public class ApiSignatureAspect {
         // 1.2 校验 appId 是否能获取到对应的 appSecret
         String appId = request.getHeader(signature.appId());
         String appSecret = signatureRedisDAO.getAppSecret(appId);
+        recordThreadLocal.get().put("appSecret", appSecret);
         Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
 
         // 2. 校验签名【重要!】
         String clientSignature = request.getHeader(signature.sign()); // 客户端签名
         String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
         String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
+        recordThreadLocal.get().put("clientSignature", clientSignature);
+        recordThreadLocal.get().put("serverSignatureString", serverSignatureString);
+        recordThreadLocal.get().put("serverSignature", serverSignature);
         if (ObjUtil.notEqual(clientSignature, serverSignature)) {
+            recordThreadLocal.get().put("reason", "ObjUtil.notEqual(clientSignature, serverSignature)");
             return false;
         }
 
@@ -124,19 +143,27 @@ public class ApiSignatureAspect {
     private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
         // 1. 非空校验
         String appId = request.getHeader(signature.appId());
+        recordThreadLocal.get().put("appId", appId);
         if (StrUtil.isBlank(appId)) {
+            recordThreadLocal.get().put("reason", "appId is null");
             return false;
         }
         String timestamp = request.getHeader(signature.timestamp());
+        recordThreadLocal.get().put("timestamp", timestamp);
         if (StrUtil.isBlank(timestamp)) {
+            recordThreadLocal.get().put("reason", "timestamp is null");
             return false;
         }
         String nonce = request.getHeader(signature.nonce());
+        recordThreadLocal.get().put("nonce", nonce);
         if (StrUtil.length(nonce) < 10) {
+            recordThreadLocal.get().put("reason", "timestamp length < 10");
             return false;
         }
         String sign = request.getHeader(signature.sign());
+        recordThreadLocal.get().put("sign", sign);
         if (StrUtil.isBlank(sign)) {
+            recordThreadLocal.get().put("reason", "sign is null");
             return false;
         }
 
@@ -144,12 +171,20 @@ public class ApiSignatureAspect {
         long expireTime = signature.timeUnit().toMillis(signature.timeout());
         long requestTimestamp = Long.parseLong(timestamp);
         long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
+        recordThreadLocal.get().put("expireTime", expireTime);
+        recordThreadLocal.get().put("requestTimestamp", requestTimestamp);
+        recordThreadLocal.get().put("timestampDisparity", timestampDisparity);
         if (timestampDisparity > expireTime) {
+            recordThreadLocal.get().put("reason", "timestampDisparity > expireTime 超出范围");
             return false;
         }
 
         // 3. 检查 nonce 是否存在,有且仅能使用一次
-        return signatureRedisDAO.getNonce(appId, nonce) == null;
+        boolean result = signatureRedisDAO.getNonce(appId, nonce) == null;
+        if (!result) {
+            recordThreadLocal.get().put("reason", "signatureRedisDAO.getNonce(appId, nonce) != null");
+        }
+        return result;
     }
 
     /**

+ 15 - 10
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/operatelog/core/service/LogRecordServiceImpl.java

@@ -9,6 +9,7 @@ import com.citu.module.system.api.logger.dto.OperateLogCreateReqDTO;
 import com.mzt.logapi.beans.LogRecord;
 import com.mzt.logapi.service.ILogRecordService;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
@@ -29,18 +30,22 @@ public class LogRecordServiceImpl implements ILogRecordService {
 
     @Override
     public void record(LogRecord logRecord) {
-        // 1. 补全通用字段
         OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO();
-        reqDTO.setTraceId(TracerUtils.getTraceId());
-        // 补充用户信息
-        fillUserFields(reqDTO);
-        // 补全模块信息
-        fillModuleFields(reqDTO, logRecord);
-        // 补全请求信息
-        fillRequestFields(reqDTO);
+        try {
+            reqDTO.setTraceId(TracerUtils.getTraceId());
+            // 补充用户信息
+            fillUserFields(reqDTO);
+            // 补全模块信息
+            fillModuleFields(reqDTO, logRecord);
+            // 补全请求信息
+            fillRequestFields(reqDTO);
 
-        // 2. 异步记录日志
-        operateLogApi.createOperateLog(reqDTO);
+            // 2. 异步记录日志
+            operateLogApi.createOperateLogAsync(reqDTO);
+        } catch (Throwable ex) {
+            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
+            log.error("[record][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
+        }
     }
 
     private static void fillUserFields(OperateLogCreateReqDTO reqDTO) {

+ 2 - 1
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/config/AuthorizeRequestsCustomizer.java

@@ -4,6 +4,7 @@ import com.citu.framework.web.config.WebProperties;
 import org.springframework.core.Ordered;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
 
 import javax.annotation.Resource;
@@ -15,7 +16,7 @@ import javax.annotation.Resource;
  * @author Rayson
  */
 public abstract class AuthorizeRequestsCustomizer
-        implements Customizer<ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry>, Ordered {
+        implements Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>, Ordered {
 
     @Resource
     private WebProperties webProperties;

+ 0 - 9
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/config/CituSecurityAutoConfiguration.java

@@ -1,6 +1,5 @@
 package com.citu.framework.security.config;
 
-import com.citu.framework.security.core.aop.PreAuthenticatedAspect;
 import com.citu.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
 import com.citu.framework.security.core.filter.TokenAuthenticationFilter;
 import com.citu.framework.security.core.handler.AccessDeniedHandlerImpl;
@@ -39,14 +38,6 @@ public class CituSecurityAutoConfiguration {
     @Resource
     private SecurityProperties securityProperties;
 
-    /**
-     * 处理用户未登录拦截的切面的 Bean
-     */
-    @Bean
-    public PreAuthenticatedAspect preAuthenticatedAspect() {
-        return new PreAuthenticatedAspect();
-    }
-
     /**
      * 认证失败处理类 Bean
      */

+ 18 - 19
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/config/CituWebSecurityConfigurerAdapter.java

@@ -90,7 +90,7 @@ public class CituWebSecurityConfigurerAdapter {
 
     /**
      * 配置 URL 的安全配置
-     * <p>
+     *
      * anyRequest          |   匹配所有请求路径
      * access              |   SpringEl表达式结果为true时可以访问
      * anonymous           |   匿名可以访问
@@ -126,26 +126,25 @@ public class CituWebSecurityConfigurerAdapter {
         // 设置每个请求的权限
         httpSecurity
                 // ①:全局共享规则
-                .authorizeRequests()
-                // 1.1 静态资源,可匿名访问
-                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
-                // 1.2 设置 @PermitAll 无需认证
-                .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
-                .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
-                .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
-                .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
-                // 1.3 基于 citu.security.permit-all-urls 无需认证
-                .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
-                // 1.4 设置 App API 无需认证
-                .antMatchers(buildAppApi("/**")).permitAll()
-                // 1.5 验证码captcha 允许匿名访问
-                .antMatchers("/captcha/get", "/captcha/check").permitAll()
+                .authorizeHttpRequests(c -> c
+                        // 1.1 静态资源,可匿名访问
+                        .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
+                        // 1.2 设置 @PermitAll 无需认证
+                        .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll()
+                        .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll()
+                        // 1.3 基于 citu.security.permit-all-urls 无需认证
+                        .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
+                        // 1.4 设置 App API 默认无需认证
+                        .requestMatchers(buildAppApi("/**")).permitAll()
+                )
                 // ②:每个项目的自定义规则
-                .and().authorizeRequests(registry -> // 下面,循环设置自定义规则
-                        authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
+                .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c)))
                 // ③:兜底规则,必须认证
-                .authorizeRequests()
-                .anyRequest().authenticated();
+                .authorizeHttpRequests(c -> c.anyRequest().authenticated());
 
         // 添加 Token Filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

+ 5 - 0
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/LoginUser.java

@@ -5,6 +5,7 @@ import com.citu.framework.common.enums.UserTypeEnum;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Data;
 
+import java.time.LocalDateTime;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -44,6 +45,10 @@ public class LoginUser {
      * 授权范围
      */
     private List<String> scopes;
+    /**
+     * 过期时间
+     */
+    private LocalDateTime expiresTime;
 
     // ========== 上下文 ==========
     /**

+ 2 - 2
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/annotations/PreAuthenticated.java

@@ -4,10 +4,10 @@ import java.lang.annotation.*;
 
 /**
  * 声明用户需要登录
- *
+ * <p>
  * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @Target({ElementType.METHOD})
 @Retention(RetentionPolicy.RUNTIME)

+ 2 - 0
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/aop/PreAuthenticatedAspect.java

@@ -1,5 +1,6 @@
 package com.citu.framework.security.core.aop;
 
+
 import com.citu.framework.security.core.annotations.PreAuthenticated;
 import com.citu.framework.security.core.util.SecurityFrameworkUtils;
 import lombok.extern.slf4j.Slf4j;
@@ -10,6 +11,7 @@ import org.aspectj.lang.annotation.Aspect;
 import static com.citu.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
 import static com.citu.framework.common.exception.util.ServiceExceptionUtil.exception;
 
+
 @Aspect
 @Slf4j
 public class PreAuthenticatedAspect {

+ 2 - 1
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/filter/TokenAuthenticationFilter.java

@@ -98,7 +98,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
             // 构建登录用户
             return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
                     .setInfo(accessToken.getUserInfo()) // 额外的用户信息
-                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
+                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes())
+                    .setExpiresTime(accessToken.getExpiresTime());
         } catch (ServiceException serviceException) {
             // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
             return null;

+ 10 - 2
citu-framework/citu-spring-boot-starter-security/src/main/java/com/citu/framework/security/core/service/SecurityFrameworkServiceImpl.java

@@ -70,7 +70,11 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
     @Override
     @SneakyThrows
     public boolean hasAnyPermissions(String... permissions) {
-        return hasAnyPermissionsCache.get(new KeyValue<>(getLoginUserId(), Arrays.asList(permissions)));
+        Long userId = getLoginUserId();
+        if (userId == null) {
+            return false;
+        }
+        return hasAnyPermissionsCache.get(new KeyValue<>(userId, Arrays.asList(permissions)));
     }
 
     @Override
@@ -81,7 +85,11 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
     @Override
     @SneakyThrows
     public boolean hasAnyRoles(String... roles) {
-        return hasAnyRolesCache.get(new KeyValue<>(getLoginUserId(), Arrays.asList(roles)));
+        Long userId = getLoginUserId();
+        if (userId == null) {
+            return false;
+        }
+        return hasAnyRolesCache.get(new KeyValue<>(userId, Arrays.asList(roles)));
     }
 
     @Override

+ 4 - 0
citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/ut/BaseDbAndRedisUnitTest.java

@@ -1,5 +1,6 @@
 package com.citu.framework.test.core.ut;
 
+import cn.hutool.extra.spring.SpringUtil;
 import com.citu.framework.datasource.config.CituDataSourceAutoConfiguration;
 import com.citu.framework.mybatis.config.CituMybatisAutoConfiguration;
 import com.citu.framework.redis.config.CituRedisAutoConfiguration;
@@ -45,6 +46,9 @@ public class BaseDbAndRedisUnitTest {
             RedisAutoConfiguration.class, // Spring Redis 自动配置类
             RedissonAutoConfiguration.class, // Redisson 自动高配置类
             RedissonAutoConfiguration.class, // Redisson 自动配置类
+
+            // 其它配置类
+            SpringUtil.class
     })
     public static class Application {
     }

+ 4 - 0
citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/ut/BaseDbUnitTest.java

@@ -1,5 +1,6 @@
 package com.citu.framework.test.core.ut;
 
+import cn.hutool.extra.spring.SpringUtil;
 import com.citu.framework.datasource.config.CituDataSourceAutoConfiguration;
 import com.citu.framework.mybatis.config.CituMybatisAutoConfiguration;
 import com.citu.framework.test.config.SqlInitializationTestConfiguration;
@@ -36,6 +37,9 @@ public class BaseDbUnitTest {
             CituMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
             MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
             MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类
+
+            // 其它配置类
+            SpringUtil.class
     })
     public static class Application {
     }

+ 4 - 0
citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/ut/BaseRedisUnitTest.java

@@ -1,5 +1,6 @@
 package com.citu.framework.test.core.ut;
 
+import cn.hutool.extra.spring.SpringUtil;
 import com.citu.framework.redis.config.CituRedisAutoConfiguration;
 import com.citu.framework.test.config.RedisTestConfiguration;
 import org.redisson.spring.starter.RedissonAutoConfiguration;
@@ -26,6 +27,9 @@ public class BaseRedisUnitTest {
             CituRedisAutoConfiguration.class, // 自己的 Redis 配置类
             RedissonAutoConfiguration.class, // Redisson 自动高配置类
             RedissonAutoConfiguration.class, // Redisson 自动配置类
+
+            // 其它配置类
+            SpringUtil.class
     })
     public static class Application {
     }

+ 9 - 0
citu-framework/citu-spring-boot-starter-test/src/main/java/com/citu/framework/test/core/util/RandomUtils.java

@@ -103,6 +103,10 @@ public class RandomUtils {
         return randomString() + "@qq.com";
     }
 
+    public static String randomMobile() {
+        return "13800138" + RandomUtil.randomNumbers(3);
+    }
+
     public static String randomURL() {
         return "https://www.iocoder.cn/" + randomString();
     }
@@ -130,6 +134,11 @@ public class RandomUtils {
     @SafeVarargs
     public static <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) {
         int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH);
+        return randomPojoList(clazz, size, consumers);
+    }
+
+    @SafeVarargs
+    public static <T> List<T> randomPojoList(Class<T> clazz, int size, Consumer<T>... consumers) {
         return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers))
                 .collect(Collectors.toList());
     }

+ 10 - 25
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/config/CituApiLogAutoConfiguration.java

@@ -2,15 +2,10 @@ package com.citu.framework.apilog.config;
 
 import com.citu.framework.apilog.core.filter.ApiAccessLogFilter;
 import com.citu.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
-import com.citu.framework.apilog.core.service.ApiAccessLogFrameworkService;
-import com.citu.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
-import com.citu.framework.apilog.core.service.ApiErrorLogFrameworkService;
-import com.citu.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
 import com.citu.framework.common.enums.WebFilterOrderEnum;
-import com.citu.framework.web.config.WebProperties;
 import com.citu.framework.web.config.CituWebAutoConfiguration;
+import com.citu.framework.web.config.WebProperties;
 import com.citu.module.infra.api.logger.ApiAccessLogApi;
-import com.citu.module.infra.api.logger.ApiErrorLogApi;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -24,36 +19,26 @@ import javax.servlet.Filter;
 @AutoConfiguration(after = CituWebAutoConfiguration.class)
 public class CituApiLogAutoConfiguration implements WebMvcConfigurer {
 
-    @Bean
-    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
-        return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
-    }
-
-    @Bean
-    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
-        return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
+    private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
+        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
+        bean.setOrder(order);
+        return bean;
     }
 
     /**
      * 创建 ApiAccessLogFilter Bean,记录 API 请求日志
      */
     @Bean
-    @ConditionalOnProperty(prefix = "citu.access-log", value = "enable", matchIfMissing = true) // 允许使用 citu.access-log.enable=false 禁用访问日志
+    @ConditionalOnProperty(prefix = "citu.access-log", value = "enable", matchIfMissing = true)
+    // 允许使用 citu.access-log.enable=false 禁用访问日志
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
                                                                          @Value("${spring.application.name}") String applicationName,
-                                                                         ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
-        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
+                                                                         ApiAccessLogApi apiAccessLogApi) {
+        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi);
         return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
     }
 
-    private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
-        FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
-        bean.setOrder(order);
-        return bean;
-    }
-
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(new ApiAccessLogInterceptor());

+ 1 - 2
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/config/CituApiLogRpcAutoConfiguration.java

@@ -12,7 +12,6 @@ import org.springframework.context.annotation.Configuration;
  * @author Rayson
  */
 @AutoConfiguration
-@EnableFeignClients(clients = {ApiAccessLogApi.class, // 主要是引入相关的 API 服务
-        ApiErrorLogApi.class})
+@EnableFeignClients(clients = {ApiAccessLogApi.class, ApiErrorLogApi.class}) // 主要是引入相关的 API 服务
 public class CituApiLogRpcAutoConfiguration {
 }

+ 99 - 98
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/filter/ApiAccessLogFilter.java

@@ -9,7 +9,6 @@ import cn.hutool.core.util.BooleanUtil;
 import cn.hutool.core.util.StrUtil;
 import com.citu.framework.apilog.core.annotation.ApiAccessLog;
 import com.citu.framework.apilog.core.enums.OperateTypeEnum;
-import com.citu.framework.apilog.core.service.ApiAccessLogFrameworkService;
 import com.citu.framework.common.exception.enums.GlobalErrorCodeConstants;
 import com.citu.framework.common.pojo.CommonResult;
 import com.citu.framework.common.util.json.JsonUtils;
@@ -18,6 +17,7 @@ import com.citu.framework.common.util.servlet.ServletUtils;
 import com.citu.framework.web.config.WebProperties;
 import com.citu.framework.web.core.filter.ApiRequestFilter;
 import com.citu.framework.web.core.util.WebFrameworkUtils;
+import com.citu.module.infra.api.logger.ApiAccessLogApi;
 import com.citu.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
 import com.fasterxml.jackson.databind.JsonNode;
 import io.swagger.v3.oas.annotations.Operation;
@@ -36,12 +36,12 @@ import java.time.temporal.ChronoUnit;
 import java.util.Iterator;
 import java.util.Map;
 
-import static com.citu.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*;
+import static com.citu.framework.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD;
 import static com.citu.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
  * API 访问日志 Filter
- *
+ * <p>
  * 目的:记录 API 访问日志到数据库中
  *
  * @author Rayson
@@ -53,12 +53,103 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
 
     private final String applicationName;
 
-    private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
+    private final ApiAccessLogApi apiAccessLogApi;
 
-    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
+    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogApi apiAccessLogApi) {
         super(webProperties);
         this.applicationName = applicationName;
-        this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
+        this.apiAccessLogApi = apiAccessLogApi;
+    }
+
+    private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
+        RequestMethod requestMethod = ArrayUtil.firstMatch(method ->
+                StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values());
+        if (requestMethod == null) {
+            return OperateTypeEnum.OTHER;
+        }
+        switch (requestMethod) {
+            case GET:
+                return OperateTypeEnum.GET;
+            case POST:
+                return OperateTypeEnum.CREATE;
+            case PUT:
+                return OperateTypeEnum.UPDATE;
+            case DELETE:
+                return OperateTypeEnum.DELETE;
+            default:
+                return OperateTypeEnum.OTHER;
+        }
+    }
+
+    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
+        if (CollUtil.isEmpty(map)) {
+            return null;
+        }
+        if (sanitizeKeys != null) {
+            MapUtil.removeAny(map, sanitizeKeys);
+        }
+        MapUtil.removeAny(map, SANITIZE_KEYS);
+        return JsonUtils.toJsonString(map);
+    }
+
+    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
+        if (StrUtil.isEmpty(jsonString)) {
+            return null;
+        }
+        try {
+            JsonNode rootNode = JsonUtils.parseTree(jsonString);
+            sanitizeJson(rootNode, sanitizeKeys);
+            return JsonUtils.toJsonString(rootNode);
+        } catch (Exception e) {
+            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
+            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
+            return jsonString;
+        }
+    }
+
+    // ========== 解析 @ApiAccessLog、@Swagger 注解  ==========
+
+    private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
+        if (commonResult == null) {
+            return null;
+        }
+        String jsonString = toJsonString(commonResult);
+        try {
+            JsonNode rootNode = JsonUtils.parseTree(jsonString);
+            sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
+            return JsonUtils.toJsonString(rootNode);
+        } catch (Exception e) {
+            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
+            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
+            return jsonString;
+        }
+    }
+
+    // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========
+
+    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
+        // 情况一:数组,遍历处理
+        if (node.isArray()) {
+            for (JsonNode childNode : node) {
+                sanitizeJson(childNode, sanitizeKeys);
+            }
+            return;
+        }
+        // 情况二:非 Object,只是某个值,直接返回
+        if (!node.isObject()) {
+            return;
+        }
+        //  情况三:Object,遍历处理
+        Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
+        while (iterator.hasNext()) {
+            Map.Entry<String, JsonNode> entry = iterator.next();
+            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
+                    || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
+                iterator.remove();
+                continue;
+            }
+            sanitizeJson(entry.getValue(), sanitizeKeys);
+        }
     }
 
     @Override
@@ -91,7 +182,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
             if (!enable) {
                 return;
             }
-            apiAccessLogFrameworkService.createApiAccessLog(accessLog);
+            apiAccessLogApi.createApiAccessLogAsync(accessLog);
         } catch (Throwable th) {
             log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
         }
@@ -157,95 +248,5 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
         return true;
     }
 
-    // ========== 解析 @ApiAccessLog、@Swagger 注解  ==========
-
-    private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
-        RequestMethod requestMethod = ArrayUtil.firstMatch(method ->
-                StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values());
-        if (requestMethod == null) {
-            return OperateTypeEnum.OTHER;
-        }
-        switch (requestMethod) {
-            case GET:
-                return OperateTypeEnum.GET;
-            case POST:
-                return OperateTypeEnum.CREATE;
-            case PUT:
-                return OperateTypeEnum.UPDATE;
-            case DELETE:
-                return OperateTypeEnum.DELETE;
-            default:
-                return OperateTypeEnum.OTHER;
-        }
-    }
-
-    // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========
-
-    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
-        if (CollUtil.isNotEmpty(map)) {
-            return null;
-        }
-        if (sanitizeKeys != null) {
-            MapUtil.removeAny(map, sanitizeKeys);
-        }
-        MapUtil.removeAny(map, SANITIZE_KEYS);
-        return JsonUtils.toJsonString(map);
-    }
-
-    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
-        if (StrUtil.isEmpty(jsonString)) {
-            return null;
-        }
-        try {
-            JsonNode rootNode = JsonUtils.parseTree(jsonString);
-            sanitizeJson(rootNode, sanitizeKeys);
-            return JsonUtils.toJsonString(rootNode);
-        } catch (Exception e) {
-            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
-            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
-            return jsonString;
-        }
-    }
-
-    private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
-        if (commonResult == null) {
-            return null;
-        }
-        String jsonString = toJsonString(commonResult);
-        try {
-            JsonNode rootNode = JsonUtils.parseTree(jsonString);
-            sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
-            return JsonUtils.toJsonString(rootNode);
-        } catch (Exception e) {
-            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
-            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
-            return jsonString;
-        }
-    }
-
-    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
-        // 情况一:数组,遍历处理
-        if (node.isArray()) {
-            for (JsonNode childNode : node) {
-                sanitizeJson(childNode, sanitizeKeys);
-            }
-            return;
-        }
-        // 情况二:非 Object,只是某个值,直接返回
-        if (!node.isObject()) {
-            return;
-        }
-        //  情况三:Object,遍历处理
-        Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
-        while (iterator.hasNext()) {
-            Map.Entry<String, JsonNode> entry = iterator.next();
-            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
-                    || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
-                iterator.remove();
-                continue;
-            }
-            sanitizeJson(entry.getValue(), sanitizeKeys);
-        }
-    }
-
 }
+

+ 1 - 1
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java

@@ -65,7 +65,7 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor {
     }
 
     private boolean isVersionUrl(HttpServletRequest request) {
-        return request.getRequestURL().toString().contains("/version");
+        return request.getRequestURL().toString().contains("get/version");
     }
 
 }

+ 0 - 19
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiAccessLogFrameworkService.java

@@ -1,19 +0,0 @@
-package com.citu.framework.apilog.core.service;
-
-import com.citu.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
-
-/**
- * API 访问日志 Framework Service 接口
- *
- * @author Rayson
- */
-public interface ApiAccessLogFrameworkService {
-
-    /**
-     * 创建 API 访问日志
-     *
-     * @param reqDTO API 访问日志
-     */
-    void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO);
-
-}

+ 0 - 33
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java

@@ -1,33 +0,0 @@
-package com.citu.framework.apilog.core.service;
-
-import com.citu.module.infra.api.logger.ApiAccessLogApi;
-import com.citu.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
-
-/**
- * API 访问日志 Framework Service 实现类
- *
- * 基于 {@link ApiAccessLogApi} 服务,记录访问日志
- *
- * @author Rayson
- */
-@Slf4j
-@RequiredArgsConstructor
-public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
-
-    private final ApiAccessLogApi apiAccessLogApi;
-
-    @Override
-    @Async
-    public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) {
-        try {
-            apiAccessLogApi.createApiAccessLog(reqDTO);
-        } catch (Throwable ex) {
-            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
-            log.error("[createApiAccessLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
-        }
-    }
-
-}

+ 0 - 19
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiErrorLogFrameworkService.java

@@ -1,19 +0,0 @@
-package com.citu.framework.apilog.core.service;
-
-import com.citu.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
-
-/**
- * API 错误日志 Framework Service 接口
- *
- * @author Rayson
- */
-public interface ApiErrorLogFrameworkService {
-
-    /**
-     * 创建 API 错误日志
-     *
-     * @param reqDTO API 错误日志
-     */
-    void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO);
-
-}

+ 0 - 26
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java

@@ -1,26 +0,0 @@
-package com.citu.framework.apilog.core.service;
-
-import com.citu.module.infra.api.logger.ApiErrorLogApi;
-import com.citu.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.scheduling.annotation.Async;
-
-/**
- * API 错误日志 Framework Service 实现类
- *
- * 基于 {@link ApiErrorLogApi} 服务,记录错误日志
- *
- * @author Rayson
- */
-@RequiredArgsConstructor
-public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
-
-    private final ApiErrorLogApi apiErrorLogApi;
-
-    @Override
-    @Async
-    public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) {
-        apiErrorLogApi.createApiErrorLog(reqDTO);
-    }
-
-}

+ 1 - 1
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java

@@ -17,7 +17,7 @@ public abstract class AbstractSliderDesensitizationHandler<T extends Annotation>
     public String desensitize(String origin, T annotation) {
         // 1. 判断是否禁用脱敏
         Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation));
-        if (Boolean.FALSE.equals(disable)) {
+        if (Boolean.TRUE.equals(disable)) {
             return origin;
         }
 

+ 9 - 1
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/i18n/config/CituI18nAutoConfiguration.java

@@ -12,6 +12,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 import org.springframework.web.servlet.i18n.SessionLocaleResolver;
 
+import javax.annotation.Resource;
 import java.util.Locale;
 
 /**
@@ -25,7 +26,11 @@ import java.util.Locale;
 // 设置为 false 时,禁用
 public class CituI18nAutoConfiguration implements WebMvcConfigurer {
 
+    @Resource
+    private I18nProperties i18nProperties;
+
     @Bean
+    @ConditionalOnProperty(value = "citu.i18n.enable", havingValue = "true")
     public LocaleResolver localeResolver() {
         SessionLocaleResolver localeResolver = new SessionLocaleResolver();
         localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
@@ -34,6 +39,7 @@ public class CituI18nAutoConfiguration implements WebMvcConfigurer {
     }
 
     @Bean
+    @ConditionalOnProperty(value = "citu.i18n.enable", havingValue = "true")
     public Validator validator(MessageSource messageSource) {
         LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
         validator.setValidationMessageSource(messageSource);
@@ -42,6 +48,8 @@ public class CituI18nAutoConfiguration implements WebMvcConfigurer {
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        registry.addInterceptor(new LocaleInterceptor());
+        if (i18nProperties.isEnable()) {
+            registry.addInterceptor(new LocaleInterceptor());
+        }
     }
 }

+ 3 - 4
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/jackson/config/CituJacksonAutoConfiguration.java

@@ -2,12 +2,11 @@ package com.citu.framework.jackson.config;
 
 import cn.hutool.core.collection.CollUtil;
 import com.citu.framework.common.util.json.JsonUtils;
-import com.citu.framework.jackson.core.databind.NumberSerializer;
-import com.citu.framework.jackson.core.databind.TimestampLocalDateTimeDeserializer;
-import com.citu.framework.jackson.core.databind.TimestampLocalDateTimeSerializer;
+import com.citu.framework.common.util.json.databind.NumberSerializer;
+import com.citu.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
+import com.citu.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
 import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
 import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;

+ 1 - 1
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/swagger/config/CituSwaggerAutoConfiguration.java

@@ -118,7 +118,7 @@ public class CituSwaggerAutoConfiguration {
     public static GroupedOpenApi buildGroupedOpenApi(String group, String path) {
         return GroupedOpenApi.builder()
                 .group(group)
-                .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**", "/app-admin-api/" + path + "/**")
+                .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**")
                 .addOperationCustomizer((operation, handlerMethod) -> operation
                         .addParametersItem(buildTenantHeaderParameter())
                         .addParametersItem(buildSecurityHeaderParameter())

+ 3 - 3
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/web/config/CituWebAutoConfiguration.java

@@ -1,6 +1,5 @@
 package com.citu.framework.web.config;
 
-import com.citu.framework.apilog.core.service.ApiErrorLogFrameworkService;
 import com.citu.framework.common.enums.WebFilterOrderEnum;
 import com.citu.framework.i18n.config.I18nProperties;
 import com.citu.framework.web.core.filter.CacheRequestBodyFilter;
@@ -8,6 +7,7 @@ import com.citu.framework.web.core.filter.DemoFilter;
 import com.citu.framework.web.core.handler.GlobalExceptionHandler;
 import com.citu.framework.web.core.handler.GlobalResponseBodyHandler;
 import com.citu.framework.web.core.util.WebFrameworkUtils;
+import com.citu.module.infra.api.logger.ApiErrorLogApi;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -73,8 +73,8 @@ public class CituWebAutoConfiguration implements WebMvcConfigurer {
     }
 
     @Bean
-    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) {
-        return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService, i18nProperties.isEnable(), messageSource);
+    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogApi apiErrorLogApi) {
+        return new GlobalExceptionHandler(applicationName, apiErrorLogApi, i18nProperties.isEnable(), messageSource);
     }
 
     @Bean

+ 49 - 17
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/web/core/handler/GlobalExceptionHandler.java

@@ -4,7 +4,6 @@ import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
-import com.citu.framework.apilog.core.service.ApiErrorLogFrameworkService;
 import com.citu.framework.common.exception.ErrorCode;
 import com.citu.framework.common.exception.ServiceException;
 import com.citu.framework.common.pojo.CommonResult;
@@ -14,12 +13,15 @@ import com.citu.framework.common.util.monitor.TracerUtils;
 import com.citu.framework.common.util.servlet.ServletUtils;
 import com.citu.framework.common.util.string.StrUtils;
 import com.citu.framework.web.core.util.WebFrameworkUtils;
+import com.citu.module.infra.api.logger.ApiErrorLogApi;
 import com.citu.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
+import com.fasterxml.jackson.databind.exc.InvalidFormatException;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.InvalidPropertyException;
 import org.springframework.context.MessageSource;
 import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.util.Assert;
 import org.springframework.validation.BindException;
@@ -61,7 +63,7 @@ public class GlobalExceptionHandler {
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     private final String applicationName;
 
-    private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
+    private final ApiErrorLogApi apiErrorLogApi;
 
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     private final boolean enableI18n;
@@ -202,6 +204,24 @@ public class GlobalExceptionHandler {
         return CommonResult.error(BAD_REQUEST.getCode(), String.format("%s", fieldError.getDefaultMessage()));
     }
 
+    /**
+     * 处理 SpringMVC 请求参数类型错误
+     * <p>
+     * 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String
+     */
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
+        log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
+        if (ex.getCause() instanceof InvalidFormatException) {
+            InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause();
+//            return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
+            return CommonResult.error(BAD_REQUEST.getCode(), String.format("%s", invalidFormatException.getValue()));
+        } else {
+            return defaultExceptionHandler(ServletUtils.getRequest(), ex);
+        }
+    }
+
+
     /**
      * 处理 Validator 校验不通过产生的异常
      */
@@ -318,7 +338,7 @@ public class GlobalExceptionHandler {
             // 初始化 errorLog
             buildExceptionLog(errorLog, req, e);
             // 执行插入 errorLog
-            apiErrorLogFrameworkService.createApiErrorLog(errorLog);
+            apiErrorLogApi.createApiErrorLogAsync(errorLog);
         } catch (Throwable th) {
             log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
         }
@@ -367,45 +387,57 @@ public class GlobalExceptionHandler {
         }
         // 1. 数据报表
         if (message.contains("report_")) {
-            log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
+            log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[报表模块 yudao-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]");
+                    "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
         }
         // 2. 工作流
         if (message.contains("bpm_")) {
-            log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
+            log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]");
+                    "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
         }
         // 3. 微信公众号
         if (message.contains("mp_")) {
-            log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
+            log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]");
+                    "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
         }
         // 4. 商城系统
         if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
-            log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
+            log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]");
+                    "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
         }
         // 5. ERP 系统
         if (message.contains("erp_")) {
-            log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
+            log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://doc.iocoder.cn/erp/build/ 开启]");
+                    "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
         }
         // 6. CRM 系统
         if (message.contains("crm_")) {
-            log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
+            log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://doc.iocoder.cn/crm/build/ 开启]");
+                    "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
         }
         // 7. 支付平台
         if (message.contains("pay_")) {
-            log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
+            log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
+        }
+        // 8. AI 大模型
+        if (message.contains("ai_")) {
+            log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
+            return CommonResult.error(NOT_IMPLEMENTED.getCode(),
+                    "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
+        }
+        // 9. IOT 物联网
+        if (message.contains("iot_")) {
+            log.error("[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
-                    "[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
+                    "[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
         }
         return null;
     }

+ 11 - 5
citu-framework/citu-spring-boot-starter-websocket/src/main/java/com/citu/framework/websocket/config/CituWebSocketAutoConfiguration.java

@@ -5,6 +5,7 @@ import com.citu.framework.mq.redis.core.RedisMQTemplate;
 import com.citu.framework.websocket.core.handler.JsonWebSocketMessageHandler;
 import com.citu.framework.websocket.core.listener.WebSocketMessageListener;
 import com.citu.framework.websocket.core.security.LoginUserHandshakeInterceptor;
+import com.citu.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer;
 import com.citu.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
 import com.citu.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
 import com.citu.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
@@ -76,10 +77,15 @@ public class CituWebSocketAutoConfiguration {
         return new WebSocketSessionManagerImpl();
     }
 
+    @Bean
+    public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
+        return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
+    }
+
     // ==================== Sender 相关 ====================
 
     @Configuration
-    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
+    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "local")
     public class LocalWebSocketMessageSenderConfiguration {
 
         @Bean
@@ -90,7 +96,7 @@ public class CituWebSocketAutoConfiguration {
     }
 
     @Configuration
-    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
+    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "redis")
     public class RedisWebSocketMessageSenderConfiguration {
 
         @Bean
@@ -108,7 +114,7 @@ public class CituWebSocketAutoConfiguration {
     }
 
     @Configuration
-    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
+    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "rocketmq")
     public class RocketMQWebSocketMessageSenderConfiguration {
 
         @Bean
@@ -127,7 +133,7 @@ public class CituWebSocketAutoConfiguration {
     }
 
     @Configuration
-    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
+    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "rabbitmq")
     public class RabbitMQWebSocketMessageSenderConfiguration {
 
         @Bean
@@ -156,7 +162,7 @@ public class CituWebSocketAutoConfiguration {
     }
 
     @Configuration
-    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
+    @ConditionalOnProperty(prefix = "citu.websocket", name = "sender-type", havingValue = "kafka")
     public class KafkaWebSocketMessageSenderConfiguration {
 
         @Bean

+ 3 - 2
citu-framework/citu-spring-boot-starter-websocket/src/main/java/com/citu/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java

@@ -4,6 +4,7 @@ import com.citu.framework.security.config.AuthorizeRequestsCustomizer;
 import com.citu.framework.websocket.config.WebSocketProperties;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
 
 /**
@@ -17,8 +18,8 @@ public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCusto
     private final WebSocketProperties webSocketProperties;
 
     @Override
-    public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
-        registry.antMatchers(webSocketProperties.getPath()).permitAll();
+    public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
+        registry.requestMatchers(webSocketProperties.getPath()).permitAll();
     }
 
 }

+ 4 - 2
citu-framework/citu-spring-boot-starter-websocket/src/main/java/com/citu/framework/websocket/core/sender/AbstractWebSocketMessageSender.java

@@ -64,8 +64,10 @@ public abstract class AbstractWebSocketMessageSender implements WebSocketMessage
             sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
         }
         if (CollUtil.isEmpty(sessions)) {
-            log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
-                    sessionId, userType, userId, messageType, messageContent);
+            if (log.isDebugEnabled()) {
+                log.debug("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
+                        sessionId, userType, userId, messageType, messageContent);
+            }
         }
         // 2. 执行发送
         doSend(sessions, messageType, messageContent);

+ 1 - 4
citu-gateway/Dockerfile

@@ -2,11 +2,8 @@
 ## 感谢复旦核博士的建议!灰子哥,牛皮!
 FROM adoptopenjdk/openjdk11
 
-## 创建目录,并使用它作为工作目录
-RUN mkdir -p /citu-gateway
-WORKDIR /citu-gateway
 ## 将后端项目的 Jar 文件,复制到镜像中
-COPY ./target/citu-gateway.jar app.jar
+ADD target/citu-gateway.jar app.jar
 
 ## 设置 TZ 时区
 ## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖

+ 0 - 1
citu-gateway/pom.xml

@@ -22,7 +22,6 @@
             <artifactId>spring-cloud-starter-bootstrap</artifactId>
         </dependency>
 
-        <!-- 业务组件 -->
         <dependency>
             <groupId>com.citu</groupId>
             <artifactId>citu-module-system-api</artifactId>

+ 5 - 1
citu-gateway/src/main/java/com/citu/gateway/filter/security/LoginUser.java

@@ -2,6 +2,7 @@ package com.citu.gateway.filter.security;
 
 import lombok.Data;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 
@@ -35,5 +36,8 @@ public class LoginUser {
      * 授权范围
      */
     private List<String> scopes;
-
+    /**
+     * 过期时间
+     */
+    private LocalDateTime expiresTime;
 }

+ 9 - 5
citu-gateway/src/main/java/com/citu/gateway/filter/security/TokenAuthenticationFilter.java

@@ -3,6 +3,7 @@ package com.citu.gateway.filter.security;
 import cn.hutool.core.util.StrUtil;
 import com.citu.framework.common.core.KeyValue;
 import com.citu.framework.common.pojo.CommonResult;
+import com.citu.framework.common.util.date.LocalDateTimeUtils;
 import com.citu.framework.common.util.json.JsonUtils;
 import com.citu.gateway.util.SecurityFrameworkUtils;
 import com.citu.gateway.util.WebFrameworkUtils;
@@ -41,11 +42,12 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
      * CommonResult<OAuth2AccessTokenCheckRespDTO> 对应的 TypeReference 结果,用于解析 checkToken 的结果
      */
     private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE
-            = new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};
+            = new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {
+    };
 
     /**
      * 空的 LoginUser 的结果
-     *
+     * <p>
      * 用于解决如下问题:
      * 1. {@link #getLoginUser(ServerWebExchange, String)} 返回 Mono.empty() 时,会导致后续的 flatMap 无法进行处理的问题。
      * 2. {@link #buildUser(String)} 时,如果 Token 已经过期,返回 LOGIN_USER_EMPTY 对象,避免缓存无法刷新
@@ -56,7 +58,7 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
 
     /**
      * 登录用户的本地缓存
-     *
+     * <p>
      * key1:多租户的编号
      * key2:访问令牌
      */
@@ -94,7 +96,8 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
         // 重要说明:defaultIfEmpty 作用,保证 Mono.empty() 情况,可以继续执行 `flatMap 的 chain.filter(exchange)` 逻辑,避免返回给前端空的 Response!!
         return getLoginUser(exchange, token).defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> {
             // 1. 无用户,直接 filter 继续请求
-            if (user == LOGIN_USER_EMPTY) {
+            if (user == LOGIN_USER_EMPTY || // 下面 expiresTime 的判断,为了解决 token 实际已经过期的情况
+                    user.getExpiresTime() == null || LocalDateTimeUtils.beforeNow(user.getExpiresTime())) {
                 return chain.filter(exchange);
             }
 
@@ -153,7 +156,8 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
         OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData();
         return new LoginUser().setId(tokenInfo.getUserId()).setUserType(tokenInfo.getUserType())
                 .setInfo(tokenInfo.getUserInfo()) // 额外的用户信息
-                .setTenantId(tokenInfo.getTenantId()).setScopes(tokenInfo.getScopes());
+                .setTenantId(tokenInfo.getTenantId()).setScopes(tokenInfo.getScopes())
+                .setExpiresTime(tokenInfo.getExpiresTime());
     }
 
     @Override

+ 51 - 0
citu-gateway/src/main/java/com/citu/gateway/jackson/JacksonAutoConfiguration.java

@@ -0,0 +1,51 @@
+package com.citu.gateway.jackson;
+
+import cn.hutool.core.collection.CollUtil;
+import com.citu.framework.common.util.json.JsonUtils;
+import com.citu.framework.common.util.json.databind.NumberSerializer;
+import com.citu.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
+import com.citu.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+@Configuration
+@Slf4j
+public class JacksonAutoConfiguration {
+
+    @Bean
+    public JsonUtils jsonUtils(List<ObjectMapper> objectMappers) {
+        // 1.1 创建 SimpleModule 对象
+        SimpleModule simpleModule = new SimpleModule();
+        simpleModule
+                // 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型
+                .addSerializer(Long.class, NumberSerializer.INSTANCE)
+                .addSerializer(Long.TYPE, NumberSerializer.INSTANCE)
+                .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE)
+                .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
+                .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE)
+                .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
+                // 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳
+                .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
+                .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
+        // 1.2 注册到 objectMapper
+        objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule));
+
+        // 2. 设置 objectMapper 到 JsonUtils
+        JsonUtils.init(CollUtil.getFirst(objectMappers));
+        log.info("[init][初始化 JsonUtils 成功]");
+        return new JsonUtils();
+    }
+
+}

+ 14 - 0
citu-gateway/src/main/resources/application-dev.yaml

@@ -0,0 +1,14 @@
+--- #################### 注册中心 + 配置中心相关配置 ####################
+
+spring:
+  cloud:
+    nacos:
+      server-addr: 127.0.0.1:8848 # Nacos 服务器地址
+      username: # Nacos 账号
+      password: # Nacos 密码
+      discovery: # 【配置中心】配置项
+        namespace: dev # 命名空间。这里使用 dev 开发环境
+        group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
+      config: # 【注册中心】配置项
+        namespace: dev # 命名空间。这里使用 dev 开发环境
+        group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP

+ 4 - 2
citu-gateway/src/main/resources/bootstrap-local.yaml

@@ -6,6 +6,8 @@ spring:
       server-addr: ${nacosHost:127.0.0.1:8848}
       discovery:
         namespace: dev # 命名空间。这里使用 dev 开发环境
+        metadata:
+          version: 1.0.0 # 服务实例的版本号,可用于灰度发布
 
 --- #################### 配置中心相关配置 ####################
 
@@ -15,7 +17,7 @@ spring:
       # Nacos Config 配置项,对应 NacosConfigProperties 配置属性类
       config:
         server-addr: ${nacosHost:127.0.0.1:8848} # Nacos 服务器地址
-        namespace: dev # 命名空间。这里使用 dev 开发环境
+        namespace: dev # 命名空间 dev 的ID,不能直接使用 dev 名称创建命名空间的时候需要指定ID为 dev,这里使用 dev 开发环境
         group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
-        name: # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name
+        name: ${spring.application.name} # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name
         file-extension: yaml # 使用的 Nacos 配置集的 dataId 的文件拓展名,同时也是 Nacos 配置集的配置格式,默认为 properties

+ 2 - 2
citu-gateway/src/main/resources/logback-spring.xml

@@ -29,13 +29,13 @@
             <!-- 滚动后的日志文件名 -->
             <fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
             <!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
-            <cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
+            <cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-true}</cleanHistoryOnStart>
             <!-- 日志文件,到达多少容量,进行滚动 -->
             <maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-200MB}</maxFileSize>
             <!-- 日志文件的总大小,0 表示不限制 -->
             <totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
             <!-- 日志文件的保留天数 -->
-            <maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}</maxHistory>
+            <maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-3}</maxHistory>
         </rollingPolicy>
     </appender>
     <!-- 异步写入日志,提升性能 -->

+ 1 - 1
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/ApiConstants.java

@@ -5,7 +5,7 @@ import com.citu.framework.common.enums.RpcConstants;
 /**
  * API 相关的枚举
  *
- * @author Rayson
+ * @author 芋道源码
  */
 public class ApiConstants {
 

+ 1 - 4
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/DictTypeConstants.java

@@ -3,11 +3,8 @@ package com.citu.module.bpm.enums;
 /**
  * BPM 字典类型的枚举类
  *
- * @author Rayson
+ * @author 芋道源码
  */
 public interface DictTypeConstants {
 
-    String TASK_ASSIGN_RULE_TYPE = "bpm_task_assign_rule_type"; // 任务分配规则类型
-    String TASK_ASSIGN_SCRIPT = "bpm_task_assign_script"; // 任务分配自定义脚本
-
 }

+ 5 - 3
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/ErrorCodeConstants.java

@@ -23,6 +23,7 @@ public interface ErrorCodeConstants {
             "原因:用户任务({})未配置审批人,请点击【流程设计】按钮,选择该它的【任务(审批人)】进行配置");
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败,原因:BPMN 流程图中,没有开始事件");
     ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在");
+    ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员");
 
     // ========== 流程定义 1-009-003-000 ==========
     ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
@@ -34,15 +35,16 @@ public interface ErrorCodeConstants {
     ErrorCode PROCESS_INSTANCE_NOT_EXISTS = new ErrorCode(1_009_004_000, "流程实例不存在");
     ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS = new ErrorCode(1_009_004_001, "流程取消失败,流程不处于运行中");
     ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1_009_004_002, "流程取消失败,该流程不是你发起的");
-    ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "审批任务({})的审批人未配置");
-    ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "审批任务({})的审批人({})不存在");
+    ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "任务({})的候选人未配置");
+    ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在");
+    ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
 
     // ========== 流程任务 1-009-005-000 ==========
     ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");
     ErrorCode TASK_NOT_EXISTS = new ErrorCode(1_009_005_002, "流程任务不存在");
     ErrorCode TASK_IS_PENDING = new ErrorCode(1_009_005_003, "当前任务处于挂起状态,不能操作");
     ErrorCode TASK_TARGET_NODE_NOT_EXISTS = new ErrorCode(1_009_005_004, " 目标节点不存在");
-    ErrorCode TASK_RETURN_FAIL_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_006, "退任务失败,目标节点是在并行网关上或非同一路线上,不可跳转");
+    ErrorCode TASK_RETURN_FAIL_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_006, "退任务失败,目标节点是在并行网关上或非同一路线上,不可跳转");
     ErrorCode TASK_DELEGATE_FAIL_USER_REPEAT = new ErrorCode(1_009_005_007, "任务委派失败,委派人和当前审批人为同一人");
     ErrorCode TASK_DELEGATE_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_008, "任务委派失败,被委派人不存在");
     ErrorCode TASK_SIGN_CREATE_USER_NOT_EXIST = new ErrorCode(1_009_005_009, "任务加签:选择的用户不存在");

+ 25 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmBoundaryEventType.java

@@ -0,0 +1,25 @@
+package com.citu.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * BPM 边界事件 (boundary event) 自定义类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmBoundaryEventType {
+
+    USER_TASK_TIMEOUT(1,"用户任务超时");
+
+    private final Integer type;
+    private final String name;
+
+    public static BpmBoundaryEventType typeOf(Integer type) {
+        return ArrayUtil.firstMatch(eventType -> eventType.getType().equals(type), values());
+    }
+
+}

+ 33 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmFieldPermissionEnum.java

@@ -0,0 +1,33 @@
+package com.citu.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * BPM 表单权限的枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmFieldPermissionEnum {
+
+    READ(1, "只读"),
+    WRITE(2, "可编辑"),
+    NONE(3, "隐藏");
+
+    /**
+     * 权限
+     */
+    private final Integer permission;
+    /**
+     * 名字
+     */
+    private final String name;
+
+    public static BpmFieldPermissionEnum valueOf(Integer permission) {
+        return ArrayUtil.firstMatch(item -> item.getPermission().equals(permission), values());
+    }
+
+}

+ 1 - 1
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmModelFormTypeEnum.java

@@ -9,7 +9,7 @@ import java.util.Arrays;
 /**
  * BPM 模型的表单类型的枚举
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @Getter
 @AllArgsConstructor

+ 31 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmModelTypeEnum.java

@@ -0,0 +1,31 @@
+package com.citu.module.bpm.enums.definition;
+
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 模型的类型的枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmModelTypeEnum implements IntArrayValuable {
+
+    BPMN(10, "BPMN 设计器"), // https://bpmn.io/toolkit/bpmn-js/
+    SIMPLE(20, "SIMPLE 设计器"); // 参考钉钉、飞书工作流的设计器
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmModelTypeEnum::getType).toArray();
+
+    private final Integer type;
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 1 - 1
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmProcessListenerType.java

@@ -6,7 +6,7 @@ import lombok.Getter;
 /**
  * BPM 流程监听器的类型
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @Getter
 @AllArgsConstructor

+ 1 - 1
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmProcessListenerValueType.java

@@ -6,7 +6,7 @@ import lombok.Getter;
 /**
  * BPM 流程监听器的值类型
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @Getter
 @AllArgsConstructor

+ 36 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmSimpleModeConditionType.java

@@ -0,0 +1,36 @@
+package com.citu.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 仿钉钉的流程器设计器条件节点的条件类型
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmSimpleModeConditionType implements IntArrayValuable {
+
+    EXPRESSION(1, "条件表达式"),
+    RULE(2, "条件规则");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModeConditionType::getType).toArray();
+
+    private final Integer type;
+
+    private final String name;
+
+    public static BpmSimpleModeConditionType valueOf(Integer type) {
+        return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 62 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmSimpleModelNodeType.java

@@ -0,0 +1,62 @@
+package com.citu.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * 仿钉钉的流程器设计器的模型节点类型
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmSimpleModelNodeType implements IntArrayValuable {
+
+    // 0 ~ 1 开始和结束
+    START_NODE(0, "开始", "startEvent"),
+    END_NODE(1, "结束", "endEvent"),
+
+    // 10 ~ 49 各种节点
+    START_USER_NODE(10, "发起人", "userTask"), // 发起人节点。前端的开始节点,Id 固定
+    APPROVE_NODE(11, "审批人", "userTask"),
+    COPY_NODE(12, "抄送人", "serviceTask"),
+
+    // 50 ~ 条件分支
+    CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式
+    CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"),
+    PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"),
+    INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"),
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModelNodeType::getType).toArray();
+
+    private final Integer type;
+    private final String name;
+    private final String bpmnType;
+
+    /**
+     * 判断是否为分支节点
+     *
+     * @param type 节点类型
+     */
+    public static boolean isBranchNode(Integer type) {
+        return Objects.equals(CONDITION_BRANCH_NODE.getType(), type)
+                || Objects.equals(PARALLEL_BRANCH_NODE.getType(), type)
+                || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type);
+    }
+
+    public static BpmSimpleModelNodeType valueOf(Integer type) {
+        return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 47 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java

@@ -0,0 +1,47 @@
+package com.citu.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 多人审批方式的枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskApproveMethodEnum implements IntArrayValuable {
+
+    RANDOM(1, "随机挑选一人审批", null),
+    RATIO(2, "多人会签(按通过比例)", "${ nrOfCompletedInstances/nrOfInstances >= %s}"), // 会签(按通过比例)
+    ANY(3, "多人或签(一人通过或拒绝)", "${ nrOfCompletedInstances > 0 }"), // 或签(通过只需一人,拒绝只需一人)
+    SEQUENTIAL(4, "依次审批", "${ nrOfCompletedInstances >= nrOfInstances }"); // 依次审批
+
+    /**
+     * 审批方式
+     */
+    private final Integer method;
+    /**
+     * 名字
+     */
+    private final String name;
+    /**
+     * 完成表达式
+     */
+    private final String completionCondition;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveMethodEnum::getMethod).toArray();
+
+    public static BpmUserTaskApproveMethodEnum valueOf(Integer method) {
+        return ArrayUtil.firstMatch(item -> item.getMethod().equals(method), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 31 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java

@@ -0,0 +1,31 @@
+package com.citu.module.bpm.enums.definition;
+
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 用户任务的审批类型枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskApproveTypeEnum implements IntArrayValuable {
+
+    USER(1), // 人工审批
+    AUTO_APPROVE(2), // 自动通过
+    AUTO_REJECT(3); // 自动拒绝
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveTypeEnum::getType).toArray();
+
+    private final Integer type;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 33 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java

@@ -0,0 +1,33 @@
+package com.citu.module.bpm.enums.definition;
+
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * BPM 用户任务的审批人为空时,处理类型枚举
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Getter
+public enum BpmUserTaskAssignEmptyHandlerTypeEnum implements IntArrayValuable {
+
+    APPROVE(1), // 自动通过
+    REJECT(2), // 自动拒绝
+    ASSIGN_USER(3), // 指定人员审批
+    ASSIGN_ADMIN(4), // 转交给流程管理员
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskAssignEmptyHandlerTypeEnum::getType).toArray();
+
+    private final Integer type;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 31 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java

@@ -0,0 +1,31 @@
+package com.citu.module.bpm.enums.definition;
+
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * BPM 用户任务的审批人与发起人相同时,处理类型枚举
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Getter
+public enum BpmUserTaskAssignStartUserHandlerTypeEnum implements IntArrayValuable {
+
+    START_USER_AUDIT(1), // 由发起人对自己审批
+    SKIP(2), // 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过
+    TRANSFER_DEPT_LEADER(3); // 转交给部门负责人审批【参考飞书】:若部门负责人为空,则自动通过
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskAssignStartUserHandlerTypeEnum::getType).toArray();
+
+    private final Integer type;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 35 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java

@@ -0,0 +1,35 @@
+package com.citu.module.bpm.enums.definition;
+
+import cn.hutool.core.util.ArrayUtil;
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * BPM 用户任务拒绝处理类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskRejectHandlerType implements IntArrayValuable {
+
+    FINISH_PROCESS_INSTANCE(1, "终止流程"),
+    RETURN_USER_TASK(2, "驳回到指定任务节点");
+
+    private final Integer type;
+    private final String name;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskRejectHandlerType::getType).toArray();
+
+    public static BpmUserTaskRejectHandlerType typeOf(Integer type) {
+        return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

+ 32 - 0
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java

@@ -0,0 +1,32 @@
+package com.citu.module.bpm.enums.definition;
+
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 用户任务超时处理类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmUserTaskTimeoutHandlerTypeEnum implements IntArrayValuable {
+
+    REMINDER(1,"自动提醒"),
+    APPROVE(2, "自动同意"),
+    REJECT(3, "自动拒绝");
+
+    private final Integer type;
+    private final String name;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskTimeoutHandlerTypeEnum::getType).toArray();
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 3 - 2
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/message/BpmMessageEnum.java

@@ -6,7 +6,7 @@ import lombok.Getter;
 /**
  * Bpm 消息的枚举
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @AllArgsConstructor
 @Getter
@@ -14,7 +14,8 @@ public enum BpmMessageEnum {
 
     PROCESS_INSTANCE_APPROVE("bpm_process_instance_approve"), // 流程任务被审批通过时,发送给申请人
     PROCESS_INSTANCE_REJECT("bpm_process_instance_reject"), // 流程任务被审批不通过时,发送给申请人
-    TASK_ASSIGNED("bpm_task_assigned"); // 任务被分配时,发送给审批人
+    TASK_ASSIGNED("bpm_task_assigned"), // 任务被分配时,发送给审批人
+    TASK_TIMEOUT("bpm_task_timeout"); // 任务审批超时时,发送给审批人
 
     /**
      * 短信模板的标识

+ 13 - 2
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java

@@ -1,6 +1,7 @@
 package com.citu.module.bpm.enums.task;
 
 import com.citu.framework.common.core.IntArrayValuable;
+import com.citu.framework.common.util.object.ObjectUtils;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
@@ -9,12 +10,13 @@ import java.util.Arrays;
 /**
  * 流程实例 ProcessInstance 的状态
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @Getter
 @AllArgsConstructor
 public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
 
+    NOT_START(-1, "未开始"),
     RUNNING(1, "审批中"),
     APPROVE(2, "审批通过"),
     REJECT(3, "审批不通过"),
@@ -33,7 +35,16 @@ public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
 
     @Override
     public int[] array() {
-        return new int[0];
+        return ARRAYS;
+    }
+
+    public static boolean isRejectStatus(Integer status) {
+        return REJECT.getStatus().equals(status);
+    }
+
+    public static boolean isProcessEndStatus(Integer status) {
+        return ObjectUtils.equalsAny(status,
+                APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus());
     }
 
 }

+ 13 - 9
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/task/BpmDeleteReasonEnum.java → citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/task/BpmReasonEnum.java

@@ -5,13 +5,13 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 
 /**
- * 流程实例/任务的删除原因枚举
+ * 流程实例/任务的的处理原因枚举
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @Getter
 @AllArgsConstructor
-public enum BpmDeleteReasonEnum {
+public enum BpmReasonEnum {
 
     // ========== 流程实例的独有原因 ==========
 
@@ -22,6 +22,16 @@ public enum BpmDeleteReasonEnum {
     // ========== 流程任务的独有原因 ==========
 
     CANCEL_BY_SYSTEM("系统自动取消"), // 场景:非常多,比如说:1)多任务审批已经满足条件,无需审批该任务;2)流程实例被取消,无需审批该任务;等等
+    TIMEOUT_APPROVE("审批超时,系统自动通过"),
+    TIMEOUT_REJECT("审批超时,系统自动不通过"),
+    ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"),
+    ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"),
+    ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"),
+    ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"),
+    ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"),
+    ASSIGN_EMPTY_REJECT("审批人为空,自动不通过"),
+    APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"),
+    APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"),
     ;
 
     private final String reason;
@@ -36,10 +46,4 @@ public enum BpmDeleteReasonEnum {
         return StrUtil.format(reason, args);
     }
 
-    // ========== 逻辑 ==========
-
-    public static boolean isRejectReason(String reason) {
-        return StrUtil.startWith(reason, "审批不通过任务,原因:");
-    }
-
 }

+ 10 - 1
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/enums/task/BpmTaskStatusEnum.java

@@ -1,5 +1,6 @@
 package com.citu.module.bpm.enums.task;
 
+import cn.hutool.core.util.ObjUtil;
 import com.citu.framework.common.util.object.ObjectUtils;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
@@ -13,13 +14,13 @@ import lombok.Getter;
 @AllArgsConstructor
 public enum BpmTaskStatusEnum {
 
+    NOT_START(-1, "未开始"),
     RUNNING(1, "审批中"),
     APPROVE(2, "审批通过"),
     REJECT(3, "审批不通过"),
     CANCEL(4, "已取消"),
 
     RETURN(5, "已退回"),
-    DELEGATE(6, "委派中"),
 
     /**
      * 使用场景:
@@ -44,6 +45,10 @@ public enum BpmTaskStatusEnum {
      */
     private final String name;
 
+    public static boolean isRejectStatus(Integer status) {
+        return REJECT.getStatus().equals(status);
+    }
+
     /**
      * 判断该状态是否已经处于 End 最终状态
      * <p>
@@ -58,4 +63,8 @@ public enum BpmTaskStatusEnum {
                 RETURN.getStatus(), APPROVING.getStatus());
     }
 
+    public static boolean isCancelStatus(Integer status) {
+        return ObjUtil.equal(status, CANCEL.getStatus());
+    }
+
 }

+ 1 - 1
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/event/BpmProcessInstanceStatusEvent.java

@@ -8,7 +8,7 @@ import javax.validation.constraints.NotNull;
 /**
  * 流程实例的状态(结果)发生变化的 Event
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @SuppressWarnings("ALL")
 @Data

+ 1 - 1
citu-module-bpm/citu-module-bpm-api/src/main/java/com/citu/module/bpm/event/BpmProcessInstanceStatusEventListener.java

@@ -6,7 +6,7 @@ import org.springframework.context.ApplicationListener;
 /**
  * {@link BpmProcessInstanceStatusEvent} 的监听器
  *
- * @author Rayson
+ * @author 芋道源码
  */
 public abstract class BpmProcessInstanceStatusEventListener
         implements ApplicationListener<BpmProcessInstanceStatusEvent> {

+ 1 - 4
citu-module-bpm/citu-module-bpm-biz/Dockerfile

@@ -2,11 +2,8 @@
 ## 感谢复旦核博士的建议!灰子哥,牛皮!
 FROM adoptopenjdk/openjdk11
 
-## 创建目录,并使用它作为工作目录
-RUN mkdir -p /citu-module-bpm-biz
-WORKDIR /citu-module-bpm-biz
 ## 将后端项目的 Jar 文件,复制到镜像中
-COPY ./target/citu-module-bpm-biz.jar app.jar
+ADD target/citu-module-bpm-biz.jar app.jar
 
 ## 设置 TZ 时区
 ## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖

+ 1 - 1
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/BpmServerApplication.java

@@ -10,7 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
  * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
  * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
  *
- * @author Rayson
+ * @author 芋道源码
  */
 @SpringBootApplication
 public class BpmServerApplication {

+ 1 - 1
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/api/task/BpmProcessInstanceApiImpl.java

@@ -14,7 +14,7 @@ import static com.citu.framework.common.pojo.CommonResult.success;
 /**
  * Flowable 流程实例 Api 实现类
  *
- * @author Rayson
+ * @author 芋道源码
  * @author jason
  */
 @RestController

+ 4 - 0
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/base/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 基础包,放一些通用的 VO 类
+ */
+package com.citu.module.bpm.controller.admin.base;

+ 22 - 0
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java

@@ -0,0 +1,22 @@
+package com.citu.module.bpm.controller.admin.base.user;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "用户精简信息 VO")
+@Data
+public class UserSimpleBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long id;
+    @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    private String nickname;
+    @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
+    private String avatar;
+
+    @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long deptId;
+    @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部")
+    private String deptName;
+
+}

+ 9 - 0
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/BpmCategoryController.java

@@ -48,6 +48,15 @@ public class BpmCategoryController {
         return success(true);
     }
 
+    @PutMapping("/update-sort-batch")
+    @Operation(summary = "批量更新流程分类的排序")
+    @Parameter(name = "ids", description = "分类编号列表", required = true, example = "1,2,3")
+    @PreAuthorize("@ss.hasPermission('bpm:category:update')")
+    public CommonResult<Boolean> updateCategorySortBatch(@RequestParam("ids") List<Long> ids) {
+        categoryService.updateCategorySortBatch(ids);
+        return success(true);
+    }
+
     @DeleteMapping("/delete")
     @Operation(summary = "删除流程分类")
     @Parameter(name = "id", description = "编号", required = true)

+ 71 - 39
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/BpmModelController.java

@@ -2,12 +2,9 @@ package com.citu.module.bpm.controller.admin.definition;
 
 import cn.hutool.core.collection.CollUtil;
 import com.citu.framework.common.pojo.CommonResult;
-import com.citu.framework.common.pojo.PageResult;
-import com.citu.framework.common.util.collection.CollectionUtils;
-import com.citu.framework.common.util.io.IoUtils;
-import com.citu.framework.common.util.json.JsonUtils;
-import com.citu.framework.common.util.object.BeanUtils;
 import com.citu.module.bpm.controller.admin.definition.vo.model.*;
+import com.citu.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import com.citu.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
 import com.citu.module.bpm.convert.definition.BpmModelConvert;
 import com.citu.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import com.citu.module.bpm.dal.dataobject.definition.BpmFormDO;
@@ -15,7 +12,8 @@ import com.citu.module.bpm.service.definition.BpmCategoryService;
 import com.citu.module.bpm.service.definition.BpmFormService;
 import com.citu.module.bpm.service.definition.BpmModelService;
 import com.citu.module.bpm.service.definition.BpmProcessDefinitionService;
-import com.citu.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
+import com.citu.module.system.api.user.AdminUserApi;
+import com.citu.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -28,15 +26,15 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
-import java.io.IOException;
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import static com.citu.framework.common.pojo.CommonResult.success;
-import static com.citu.framework.common.util.collection.CollectionUtils.convertMap;
-import static com.citu.framework.common.util.collection.CollectionUtils.convertSet;
+import static com.citu.framework.common.util.collection.CollectionUtils.*;
+import static com.citu.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 流程模型")
 @RestController
@@ -53,32 +51,42 @@ public class BpmModelController {
     @Resource
     private BpmProcessDefinitionService processDefinitionService;
 
-    @GetMapping("/page")
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @GetMapping("/list")
     @Operation(summary = "获得模型分页")
-    public CommonResult<PageResult<BpmModelRespVO>> getModelPage(BpmModelPageReqVO pageVO) {
-        PageResult<Model> pageResult = modelService.getModelPage(pageVO);
-        if (CollUtil.isEmpty(pageResult.getList())) {
-            return success(PageResult.empty(pageResult.getTotal()));
+    @Parameter(name = "name", description = "模型名称", example = "芋艿")
+    public CommonResult<List<BpmModelRespVO>> getModelPage(@RequestParam(value = "name", required = false) String name) {
+        List<Model> list = modelService.getModelList(name);
+        if (CollUtil.isEmpty(list)) {
+            return success(Collections.emptyList());
         }
 
-        // 拼接数据
         // 获得 Form 表单
-        Set<Long> formIds = convertSet(pageResult.getList(), model -> {
-            BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
+        Set<Long> formIds = convertSet(list, model -> {
+            BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
             return metaInfo != null ? metaInfo.getFormId() : null;
         });
         Map<Long, BpmFormDO> formMap = formService.getFormMap(formIds);
         // 获得 Category Map
         Map<String, BpmCategoryDO> categoryMap = categoryService.getCategoryMap(
-                convertSet(pageResult.getList(), Model::getCategory));
+                convertSet(list, Model::getCategory));
         // 获得 Deployment Map
-        Set<String> deploymentIds = new HashSet<>();
-        pageResult.getList().forEach(model -> CollectionUtils.addIfNotNull(deploymentIds, model.getDeploymentId()));
-        Map<String, Deployment> deploymentMap = processDefinitionService.getDeploymentMap(deploymentIds);
+        Map<String, Deployment> deploymentMap = processDefinitionService.getDeploymentMap(
+                convertSet(list, Model::getDeploymentId));
         // 获得 ProcessDefinition Map
-        List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);
+        List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(
+                deploymentMap.keySet());
         Map<String, ProcessDefinition> processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId);
-        return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult, formMap, categoryMap, deploymentMap, processDefinitionMap));
+        // 获得 User Map
+        Set<Long> userIds = convertSetByFlatMap(list, model -> {
+            BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
+            return metaInfo != null ? metaInfo.getStartUserIds().stream() : Stream.empty();
+        });
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+        return success(BpmModelConvert.INSTANCE.buildModelList(list,
+                formMap, categoryMap, deploymentMap, processDefinitionMap, userMap));
     }
 
     @GetMapping("/get")
@@ -97,26 +105,25 @@ public class BpmModelController {
     @PostMapping("/create")
     @Operation(summary = "新建模型")
     @PreAuthorize("@ss.hasPermission('bpm:model:create')")
-    public CommonResult<String> createModel(@Valid @RequestBody BpmModelCreateReqVO createRetVO) {
-        return success(modelService.createModel(createRetVO, null));
+    public CommonResult<String> createModel(@Valid @RequestBody BpmModelSaveReqVO createRetVO) {
+        return success(modelService.createModel(createRetVO));
     }
 
+
     @PutMapping("/update")
     @Operation(summary = "修改模型")
     @PreAuthorize("@ss.hasPermission('bpm:model:update')")
-    public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelUpdateReqVO modelVO) {
-        modelService.updateModel(modelVO);
+    public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelSaveReqVO modelVO) {
+        modelService.updateModel(getLoginUserId(), modelVO);
         return success(true);
     }
 
-    @PostMapping("/import")
-    @Operation(summary = "导入模型")
-    @PreAuthorize("@ss.hasPermission('bpm:model:import')")
-    public CommonResult<String> importModel(@Valid BpmModeImportReqVO importReqVO) throws IOException {
-        BpmModelCreateReqVO createReqVO = BeanUtils.toBean(importReqVO, BpmModelCreateReqVO.class);
-        // 读取文件
-        String bpmnXml = IoUtils.readUtf8(importReqVO.getBpmnFile().getInputStream(), false);
-        return success(modelService.createModel(createReqVO, bpmnXml));
+    @PutMapping("/update-sort-batch")
+    @Operation(summary = "批量修改模型排序")
+    @Parameter(name = "ids", description = "编号数组", required = true, example = "1,2,3")
+    public CommonResult<Boolean> updateModelSortBatch(@RequestParam("ids") List<String> ids) {
+        modelService.updateModelSortBatch(getLoginUserId(), ids);
+        return success(true);
     }
 
     @PostMapping("/deploy")
@@ -124,7 +131,7 @@ public class BpmModelController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('bpm:model:deploy')")
     public CommonResult<Boolean> deployModel(@RequestParam("id") String id) {
-        modelService.deployModel(id);
+        modelService.deployModel(getLoginUserId(), id);
         return success(true);
     }
 
@@ -132,7 +139,15 @@ public class BpmModelController {
     @Operation(summary = "修改模型的状态", description = "实际更新的部署的流程定义的状态")
     @PreAuthorize("@ss.hasPermission('bpm:model:update')")
     public CommonResult<Boolean> updateModelState(@Valid @RequestBody BpmModelUpdateStateReqVO reqVO) {
-        modelService.updateModelState(reqVO.getId(), reqVO.getState());
+        modelService.updateModelState(getLoginUserId(), reqVO.getId(), reqVO.getState());
+        return success(true);
+    }
+
+    @PutMapping("/update-bpmn")
+    @Operation(summary = "修改模型的 BPMN")
+    @PreAuthorize("@ss.hasPermission('bpm:model:update')")
+    public CommonResult<Boolean> updateModelBpmn(@Valid @RequestBody BpmModeUpdateBpmnReqVO reqVO) {
+        modelService.updateModelBpmnXml(reqVO.getId(), reqVO.getBpmnXml());
         return success(true);
     }
 
@@ -141,8 +156,25 @@ public class BpmModelController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('bpm:model:delete')")
     public CommonResult<Boolean> deleteModel(@RequestParam("id") String id) {
-        modelService.deleteModel(id);
+        modelService.deleteModel(getLoginUserId(), id);
         return success(true);
     }
 
+    // ========== 仿钉钉/飞书的精简模型 =========
+
+    @GetMapping("/simple/get")
+    @Operation(summary = "获得仿钉钉流程设计模型")
+    @Parameter(name = "modelId", description = "流程模型编号", required = true, example = "a2c5eee0-eb6c-11ee-abf4-0c37967c420a")
+    public CommonResult<BpmSimpleModelNodeVO> getSimpleModel(@RequestParam("id") String modelId){
+        return success(modelService.getSimpleModel(modelId));
+    }
+
+    @PostMapping("/simple/update")
+    @Operation(summary = "保存仿钉钉流程设计模型")
+    @PreAuthorize("@ss.hasPermission('bpm:model:update')")
+    public CommonResult<Boolean> updateSimpleModel(@Valid @RequestBody BpmSimpleModelUpdateReqVO reqVO) {
+        modelService.updateSimpleModel(getLoginUserId(), reqVO);
+        return success(Boolean.TRUE);
+    }
+
 }

+ 14 - 8
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java

@@ -9,7 +9,6 @@ import com.citu.module.bpm.convert.definition.BpmProcessDefinitionConvert;
 import com.citu.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import com.citu.module.bpm.dal.dataobject.definition.BpmFormDO;
 import com.citu.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
-import com.citu.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy;
 import com.citu.module.bpm.service.definition.BpmCategoryService;
 import com.citu.module.bpm.service.definition.BpmFormService;
 import com.citu.module.bpm.service.definition.BpmProcessDefinitionService;
@@ -17,7 +16,6 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.flowable.bpmn.model.BpmnModel;
-import org.flowable.bpmn.model.UserTask;
 import org.flowable.engine.repository.Deployment;
 import org.flowable.engine.repository.ProcessDefinition;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -34,6 +32,7 @@ import java.util.Map;
 
 import static com.citu.framework.common.pojo.CommonResult.success;
 import static com.citu.framework.common.util.collection.CollectionUtils.convertSet;
+import static com.citu.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 流程定义")
 @RestController
@@ -77,17 +76,25 @@ public class BpmProcessDefinitionController {
     @GetMapping ("/list")
     @Operation(summary = "获得流程定义列表")
     @Parameter(name = "suspensionState", description = "挂起状态", required = true, example = "1") // 参见 Flowable SuspensionState 枚举
-    @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')")
     public CommonResult<List<BpmProcessDefinitionRespVO>> getProcessDefinitionList(
             @RequestParam("suspensionState") Integer suspensionState) {
+        // 1.1 获得开启的流程定义
         List<ProcessDefinition> list = processDefinitionService.getProcessDefinitionListBySuspensionState(suspensionState);
         if (CollUtil.isEmpty(list)) {
             return success(Collections.emptyList());
         }
-
-        // 获得 BpmProcessDefinitionInfoDO Map
+        // 1.2 移除不可见的流程定义
         Map<String, BpmProcessDefinitionInfoDO> processDefinitionMap = processDefinitionService.getProcessDefinitionInfoMap(
                 convertSet(list, ProcessDefinition::getId));
+        Long userId = getLoginUserId();
+        list.removeIf(processDefinition -> {
+            BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionMap.get(processDefinition.getId());
+            return processDefinitionInfo == null // 不存在
+                    || Boolean.FALSE.equals(processDefinitionInfo.getVisible()) // visible 不可见
+                    || !processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId); // 无权限发起
+        });
+
+        // 2. 拼接 VO 返回
         return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinitionList(
                 list, null, processDefinitionMap, null, null));
     }
@@ -96,7 +103,6 @@ public class BpmProcessDefinitionController {
     @Operation(summary = "获得流程定义")
     @Parameter(name = "id", description = "流程编号", required = true, example = "1024")
     @Parameter(name = "key", description = "流程定义标识", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')")
     public CommonResult<BpmProcessDefinitionRespVO> getProcessDefinition(
             @RequestParam(value = "id", required = false) String id,
             @RequestParam(value = "key", required = false) String key) {
@@ -105,10 +111,10 @@ public class BpmProcessDefinitionController {
         if (processDefinition == null) {
             return success(null);
         }
+        BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(processDefinition.getId());
         BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId());
-        List<UserTask> userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel);
         return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition(
-                processDefinition, null, null, null, null, bpmnModel, userTaskList));
+                processDefinition, null, processDefinitionInfo, null, null, bpmnModel));
     }
 
 }

+ 1 - 1
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java

@@ -16,7 +16,7 @@ public class BpmUserGroupRespVO {
     @Schema(description = "组名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
     private String name;
 
-    @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "Rayson")
+    @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
     private String description;
 
     @Schema(description = "成员编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")

+ 1 - 1
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSaveReqVO.java

@@ -17,7 +17,7 @@ public class BpmUserGroupSaveReqVO {
     @NotNull(message = "组名不能为空")
     private String name;
 
-    @Schema(description = "描述", example = "Rayson")
+    @Schema(description = "描述", example = "芋道源码")
     private String description;
 
     @Schema(description = "成员编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")

+ 0 - 17
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java

@@ -1,17 +0,0 @@
-package com.citu.module.bpm.controller.admin.definition.vo.model;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import org.springframework.web.multipart.MultipartFile;
-
-import javax.validation.constraints.NotNull;
-
-@Schema(description = "管理后台 - 流程模型的导入 Request VO 相比流程模型的新建来说,只是多了一个 bpmnFile 文件")
-@Data
-public class BpmModeImportReqVO extends BpmModelCreateReqVO {
-
-    @Schema(description = "BPMN 文件", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "BPMN 文件不能为空")
-    private MultipartFile bpmnFile;
-
-}

+ 20 - 0
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java

@@ -0,0 +1,20 @@
+package com.citu.module.bpm.controller.admin.definition.vo.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+
+@Schema(description = "管理后台 - 流程模型的更新 BPMN XML Request VO")
+@Data
+public class BpmModeUpdateBpmnReqVO {
+
+    @Schema(description = "流程编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotEmpty(message = "流程编号不能为空")
+    private String id;
+
+    @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "BPMN XML 不能为空")
+    private String bpmnXml;
+
+}

+ 65 - 0
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java

@@ -0,0 +1,65 @@
+package com.citu.module.bpm.controller.admin.definition.vo.model;
+
+import com.citu.framework.common.validation.InEnum;
+import com.citu.module.bpm.enums.definition.BpmModelFormTypeEnum;
+import com.citu.module.bpm.enums.definition.BpmModelTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * BPM 流程 MetaInfo Response DTO
+ * 主要用于 { Model#setMetaInfo(String)} 的存储
+ *
+ * 最终,它的字段和
+ * {@link com.citu.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO}
+ * 是一致的
+ *
+ * @author 芋道源码
+ */
+@Data
+public class BpmModelMetaInfoVO {
+
+    @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
+    @NotEmpty(message = "流程图标不能为空")
+    @URL(message = "流程图标格式不正确")
+    private String icon;
+
+    @Schema(description = "流程描述", example = "我是描述")
+    private String description;
+
+    @Schema(description = "流程类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @InEnum(BpmModelTypeEnum.class)
+    @NotNull(message = "流程类型不能为空")
+    private Integer type;
+
+    @Schema(description = "表单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @InEnum(BpmModelFormTypeEnum.class)
+    @NotNull(message = "表单类型不能为空")
+    private Integer formType;
+    @Schema(description = "表单编号", example = "1024")
+    private Long formId; // formType 为 NORMAL 使用,必须非空
+    @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create")
+    private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空
+    @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view")
+    private String formCustomViewPath; // 表单类型为 CUSTOM 时,必须非空
+
+    @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否可见不能为空")
+    private Boolean visible;
+
+    @Schema(description = "可发起用户编号数组", example = "[1,2,3]")
+    private List<Long> startUserIds;
+
+    @Schema(description = "可管理用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2,4,6]")
+    @NotEmpty(message = "可管理用户编号数组不能为空")
+    private List<Long> managerUserIds;
+
+    @Schema(description = "排序", example = "1")
+    private Long sort; // 创建时,后端自动生成
+
+}

+ 8 - 16
citu-module-bpm/citu-module-bpm-biz/src/main/java/com/citu/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java

@@ -1,54 +1,46 @@
 package com.citu.module.bpm.controller.admin.definition.vo.model;
 
+import com.citu.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
 import com.citu.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 @Schema(description = "管理后台 - 流程模型 Response VO")
 @Data
-public class BpmModelRespVO {
+public class BpmModelRespVO extends BpmModelMetaInfoVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private String id;
 
-    @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_citu")
+    @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yudao")
     private String key;
 
     @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
     private String name;
 
-    @Schema(description = "流程图标", example = "https://www.iocoder.cn/citu.jpg")
+    @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
     private String icon;
 
-    @Schema(description = "流程描述", example = "我是描述")
-    private String description;
-
     @Schema(description = "流程分类编码", example = "1")
     private String category;
     @Schema(description = "流程分类名字", example = "请假")
     private String categoryName;
 
-    @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
-    private Integer formType;
-
-    @Schema(description = "表单编号", example = "1024")
-    private Long formId; // 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空
     @Schema(description = "表单名字", example = "请假表单")
     private String formName;
 
-    @Schema(description = "自定义表单的提交路径", example = "/bpm/oa/leave/create")
-    private String formCustomCreatePath; // 使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空
-    @Schema(description = "自定义表单的查看路径", example = "/bpm/oa/leave/view")
-    private String formCustomViewPath; // ,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空
-
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
     @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
     private String bpmnXml;
 
+    @Schema(description = "可发起的用户数组")
+    private List<UserSimpleBaseVO> startUsers;
+
     /**
      * 最新部署的流程定义
      */

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels