소스 검색

Merge remote-tracking branch 'yudao/master-jdk17' into master-jdk17

# Conflicts:
#	yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
puhui999 6 달 전
부모
커밋
28c818e9bc
100개의 변경된 파일1073개의 추가작업 그리고 490개의 파일을 삭제
  1. 2 2
      README.md
  2. 1 1
      pom.xml
  3. 1 1
      sql/mysql/ruoyi-vue-pro.sql
  4. 29 30
      yudao-dependencies/pom.xml
  5. 14 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  6. 0 23
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
  7. 12 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
  8. 3 3
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
  9. 72 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java
  10. 3 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java
  11. 3 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java
  12. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
  13. 11 7
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java
  14. 3 2
      yudao-module-infra/yudao-module-infra-biz/pom.xml
  15. 3 4
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java
  16. 12 4
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
  17. 10 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java
  18. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java
  19. 13 5
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java
  20. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java
  21. 1 2
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java
  22. 2 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java
  23. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java
  24. 5 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
  25. 9 11
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java
  26. 1 5
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java
  27. 93 65
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
  28. 6 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java
  29. 9 5
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java
  30. 23 2
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
  31. 8 8
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  32. 16 11
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java
  33. 87 29
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
  34. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm
  35. 9 2
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_erp.vue.vm
  36. 10 3
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm
  37. 2 10
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/data.ts.vm
  38. 9 2
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/form.vue.vm
  39. 9 2
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_erp.vue.vm
  40. 10 3
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_normal.vue.vm
  41. 19 4
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/ftp/FtpFileClientTest.java
  42. 15 5
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/sftp/SftpFileClientTest.java
  43. 178 12
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
  44. 4 6
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java
  45. 8 10
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java
  46. 2 4
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java
  47. 2 4
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java
  48. 14 16
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java
  49. 1 3
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java
  50. 8 10
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java
  51. 4 6
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java
  52. 13 0
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java
  53. 1 1
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java
  54. 3 5
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
  55. 1 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteBatchReqVO.java
  56. 1 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteReqVO.java
  57. 2 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteRespVO.java
  58. 1 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryDeleteReqVO.java
  59. 7 9
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryRespVO.java
  60. 5 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/sku/ProductSkuMapper.java
  61. 5 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java
  62. 2 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
  63. 9 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuService.java
  64. 8 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceImpl.java
  65. 9 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java
  66. 8 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java
  67. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/PointActivityController.java
  68. 7 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java
  69. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/AppPointActivityController.java
  70. 9 8
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
  71. 8 6
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java
  72. 20 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/vo/AppAfterSalePageReqVO.java
  73. 6 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java
  74. 14 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java
  75. 0 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java
  76. 3 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/cart/TradeCartConvert.java
  77. 5 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java
  78. 3 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleService.java
  79. 5 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java
  80. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
  81. 18 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  82. 10 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBrokerageOrderHandler.java
  83. 2 3
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
  84. 1 23
      yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplTest.java
  85. 1 1
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java
  86. 4 0
      yudao-module-report/yudao-module-report-biz/pom.xml
  87. 9 2
      yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/config/JmReportConfiguration.java
  88. 68 0
      yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java
  89. 5 1
      yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/security/config/SecurityConfiguration.java
  90. 1 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  91. 1 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java
  92. 8 4
      yudao-module-system/yudao-module-system-biz/pom.xml
  93. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
  94. 6 7
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
  95. 0 15
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java
  96. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialClientDO.java
  97. 2 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailAccountMapper.java
  98. 2 6
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java
  99. 9 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/YudaoCaptchaConfiguration.java
  100. 2 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/RedisCaptchaServiceImpl.java

+ 2 - 2
README.md

@@ -1,5 +1,5 @@
 <p align="center">
- <img src="https://img.shields.io/badge/Spring%20Boot-3.4.1-blue.svg" alt="Downloads">
+ <img src="https://img.shields.io/badge/Spring%20Boot-3.4.5-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/ruoyi-vue-pro" alt="Downloads" />
 </p>
@@ -308,7 +308,7 @@
 
 | 框架                                                                                          | 说明               | 版本             | 学习指南                                                           |
 |---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
-| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 3.4.1          | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
+| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 3.4.5          | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
 | [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器           | 5.7 / 8.0+     |                                                                |
 | [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件    | 1.2.23         | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
 | [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.7          | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |

+ 1 - 1
pom.xml

@@ -42,7 +42,7 @@
         <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
         <!-- 看看咋放到 bom 里 -->
         <lombok.version>1.18.36</lombok.version>
-        <spring.boot.version>3.4.1</spring.boot.version>
+        <spring.boot.version>3.4.5</spring.boot.version>
         <mapstruct.version>1.6.3</mapstruct.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>

+ 1 - 1
sql/mysql/ruoyi-vue-pro.sql

@@ -1055,7 +1055,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2172, 31, 'RABBITMQ', '31', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:47', '1', '2025-03-17 09:40:46', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2173, 32, 'KAFKA', '32', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:59', '1', '2025-03-17 09:40:46', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', '2025-03-23 12:15:46', '1', '2025-03-23 12:15:46', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3001, 50, 'Vben5.0 Ant Design Schema 模版', '50', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-04-23 21:47:47', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3001, 50, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-04-23 21:47:47', b'0');
 COMMIT;
 
 -- ----------------------------

+ 29 - 30
yudao-dependencies/pom.xml

@@ -17,14 +17,13 @@
         <revision>2.4.2-SNAPSHOT</revision>
         <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
         <!-- 统一依赖管理 -->
-        <spring.boot.version>3.4.1</spring.boot.version>
+        <spring.boot.version>3.4.5</spring.boot.version>
         <!-- Web 相关 -->
-        <springdoc.version>2.7.0</springdoc.version>
+        <springdoc.version>2.8.3</springdoc.version>
         <knife4j.version>4.6.0</knife4j.version>
         <!-- DB 相关 -->
         <druid.version>1.2.24</druid.version>
-        <mybatis.version>3.5.17</mybatis.version>
-        <mybatis-plus.version>3.5.9</mybatis-plus.version>
+        <mybatis-plus.version>3.5.10.1</mybatis-plus.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.6</easy-trans.version>
@@ -39,7 +38,7 @@
         <lock4j.version>2.2.7</lock4j.version>
         <!-- 监控相关 -->
         <skywalking.version>9.0.0</skywalking.version>
-        <spring-boot-admin.version>3.4.1</spring-boot-admin.version>
+        <spring-boot-admin.version>3.4.5</spring-boot-admin.version>
         <opentracing.version>0.33.0</opentracing.version>
         <!-- Test 测试相关 -->
         <podam.version>8.0.2.RELEASE</podam.version>
@@ -48,7 +47,7 @@
         <!-- Bpm 工作流相关 -->
         <flowable.version>7.0.1</flowable.version>
         <!-- 工具类相关 -->
-        <captcha-plus.version>2.0.3</captcha-plus.version>
+        <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
         <jsoup.version>1.18.3</jsoup.version>
         <lombok.version>1.18.36</lombok.version>
         <mapstruct.version>1.6.3</mapstruct.version>
@@ -71,9 +70,10 @@
         <!-- 三方云服务相关 -->
         <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>2.0.5</justauth.version>
-        <jimureport.version>1.8.1</jimureport.version>
+        <awssdk.version>2.30.14</awssdk.version>
+        <justauth.version>1.16.7</justauth.version>
+        <justauth-starter.version>1.4.0</justauth-starter.version>
+        <jimureport.version>1.9.4</jimureport.version>
         <weixin-java.version>4.7.2.B</weixin-java.version>
     </properties>
 
@@ -173,11 +173,6 @@
                 <artifactId>druid-spring-boot-3-starter</artifactId>
                 <version>${druid.version}</version>
             </dependency>
-            <dependency>
-                <groupId>org.mybatis</groupId>
-                <artifactId>mybatis</artifactId>
-                <version>${mybatis.version}</version>
-            </dependency>
             <dependency>
                 <groupId>com.baomidou</groupId>
                 <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
@@ -534,9 +529,9 @@
             </dependency>
 
             <dependency>
-                <groupId>com.xingyuv</groupId>
-                <artifactId>spring-boot-starter-captcha-plus</artifactId>
-                <version>${captcha-plus.version}</version>
+                <groupId>com.anji-plus</groupId>
+                <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
+                <version>${anji-plus-captcha.version}</version>
             </dependency>
 
             <dependency>
@@ -553,21 +548,20 @@
 
             <!-- 三方云服务相关 -->
             <dependency>
-                <groupId>com.amazonaws</groupId>
-                <artifactId>aws-java-sdk-s3</artifactId>
-                <version>${aws-java-sdk-s3.version}</version>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3</artifactId>
+                <version>${awssdk.version}</version>
             </dependency>
 
             <dependency>
-                <groupId>com.xingyuv</groupId>
-                <artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
+                <groupId>me.zhyd.oauth</groupId>
+                <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
                 <version>${justauth.version}</version>
-                <exclusions>
-                    <exclusion>
-                        <groupId>cn.hutool</groupId>
-                        <artifactId>hutool-core</artifactId>
-                    </exclusion>
-                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>com.xkcoding.justauth</groupId>
+                <artifactId>justauth-spring-boot-starter</artifactId>
+                <version>${justauth-starter.version}</version>
             </dependency>
 
             <dependency>
@@ -591,10 +585,15 @@
                 <groupId>org.jeecgframework.jimureport</groupId>
                 <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
                 <version>${jimureport.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.jeecgframework.jimureport</groupId>
+                <artifactId>jimubi-spring-boot3-starter</artifactId>
+                <version>${jimureport.version}</version>
                 <exclusions>
                     <exclusion>
-                        <groupId>com.alibaba</groupId>
-                        <artifactId>druid</artifactId>
+                        <groupId>com.github.jsqlparser</groupId>
+                        <artifactId>jsqlparser</artifactId>
                     </exclusion>
                 </exclusions>
             </dependency>

+ 14 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java

@@ -11,6 +11,7 @@ import java.util.function.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import static cn.hutool.core.convert.Convert.toCollection;
 import static java.util.Arrays.asList;
 
 /**
@@ -335,4 +336,17 @@ public class CollectionUtils {
         return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
     }
 
+    /**
+     * 转换为 LinkedHashSet
+     *
+     * @param <T>         元素类型
+     * @param elementType 集合中元素类型
+     * @param value       被转换的值
+     * @return {@link LinkedHashSet}
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> LinkedHashSet<T> toLinkedHashSet(Class<T> elementType, Object value) {
+        return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
+    }
+
 }

+ 0 - 23
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java

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

+ 12 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java

@@ -6,6 +6,7 @@ import cn.hutool.system.SystemUtil;
 import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
 import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
 import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
+import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob;
 import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
 import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
@@ -73,6 +74,17 @@ public class YudaoRedisMQConsumerAutoConfiguration {
         return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
     }
 
+    /**
+     * 创建 Redis Stream 消息清理任务
+     */
+    @Bean
+    @ConditionalOnBean(AbstractRedisStreamMessageListener.class)
+    public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
+                                                                     RedisMQTemplate redisTemplate,
+                                                                     RedissonClient redissonClient) {
+        return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient);
+    }
+
     /**
      * 创建 Redis Stream 集群消费的容器
      *

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java

@@ -23,13 +23,13 @@ import java.util.Objects;
 @AllArgsConstructor
 public class RedisPendingMessageResendJob {
 
-    private static final String LOCK_KEY = "redis:pending:msg:lock";
+    private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
 
     /**
      * 消息超时时间,默认 5 分钟
      *
      * 1. 超时的消息才会被重新投递
-     * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到
+     * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息 5 分钟过期后,再等 1 分钟才会被扫瞄到
      */
     private static final int EXPIRE_TIME = 5 * 60;
 
@@ -39,7 +39,7 @@ public class RedisPendingMessageResendJob {
     private final RedissonClient redissonClient;
 
     /**
-     * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
+     * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
      */
     @Scheduled(cron = "35 * * * * ?")
     public void messageResend() {

+ 72 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.framework.mq.redis.core.job;
+
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.data.redis.core.StreamOperations;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.List;
+
+/**
+ * Redis Stream 消息清理任务
+ * 用于定期清理已消费的消息,防止内存占用过大
+ *
+ * @see <a href="https://www.cnblogs.com/nanxiang/p/16179519.html">记一次 redis stream 数据类型内存不释放问题</a>
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@AllArgsConstructor
+public class RedisStreamMessageCleanupJob {
+
+    private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
+
+    /**
+     * 保留的消息数量,默认保留最近 10000 条消息
+     */
+    private static final long MAX_COUNT = 10000;
+
+    private final List<AbstractRedisStreamMessageListener<?>> listeners;
+    private final RedisMQTemplate redisTemplate;
+    private final RedissonClient redissonClient;
+
+    /**
+     * 每小时执行一次清理任务
+     */
+    @Scheduled(cron = "0 0 * * * ?")
+    public void cleanup() {
+        RLock lock = redissonClient.getLock(LOCK_KEY);
+        // 尝试加锁
+        if (lock.tryLock()) {
+            try {
+                execute();
+            } catch (Exception ex) {
+                log.error("[cleanup][执行异常]", ex);
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 执行清理逻辑
+     */
+    private void execute() {
+        StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
+        listeners.forEach(listener -> {
+            try {
+                // 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息
+                Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
+                if (trimCount != null && trimCount > 0) {
+                    log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
+                }
+            } catch (Exception ex) {
+                log.error("[execute][Stream({}) 清理异常]", listener.getStreamKey(), ex);
+            }
+        });
+    }
+}

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form;
 
-import cn.hutool.core.convert.Convert;
 import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
@@ -33,7 +33,7 @@ public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrateg
     @Override
     public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
         Object result = execution.getVariable(param);
-        return Convert.toSet(Long.class, result);
+        return CollectionUtils.toLinkedHashSet(Long.class, result);
     }
 
     @Override
@@ -41,7 +41,7 @@ public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrateg
                                               String param, Long startUserId, String processDefinitionId,
                                               Map<String, Object> processVariables) {
         Object result = processVariables == null ? null : processVariables.get(param);
-        return Convert.toSet(Long.class, result);
+        return CollectionUtils.toLinkedHashSet(Long.class, result);
     }
 
 }

+ 3 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other;
 
-import cn.hutool.core.convert.Convert;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
@@ -37,7 +37,7 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
     @Override
     public Set<Long> calculateUsersByTask(DelegateExecution execution, String param) {
         Object result = FlowableUtils.getExpressionValue(execution, param);
-        return Convert.toSet(Long.class, result);
+        return CollectionUtils.toLinkedHashSet(Long.class, result);
     }
 
     @Override
@@ -46,7 +46,7 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
         Map<String, Object> variables = processVariables == null ? new HashMap<>() : processVariables;
         try {
             Object result = FlowableUtils.getExpressionValue(variables, param);
-            return Convert.toSet(Long.class, result);
+            return CollectionUtils.toLinkedHashSet(Long.class, result);
         } catch (FlowableException ex) {
             // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常,
             log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex);

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java

@@ -6,7 +6,6 @@ import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.*;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@@ -882,6 +881,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                 return;
             }
             runExecutionIds.add(task.getExecutionId());
+
             // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
             if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
                 // 2.1.1 添加评论

+ 11 - 7
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.infra.api.file;
 
+import jakarta.validation.constraints.NotEmpty;
+
 /**
  * 文件 API 接口
  *
@@ -14,28 +16,30 @@ public interface FileApi {
      * @return 文件路径
      */
     default String createFile(byte[] content) {
-        return createFile(null, null, content);
+        return createFile(content, null, null, null);
     }
 
     /**
      * 保存文件,并返回文件的访问路径
      *
-     * @param path 文件路径
      * @param content 文件内容
+     * @param name 文件名称,允许空
      * @return 文件路径
      */
-    default String createFile(String path, byte[] content) {
-        return createFile(null, path, content);
+    default String createFile(byte[] content, String name) {
+        return createFile(content, name, null, null);
     }
 
     /**
      * 保存文件,并返回文件的访问路径
      *
-     * @param name 文件名称
-     * @param path 文件路径
      * @param content 文件内容
+     * @param name 文件名称,允许空
+     * @param directory 目录,允许空
+     * @param type 文件的 MIME 类型,允许空
      * @return 文件路径
      */
-    String createFile(String name, String path, byte[] content);
+    String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
+                      String name, String directory, String type);
 
 }

+ 3 - 2
yudao-module-infra/yudao-module-infra-biz/pom.xml

@@ -115,9 +115,10 @@
             <groupId>com.jcraft</groupId>
             <artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 -->
         </dependency>
+        <!-- 文件客户端:解决阿里云、腾讯云、minio 等 S3 连接 -->
         <dependency>
-            <groupId>com.amazonaws</groupId>
-            <artifactId>aws-java-sdk-s3</artifactId><!-- 文件客户端:解决阿里云、腾讯云、minio 等 S3 连接 -->
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
         </dependency>
 
         <dependency>

+ 3 - 4
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java

@@ -1,11 +1,10 @@
 package cn.iocoder.yudao.module.infra.api.file;
 
 import cn.iocoder.yudao.module.infra.service.file.FileService;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
-
 /**
  * 文件 API 实现类
  *
@@ -19,8 +18,8 @@ public class FileApiImpl implements FileApi {
     private FileService fileService;
 
     @Override
-    public String createFile(String name, String path, byte[] content) {
-        return fileService.createFile(name, path, content);
+    public String createFile(byte[] content, String name, String directory, String type) {
+        return fileService.createFile(content, name, directory, type);
     }
 
 }

+ 12 - 4
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java

@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.service.file.FileService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.annotation.security.PermitAll;
@@ -41,14 +42,21 @@ public class FileController {
     @Operation(summary = "上传文件", description = "模式一:后端上传文件")
     public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
         MultipartFile file = uploadReqVO.getFile();
-        String path = uploadReqVO.getPath();
-        return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
+        byte[] content = IoUtil.readBytes(file.getInputStream());
+        return success(fileService.createFile(content, file.getOriginalFilename(),
+                uploadReqVO.getDirectory(), file.getContentType()));
     }
 
     @GetMapping("/presigned-url")
     @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
-    public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
-        return success(fileService.getFilePresignedUrl(path));
+    @Parameters({
+            @Parameter(name = "name", description = "文件名称", required = true),
+            @Parameter(name = "directory", description = "文件目录")
+    })
+    public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
+            @RequestParam("name") String name,
+            @RequestParam(value = "directory", required = false) String directory) {
+        return success(fileService.getFilePresignedUrl(name, directory));
     }
 
     @PostMapping("/create")

+ 10 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java

@@ -14,7 +14,8 @@ public class FilePresignedUrlRespVO {
     @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
     private Long configId;
 
-    @Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://s3.cn-south-1.qiniucs.com/ruoyi-vue-pro/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5")
+    @Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED,
+            example = "https://s3.cn-south-1.qiniucs.com/ruoyi-vue-pro/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5")
     private String uploadUrl;
 
     /**
@@ -26,4 +27,12 @@ public class FilePresignedUrlRespVO {
             example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png")
     private String url;
 
+    /**
+     * 为什么要返回 path 字段?
+     *
+     * 前端上传完文件后,需要调用 createFile 记录下 path 路径
+     */
+    @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx.png")
+    private String path;
+
 }

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java

@@ -14,7 +14,7 @@ public class FileUploadReqVO {
     @NotNull(message = "文件附件不能为空")
     private MultipartFile file;
 
-    @Schema(description = "文件附件", example = "yudaoyuanma.png")
-    private String path;
+    @Schema(description = "文件目录", example = "XXX/YYY")
+    private String directory;
 
 }

+ 13 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned
 import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO;
 import cn.iocoder.yudao.module.infra.service.file.FileService;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.annotation.security.PermitAll;
@@ -33,15 +35,21 @@ public class AppFileController {
     @PermitAll
     public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
         MultipartFile file = uploadReqVO.getFile();
-        String path = uploadReqVO.getPath();
-        return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
+        byte[] content = IoUtil.readBytes(file.getInputStream());
+        return success(fileService.createFile(content, file.getOriginalFilename(),
+                uploadReqVO.getDirectory(), file.getContentType()));
     }
 
     @GetMapping("/presigned-url")
     @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
-    @PermitAll
-    public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
-        return success(fileService.getFilePresignedUrl(path));
+    @Parameters({
+            @Parameter(name = "name", description = "文件名称", required = true),
+            @Parameter(name = "directory", description = "文件目录")
+    })
+    public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(
+            @RequestParam("name") String name,
+            @RequestParam(value = "directory", required = false) String directory) {
+        return success(fileService.getFilePresignedUrl(name, directory));
     }
 
     @PostMapping("/create")

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java

@@ -14,7 +14,7 @@ public class AppFileUploadReqVO {
     @NotNull(message = "文件附件不能为空")
     private MultipartFile file;
 
-    @Schema(description = "文件附件", example = "yudaoyuanma.png")
-    private String path;
+    @Schema(description = "文件目录", example = "XXX/YYY")
+    private String directory;
 
 }

+ 1 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java

@@ -23,8 +23,7 @@ public interface CodegenTableMapper extends BaseMapperX<CodegenTableDO> {
                 .likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment())
                 .likeIfPresent(CodegenTableDO::getClassName, pageReqVO.getClassName())
                 .betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getCreateTime())
-                .orderByDesc(CodegenTableDO::getUpdateTime)
-        );
+                .orderByDesc(CodegenTableDO::getUpdateTime));
     }
 
     default List<CodegenTableDO> selectListByDataSourceConfigId(Long dataSourceConfigId) {

+ 2 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java

@@ -19,7 +19,8 @@ public interface ConfigMapper extends BaseMapperX<ConfigDO> {
                 .likeIfPresent(ConfigDO::getName, reqVO.getName())
                 .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey())
                 .eqIfPresent(ConfigDO::getType, reqVO.getType())
-                .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime()));
+                .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ConfigDO::getId));
     }
 
 }

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java

@@ -24,7 +24,7 @@ public interface JobMapper extends BaseMapperX<JobDO> {
                 .likeIfPresent(JobDO::getName, reqVO.getName())
                 .eqIfPresent(JobDO::getStatus, reqVO.getStatus())
                 .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName())
-        );
+                .orderByDesc(JobDO::getId));
     }
 
 }

+ 5 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java

@@ -13,10 +13,14 @@ import lombok.Getter;
 public enum CodegenFrontTypeEnum {
 
     VUE2_ELEMENT_UI(10), // Vue2 Element UI 标准模版
+
     VUE3_ELEMENT_PLUS(20), // Vue3 Element Plus 标准模版
+
     VUE3_VBEN2_ANTD_SCHEMA(30), // Vue3 VBEN2 + ANTD + Schema 模版
+
     VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
-    VUE3_VBEN5_ANTD(50), // Vue3 VBEN5 + ANTD 模版
+
+    VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版
     ;
 
     /**

+ 9 - 11
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java

@@ -26,12 +26,6 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
 
     @Override
     protected void doInit() {
-        // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况
-        config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH));
-        // ftp的路径是 / 结尾
-        if (!config.getBasePath().endsWith(StrUtil.SLASH)) {
-            config.setBasePath(config.getBasePath() + StrUtil.SLASH);
-        }
         // 初始化 Ftp 对象
         this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
                 CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
@@ -43,8 +37,8 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
         String filePath = getFilePath(path);
         String fileName = FileUtil.getName(filePath);
         String dir = StrUtil.removeSuffix(filePath, fileName);
-        ftp.reconnectIfTimeout();
-        boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content));
+        reconnectIfTimeout();
+        boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); // 不需要主动创建目录,ftp 内部已经处理(见源码)
         if (!success) {
             throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath));
         }
@@ -55,7 +49,7 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
     @Override
     public void delete(String path) {
         String filePath = getFilePath(path);
-        ftp.reconnectIfTimeout();
+        reconnectIfTimeout();
         ftp.delFile(filePath);
     }
 
@@ -65,13 +59,17 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
         String fileName = FileUtil.getName(filePath);
         String dir = StrUtil.removeSuffix(filePath, fileName);
         ByteArrayOutputStream out = new ByteArrayOutputStream();
-        ftp.reconnectIfTimeout();
+        reconnectIfTimeout();
         ftp.download(dir, fileName, out);
         return out.toByteArray();
     }
 
     private String getFilePath(String path) {
-        return config.getBasePath() + path;
+        return config.getBasePath() + StrUtil.SLASH + path;
+    }
+
+    private synchronized void reconnectIfTimeout() {
+        ftp.reconnectIfTimeout();
     }
 
 }

+ 1 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java

@@ -18,10 +18,6 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
 
     @Override
     protected void doInit() {
-        // 补全风格。例如说 Linux 是 /,Windows 是 \
-        if (!config.getBasePath().endsWith(File.separator)) {
-            config.setBasePath(config.getBasePath() + File.separator);
-        }
     }
 
     @Override
@@ -46,7 +42,7 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
     }
 
     private String getFilePath(String path) {
-        return config.getBasePath() + path;
+        return config.getBasePath() + File.separator + path;
     }
 
 }

+ 93 - 65
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java

@@ -4,29 +4,31 @@ import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
-import com.amazonaws.HttpMethod;
-import com.amazonaws.auth.AWSStaticCredentialsProvider;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.s3.AmazonS3Client;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.model.ObjectMetadata;
-import com.amazonaws.services.s3.model.S3Object;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
 
-import java.io.ByteArrayInputStream;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
+import java.net.URI;
+import java.time.Duration;
 
 /**
  * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
- * <p>
- * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
  *
  * @author 芋道源码
  */
 public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
 
-    private AmazonS3Client client;
+    private S3Client client;
+    private S3Presigner presigner;
 
     public S3FileClient(Long id, S3FileClientConfig config) {
         super(id, config);
@@ -38,31 +40,80 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
         if (StrUtil.isEmpty(config.getDomain())) {
             config.setDomain(buildDomain());
         }
-        // 初始化客户端
-        client = (AmazonS3Client)AmazonS3ClientBuilder.standard()
-                .withCredentials(buildCredentials())
-                .withEndpointConfiguration(buildEndpointConfiguration())
+        // 初始化 S3 客户端
+        Region region = Region.of("us-east-1"); // 必须填,但填什么都行,常见的值有 "us-east-1",不填会报错
+        AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
+                AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
+        URI endpoint = URI.create(buildEndpoint());
+        S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
+                .pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
+                .chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
+                .build();
+        client = S3Client.builder()
+                .credentialsProvider(credentialsProvider)
+                .region(region)
+                .endpointOverride(endpoint)
+                .serviceConfiguration(serviceConfiguration)
+                .build();
+        presigner = S3Presigner.builder()
+                .credentialsProvider(credentialsProvider)
+                .region(region)
+                .endpointOverride(endpoint)
+                .serviceConfiguration(serviceConfiguration)
                 .build();
     }
 
-    /**
-     * 基于 config 秘钥,构建 S3 客户端的认证信息
-     *
-     * @return S3 客户端的认证信息
-     */
-    private AWSStaticCredentialsProvider buildCredentials() {
-        return new AWSStaticCredentialsProvider(
-                new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));
+    @Override
+    public String upload(byte[] content, String path, String type) {
+        // 构造 PutObjectRequest
+        PutObjectRequest putRequest = PutObjectRequest.builder()
+                .bucket(config.getBucket())
+                .key(path)
+                .contentType(type)
+                .contentLength((long) content.length)
+                .build();
+        // 上传文件
+        client.putObject(putRequest, RequestBody.fromBytes(content));
+        // 拼接返回路径
+        return config.getDomain() + "/" + path;
+    }
+
+    @Override
+    public void delete(String path) {
+        DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
+                .bucket(config.getBucket())
+                .key(path)
+                .build();
+        client.deleteObject(deleteRequest);
+    }
+
+    @Override
+    public byte[] getContent(String path) {
+        GetObjectRequest getRequest = GetObjectRequest.builder()
+                .bucket(config.getBucket())
+                .key(path)
+                .build();
+        return IoUtil.readBytes(client.getObject(getRequest));
+    }
+
+    @Override
+    public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) {
+        Duration expiration = Duration.ofHours(24);
+        return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path);
     }
 
     /**
-     * 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint
+     * 生成动态的预签名上传 URL
      *
-     * @return  S3 客户端的 EndpointConfiguration 配置
+     * @param path     相对路径
+     * @param expiration 过期时间
+     * @return 生成的上传 URL
      */
-    private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {
-        return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),
-                null); // 无需设置 region
+    private String getPresignedUrl(String path, Duration expiration) {
+        return presigner.presignPutObject(PutObjectPresignRequest.builder()
+                .signatureDuration(expiration)
+                .putObjectRequest(b -> b.bucket(config.getBucket()).key(path))
+                .build()).url().toString();
     }
 
     /**
@@ -79,40 +130,17 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
         return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
     }
 
-    @Override
-    public String upload(byte[] content, String path, String type) throws Exception {
-        // 元数据,主要用于设置文件类型
-        ObjectMetadata objectMetadata = new ObjectMetadata();
-        objectMetadata.setContentType(type);
-        objectMetadata.setContentLength(content.length); // 如果不设置,会有 “ No content length specified for stream data” 警告日志
-        // 执行上传
-        client.putObject(config.getBucket(),
-                path, // 相对路径
-                new ByteArrayInputStream(content), // 文件内容
-                objectMetadata);
-
-        // 拼接返回路径
-        return config.getDomain() + "/" + path;
-    }
-
-    @Override
-    public void delete(String path) throws Exception {
-        client.deleteObject(config.getBucket(), path);
-    }
-
-    @Override
-    public byte[] getContent(String path) throws Exception {
-        S3Object tempS3Object = client.getObject(config.getBucket(), path);
-        return IoUtil.readBytes(tempS3Object.getObjectContent());
-    }
-
-    @Override
-    public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
-        // 设定过期时间为 10 分钟。取值范围:1 秒 ~ 7 天
-        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));
-        // 生成上传 URL
-        String uploadUrl = String.valueOf(client.generatePresignedUrl(config.getBucket(), path, expiration , HttpMethod.PUT));
-        return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);
+    /**
+     * 节点地址补全协议头
+     *
+     * @return 节点地址
+     */
+    private String buildEndpoint() {
+        // 如果已经是 http 或者 https,则不进行拼接
+        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
+            return config.getEndpoint();
+        }
+        return StrUtil.format("https://{}", config.getEndpoint());
     }
 
 }

+ 6 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java

@@ -67,6 +67,12 @@ public class S3FileClientConfig implements FileClientConfig {
     @NotNull(message = "accessSecret 不能为空")
     private String accessSecret;
 
+    /**
+     * 是否启用 PathStyle 访问
+     */
+    @NotNull(message = "enablePathStyleAccess 不能为空")
+    private Boolean enablePathStyleAccess;
+
     @SuppressWarnings("RedundantIfStatement")
     @AssertTrue(message = "domain 不能为空")
     @JsonIgnore

+ 9 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java

@@ -22,10 +22,6 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
 
     @Override
     protected void doInit() {
-        // 补全风格。例如说 Linux 是 /,Windows 是 \
-        if (!config.getBasePath().endsWith(File.separator)) {
-            config.setBasePath(config.getBasePath() + File.separator);
-        }
         // 初始化 Ftp 对象
         this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
     }
@@ -35,6 +31,8 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
         // 执行写入
         String filePath = getFilePath(path);
         File file = FileUtils.createTempFile(content);
+        reconnectIfTimeout();
+        sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录,不然会报错
         sftp.upload(filePath, file);
         // 拼接返回路径
         return super.formatFileUrl(config.getDomain(), path);
@@ -43,6 +41,7 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
     @Override
     public void delete(String path) {
         String filePath = getFilePath(path);
+        reconnectIfTimeout();
         sftp.delFile(filePath);
     }
 
@@ -50,12 +49,17 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
     public byte[] getContent(String path) {
         String filePath = getFilePath(path);
         File destFile = FileUtils.createTempFile();
+        reconnectIfTimeout();
         sftp.download(filePath, destFile);
         return FileUtil.readBytes(destFile);
     }
 
     private String getFilePath(String path) {
-        return config.getBasePath() + path;
+        return config.getBasePath() + File.separator + path;
+    }
+
+    private synchronized void reconnectIfTimeout() {
+        sftp.reconnectIfTimeout();
     }
 
 }

+ 23 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java

@@ -6,7 +6,10 @@ import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import com.alibaba.ttl.TransmittableThreadLocal;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.tika.Tika;
+import org.apache.tika.mime.MimeTypeException;
+import org.apache.tika.mime.MimeTypes;
 
 import java.io.IOException;
 
@@ -15,12 +18,13 @@ import java.io.IOException;
  *
  * @author 芋道源码
  */
+@Slf4j
 public class FileTypeUtils {
 
     private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new);
 
     /**
-     * 获得文件的 mineType,对于doc,jar等文件会有误差
+     * 获得文件的 mineType,对于 doc,jar 等文件会有误差
      *
      * @param data 文件内容
      * @return mineType 无法识别时会返回“application/octet-stream”
@@ -31,7 +35,7 @@ public class FileTypeUtils {
     }
 
     /**
-     * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确
+     * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确
      *
      * @param name 文件名
      * @return mineType 无法识别时会返回“application/octet-stream”
@@ -51,6 +55,23 @@ public class FileTypeUtils {
         return TIKA.get().detect(data, name);
     }
 
+    /**
+     * 根据 mineType 获得文件后缀
+     *
+     * 注意:如果获取不到,或者发生异常,都返回 null
+     *
+     * @param mineType 类型
+     * @return 后缀,例如说 .pdf
+     */
+    public static String getExtension(String mineType) {
+        try {
+            return MimeTypes.getDefaultMimeTypes().forName(mineType).getExtension();
+        } catch (MimeTypeException e) {
+            log.warn("[getExtension][获取文件后缀({}) 失败]", mineType, e);
+            return null;
+        }
+    }
+
     /**
      * 返回附件
      *

+ 8 - 8
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java

@@ -164,21 +164,21 @@ public class CodegenEngine {
             .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
             // VUE3_VBEN5_ANTD
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"),
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"),
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
                     vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"),  // 特殊:主子表专属逻辑
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"),  // 特殊:主子表专属逻辑
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"),  // 特殊:主子表专属逻辑
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"),  // 特殊:主子表专属逻辑
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"),  // 特殊:主子表专属逻辑
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"),  // 特殊:主子表专属逻辑
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"),  // 特殊:主子表专属逻辑
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"),  // 特殊:主子表专属逻辑
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
             .build();
 

+ 16 - 11
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReq
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
+import jakarta.validation.constraints.NotEmpty;
 
 /**
  * 文件 Service 接口
@@ -24,12 +25,24 @@ public interface FileService {
     /**
      * 保存文件,并返回文件的访问路径
      *
-     * @param name    文件名称
-     * @param path    文件路径
      * @param content 文件内容
+     * @param name    文件名称,允许空
+     * @param directory 目录,允许空
+     * @param type    文件的 MIME 类型,允许空
      * @return 文件路径
      */
-    String createFile(String name, String path, byte[] content);
+    String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
+                      String name, String directory, String type);
+
+    /**
+     * 生成文件预签名地址信息
+     *
+     * @param name 文件名
+     * @param directory 目录
+     * @return 预签名地址信息
+     */
+    FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name,
+                                               String directory);
 
     /**
      * 创建文件
@@ -55,12 +68,4 @@ public interface FileService {
      */
     byte[] getFileContent(Long configId, String path) throws Exception;
 
-    /**
-     * 生成文件预签名地址信息
-     *
-     * @param path 文件路径
-     * @return 预签名地址信息
-     */
-    FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception;
-
 }

+ 87 - 29
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java

@@ -1,22 +1,26 @@
 package cn.iocoder.yudao.module.infra.service.file;
 
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.io.FileUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
-import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
-import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
+import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
+import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
+import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
+import com.google.common.annotations.VisibleForTesting;
 import jakarta.annotation.Resource;
 import lombok.SneakyThrows;
 import org.springframework.stereotype.Service;
 
+import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
 
@@ -28,6 +32,20 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX
 @Service
 public class FileServiceImpl implements FileService {
 
+    /**
+     * 上传文件的前缀,是否包含日期(yyyyMMdd)
+     *
+     * 目的:按照日期,进行分目录
+     */
+    static boolean PATH_PREFIX_DATE_ENABLE = true;
+    /**
+     * 上传文件的后缀,是否包含时间戳
+     *
+     * 目的:保证文件的唯一性,避免覆盖
+     * 定制:可按需调整成 UUID、或者其他方式
+     */
+    static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
+
     @Resource
     private FileConfigService fileConfigService;
 
@@ -41,34 +59,82 @@ public class FileServiceImpl implements FileService {
 
     @Override
     @SneakyThrows
-    public String createFile(String name, String path, byte[] content) {
-        // 计算默认的 path 名
-        String type = FileTypeUtils.getMineType(content, name);
-        if (StrUtil.isEmpty(path)) {
-            path = FileUtils.generatePath(content, name);
+    public String createFile(byte[] content, String name, String directory, String type) {
+        // 1.1 处理 type 为空的情况
+        if (StrUtil.isEmpty(type)) {
+            type = FileTypeUtils.getMineType(content, name);
         }
-        // 如果 name 为空,则使用 path 填充
+        // 1.2 处理 name 为空的情况
         if (StrUtil.isEmpty(name)) {
-            name = path;
+            name = DigestUtil.sha256Hex(content);
+        }
+        if (StrUtil.isEmpty(FileUtil.extName(name))) {
+            // 如果 name 没有后缀 type,则补充后缀
+            String extension = FileTypeUtils.getExtension(type);
+            if (StrUtil.isNotEmpty(extension)) {
+                name = name + extension;
+            }
         }
 
-        // 上传到文件存储器
+        // 2.1 生成上传的 path,需要保证唯一
+        String path = generateUploadPath(name, directory);
+        // 2.2 上传到文件存储器
         FileClient client = fileConfigService.getMasterFileClient();
         Assert.notNull(client, "客户端(master) 不能为空");
         String url = client.upload(content, path, type);
 
-        // 保存到数据库
-        FileDO file = new FileDO();
-        file.setConfigId(client.getId());
-        file.setName(name);
-        file.setPath(path);
-        file.setUrl(url);
-        file.setType(type);
-        file.setSize(content.length);
-        fileMapper.insert(file);
+        // 3. 保存到数据库
+        fileMapper.insert(new FileDO().setConfigId(client.getId())
+                .setName(name).setPath(path).setUrl(url)
+                .setType(type).setSize(content.length));
         return url;
     }
 
+    @VisibleForTesting
+    String generateUploadPath(String name, String directory) {
+        // 1. 生成前缀、后缀
+        String prefix = null;
+        if (PATH_PREFIX_DATE_ENABLE) {
+            prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
+        }
+        String suffix = null;
+        if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
+            suffix = String.valueOf(System.currentTimeMillis());
+        }
+
+        // 2.1 先拼接 suffix 后缀
+        if (StrUtil.isNotEmpty(suffix)) {
+            String ext = FileUtil.extName(name);
+            if (StrUtil.isNotEmpty(ext)) {
+                name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
+            } else {
+                name = name + StrUtil.C_UNDERLINE + suffix;
+            }
+        }
+        // 2.2 再拼接 prefix 前缀
+        if (StrUtil.isNotEmpty(prefix)) {
+            name = prefix + StrUtil.SLASH + name;
+        }
+        // 2.3 最后拼接 directory 目录
+        if (StrUtil.isNotEmpty(directory)) {
+            name = directory + StrUtil.SLASH + name;
+        }
+        return name;
+    }
+
+    @Override
+    @SneakyThrows
+    public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) {
+        // 1. 生成上传的 path,需要保证唯一
+        String path = generateUploadPath(name, directory);
+
+        // 2. 获取文件预签名地址
+        FileClient fileClient = fileConfigService.getMasterFileClient();
+        FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
+        return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
+                object -> object.setConfigId(fileClient.getId()).setPath(path));
+    }
+
     @Override
     public Long createFile(FileCreateReqVO createReqVO) {
         FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
@@ -105,12 +171,4 @@ public class FileServiceImpl implements FileService {
         return client.getContent(path);
     }
 
-    @Override
-    public FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception {
-        FileClient fileClient = fileConfigService.getMasterFileClient();
-        FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
-        return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
-                object -> object.setConfigId(fileClient.getId()));
-    }
-
 }

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm

@@ -147,7 +147,7 @@ const [Modal, modalApi] = useVbenModal({
         key: 'action_process_msg',
       });
     } finally {
-      modalApi.lock(false);
+      modalApi.unlock();
     }
   },
   async onOpenChange(isOpen: boolean) {
@@ -165,7 +165,7 @@ const [Modal, modalApi] = useVbenModal({
       try {
         data = await get${simpleClassName}(data.id);
       } finally {
-        modalApi.lock(false);
+        modalApi.unlock();
       }
     }
     formData.value = data;

+ 9 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_erp.vue.vm

@@ -24,6 +24,13 @@
   });
 
   const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: 'w-full',
+      },
+      formItemClass: 'col-span-2',
+      labelWidth: 80,
+    },
     layout: 'horizontal',
     schema: use${subSimpleClassName}FormSchema(),
     showDefaultActions: false
@@ -50,7 +57,7 @@
           key: 'action_process_msg',
         });
       } finally {
-        modalApi.lock(false);
+        modalApi.unlock();
       }
     },
     async onOpenChange(isOpen: boolean) {
@@ -69,7 +76,7 @@
         try {
           data = await get${subSimpleClassName}(data.id);
         } finally {
-          modalApi.lock(false);
+          modalApi.unlock();
         }
       }
       // 设置到 values

+ 10 - 3
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm

@@ -96,9 +96,16 @@ watch(
 );
 #else
 const [Form, formApi] = useVbenForm({
-layout: 'horizontal',
-schema: use${subSimpleClassName}FormSchema(),
-showDefaultActions: false
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-2',
+    labelWidth: 80,
+  },
+  layout: 'horizontal',
+  schema: use${subSimpleClassName}FormSchema(),
+  showDefaultActions: false
 });
 
 /** 暴露出表单校验方法和表单值获取方法 */

+ 2 - 10
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/data.ts.vm

@@ -1,6 +1,5 @@
-import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
 import type { VbenFormSchema } from '#/adapter/form';
-import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
 
 import { z } from '#/adapter/form';
@@ -40,7 +39,6 @@ export function useFormSchema(): VbenFormSchema[] {
           });
           return handleTree(data);
         },
-        class: 'w-full',
         labelField: '${treeNameColumn.javaField}',
         valueField: 'id',
         childrenField: 'children',
@@ -90,7 +88,6 @@ export function useFormSchema(): VbenFormSchema[] {
         options: [],
         #end
         placeholder: '请选择${comment}',
-        class: 'w-full',
       },
   #elseif($column.htmlType == "checkbox")## 多选框
       component: 'Checkbox',
@@ -128,7 +125,6 @@ export function useFormSchema(): VbenFormSchema[] {
       component: 'InputNumber',
       componentProps: {
         min: 0,
-        class: 'w-full',
         controlsPosition: 'right',
         placeholder: '请输入${comment}',
       },
@@ -326,7 +322,6 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
                                         options: [],
                                     #end
                                     placeholder: '请选择${comment}',
-                                    class: 'w-full',
                                 },
                             #elseif($column.htmlType == "checkbox")## 多选框
                                 component: 'Checkbox',
@@ -364,7 +359,6 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
                                 component: 'InputNumber',
                                 componentProps: {
                                     min: 0,
-                                    class: 'w-full',
                                     controlsPosition: 'right',
                                     placeholder: '请输入${comment}',
                                 },
@@ -601,7 +595,6 @@ export function use${subSimpleClassName}GridColumns(
                                             options: [],
                                         #end
                                         placeholder: '请选择${comment}',
-                                        class: 'w-full',
                                     },
                                 #elseif($column.htmlType == "checkbox")## 多选框
                                     component: 'Checkbox',
@@ -639,7 +632,6 @@ export function use${subSimpleClassName}GridColumns(
                                     component: 'InputNumber',
                                     componentProps: {
                                         min: 0,
-                                        class: 'w-full',
                                         controlsPosition: 'right',
                                         placeholder: '请输入${comment}',
                                     },
@@ -682,4 +674,4 @@ export function use${subSimpleClassName}GridColumns(
 
     #end
 #end
-#end
+#end

+ 9 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/form.vue.vm

@@ -42,6 +42,13 @@ const getTitle = computed(() => {
 #end
 
 const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-2',
+    labelWidth: 80,
+  },
   layout: 'horizontal',
   schema: useFormSchema(),
   showDefaultActions: false
@@ -100,7 +107,7 @@ const [Modal, modalApi] = useVbenModal({
         key: 'action_process_msg',
       });
     } finally {
-      modalApi.lock(false);
+      modalApi.unlock();
     }
   },
   async onOpenChange(isOpen: boolean) {
@@ -118,7 +125,7 @@ const [Modal, modalApi] = useVbenModal({
       try {
         data = await get${simpleClassName}(data.id);
       } finally {
-        modalApi.lock(false);
+        modalApi.unlock();
       }
     }
     // 设置到 values

+ 9 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_erp.vue.vm

@@ -24,6 +24,13 @@
   });
 
   const [Form, formApi] = useVbenForm({
+    commonConfig: {
+      componentProps: {
+        class: 'w-full',
+      },
+      formItemClass: 'col-span-2',
+      labelWidth: 80,
+    },
     layout: 'horizontal',
     schema: use${subSimpleClassName}FormSchema(),
     showDefaultActions: false
@@ -50,7 +57,7 @@
           key: 'action_process_msg',
         });
       } finally {
-        modalApi.lock(false);
+        modalApi.unlock();
       }
     },
     async onOpenChange(isOpen: boolean) {
@@ -69,7 +76,7 @@
         try {
           data = await get${subSimpleClassName}(data.id);
         } finally {
-          modalApi.lock(false);
+          modalApi.unlock();
         }
       }
       // 设置到 values

+ 10 - 3
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/form_sub_normal.vue.vm

@@ -96,9 +96,16 @@ watch(
 );
 #else
 const [Form, formApi] = useVbenForm({
-layout: 'horizontal',
-schema: use${subSimpleClassName}FormSchema(),
-showDefaultActions: false
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-2',
+    labelWidth: 80,
+  },
+  layout: 'horizontal',
+  schema: use${subSimpleClassName}FormSchema(),
+  showDefaultActions: false
 });
 
 /** 暴露出表单校验方法和表单值获取方法 */

+ 19 - 4
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/ftp/FtpFileClientTest.java

@@ -8,8 +8,23 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClien
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
+/**
+ * {@link FtpFileClient} 集成测试
+ *
+ * @author 芋道源码
+ */
 public class FtpFileClientTest {
 
+//    docker run -d \
+//            -p 2121:21 -p 30000-30009:30000-30009 \
+//            -e FTP_USER=foo \
+//            -e FTP_PASS=pass \
+//            -e PASV_ADDRESS=127.0.0.1 \
+//            -e PASV_MIN_PORT=30000 \
+//            -e PASV_MAX_PORT=30009 \
+//            -v $(pwd)/ftp-data:/home/vsftpd \
+//    fauria/vsftpd
+
     @Test
     @Disabled
     public void test() {
@@ -17,10 +32,10 @@ public class FtpFileClientTest {
         FtpFileClientConfig config = new FtpFileClientConfig();
         config.setDomain("http://127.0.0.1:48080");
         config.setBasePath("/home/ftp");
-        config.setHost("kanchai.club");
-        config.setPort(221);
-        config.setUsername("");
-        config.setPassword("");
+        config.setHost("127.0.0.1");
+        config.setPort(2121);
+        config.setUsername("foo");
+        config.setPassword("pass");
         config.setMode(FtpMode.Passive.name());
         FtpFileClient client = new FtpFileClient(0L, config);
         client.init();

+ 15 - 5
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/sftp/SftpFileClientTest.java

@@ -7,19 +7,29 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileCli
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
+/**
+ * {@link SftpFileClient} 集成测试
+ *
+ * @author 芋道源码
+ */
 public class SftpFileClientTest {
 
+//    docker run -p 2222:22 -d \
+//            -v $(pwd)/sftp-data:/home/foo/upload \
+//    atmoz/sftp \
+//    foo:pass:1001
+
     @Test
     @Disabled
     public void test() {
         // 创建客户端
         SftpFileClientConfig config = new SftpFileClientConfig();
         config.setDomain("http://127.0.0.1:48080");
-        config.setBasePath("/home/ftp");
-        config.setHost("kanchai.club");
-        config.setPort(222);
-        config.setUsername("");
-        config.setPassword("");
+        config.setBasePath("/upload"); // 注意,这个是相对路径,不是实际 linux 上的路径!!!
+        config.setHost("127.0.0.1");
+        config.setPort(2222);
+        config.setUsername("foo");
+        config.setPassword("pass");
         SftpFileClient client = new SftpFileClient(0L, config);
         client.init();
         // 上传文件

+ 178 - 12
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java

@@ -3,19 +3,20 @@ package cn.iocoder.yudao.module.infra.service.file;
 import cn.hutool.core.io.resource.ResourceUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
-import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
+import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
-import jakarta.annotation.Resource;
-
 import java.time.LocalDateTime;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
@@ -29,7 +30,7 @@ import static org.mockito.Mockito.*;
 public class FileServiceImplTest extends BaseDbUnitTest {
 
     @Resource
-    private FileService fileService;
+    private FileServiceImpl fileService;
 
     @Resource
     private FileMapper fileMapper;
@@ -37,6 +38,12 @@ public class FileServiceImplTest extends BaseDbUnitTest {
     @MockBean
     private FileConfigService fileConfigService;
 
+    @BeforeEach
+    public void setUp() {
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
+    }
+
     @Test
     public void testGetFilePage() {
         // mock 数据
@@ -70,28 +77,69 @@ public class FileServiceImplTest extends BaseDbUnitTest {
         AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
     }
 
+    /**
+     * content、name、directory、type 都非空
+     */
     @Test
-    public void testCreateFile_success() throws Exception {
+    public void testCreateFile_success_01() throws Exception {
         // 准备参数
-        String path = randomString();
         byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
+        String name = "单测文件名";
+        String directory = randomString();
+        String type = "image/jpeg";
         // mock Master 文件客户端
         FileClient client = mock(FileClient.class);
         when(fileConfigService.getMasterFileClient()).thenReturn(client);
         String url = randomString();
-        when(client.upload(same(content), same(path), eq("image/jpeg"))).thenReturn(url);
+        AtomicReference<String> pathRef = new AtomicReference<>();
+        when(client.upload(same(content), argThat(path -> {
+            assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg"));
+            pathRef.set(path);
+            return true;
+        }), eq(type))).thenReturn(url);
         when(client.getId()).thenReturn(10L);
-        String name = "单测文件名";
         // 调用
-        String result = fileService.createFile(name, path, content);
+        String result = fileService.createFile(content, name, directory, type);
         // 断言
         assertEquals(result, url);
         // 校验数据
-        FileDO file = fileMapper.selectOne(FileDO::getPath, path);
+        FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
         assertEquals(10L, file.getConfigId());
-        assertEquals(path, file.getPath());
+        assertEquals(pathRef.get(), file.getPath());
         assertEquals(url, file.getUrl());
-        assertEquals("image/jpeg", file.getType());
+        assertEquals(type, file.getType());
+        assertEquals(content.length, file.getSize());
+    }
+
+    /**
+     * content 非空,其它都空
+     */
+    @Test
+    public void testCreateFile_success_02() throws Exception {
+        // 准备参数
+        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
+        // mock Master 文件客户端
+        String type = "image/jpeg";
+        FileClient client = mock(FileClient.class);
+        when(fileConfigService.getMasterFileClient()).thenReturn(client);
+        String url = randomString();
+        AtomicReference<String> pathRef = new AtomicReference<>();
+        when(client.upload(same(content), argThat(path -> {
+            assertTrue(path.matches("\\d{8}/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e_\\d+\\.jpg"));
+            pathRef.set(path);
+            return true;
+        }), eq(type))).thenReturn(url);
+        when(client.getId()).thenReturn(10L);
+        // 调用
+        String result = fileService.createFile(content, null, null, null);
+        // 断言
+        assertEquals(result, url);
+        // 校验数据
+        FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
+        assertEquals(10L, file.getConfigId());
+        assertEquals(pathRef.get(), file.getPath());
+        assertEquals(url, file.getUrl());
+        assertEquals(type, file.getType());
         assertEquals(content.length, file.getSize());
     }
 
@@ -140,4 +188,122 @@ public class FileServiceImplTest extends BaseDbUnitTest {
         assertSame(result, content);
     }
 
+    @Test
+    public void testGenerateUploadPath_AllEnabled() {
+        // 准备参数
+        String name = "test.jpg";
+        String directory = "avatar";
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
+
+        // 调用
+        String path = fileService.generateUploadPath(name, directory);
+
+        // 断言
+        // 格式为:avatar/yyyyMMdd/test_timestamp.jpg
+        assertTrue(path.startsWith(directory + "/"));
+        // 包含日期格式:8 位数字,如 20240517
+        assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
+    }
+
+    @Test
+    public void testGenerateUploadPath_PrefixEnabled_SuffixDisabled() {
+        // 准备参数
+        String name = "test.jpg";
+        String directory = "avatar";
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
+
+        // 调用
+        String path = fileService.generateUploadPath(name, directory);
+
+        // 断言
+        // 格式为:avatar/yyyyMMdd/test.jpg
+        assertTrue(path.startsWith(directory + "/"));
+        // 包含日期格式:8 位数字,如 20240517
+        assertTrue(path.matches(directory + "/\\d{8}/test\\.jpg"));
+    }
+
+    @Test
+    public void testGenerateUploadPath_PrefixDisabled_SuffixEnabled() {
+        // 准备参数
+        String name = "test.jpg";
+        String directory = "avatar";
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
+
+        // 调用
+        String path = fileService.generateUploadPath(name, directory);
+
+        // 断言
+        // 格式为:avatar/test_timestamp.jpg
+        assertTrue(path.startsWith(directory + "/"));
+        assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
+    }
+
+    @Test
+    public void testGenerateUploadPath_AllDisabled() {
+        // 准备参数
+        String name = "test.jpg";
+        String directory = "avatar";
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
+
+        // 调用
+        String path = fileService.generateUploadPath(name, directory);
+
+        // 断言
+        // 格式为:avatar/test.jpg
+        assertEquals(directory + "/" + name, path);
+    }
+
+    @Test
+    public void testGenerateUploadPath_NoExtension() {
+        // 准备参数
+        String name = "test";
+        String directory = "avatar";
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
+
+        // 调用
+        String path = fileService.generateUploadPath(name, directory);
+
+        // 断言
+        // 格式为:avatar/yyyyMMdd/test_timestamp
+        assertTrue(path.startsWith(directory + "/"));
+        assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
+    }
+
+    @Test
+    public void testGenerateUploadPath_DirectoryNull() {
+        // 准备参数
+        String name = "test.jpg";
+        String directory = null;
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
+
+        // 调用
+        String path = fileService.generateUploadPath(name, directory);
+
+        // 断言
+        // 格式为:yyyyMMdd/test_timestamp.jpg
+        assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
+    }
+
+    @Test
+    public void testGenerateUploadPath_DirectoryEmpty() {
+        // 准备参数
+        String name = "test.jpg";
+        String directory = "";
+        FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
+        FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
+
+        // 调用
+        String path = fileService.generateUploadPath(name, directory);
+
+        // 断言
+        // 格式为:yyyyMMdd/test_timestamp.jpg
+        assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
+    }
+
 }

+ 4 - 6
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java

@@ -5,24 +5,22 @@ import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Schema(description = "管理后台 - IoT OTA 固件创建 Request VO")
 @Data
 public class IotOtaFirmwareCreateReqVO {
 
-    @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件")
+    @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件")
     @NotEmpty(message = "固件名称不能为空")
     private String name;
 
     @Schema(description = "固件描述", example = "某品牌型号固件,测试用")
     private String description;
 
-    @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0")
+    @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0")
     @NotEmpty(message = "版本号不能为空")
     private String version;
 
-    @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "产品编号不能为空")
     private String productId;
 
@@ -30,7 +28,7 @@ public class IotOtaFirmwareCreateReqVO {
     // TODO @li:是不是必传哈
     private String signMethod;
 
-    @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip")
+    @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip")
     @NotEmpty(message = "固件文件 URL 不能为空")
     private String fileUrl;
 

+ 8 - 10
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java

@@ -7,8 +7,6 @@ import com.fhs.core.trans.vo.VO;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Data
 @Schema(description = "管理后台 - IoT OTA 固件 Response VO")
 public class IotOtaFirmwareRespVO implements VO {
@@ -16,12 +14,12 @@ public class IotOtaFirmwareRespVO implements VO {
     /**
      * 固件编号
      */
-    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long id;
     /**
      * 固件名称
      */
-    @Schema(description = "固件名称", requiredMode = REQUIRED, example = "OTA固件")
+    @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "OTA固件")
     private String name;
     /**
      * 固件描述
@@ -31,7 +29,7 @@ public class IotOtaFirmwareRespVO implements VO {
     /**
      * 版本号
      */
-    @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0")
+    @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0")
     private String version;
 
     /**
@@ -39,7 +37,7 @@ public class IotOtaFirmwareRespVO implements VO {
      * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
      */
-    @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"})
     private String productId;
     /**
@@ -47,12 +45,12 @@ public class IotOtaFirmwareRespVO implements VO {
      * <p>
      * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()}
      */
-    @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot-product-key")
+    @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot-product-key")
     private String productKey;
     /**
      * 产品名称
      */
-    @Schema(description = "产品名称", requiredMode = REQUIRED, example = "OTA产品")
+    @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "OTA产品")
     private String productName;
     /**
      * 签名方式
@@ -69,12 +67,12 @@ public class IotOtaFirmwareRespVO implements VO {
     /**
      * 固件文件大小
      */
-    @Schema(description = "固件文件大小", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "固件文件大小", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long fileSize;
     /**
      * 固件文件 URL
      */
-    @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn")
+    @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
     private String fileUrl;
     /**
      * 自定义信息,建议使用 JSON 格式

+ 2 - 4
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java

@@ -5,18 +5,16 @@ import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Schema(description = "管理后台 - IoT OTA 固件更新 Request VO")
 @Data
 public class IotOtaFirmwareUpdateReqVO {
 
-    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "固件编号不能为空")
     private Long id;
 
     // TODO @li:name 是不是可以飞必传哈
-    @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件")
+    @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件")
     @NotEmpty(message = "固件名称不能为空")
     private String name;
 

+ 2 - 4
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java

@@ -5,8 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Data
 @Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO")
 public class IotOtaUpgradeRecordPageReqVO extends PageParam {
@@ -17,7 +15,7 @@ public class IotOtaUpgradeRecordPageReqVO extends PageParam {
      * <p>
      * 该字段用于标识升级任务的唯一编号,不能为空。
      */
-    @Schema(description = "升级任务编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "升级任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "升级任务编号不能为空")
     private Long taskId;
 
@@ -26,7 +24,7 @@ public class IotOtaUpgradeRecordPageReqVO extends PageParam {
      * <p>
      * 该字段用于标识设备的名称,通常用于区分不同的设备。
      */
-    @Schema(description = "设备标识", requiredMode = REQUIRED, example = "摄像头A1-1")
+    @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "摄像头A1-1")
     private String deviceName;
 
 }

+ 14 - 16
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java

@@ -10,8 +10,6 @@ import lombok.Data;
 
 import java.time.LocalDateTime;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Data
 @Schema(description = "管理后台 - IoT OTA 升级记录 Response VO")
 public class IotOtaUpgradeRecordRespVO {
@@ -19,73 +17,73 @@ public class IotOtaUpgradeRecordRespVO {
     /**
      * 升级记录编号
      */
-    @Schema(description = "升级记录编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "升级记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long id;
     /**
      * 固件编号
      * <p>
      * 关联 {@link IotOtaFirmwareDO#getId()}
      */
-    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"})
     private Long firmwareId;
     /**
      * 固件版本
      */
-    @Schema(description = "固件版本", requiredMode = REQUIRED, example = "v1.0.0")
+    @Schema(description = "固件版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0.0")
     private String firmwareVersion;
     /**
      * 任务编号
      * <p>
      * 关联 {@link IotOtaUpgradeTaskDO#getId()}
      */
-    @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long taskId;
     /**
      * 产品标识
      * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
      */
-    @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot")
+    @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot")
     private String productKey;
     /**
      * 设备名称
      * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
      */
-    @Schema(description = "设备名称", requiredMode = REQUIRED, example = "iot")
+    @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "iot")
     private String deviceName;
     /**
      * 设备编号
      * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()}
      */
-    @Schema(description = "设备编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private String deviceId;
     /**
      * 来源的固件编号
      * <p>
      * 关联 {@link IotDeviceDO#getFirmwareId()}
      */
-    @Schema(description = "来源的固件编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "来源的固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"})
     private Long fromFirmwareId;
     /**
      * 来源的固件版本
      */
-    @Schema(description = "来源的固件版本", requiredMode = REQUIRED, example = "v1.0.0")
+    @Schema(description = "来源的固件版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0.0")
     private String fromFirmwareVersion;
     /**
      * 升级状态
      * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum}
      */
-    @Schema(description = "升级状态", requiredMode = REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"})
+    @Schema(description = "升级状态", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"})
     private Integer status;
     /**
      * 升级进度,百分比
      */
-    @Schema(description = "升级进度,百分比", requiredMode = REQUIRED, example = "10")
+    @Schema(description = "升级进度,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
     private Integer progress;
     /**
      * 升级进度描述
@@ -93,17 +91,17 @@ public class IotOtaUpgradeRecordRespVO {
      * 注意,只记录设备最后一次的升级进度描述
      * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志
      */
-    @Schema(description = "升级进度描述", requiredMode = REQUIRED, example = "10")
+    @Schema(description = "升级进度描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
     private String description;
     /**
      * 升级开始时间
      */
-    @Schema(description = "升级开始时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
+    @Schema(description = "升级开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
     private LocalDateTime startTime;
     /**
      * 升级结束时间
      */
-    @Schema(description = "升级结束时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
+    @Schema(description = "升级结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
     private LocalDateTime endTime;
 
 }

+ 1 - 3
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java

@@ -5,8 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Data
 @Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO")
 public class IotOtaUpgradeTaskPageReqVO extends PageParam {
@@ -21,7 +19,7 @@ public class IotOtaUpgradeTaskPageReqVO extends PageParam {
      * 固件编号字段,用于唯一标识固件,不能为空
      */
     @NotNull(message = "固件编号不能为空")
-    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long firmwareId;
 
 }

+ 8 - 10
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java

@@ -9,8 +9,6 @@ import lombok.Data;
 import java.time.LocalDateTime;
 import java.util.List;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Data
 @Schema(description = "管理后台 - IoT OTA 升级任务 Response VO")
 public class IotOtaUpgradeTaskRespVO implements VO {
@@ -18,12 +16,12 @@ public class IotOtaUpgradeTaskRespVO implements VO {
     /**
      * 任务编号
      */
-    @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long id;
     /**
      * 任务名称
      */
-    @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务")
+    @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务")
     private String name;
     /**
      * 任务描述
@@ -35,31 +33,31 @@ public class IotOtaUpgradeTaskRespVO implements VO {
      * <p>
      * 关联 {@link IotOtaFirmwareDO#getId()}
      */
-    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long firmwareId;
     /**
      * 任务状态
      * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum}
      */
-    @Schema(description = "任务状态", requiredMode = REQUIRED, allowableValues = {"10", "20", "21", "30"})
+    @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"10", "20", "21", "30"})
     private Integer status;
     /**
      * 任务状态名称
      */
-    @Schema(description = "任务状态名称", requiredMode = REQUIRED, example = "进行中")
+    @Schema(description = "任务状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中")
     private String statusName;
     /**
      * 升级范围
      * <p>
      * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum}
      */
-    @Schema(description = "升级范围", requiredMode = REQUIRED, allowableValues = {"1", "2"})
+    @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"1", "2"})
     private Integer scope;
     /**
      * 设备数量
      */
-    @Schema(description = "设备数量", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long deviceCount;
     /**
      * 选中的设备编号数组
@@ -78,7 +76,7 @@ public class IotOtaUpgradeTaskRespVO implements VO {
     /**
      * 创建时间
      */
-    @Schema(description = "创建时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00")
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00")
     private LocalDateTime createTime;
 
 }

+ 4 - 6
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java

@@ -11,8 +11,6 @@ import lombok.Data;
 
 import java.util.List;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Data
 @Schema(description = "管理后台 - IoT OTA 升级任务创建/修改 Request VO")
 public class IotOtaUpgradeTaskSaveReqVO {
@@ -24,7 +22,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
      * 任务名称
      */
     @NotEmpty(message = "任务名称不能为空")
-    @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务")
+    @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务")
     private String name;
 
     /**
@@ -39,7 +37,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
      * 关联 {@link IotOtaFirmwareDO#getId()}
      */
     @NotNull(message = "固件编号不能为空")
-    @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024")
+    @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long firmwareId;
 
     /**
@@ -49,7 +47,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
      */
     @NotNull(message = "升级范围不能为空")
     @InEnum(value = IotOtaUpgradeTaskScopeEnum.class)
-    @Schema(description = "升级范围", requiredMode = REQUIRED, example = "1")
+    @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Integer scope;
 
     /**
@@ -57,7 +55,7 @@ public class IotOtaUpgradeTaskSaveReqVO {
      * <p>
      * 关联 {@link IotDeviceDO#getId()}
      */
-    @Schema(description = "选中的设备编号数组", requiredMode = REQUIRED, example = "[1,2,3,4]")
+    @Schema(description = "选中的设备编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3,4]")
     private List<Long> deviceIds;
 
 }

+ 13 - 0
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/ProductSkuApi.java

@@ -5,6 +5,9 @@ import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
 /**
  * 商品 SKU API 接口
@@ -30,6 +33,16 @@ public interface ProductSkuApi {
      */
     List<ProductSkuRespDTO> getSkuList(Collection<Long> ids);
 
+    /**
+     * 批量查询 SKU MAP
+     *
+     * @param ids SKU 编号列表
+     * @return SKU MAP
+     */
+    default Map<Long, ProductSkuRespDTO> getSkuMap(Collection<Long> ids) {
+        return convertMap(getSkuList(ids), ProductSkuRespDTO::getId);
+    }
+
     /**
      * 批量查询 SKU 数组
      *

+ 1 - 1
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/ProductSpuApi.java

@@ -30,7 +30,7 @@ public interface ProductSpuApi {
      * @param ids SPU 编号列表
      * @return SPU MAP
      */
-    default Map<Long, ProductSpuRespDTO> getSpusMap(Collection<Long> ids) {
+    default Map<Long, ProductSpuRespDTO> getSpuMap(Collection<Long> ids) {
         return convertMap(getSpuList(ids), ProductSpuRespDTO::getId);
     }
 

+ 3 - 5
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java

@@ -4,17 +4,15 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Schema(description = "管理后台 - 商品浏览记录 Response VO")
 @Data
 @ExcelIgnoreUnannotated
 public class ProductBrowseHistoryRespVO {
 
-    @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Long id;
 
-    @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
     private Long spuId;
 
     // ========== 商品相关字段 ==========
@@ -34,4 +32,4 @@ public class ProductBrowseHistoryRespVO {
     @Schema(description = "库存", example = "100")
     private Integer stock;
 
-}
+}

+ 1 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteBatchReqVO.java

@@ -6,13 +6,11 @@ import lombok.Data;
 import jakarta.validation.constraints.NotEmpty;
 import java.util.List;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Schema(description = "用户 APP - 商品收藏的批量 Request VO") // 用于收藏、取消收藏、获取收藏
 @Data
 public class AppFavoriteBatchReqVO {
 
-    @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
+    @Schema(description = "商品 SPU 编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
     @NotEmpty(message = "商品 SPU 编号数组不能为空")
     private List<Long> spuIds;
 

+ 1 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteReqVO.java

@@ -5,13 +5,11 @@ import lombok.Data;
 
 import jakarta.validation.constraints.NotNull;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Schema(description = "用户 APP - 商品收藏的单个 Request VO") // 用于收藏、取消收藏、获取收藏
 @Data
 public class AppFavoriteReqVO {
 
-    @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
     @NotNull(message = "商品 SPU 编号不能为空")
     private Long spuId;
 

+ 2 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/favorite/vo/AppFavoriteRespVO.java

@@ -2,16 +2,15 @@ package cn.iocoder.yudao.module.product.controller.app.favorite.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
 
 @Schema(description = "用户 App - 商品收藏 Response VO")
 @Data
 public class AppFavoriteRespVO {
 
-    @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Long id;
 
-    @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
     private Long spuId;
 
     // ========== 商品相关字段 ==========

+ 1 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryDeleteReqVO.java

@@ -6,13 +6,11 @@ import lombok.Data;
 
 import java.util.List;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Schema(description = "用户 APP - 删除商品浏览记录的 Request VO")
 @Data
 public class AppProductBrowseHistoryDeleteReqVO {
 
-    @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
+    @Schema(description = "商品 SPU 编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
     @NotEmpty(message = "商品 SPU 编号数组不能为空")
     private List<Long> spuIds;
 

+ 7 - 9
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/history/vo/AppProductBrowseHistoryRespVO.java

@@ -3,33 +3,31 @@ package cn.iocoder.yudao.module.product.controller.app.history.vo;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
 @Schema(description = "用户 App - 商品浏览记录 Response VO")
 @Data
 public class AppProductBrowseHistoryRespVO {
 
-    @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Long id;
 
-    @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29502")
     private Long spuId;
 
     // ========== 商品相关字段 ==========
 
-    @Schema(description = "商品 SPU 名称", requiredMode = REQUIRED, example = "赵六")
+    @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
     private String spuName;
 
-    @Schema(description = "商品封面图", requiredMode = REQUIRED, example = "https://www.iocoder.cn/pic.png")
+    @Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/pic.png")
     private String picUrl;
 
-    @Schema(description = "商品单价", requiredMode = REQUIRED, example = "50")
+    @Schema(description = "商品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
     private Integer price;
 
-    @Schema(description = "商品销量", requiredMode = REQUIRED, example = "60")
+    @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "60")
     private Integer salesCount;
 
-    @Schema(description = "库存", requiredMode = REQUIRED, example = "80")
+    @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "80")
     private Integer stock;
 
 }

+ 5 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/sku/ProductSkuMapper.java

@@ -6,6 +6,8 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.util.Collection;
 import java.util.List;
@@ -13,6 +15,9 @@ import java.util.List;
 @Mapper
 public interface ProductSkuMapper extends BaseMapperX<ProductSkuDO> {
 
+    @Select("SELECT * FROM product_sku WHERE id = #{id}")
+    ProductSkuDO selectByIdIncludeDeleted(@Param("id") Long id);
+
     default List<ProductSkuDO> selectListBySpuId(Long spuId) {
         return selectList(ProductSkuDO::getSpuId, spuId);
     }

+ 5 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java

@@ -11,6 +11,8 @@ import cn.iocoder.yudao.module.product.enums.ProductConstants;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.util.Objects;
 import java.util.Set;
@@ -18,6 +20,9 @@ import java.util.Set;
 @Mapper
 public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
 
+    @Select("SELECT * FROM product_spu WHERE id = #{id}")
+    ProductSpuDO selectByIdIncludeDeleted(@Param("id") Long id);
+
     /**
      * 获取商品 SPU 分页列表数据
      *

+ 2 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java

@@ -91,7 +91,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
     }
 
     private ProductSkuDO validateSku(Long skuId) {
-        ProductSkuDO sku = productSkuService.getSku(skuId);
+        ProductSkuDO sku = productSkuService.getSku(skuId, true);
         if (sku == null) {
             throw exception(SKU_NOT_EXISTS);
         }
@@ -99,7 +99,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
     }
 
     private ProductSpuDO validateSpu(Long spuId) {
-        ProductSpuDO spu = productSpuService.getSpu(spuId);
+        ProductSpuDO spu = productSpuService.getSpu(spuId, true);
         if (null == spu) {
             throw exception(SPU_NOT_EXISTS);
         }

+ 9 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuService.java

@@ -29,6 +29,15 @@ public interface ProductSkuService {
      */
     ProductSkuDO getSku(Long id);
 
+    /**
+     * 获得商品 SKU 信息
+     *
+     * @param id 编号
+     * @param includeDeleted 是否包含已删除的
+     * @return 商品 SKU 信息
+     */
+    ProductSkuDO getSku(Long id, boolean includeDeleted);
+
     /**
      * 获得商品 SKU 列表
      *

+ 8 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceImpl.java

@@ -68,6 +68,14 @@ public class ProductSkuServiceImpl implements ProductSkuService {
         return productSkuMapper.selectById(id);
     }
 
+    @Override
+    public ProductSkuDO getSku(Long id, boolean includeDeleted) {
+        if (includeDeleted) {
+            return productSkuMapper.selectByIdIncludeDeleted(id);
+        }
+        return getSku(id);
+    }
+
     @Override
     public List<ProductSkuDO> getSkuList(Collection<Long> ids) {
         if (CollUtil.isEmpty(ids)) {

+ 9 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java

@@ -51,6 +51,15 @@ public interface ProductSpuService {
      */
     ProductSpuDO getSpu(Long id);
 
+    /**
+     * 获得商品 SPU
+     *
+     * @param id 编号
+     * @param includeDeleted 是否包含已删除的
+     * @return 商品 SPU
+     */
+    ProductSpuDO getSpu(Long id, boolean includeDeleted);
+
     /**
      * 获得商品 SPU 列表
      *

+ 8 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java

@@ -189,6 +189,14 @@ public class ProductSpuServiceImpl implements ProductSpuService {
         return productSpuMapper.selectById(id);
     }
 
+    @Override
+    public ProductSpuDO getSpu(Long id, boolean includeDeleted) {
+        if (includeDeleted) {
+            return productSpuMapper.selectByIdIncludeDeleted(id);
+        }
+        return getSpu(id);
+    }
+
     @Override
     public List<ProductSpuDO> getSpuList(Collection<Long> ids) {
         if (CollUtil.isEmpty(ids)) {

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/PointActivityController.java

@@ -124,7 +124,7 @@ public class PointActivityController {
         List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
                 convertSet(activityList, PointActivityDO::getId));
         Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
-        Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap(
+        Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpuMap(
                 convertSet(activityList, PointActivityDO::getSpuId));
         List<PointActivityRespVO> result = BeanUtils.toBean(activityList, PointActivityRespVO.class);
         result.forEach(activity -> {

+ 7 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java

@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.template;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
 import lombok.Data;
 
-import jakarta.validation.constraints.Min;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -20,6 +20,9 @@ public class AppCouponTemplateRespVO {
     @Schema(description = "优惠券说明", example = "优惠券使用说明")
     private String description;
 
+    @Schema(description = "发行总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // -1 - 则表示不限制发放数量
+    private Integer totalCount;
+
     @Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制
     private Integer takeLimitCount;
 
@@ -62,6 +65,9 @@ public class AppCouponTemplateRespVO {
     @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用
     private Integer discountLimitPrice;
 
+    @Schema(description = "领取优惠券的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Integer takeCount;
+
     // ========== 用户相关字段 ==========
 
     @Schema(description = "是否可以领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/AppPointActivityController.java

@@ -104,7 +104,7 @@ public class AppPointActivityController {
         List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
                 convertSet(activityList, PointActivityDO::getId));
         Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
-        Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap(
+        Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpuMap(
                 convertSet(activityList, PointActivityDO::getSpuId));
         List<AppPointActivityRespVO> result = BeanUtils.toBean(activityList, AppPointActivityRespVO.class);
         result.forEach(activity -> {

+ 9 - 8
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java

@@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityType
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
@@ -180,7 +181,7 @@ public class CouponServiceImpl implements CouponService {
      * @param couponId 模版编号
      * @param userId   用户编号
      */
-    @Transactional(rollbackFor = Exception.class)
+    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // 每次调用开启一个新的事务,避免在一个大的事务里面
     public void invalidateCoupon(Long couponId, Long userId) {
         if (couponId == null || couponId <= 0) {
             return;
@@ -270,13 +271,17 @@ public class CouponServiceImpl implements CouponService {
         if (CollUtil.isEmpty(userIds)) {
             throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
         }
-
         // 校验模板
         if (couponTemplate == null) {
             throw exception(COUPON_TEMPLATE_NOT_EXISTS);
         }
-        // 校验剩余数量
-        if (ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
+        // 校验领取方式
+        if (ObjUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
+            throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
+        }
+        // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
+        if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
+                && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
                 && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
             throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
         }
@@ -286,10 +291,6 @@ public class CouponServiceImpl implements CouponService {
                 throw exception(COUPON_TEMPLATE_EXPIRED);
             }
         }
-        // 校验领取方式
-        if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
-            throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
-        }
     }
 
     /**

+ 8 - 6
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java

@@ -1,12 +1,13 @@
 package cn.iocoder.yudao.module.trade.controller.app.aftersale;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO;
-import cn.iocoder.yudao.module.trade.convert.aftersale.AfterSaleConvert;
+import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
 import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -31,16 +32,17 @@ public class AppAfterSaleController {
 
     @GetMapping(value = "/page")
     @Operation(summary = "获得售后分页")
-    public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(PageParam pageParam) {
-        return success(AfterSaleConvert.INSTANCE.convertPage02(
-                afterSaleService.getAfterSalePage(getLoginUserId(), pageParam)));
+    public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(AppAfterSalePageReqVO pageReqVO) {
+        PageResult<AfterSaleDO> pageResult = afterSaleService.getAfterSalePage(getLoginUserId(), pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AppAfterSaleRespVO.class));
     }
 
     @GetMapping(value = "/get")
     @Operation(summary = "获得售后订单")
     @Parameter(name = "id", description = "售后编号", required = true, example = "1")
     public CommonResult<AppAfterSaleRespVO> getAfterSale(@RequestParam("id") Long id) {
-        return success(AfterSaleConvert.INSTANCE.convert(afterSaleService.getAfterSale(getLoginUserId(), id)));
+        AfterSaleDO afterSale = afterSaleService.getAfterSale(getLoginUserId(), id);
+        return success(BeanUtils.toBean(afterSale, AppAfterSaleRespVO.class));
     }
 
     @PostMapping(value = "/create")

+ 20 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/vo/AppAfterSalePageReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.trade.controller.app.aftersale.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.Set;
+
+@Schema(description = "用户 App - 交易售后分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AppAfterSalePageReqVO extends PageParam {
+
+    @Schema(description = "售后状态", example = "10, 20")
+    private Set<Integer> statuses;
+
+}

+ 6 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java

@@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.trade.controller.app.base.spu;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import java.util.List;
-
 /**
  * 商品 SPU 基础 Response VO
  *
@@ -25,4 +23,10 @@ public class AppProductSpuBaseRespVO {
     @Schema(description = "商品分类编号", example = "1")
     private Long categoryId;
 
+    @Schema(description = "商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+    private Integer stock;
+
+    @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status;
+
 }

+ 14 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java

@@ -1,8 +1,12 @@
 package cn.iocoder.yudao.module.trade.controller.app.delivery.vo.pickup;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
+import java.time.LocalTime;
+
 @Schema(description = "用户 App - 自提门店 Response VO")
 @Data
 public class AppDeliveryPickUpStoreRespVO {
@@ -28,6 +32,16 @@ public class AppDeliveryPickUpStoreRespVO {
     @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号")
     private String detailAddress;
 
+    @Schema(description = "营业开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "营业开始时间不能为空")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm")
+    private LocalTime openingTime;
+
+    @Schema(description = "营业结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "营业结束时间不能为空")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm")
+    private LocalTime closingTime;
+
     @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88")
     private Double latitude;
 

+ 0 - 5
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java

@@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.trade.controller.admin.base.member.user.MemberUse
 import cn.iocoder.yudao.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderBaseVO;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
-import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleRespVO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
@@ -63,10 +62,6 @@ public interface AfterSaleConvert {
 
     ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean);
 
-    AppAfterSaleRespVO convert(AfterSaleDO bean);
-
-    PageResult<AppAfterSaleRespVO> convertPage02(PageResult<AfterSaleDO> page);
-
     default AfterSaleDetailRespVO convert(AfterSaleDO afterSale, TradeOrderDO order, TradeOrderItemDO orderItem,
                                           MemberUserRespDTO user, List<AfterSaleLogDO> logs) {
         AfterSaleDetailRespVO respVO = convert02(afterSale);

+ 3 - 5
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/cart/TradeCartConvert.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.trade.convert.cart;
 
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
@@ -33,21 +34,18 @@ public interface TradeCartConvert {
             cartVO.setId(cart.getId()).setCount(cart.getCount()).setSelected(cart.getSelected());
             ProductSpuRespDTO spu = spuMap.get(cart.getSpuId());
             ProductSkuRespDTO sku = skuMap.get(cart.getSkuId());
-            cartVO.setSpu(convert(spu)).setSku(convert(sku));
+            cartVO.setSpu(BeanUtils.toBean(spu, AppProductSpuBaseRespVO.class))
+                    .setSku(BeanUtils.toBean(sku, AppProductSkuBaseRespVO.class));
             // 如果 SPU 不存在,或者下架,或者库存不足,说明是无效的
             if (spu == null
                 || !ProductSpuStatusEnum.isEnable(spu.getStatus())
                 || spu.getStock() <= 0) {
-                cartVO.setSelected(false); // 强制设置成不可选中
                 invalidList.add(cartVO);
             } else {
-                // 虽然 SKU 可能也会不存在,但是可以通过购物车重新选择
                 validList.add(cartVO);
             }
         });
         return new AppCartListRespVO().setValidList(validList).setInvalidList(invalidList);
     }
-    AppProductSpuBaseRespVO convert(ProductSpuRespDTO spu);
-    AppProductSkuBaseRespVO convert(ProductSkuRespDTO sku);
 
 }

+ 5 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java

@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.trade.dal.mysql.aftersale;
 
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
@@ -27,9 +27,10 @@ public interface AfterSaleMapper extends BaseMapperX<AfterSaleDO> {
                 .orderByDesc(AfterSaleDO::getId));
     }
 
-    default PageResult<AfterSaleDO> selectPage(Long userId, PageParam pageParam) {
-        return selectPage(pageParam, new LambdaQueryWrapperX<AfterSaleDO>()
-                .eqIfPresent(AfterSaleDO::getUserId, userId)
+    default PageResult<AfterSaleDO> selectPage(Long userId, AppAfterSalePageReqVO pageReqVO) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<AfterSaleDO>()
+                .eq(AfterSaleDO::getUserId, userId)
+                .inIfPresent(AfterSaleDO::getStatus, pageReqVO.getStatuses())
                 .orderByDesc(AfterSaleDO::getId));
     }
 

+ 3 - 3
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleService.java

@@ -1,12 +1,12 @@
 package cn.iocoder.yudao.module.trade.service.aftersale;
 
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
 
 /**
@@ -28,10 +28,10 @@ public interface AfterSaleService {
      * 【会员】获得售后订单分页
      *
      * @param userId    用户编号
-     * @param pageParam 分页参数
+     * @param pageReqVO 分页参数
      * @return 售后订单分页
      */
-    PageResult<AfterSaleDO> getAfterSalePage(Long userId, PageParam pageParam);
+    PageResult<AfterSaleDO> getAfterSalePage(Long userId, AppAfterSalePageReqVO pageReqVO);
 
     /**
      * 【会员】获得售后单

+ 5 - 5
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.trade.service.aftersale;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
@@ -16,6 +15,7 @@ import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePage
 import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO;
+import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.AppAfterSalePageReqVO;
 import cn.iocoder.yudao.module.trade.convert.aftersale.AfterSaleConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
@@ -36,6 +36,7 @@ import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties
 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
@@ -44,7 +45,6 @@ import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -87,8 +87,8 @@ public class AfterSaleServiceImpl implements AfterSaleService {
     }
 
     @Override
-    public PageResult<AfterSaleDO> getAfterSalePage(Long userId, PageParam pageParam) {
-        return tradeAfterSaleMapper.selectPage(userId, pageParam);
+    public PageResult<AfterSaleDO> getAfterSalePage(Long userId, AppAfterSalePageReqVO pageReqVO) {
+        return tradeAfterSaleMapper.selectPage(userId, pageReqVO);
     }
 
     @Override
@@ -386,7 +386,7 @@ public class AfterSaleServiceImpl implements AfterSaleService {
             public void afterCommit() {
                 // 创建退款单
                 PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
-                        .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));;
+                        .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
                 Long payRefundId = payRefundApi.createRefund(createReqDTO);
                 // 更新售后单的退款单号
                 tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java

@@ -135,7 +135,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
     private Long createPayTransfer(BrokerageWithdrawDO withdraw) {
         // 1.1 获取微信 openid
         SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(
-                UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_APP.getType());
+                UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
         // TODO @luchi:这里,需要校验非空。如果空的话,要有业务异常哈;
         // 1.2 构建请求
         PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO()

+ 18 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -545,6 +545,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus())) {
             throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
         }
+        // 1.3 校验是否支持延迟(不允许取消)
+        if (TradeOrderStatusEnum.isUnpaid(order.getStatus())) {
+            PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId());
+            if (payOrder != null && PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
+                log.warn("[cancelOrderByMember][order({}) 支付单已支付(支付回调延迟),不支持取消]", order.getId());
+                throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
+            }
+        }
 
         // 2. 取消订单
         cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
@@ -581,6 +589,15 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     @Transactional(rollbackFor = Exception.class)
     @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_CANCEL)
     public void cancelOrderBySystem(TradeOrderDO order) {
+        // 校验是否支持延迟(不允许取消)
+        if (TradeOrderStatusEnum.isUnpaid(order.getStatus())) {
+            PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId());
+            if (payOrder != null && PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
+                log.warn("[cancelOrderBySystem][order({}) 支付单已支付(支付回调延迟),不支持取消]", order.getId());
+                return;
+            }
+        }
+
         cancelOrder0(order, TradeOrderCancelTypeEnum.PAY_TIMEOUT);
     }
 
@@ -895,12 +912,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         if (order == null) {
             throw exception(ORDER_NOT_FOUND);
         }
-
         // 1.3 校验订单是否支付
         if (!order.getPayStatus()) {
             throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
         }
-        // 1.3 校验订单是否未退款
+        // 1.4 校验订单是否未退款
         if (ObjUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
             throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
         }

+ 10 - 5
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBrokerageOrderHandler.java

@@ -20,6 +20,7 @@ import org.springframework.stereotype.Component;
 
 import jakarta.annotation.Resource;
 import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
@@ -101,13 +102,17 @@ public class TradeBrokerageOrderHandler implements TradeOrderHandler {
     protected void addBrokerage(Long userId, List<TradeOrderItemDO> orderItems) {
         MemberUserRespDTO user = memberUserApi.getUser(userId);
         Assert.notNull(user);
-        ProductSpuRespDTO spu = productSpuApi.getSpu(orderItems.get(0).getSpuId());
-        Assert.notNull(spu);
-        ProductSkuRespDTO sku = productSkuApi.getSku(orderItems.get(0).getSkuId());
+        Map<Long, ProductSpuRespDTO> spusMap = productSpuApi.getSpuMap(convertList(orderItems, TradeOrderItemDO::getSpuId));
+        Map<Long, ProductSkuRespDTO> skusMap = productSkuApi.getSkuMap(convertList(orderItems, TradeOrderItemDO::getSkuId));
 
         // 每一个订单项,都会去生成分销记录
-        List<BrokerageAddReqBO> addList = convertList(orderItems,
-                item -> TradeOrderConvert.INSTANCE.convert(user, item, spu, sku));
+        List<BrokerageAddReqBO> addList = convertList(orderItems, item -> {
+            ProductSpuRespDTO spu = spusMap.get(item.getSpuId());
+            Assert.notNull(spu);
+            ProductSkuRespDTO sku = skusMap.get(item.getSkuId());
+            Assert.notNull(sku);
+            return TradeOrderConvert.INSTANCE.convert(user, item, spu, sku);
+        });
         brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList);
     }
 

+ 2 - 3
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.member.service.auth;
 
 import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
@@ -27,11 +26,11 @@ import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
 import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
 import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import jakarta.annotation.Resource;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -147,7 +146,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
 
         // 绑定社交用户
         String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
-                SocialTypeEnum.WECHAT_MINI_APP.getType(), reqVO.getLoginCode(), reqVO.getState()));
+                SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), reqVO.getLoginCode(), reqVO.getState()));
 
         // 创建 Token 令牌,记录登录日志
         return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid);

+ 1 - 23
yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplTest.java

@@ -5,12 +5,12 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
-import cn.iocoder.yudao.module.infra.api.file.FileApi;
 import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO;
 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
 import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
 import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl;
 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
+import jakarta.annotation.Resource;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
@@ -18,7 +18,6 @@ import org.springframework.context.annotation.Import;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.security.crypto.password.PasswordEncoder;
 
-import jakarta.annotation.Resource;
 import java.util.function.Consumer;
 
 import static cn.hutool.core.util.RandomUtil.randomEle;
@@ -53,8 +52,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
 
     @MockBean
     private SmsCodeApi smsCodeApi;
-    @MockBean
-    private FileApi fileApi;
 
     // TODO 芋艿:后续重构这个单测
 //    @Test
@@ -72,25 +69,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
 //        String nickname = memberUserService.getUser(userDO.getId()).getNickname();
 //        // 断言
 //        assertEquals(newNickName,nickname);
-//    }
-//
-//    @Test
-//    public void testUpdateAvatar_success() throws Exception {
-//        // mock 数据
-//        MemberUserDO dbUser = randomUserDO();
-//        userMapper.insert(dbUser);
-//
-//        // 准备参数
-//        Long userId = dbUser.getId();
-//        byte[] avatarFileBytes = randomBytes(10);
-//        ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes);
-//        // mock 方法
-//        String avatar = randomString();
-//        when(fileApi.createFile(eq(avatarFileBytes))).thenReturn(avatar);
-//        // 调用
-//        String str = memberUserService.updateUserAvatar(userId, avatarFile);
-//        // 断言
-//        assertEquals(avatar, str);
 //    }
 
     @Test

+ 1 - 1
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java

@@ -218,7 +218,7 @@ public class MpMaterialServiceImpl implements MpMaterialService {
 
     private String uploadFile(String mediaId, File file) {
         String path = mediaId + "." + FileTypeUtil.getType(file);
-        return fileApi.createFile(path, FileUtil.readBytes(file));
+        return fileApi.createFile(FileUtil.readBytes(file), path);
     }
 
 }

+ 4 - 0
yudao-module-report/yudao-module-report-biz/pom.xml

@@ -64,6 +64,10 @@
             <groupId>org.jeecgframework.jimureport</groupId>
             <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.jeecgframework.jimureport</groupId>
+            <artifactId>jimubi-spring-boot3-starter</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>cn.iocoder.boot</groupId>

+ 9 - 2
yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/config/JmReportConfiguration.java

@@ -1,13 +1,15 @@
 package cn.iocoder.yudao.module.report.framework.jmreport.config;
 
 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
-import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
+import cn.iocoder.yudao.module.report.framework.jmreport.core.service.JmOnlDragExternalServiceImpl;
 import cn.iocoder.yudao.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl;
+import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
 import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
 import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
 
 /**
  * 积木报表的配置类
@@ -19,11 +21,16 @@ import org.springframework.context.annotation.Configuration;
 public class JmReportConfiguration {
 
     @Bean
-    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi,
                                                       PermissionApi permissionApi,
                                                       SecurityProperties securityProperties) {
         return new JmReportTokenServiceImpl(oAuth2TokenApi, permissionApi, securityProperties);
     }
 
+    @Bean // 暂时注释:可以按需实现后打开
+    @Primary
+    public JmOnlDragExternalServiceImpl jmOnlDragExternalService2() {
+        return new JmOnlDragExternalServiceImpl();
+    }
+
 }

+ 68 - 0
yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java

@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.report.framework.jmreport.core.service;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.RequiredArgsConstructor;
+import org.jeecg.modules.drag.service.IOnlDragExternalService;
+import org.jeecg.modules.drag.vo.DragDictModel;
+import org.jeecg.modules.drag.vo.DragLogDTO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link IOnlDragExternalService} 实现类,提供积木仪表盘的查询等功能
+ *
+ * 实现可参考:
+ * 1. <a href="https://github.com/jeecgboot/jimureport/blob/master/jimureport-example/src/main/java/com/jeecg/modules/jmreport/extend/JimuDragExternalServiceImpl.java">jimureport-example</a>
+ * 2. <a href="https://gitee.com/jeecg/JeecgBoot/blob/master/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/config/jimureport/JimuDragExternalServiceImpl.java">JeecgBoot 集成</a>
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class JmOnlDragExternalServiceImpl implements IOnlDragExternalService {
+
+    // ========== DictItem 相关 ==========
+
+    @Override
+    public Map<String, List<DragDictModel>> getManyDictItems(List<String> codeList, List<JSONObject> tableDictList) {
+        return IOnlDragExternalService.super.getManyDictItems(codeList, tableDictList);
+    }
+
+    @Override
+    public List<DragDictModel> getDictItems(String dictCode) {
+        return IOnlDragExternalService.super.getDictItems(dictCode);
+    }
+
+    @Override
+    public List<DragDictModel> getTableDictItems(String dictTable, String dictText, String dictCode) {
+        return IOnlDragExternalService.super.getTableDictItems(dictTable, dictText, dictCode);
+    }
+
+    @Override
+    public List<DragDictModel> getCategoryTreeDictItems(List<String> ids) {
+        return IOnlDragExternalService.super.getCategoryTreeDictItems(ids);
+    }
+
+    @Override
+    public List<DragDictModel> getUserDictItems(List<String> ids) {
+        return IOnlDragExternalService.super.getUserDictItems(ids);
+    }
+
+    @Override
+    public List<DragDictModel> getDeptsDictItems(List<String> ids) {
+        return IOnlDragExternalService.super.getDeptsDictItems(ids);
+    }
+
+    // ========== Log 相关 ==========
+
+    @Override
+    public void addLog(DragLogDTO dto) {
+        IOnlDragExternalService.super.addLog(dto);
+    }
+
+    @Override
+    public void addLog(String logMsg, int logType, int operateType) {
+        IOnlDragExternalService.super.addLog(logMsg, logType, operateType);
+    }
+
+}

+ 5 - 1
yudao-module-report/yudao-module-report-biz/src/main/java/cn/iocoder/yudao/module/report/framework/security/config/SecurityConfiguration.java

@@ -18,7 +18,11 @@ public class SecurityConfiguration {
 
             @Override
             public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
-                registry.requestMatchers("/jmreport/**").permitAll(); // 积木报表
+                // 积木报表
+                registry.requestMatchers("/jmreport/**").permitAll();
+                // 积木仪表盘
+                registry.requestMatchers("/drag/**").permitAll();
+                registry.requestMatchers("/jimubi/**").permitAll();
             }
 
         };

+ 1 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java

@@ -45,6 +45,7 @@ public interface ErrorCodeConstants {
     ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})!");
     ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空");
     ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册");
+    ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
 
     // ========== 部门模块 1-002-004-000 ==========
     ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");

+ 1 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java

@@ -52,7 +52,7 @@ public enum SocialTypeEnum implements ArrayValuable<Integer> {
      *
      * @see <a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html">接入文档</a>
      */
-    WECHAT_MINI_APP(34, "WECHAT_MINI_APP"),
+    WECHAT_MINI_PROGRAM(34, "WECHAT_MINI_PROGRAM"),
     ;
 
     public static final Integer[] ARRAYS = Arrays.stream(values()).map(SocialTypeEnum::getType).toArray(Integer[]::new);

+ 8 - 4
yudao-module-system/yudao-module-system-biz/pom.xml

@@ -97,8 +97,12 @@
 
         <!-- 三方云服务相关 -->
         <dependency>
-            <groupId>com.xingyuv</groupId>
-            <artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
+            <groupId>me.zhyd.oauth</groupId>
+            <artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
+        </dependency>
+        <dependency>
+            <groupId>com.xkcoding.justauth</groupId>
+            <artifactId>justauth-spring-boot-starter</artifactId>
         </dependency>
 
         <dependency>
@@ -111,8 +115,8 @@
         </dependency>
 
         <dependency>
-            <groupId>com.xingyuv</groupId>
-            <artifactId>spring-boot-starter-captcha-plus</artifactId> <!-- 验证码,一般用于登录使用 -->
+            <groupId>com.anji-plus</groupId>
+            <artifactId>captcha-spring-boot-starter</artifactId> <!-- 验证码,一般用于登录使用 -->
         </dependency>
 
         <dependency>

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java

@@ -84,7 +84,7 @@ public class SocialClientApiImpl implements SocialClientApi {
 
         // 2. 获得社交用户
         SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
-                SocialTypeEnum.WECHAT_MINI_APP.getType());
+                SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
         if (StrUtil.isBlankIfStr(socialUser.getOpenid())) {
             log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
             return;

+ 6 - 7
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java

@@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.system.controller.admin.captcha;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
-import com.xingyuv.captcha.model.common.ResponseModel;
-import com.xingyuv.captcha.model.vo.CaptchaVO;
-import com.xingyuv.captcha.service.CaptchaService;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.PermitAll;
+import jakarta.servlet.http.HttpServletRequest;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import jakarta.annotation.Resource;
-import jakarta.annotation.security.PermitAll;
-import jakarta.servlet.http.HttpServletRequest;
-
 @Tag(name = "管理后台 - 验证码")
 @RestController("adminCaptchaController")
 @RequestMapping("/system/captcha")

+ 0 - 15
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java

@@ -23,14 +23,11 @@ import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
 
 @Tag(name = "管理后台 - 用户个人中心")
 @RestController
@@ -79,16 +76,4 @@ public class UserProfileController {
         return success(true);
     }
 
-    @Deprecated // TODO @芋艿:逐步替换到 updateUserProfile 接口
-    @RequestMapping(value = "/update-avatar",
-            method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题
-    @Operation(summary = "上传用户个人头像")
-    public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception {
-        if (file.isEmpty()) {
-            throw exception(FILE_IS_EMPTY);
-        }
-        String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream());
-        return success(avatar);
-    }
-
 }

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialClientDO.java

@@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
-import com.xingyuv.jushauth.config.AuthConfig;
 import lombok.*;
+import me.zhyd.oauth.config.AuthConfig;
 
 /**
  * 社交客户端 DO

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailAccountMapper.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.dal.mysql.mail;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
 import cn.iocoder.yudao.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO;
 import org.apache.ibatis.annotations.Mapper;
@@ -14,7 +13,8 @@ public interface MailAccountMapper extends BaseMapperX<MailAccountDO> {
     default PageResult<MailAccountDO> selectPage(MailAccountPageReqVO pageReqVO) {
         return selectPage(pageReqVO, new LambdaQueryWrapperX<MailAccountDO>()
                 .likeIfPresent(MailAccountDO::getMail, pageReqVO.getMail())
-                .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername()));
+                .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername())
+                .orderByDesc(MailAccountDO::getId));
     }
 
 }

+ 2 - 6
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailTemplateMapper.java

@@ -3,14 +3,9 @@ package cn.iocoder.yudao.module.system.dal.mysql.mail;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
 import cn.iocoder.yudao.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO;
-import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO;
 import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Select;
-
-import java.util.Date;
 
 @Mapper
 public interface MailTemplateMapper extends BaseMapperX<MailTemplateDO> {
@@ -21,7 +16,8 @@ public interface MailTemplateMapper extends BaseMapperX<MailTemplateDO> {
                 .likeIfPresent(MailTemplateDO::getCode, pageReqVO.getCode())
                 .likeIfPresent(MailTemplateDO::getName, pageReqVO.getName())
                 .eqIfPresent(MailTemplateDO::getAccountId, pageReqVO.getAccountId())
-                .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime()));
+                .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime())
+                .orderByDesc(MailTemplateDO::getId));
     }
 
     default Long selectCountByAccountId(Long accountId) {

+ 9 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/YudaoCaptchaConfiguration.java

@@ -1,11 +1,14 @@
 package cn.iocoder.yudao.module.system.framework.captcha.config;
 
 import cn.iocoder.yudao.module.system.framework.captcha.core.RedisCaptchaServiceImpl;
-import com.xingyuv.captcha.properties.AjCaptchaProperties;
-import com.xingyuv.captcha.service.CaptchaCacheService;
-import com.xingyuv.captcha.service.impl.CaptchaServiceFactory;
+import com.anji.captcha.config.AjCaptchaAutoConfiguration;
+import com.anji.captcha.properties.AjCaptchaProperties;
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.impl.CaptchaServiceFactory;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
 import org.springframework.data.redis.core.StringRedisTemplate;
 
 /**
@@ -14,9 +17,11 @@ import org.springframework.data.redis.core.StringRedisTemplate;
  * @author 芋道源码
  */
 @Configuration(proxyBeanMethods = false)
+@ImportAutoConfiguration(AjCaptchaAutoConfiguration.class) // 目的:解决 aj-captcha 针对 SpringBoot 3.X 自动配置不生效的问题
 public class YudaoCaptchaConfiguration {
 
-    @Bean
+    @Bean(name = "AjCaptchaCacheService")
+    @Primary
     public CaptchaCacheService captchaCacheService(AjCaptchaProperties config,
                                                    StringRedisTemplate stringRedisTemplate) {
         CaptchaCacheService captchaCacheService = CaptchaServiceFactory.getCache(config.getCacheType().name());

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/RedisCaptchaServiceImpl.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.system.framework.captcha.core;
 
-import com.xingyuv.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.CaptchaCacheService;
 import lombok.Setter;
 import org.springframework.data.redis.core.StringRedisTemplate;
 
@@ -28,7 +28,7 @@ public class RedisCaptchaServiceImpl implements CaptchaCacheService {
 
     @Override
     public boolean exists(String key) {
-        return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
+        return stringRedisTemplate.hasKey(key);
     }
 
     @Override

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.