Sfoglia il codice sorgente

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

# Conflicts:
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
#	yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java
#	yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java
#	yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
#	yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
#	yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
#	yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java
#	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
#	yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java
YunaiV 9 mesi fa
parent
commit
8024d0af3f
100 ha cambiato i file con 1438 aggiunte e 1317 eliminazioni
  1. 18 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
  2. 1 3
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java
  3. 3 21
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java
  4. 5 5
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java
  5. 0 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java
  6. 0 33
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
  7. 0 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java
  8. 0 33
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
  9. 6 6
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
  10. 23 23
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
  11. 2 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
  12. 9 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  13. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
  14. 12 1
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java
  15. 12 1
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java
  16. 0 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
  17. 0 13
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  18. 1 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm
  19. 5 6
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
  20. 10 12
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
  21. 5 6
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
  22. 0 46
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm
  23. 0 124
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm
  24. 0 65
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm
  25. 0 85
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm
  26. 33 9
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm
  27. 9 0
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java
  28. 1 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
  29. 5 5
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
  30. 110 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java
  31. 25 6
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
  32. 0 27
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java
  33. 40 12
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java
  34. 2 3
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
  35. 16 4
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java
  36. 1 2
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java
  37. 5 11
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
  38. 16 7
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
  39. 22 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java
  40. 10 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java
  41. 5 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java
  42. 22 21
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java
  43. 48 20
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
  44. 18 38
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java
  45. 5 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java
  46. 5 12
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java
  47. 0 30
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java
  48. 0 16
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java
  49. 0 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java
  50. 20 17
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
  51. 0 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java
  52. 0 29
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java
  53. 0 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java
  54. 11 9
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java
  55. 0 20
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java
  56. 5 15
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
  57. 0 8
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java
  58. 0 5
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
  59. 9 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  60. 59 68
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
  61. 138 91
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
  62. 1 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java
  63. 59 71
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
  64. 68 61
      yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
  65. 3 3
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java
  66. 4 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  67. 2 1
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java
  68. 3 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
  69. 7 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleController.java
  70. 2 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleLogController.java
  71. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageRecordController.java
  72. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java
  73. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java
  74. 0 27
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverConfigController.java
  75. 9 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java
  76. 43 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java
  77. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
  78. 23 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
  79. 14 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java
  80. 2 5
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java
  81. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
  82. 15 6
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
  83. 41 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  84. 5 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java
  85. 32 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java
  86. 93 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java
  87. 67 25
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java
  88. 16 6
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java
  89. 3 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
  90. 77 11
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java
  91. 8 3
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java
  92. 20 9
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java
  93. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql
  94. 2 10
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
  95. 2 3
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java
  96. 22 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java
  97. 19 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java
  98. 4 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
  99. 12 2
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java
  100. 1 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java

+ 18 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java

@@ -111,7 +111,7 @@ public class HttpUtils {
             authorization = Base64.decodeStr(authorization);
             clientId = StrUtil.subBefore(authorization, ":", false);
             clientSecret = StrUtil.subAfter(authorization, ":", false);
-        // 再从 Param 中获取
+            // 再从 Param 中获取
         } else {
             clientId = request.getParameter("client_id");
             clientSecret = request.getParameter("client_secret");
@@ -143,4 +143,21 @@ public class HttpUtils {
         }
     }
 
+    /**
+     * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
+     *
+     * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
+     *
+     * @param url URL
+     * @param headers 请求头
+     * @return 请求结果
+     */
+    public static String get(String url, Map<String, String> headers) {
+        try (HttpResponse response = HttpRequest.get(url)
+                .addHeaders(headers)
+                .execute()) {
+            return response.body();
+        }
+    }
+
 }

+ 1 - 3
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java

@@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogCreateReqDTO;
 import com.mzt.logapi.beans.LogRecord;
 import com.mzt.logapi.service.ILogRecordService;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
@@ -29,7 +28,6 @@ public class LogRecordServiceImpl implements ILogRecordService {
     private OperateLogApi operateLogApi;
 
     @Override
-    @Async
     public void record(LogRecord logRecord) {
         OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO();
         try {
@@ -42,7 +40,7 @@ public class LogRecordServiceImpl implements ILogRecordService {
             fillRequestFields(reqDTO);
 
             // 2. 异步记录日志
-            operateLogApi.createOperateLog(reqDTO);
+            operateLogApi.createOperateLogAsync(reqDTO);
         } catch (Throwable ex) {
             // 由于 @Async 异步调用,这里打印下日志,更容易跟进
             log.error("[record][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);

+ 3 - 21
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java

@@ -2,15 +2,11 @@ package cn.iocoder.yudao.framework.apilog.config;
 
 import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter;
 import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
 import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
-import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
+import jakarta.servlet.Filter;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -19,23 +15,9 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
-import javax.servlet.Filter;
-
 @AutoConfiguration(after = YudaoWebAutoConfiguration.class)
 public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
 
-    @Bean
-    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
-        return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
-    }
-
-    @Bean
-    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
-        return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
-    }
-
     /**
      * 创建 ApiAccessLogFilter Bean,记录 API 请求日志
      */
@@ -43,8 +25,8 @@ public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer {
     @ConditionalOnProperty(prefix = "yudao.access-log", value = "enable", matchIfMissing = true) // 允许使用 yudao.access-log.enable=false 禁用访问日志
     public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
                                                                          @Value("${spring.application.name}") String applicationName,
-                                                                         ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
-        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
+                                                                         ApiAccessLogApi apiAccessLogApi) {
+        ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi);
         return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
     }
 

+ 5 - 5
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java

@@ -9,7 +9,6 @@ import cn.hutool.core.util.BooleanUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
 import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService;
 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@@ -18,6 +17,7 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
 import com.fasterxml.jackson.databind.JsonNode;
 import io.swagger.v3.oas.annotations.Operation;
@@ -53,12 +53,12 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
 
     private final String applicationName;
 
-    private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
+    private final ApiAccessLogApi apiAccessLogApi;
 
-    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
+    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogApi apiAccessLogApi) {
         super(webProperties);
         this.applicationName = applicationName;
-        this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
+        this.apiAccessLogApi = apiAccessLogApi;
     }
 
     @Override
@@ -91,7 +91,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
             if (!enable) {
                 return;
             }
-            apiAccessLogFrameworkService.createApiAccessLog(accessLog);
+            apiAccessLogApi.createApiAccessLogAsync(accessLog);
         } catch (Throwable th) {
             log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
         }

+ 0 - 19
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java

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

+ 0 - 33
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java

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

+ 0 - 19
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java

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

+ 0 - 33
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java

@@ -1,33 +0,0 @@
-package cn.iocoder.yudao.framework.apilog.core.service;
-
-import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
-import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
-
-/**
- * API 错误日志 Framework Service 实现类
- *
- * 基于 {@link ApiErrorLogApi} 服务,记录错误日志
- *
- * @author 芋道源码
- */
-@RequiredArgsConstructor
-@Slf4j
-public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
-
-    private final ApiErrorLogApi apiErrorLogApi;
-
-    @Override
-    @Async
-    public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) {
-        try {
-            apiErrorLogApi.createApiErrorLog(reqDTO);
-        } catch (Throwable ex) {
-            // 由于 @Async 异步调用,这里打印下日志,更容易跟进
-            log.error("[createApiErrorLog][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex);
-        }
-    }
-
-}

+ 6 - 6
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java

@@ -1,12 +1,14 @@
 package cn.iocoder.yudao.framework.web.config;
 
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
 import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
+import jakarta.annotation.Resource;
+import jakarta.servlet.Filter;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -25,9 +27,6 @@ import org.springframework.web.filter.CorsFilter;
 import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
-import javax.annotation.Resource;
-import javax.servlet.Filter;
-
 @AutoConfiguration
 @EnableConfigurationProperties(WebProperties.class)
 public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
@@ -59,8 +58,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
     }
 
     @Bean
-    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService apiErrorLogFrameworkService) {
-        return new GlobalExceptionHandler(applicationName, apiErrorLogFrameworkService);
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogApi apiErrorLogApi) {
+        return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
     }
 
     @Bean

+ 23 - 23
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java

@@ -4,7 +4,7 @@ import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
+import cn.hutool.extra.servlet.JakartaServletUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
@@ -13,8 +13,13 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
 import com.fasterxml.jackson.databind.exc.InvalidFormatException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.ValidationException;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -29,11 +34,8 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 import org.springframework.web.servlet.NoHandlerFoundException;
+import org.springframework.web.servlet.resource.NoResourceFoundException;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.validation.ConstraintViolation;
-import javax.validation.ConstraintViolationException;
-import javax.validation.ValidationException;
 import java.time.LocalDateTime;
 import java.util.Map;
 import java.util.Set;
@@ -58,7 +60,7 @@ public class GlobalExceptionHandler {
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     private final String applicationName;
 
-    private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
+    private final ApiErrorLogApi apiErrorLogApi;
 
     /**
      * 处理所有异常,主要是提供给 Filter 使用
@@ -90,10 +92,9 @@ public class GlobalExceptionHandler {
         if (ex instanceof NoHandlerFoundException) {
             return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
         }
-        // 仅 JDK17 + Spring Boot 3.X 才有
-//        if (ex instanceof NoResourceFoundException) {
-//            return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex);
-//        }
+        if (ex instanceof NoResourceFoundException) {
+            return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex);
+        }
         if (ex instanceof HttpRequestMethodNotSupportedException) {
             return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
         }
@@ -199,15 +200,14 @@ public class GlobalExceptionHandler {
         return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
     }
 
-    // 仅 JDK17 + Spring Boot 3.X 才有
-//    /**
-//     * 处理 SpringMVC 请求地址不存在
-//     */
-//    @ExceptionHandler(NoResourceFoundException.class)
-//    private CommonResult<?> noResourceFoundExceptionHandler(HttpServletRequest req, NoResourceFoundException ex) {
-//        log.warn("[noResourceFoundExceptionHandler]", ex);
-//        return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getResourcePath()));
-//    }
+    /**
+     * 处理 SpringMVC 请求地址不存在
+     */
+    @ExceptionHandler(NoResourceFoundException.class)
+    private CommonResult<?> noResourceFoundExceptionHandler(HttpServletRequest req, NoResourceFoundException ex) {
+        log.warn("[noResourceFoundExceptionHandler]", ex);
+        return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getResourcePath()));
+    }
 
     /**
      * 处理 SpringMVC 请求方法不正确
@@ -283,7 +283,7 @@ public class GlobalExceptionHandler {
             // 初始化 errorLog
             buildExceptionLog(errorLog, req, e);
             // 执行插入 errorLog
-            apiErrorLogFrameworkService.createApiErrorLog(errorLog);
+            apiErrorLogApi.createApiErrorLogAsync(errorLog);
         } catch (Throwable th) {
             log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(),  JsonUtils.toJsonString(errorLog), th);
         }
@@ -310,12 +310,12 @@ public class GlobalExceptionHandler {
         errorLog.setApplicationName(applicationName);
         errorLog.setRequestUrl(request.getRequestURI());
         Map<String, Object> requestParams = MapUtil.<String, Object>builder()
-                .put("query", ServletUtils.getParamMap(request))
-                .put("body", ServletUtils.getBody(request)).build();
+                .put("query", JakartaServletUtil.getParamMap(request))
+                .put("body", JakartaServletUtil.getBody(request)).build();
         errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
         errorLog.setRequestMethod(request.getMethod());
         errorLog.setUserAgent(ServletUtils.getUserAgent(request));
-        errorLog.setUserIp(ServletUtils.getClientIP(request));
+        errorLog.setUserIp(JakartaServletUtil.getClientIP(request));
         errorLog.setExceptionTime(LocalDateTime.now());
     }
 

+ 2 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java

@@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModel
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
 import org.flowable.common.engine.impl.db.SuspensionState;
 import org.flowable.engine.repository.Deployment;
@@ -55,7 +56,7 @@ public interface BpmModelConvert {
         BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model);
         BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null);
         if (ArrayUtil.isNotEmpty(bpmnBytes)) {
-            modelVO.setBpmnXml(new String(bpmnBytes));
+            modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes));
         }
         return modelVO;
     }

+ 9 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
 import org.flowable.bpmn.converter.BpmnXMLConverter;
@@ -108,7 +109,14 @@ public class BpmnModelUtils {
             return null;
         }
         BpmnXMLConverter converter = new BpmnXMLConverter();
-        return new String(converter.convertToXML(model));
+        return StrUtil.utf8Str(converter.convertToXML(model));
+    }
+
+    public static String getBpmnXml(byte[] bpmnBytes) {
+        if (ArrayUtil.isEmpty(bpmnBytes)) {
+            return null;
+        }
+        return StrUtil.utf8Str(bpmnBytes);
     }
 
     // ========== 遍历相关的方法 ==========

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java

@@ -92,7 +92,7 @@ public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
         List<Map<String, Object>> result = selectMaps(new QueryWrapper<CrmReceivableDO>()
                 .select("contract_id, SUM(price) AS total_price")
                 .in("audit_status", CrmAuditStatusEnum.DRAFT.getStatus(), // 草稿 + 审批中 + 审批通过
-                        CrmAuditStatusEnum.PROCESS, CrmAuditStatusEnum.APPROVE.getStatus())
+                        CrmAuditStatusEnum.PROCESS.getStatus(), CrmAuditStatusEnum.APPROVE.getStatus())
                 .groupBy("contract_id")
                 .in("contract_id", contractIds));
         // 获得金额

+ 12 - 1
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java

@@ -2,7 +2,8 @@ package cn.iocoder.yudao.module.infra.api.logger;
 
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
 
-import javax.validation.Valid;
+import jakarta.validation.Valid;
+import org.springframework.scheduling.annotation.Async;
 
 /**
  * API 访问日志的 API 接口
@@ -18,4 +19,14 @@ public interface ApiAccessLogApi {
      */
     void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO);
 
+    /**
+     * 【异步】创建 API 访问日志
+     *
+     * @param createDTO 访问日志 DTO
+     */
+    @Async
+    default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
+        createApiAccessLog(createDTO);
+    }
+
 }

+ 12 - 1
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java

@@ -2,7 +2,8 @@ package cn.iocoder.yudao.module.infra.api.logger;
 
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
 
-import javax.validation.Valid;
+import jakarta.validation.Valid;
+import org.springframework.scheduling.annotation.Async;
 
 /**
  * API 错误日志的 API 接口
@@ -18,4 +19,14 @@ public interface ApiErrorLogApi {
      */
     void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO);
 
+    /**
+     * 【异步】创建 API 异常日志
+     *
+     * @param createDTO 异常日志 DTO
+     */
+    @Async
+    default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
+        createApiErrorLog(createDTO);
+    }
+
 }

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

@@ -14,7 +14,6 @@ public enum CodegenFrontTypeEnum {
 
     VUE2(10), // Vue2 Element UI 标准模版
     VUE3(20), // Vue3 Element Plus 标准模版
-    VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版
     VUE3_VBEN(30), // Vue3 VBEN 模版
     ;
 

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

@@ -135,15 +135,6 @@ public class CodegenEngine {
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
             .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"),
                     vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
-            // Vue3 Schema 模版
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"),
-                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"),
-                    vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"),
-                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
-            .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"),
-                    vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
             // Vue3 vben 模版
             .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@@ -496,10 +487,6 @@ public class CodegenEngine {
                 "src/" + path;
     }
 
-    private static String vue3SchemaTemplatePath(String path) {
-        return "codegen/vue3_schema/" + path + ".vm";
-    }
-
     private static String vue3VbenTemplatePath(String path) {
         return "codegen/vue3_vben/" + path + ".vm";
     }

+ 1 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm

@@ -286,6 +286,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
         // 校验存在
         validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
         // 更新
+        ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
         ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
     }
 

+ 5 - 6
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm

@@ -64,12 +64,11 @@
           <el-checkbox
             v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
             :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-checkbox>
+            :label="dict.label"
+            :value="dict.value"
+          />
                 #else##没数据字典
-          <el-checkbox>请选择字典生成</el-checkbox>
+          <el-checkbox label="请选择字典生成" />
                 #end
         </el-checkbox-group>
       </el-form-item>
@@ -85,7 +84,7 @@
             {{ dict.label }}
           </el-radio>
                 #else##没数据字典
-          <el-radio label="1">请选择字典生成</el-radio>
+          <el-radio value="1">请选择字典生成</el-radio>
                 #end
         </el-radio-group>
       </el-form-item>

+ 10 - 12
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm

@@ -92,12 +92,11 @@
                 <el-checkbox
                   v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
                   :key="dict.value"
-                  :label="dict.value"
-                >
-                  {{ dict.label }}
-                </el-checkbox>
+                  :label="dict.label"
+                  :value="dict.value"
+                />
               #else##没数据字典
-                <el-checkbox>请选择字典生成</el-checkbox>
+                <el-checkbox label="请选择字典生成" />
               #end
             </el-checkbox-group>
           </el-form-item>
@@ -117,7 +116,7 @@
                   {{ dict.label }}
                 </el-radio>
               #else##没数据字典
-                <el-radio label="1">请选择字典生成</el-radio>
+                <el-radio value="1">请选择字典生成</el-radio>
               #end
             </el-radio-group>
           </el-form-item>
@@ -219,12 +218,11 @@
         <el-checkbox
           v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
           :key="dict.value"
-          :label="dict.value"
-        >
-          {{ dict.label }}
-        </el-checkbox>
+          :label="dict.label"
+          :value="dict.value"
+        />
               #else##没数据字典
-        <el-checkbox>请选择字典生成</el-checkbox>
+        <el-checkbox label="请选择字典生成" />
               #end
       </el-checkbox-group>
     </el-form-item>
@@ -240,7 +238,7 @@
           {{ dict.label }}
         </el-radio>
               #else##没数据字典
-        <el-radio label="1">请选择字典生成</el-radio>
+        <el-radio value="1">请选择字典生成</el-radio>
               #end
       </el-radio-group>
     </el-form-item>

+ 5 - 6
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm

@@ -75,12 +75,11 @@
           <el-checkbox
             v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
             :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-checkbox>
+            :label="dict.label"
+            :value="dict.value"
+          />
                 #else##没数据字典
-          <el-checkbox>请选择字典生成</el-checkbox>
+          <el-checkbox label="请选择字典生成" />
                 #end
         </el-checkbox-group>
       </el-form-item>
@@ -96,7 +95,7 @@
             {{ dict.label }}
           </el-radio>
                 #else##没数据字典
-          <el-radio label="1">请选择字典生成</el-radio>
+          <el-radio value="1">请选择字典生成</el-radio>
                 #end
         </el-radio-group>
       </el-form-item>

+ 0 - 46
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm

@@ -1,46 +0,0 @@
-import request from '@/config/axios'
-#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
-
-export interface ${simpleClassName}VO {
-    #foreach ($column in $columns)
-        #if ($column.createOperation || $column.updateOperation)
-            #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
-                    ${column.javaField}: number
-            #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime")
-                    ${column.javaField}: Date
-            #else
-                    ${column.javaField}: ${column.javaType.toLowerCase()}
-            #end
-        #end
-    #end
-}
-
-// 查询${table.classComment}列表
-export const get${simpleClassName}Page = async (params) => {
-  return await request.get({ url: '${baseURL}/page', params })
-}
-
-// 查询${table.classComment}详情
-export const get${simpleClassName} = async (id: number) => {
-  return await request.get({ url: '${baseURL}/get?id=' + id })
-}
-
-// 新增${table.classComment}
-export const create${simpleClassName} = async (data: ${simpleClassName}VO) => {
-  return await request.post({ url: '${baseURL}/create', data })
-}
-
-// 修改${table.classComment}
-export const update${simpleClassName} = async (data: ${simpleClassName}VO) => {
-  return await request.put({ url: '${baseURL}/update', data })
-}
-
-// 删除${table.classComment}
-export const delete${simpleClassName} = async (id: number) => {
-  return await request.delete({ url: '${baseURL}/delete?id=' + id })
-}
-
-// 导出${table.classComment} Excel
-export const export${simpleClassName}Api = async (params) => {
-  return await request.download({ url: '${baseURL}/export-excel', params })
-}

+ 0 - 124
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm

@@ -1,124 +0,0 @@
-import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
-import { dateFormatter } from '@/utils/formatTime'
-
-// 表单校验
-export const rules = reactive({
-#foreach ($column in $columns)
-#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
-#set($comment=$column.columnComment)
-  $column.javaField: [required],
-#end
-#end
-})
-
-// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
-const crudSchemas = reactive<CrudSchema[]>([
-#foreach($column in $columns)
-#if ($column.listOperation || $column.listOperationResult || $column.createOperation || $column.updateOperation)
-#set ($dictType = $column.dictType)
-#set ($javaField = $column.javaField)
-#set ($javaType = $column.javaType)
-  {
-    label: '${column.columnComment}',
-    field: '${column.javaField}',
-## ========= 字典部分 =========
-    #if ("" != $dictType)## 有数据字典
-    dictType: DICT_TYPE.$dictType.toUpperCase(),
-        #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
-    dictClass: 'number',
-        #elseif ($javaType == "String")
-    dictClass: 'string',
-        #elseif ($javaType == "Boolean")
-    dictClass: 'boolean',
-        #end
-    #end
-## ========= Table 表格部分 =========
-    #if (!$column.listOperationResult)
-    isTable: false,
-    #else
-      #if ($column.htmlType == "datetime")
-    formatter: dateFormatter,
-      #end
-    #end
-## ========= Search 表格部分 =========
-    #if ($column.listOperation)
-    isSearch: true,
-        #if ($column.htmlType == "datetime")
-    search: {
-      component: 'DatePicker',
-      componentProps: {
-        valueFormat: 'YYYY-MM-DD HH:mm:ss',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
-      }
-    },
-        #end
-    #end
-## ========= Form 表单部分 =========
-    #if ((!$column.createOperation && !$column.updateOperation) || $column.primaryKey)
-    isForm: false,
-    #else
-        #if($column.htmlType == "imageUpload")## 图片上传
-    form: {
-      component: 'UploadImg'
-    },
-        #elseif($column.htmlType == "fileUpload")## 文件上传
-    form: {
-      component: 'UploadFile'
-    },
-        #elseif($column.htmlType == "editor")## 文本编辑器
-    form: {
-      component: 'Editor',
-      componentProps: {
-        valueHtml: '',
-        height: 200
-      }
-    },
-        #elseif($column.htmlType == "select")## 下拉框
-    form: {
-      component: 'SelectV2'
-    },
-        #elseif($column.htmlType == "checkbox")## 多选框
-    form: {
-      component: 'Checkbox'
-    },
-        #elseif($column.htmlType == "radio")## 单选框
-    form: {
-      component: 'Radio'
-    },
-        #elseif($column.htmlType == "datetime")## 时间框
-    form: {
-      component: 'DatePicker',
-      componentProps: {
-        type: 'datetime',
-        valueFormat: 'x'
-      }
-    },
-        #elseif($column.htmlType == "textarea")## 文本框
-    form: {
-      component: 'Input',
-      componentProps: {
-        type: 'textarea',
-        rows: 4
-      },
-      colProps: {
-        span: 24
-      }
-    },
-        #elseif(${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer")## 文本框
-    form: {
-      component: 'InputNumber',
-      value: 0
-    },
-        #end
-    #end
-  },
-#end
-#end
-  {
-    label: '操作',
-    field: 'action',
-    isForm: false
-  }
-])
-export const { allSchemas } = useCrudSchemas(crudSchemas)

+ 0 - 65
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm

@@ -1,65 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <Form ref="formRef" :schema="allSchemas.formSchema" :rules="rules" v-loading="formLoading" />
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
-import { rules, allSchemas } from './${classNameVar}.data'
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      const data = await ${simpleClassName}Api.get${simpleClassName}(id)
-      formRef.value.setValues(data)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.getElFormRef().validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formRef.value.formModel as ${simpleClassName}Api.${simpleClassName}VO
-    if (formType.value === 'create') {
-      await ${simpleClassName}Api.create${simpleClassName}(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await ${simpleClassName}Api.update${simpleClassName}(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-</script>

+ 0 - 85
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm

@@ -1,85 +0,0 @@
-<template>
-  <!-- 搜索工作栏 -->
-  <ContentWrap>
-    <Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams">
-      <!-- 新增等操作按钮 -->
-      <template #actionMore>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['${permissionPrefix}:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-      </template>
-    </Search>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <Table
-      :columns="allSchemas.tableColumns"
-      :data="tableObject.tableList"
-      :loading="tableObject.loading"
-      :pagination="{
-        total: tableObject.total
-      }"
-      v-model:pageSize="tableObject.pageSize"
-      v-model:currentPage="tableObject.currentPage"
-    >
-      <template #action="{ row }">
-        <el-button
-          link
-          type="primary"
-          @click="openForm('update', row.id)"
-          v-hasPermi="['${permissionPrefix}:update']"
-        >
-          编辑
-        </el-button>
-        <el-button
-          link
-          type="danger"
-          v-hasPermi="['${permissionPrefix}:delete']"
-          @click="handleDelete(row.id)"
-        >
-          删除
-        </el-button>
-      </template>
-    </Table>
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <${simpleClassName}Form ref="formRef" @success="getList" />
-</template>
-<script setup lang="ts" name="${table.className}">
-import { allSchemas } from './${classNameVar}.data'
-import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
-import ${simpleClassName}Form from './${simpleClassName}Form.vue'
-
-// tableObject:表格的属性对象,可获得分页大小、条数等属性
-// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
-// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
-const { tableObject, tableMethods } = useTable({
-  getListApi: ${simpleClassName}Api.get${simpleClassName}Page, // 分页接口
-  delListApi: ${simpleClassName}Api.delete${simpleClassName} // 删除接口
-})
-// 获得表格的各种操作
-const { getList, setSearchParams } = tableMethods
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
-
-/** 删除按钮操作 */
-const handleDelete = (id: number) => {
-  tableMethods.delList(id, false)
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>

+ 33 - 9
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm

@@ -42,9 +42,17 @@ export const searchFormSchema: FormSchema[] = [
 #foreach($column in $columns)
 #if ($column.listOperation)
   #set ($dictType=$column.dictType)
+  #set ($javaType = $column.javaType)
   #set ($javaField = $column.javaField)
   #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
   #set ($comment=$column.columnComment)
+  #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+    #set ($dictMethod = "number")
+  #elseif ($javaType == "String")
+    #set ($dictMethod = "string")
+  #elseif ($javaType == "Boolean")
+    #set ($dictMethod = "boolean")
+  #end
   {
     label: '${comment}',
     field: '${javaField}',
@@ -54,16 +62,16 @@ export const searchFormSchema: FormSchema[] = [
     component: 'Select',
     componentProps: {
       #if ("" != $dictType)## 设置了 dictType 数据字典的情况
-        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()),
+        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else## 未设置 dictType 数据字典的情况
         options: [],
       #end
     },
   #elseif ($column.htmlType == "radio")
-    component: 'Radio',
+    component: 'RadioButtonGroup',
     componentProps: {
       #if ("" != $dictType)## 设置了 dictType 数据字典的情况
-        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()),
+        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else## 未设置 dictType 数据字典的情况
         options: [],
       #end
@@ -87,9 +95,17 @@ export const createFormSchema: FormSchema[] = [
 #foreach($column in $columns)
 #if ($column.createOperation)
   #set ($dictType = $column.dictType)
+  #set ($javaType = $column.javaType)
   #set ($javaField = $column.javaField)
   #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
   #set ($comment = $column.columnComment)
+  #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+    #set ($dictMethod = "number")
+  #elseif ($javaType == "String")
+    #set ($dictMethod = "string")
+  #elseif ($javaType == "Boolean")
+    #set ($dictMethod = "boolean")
+  #end
 #if (!$column.primaryKey)## 忽略主键,不用在表单里
   {
     label: '${comment}',
@@ -117,7 +133,7 @@ export const createFormSchema: FormSchema[] = [
     component: 'Select',
     componentProps: {
       #if ("" != $dictType)## 有数据字典
-        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
+        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else##没数据字典
         options:[],
       #end
@@ -126,7 +142,7 @@ export const createFormSchema: FormSchema[] = [
     component: 'Checkbox',
     componentProps: {
       #if ("" != $dictType)## 有数据字典
-        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
+        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else##没数据字典
         options:[],
       #end
@@ -135,7 +151,7 @@ export const createFormSchema: FormSchema[] = [
     component: 'RadioButtonGroup',
     componentProps: {
       #if ("" != $dictType)## 有数据字典
-        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
+        options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else##没数据字典
         options:[],
       #end
@@ -166,9 +182,17 @@ export const updateFormSchema: FormSchema[] = [
 #foreach($column in $columns)
 #if ($column.updateOperation)
 #set ($dictType = $column.dictType)
+#set ($javaType = $column.javaType)
 #set ($javaField = $column.javaField)
 #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
 #set ($comment = $column.columnComment)
+#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+  #set ($dictMethod = "number")
+#elseif ($javaType == "String")
+  #set ($dictMethod = "string")
+#elseif ($javaType == "Boolean")
+  #set ($dictMethod = "boolean")
+#end
   #if (!$column.primaryKey)## 忽略主键,不用在表单里
   {
     label: '${comment}',
@@ -196,7 +220,7 @@ export const updateFormSchema: FormSchema[] = [
     component: 'Select',
     componentProps: {
       #if ("" != $dictType)## 有数据字典
-      options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
+      options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else##没数据字典
       options:[],
       #end
@@ -205,7 +229,7 @@ export const updateFormSchema: FormSchema[] = [
     component: 'Checkbox',
     componentProps: {
       #if ("" != $dictType)## 有数据字典
-      options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
+      options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else##没数据字典
       options:[],
       #end
@@ -214,7 +238,7 @@ export const updateFormSchema: FormSchema[] = [
     component: 'RadioButtonGroup',
     componentProps: {
       #if ("" != $dictType)## 有数据字典
-      options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'),
+      options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
       #else##没数据字典
       options:[],
       #end

+ 9 - 0
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * 商品 SPU 信息 Response DTO
  *
@@ -68,6 +70,13 @@ public class ProductSpuRespDTO {
 
     // ========== 物流相关字段 =========
 
+    /**
+     * 配送方式数组
+     *
+     * 对应 DeliveryTypeEnum 枚举
+     */
+    private List<Integer> deliveryTypes;
+
     /**
      * 物流配置模板编号
      *

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

@@ -67,7 +67,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
         // 校验 SPU
         ProductSpuDO spu = validateSpu(sku.getSpuId());
         // 校验评论
-        validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderId());
+        validateCommentExists(createReqDTO.getUserId(), createReqDTO.getOrderItemId());
         // 获取用户详细信息
         MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId());
 

+ 5 - 5
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java

@@ -2,9 +2,9 @@ package cn.iocoder.yudao.module.promotion.api.combination;
 
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO;
+import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
-
-import javax.validation.Valid;
+import jakarta.validation.Valid;
 
 /**
  * 拼团记录 API 接口
@@ -33,13 +33,13 @@ public interface CombinationRecordApi {
     CombinationRecordCreateRespDTO createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
 
     /**
-     * 查询拼团记录是否成功
+     * 基于订单编号,查询拼团记录
      *
      * @param userId  用户编号
      * @param orderId 订单编号
-     * @return 拼团是否成功
+     * @return 拼团记录
      */
-    boolean isCombinationRecordSuccess(Long userId, Long orderId);
+    CombinationRecordRespDTO getCombinationRecordByOrderId(Long userId, Long orderId);
 
     /**
      * 【下单前】校验是否满足拼团活动条件

+ 110 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java

@@ -0,0 +1,110 @@
+package cn.iocoder.yudao.module.promotion.api.combination.dto;
+
+import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 拼团记录 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class CombinationRecordRespDTO {
+
+    /**
+     * 编号,主键自增
+     */
+    private Long id;
+
+    /**
+     * 拼团活动编号
+     *
+     * 关联 CombinationActivityDO 的 id 字段
+     */
+    private Long activityId;
+    /**
+     * 拼团商品单价
+     *
+     * 冗余 CombinationProductDO 的 combinationPrice 字段
+     */
+    private Integer combinationPrice;
+    /**
+     * SPU 编号
+     */
+    private Long spuId;
+    /**
+     * 商品名字
+     */
+    private String spuName;
+    /**
+     * 商品图片
+     */
+    private String picUrl;
+    /**
+     * SKU 编号
+     */
+    private Long skuId;
+    /**
+     * 购买的商品数量
+     */
+    private Integer count;
+
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 用户昵称
+     */
+    private String nickname;
+    /**
+     * 用户头像
+     */
+    private String avatar;
+
+    /**
+     * 团长编号
+     */
+    private Long headId;
+    /**
+     * 开团状态
+     *
+     * 关联 {@link CombinationRecordStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 订单编号
+     */
+    private Long orderId;
+    /**
+     * 开团需要人数
+     *
+     * 关联 CombinationActivityDO 的 userSize 字段
+     */
+    private Integer userSize;
+    /**
+     * 已加入拼团人数
+     */
+    private Integer userCount;
+    /**
+     * 是否虚拟成团
+     */
+    private Boolean virtualGroup;
+
+    /**
+     * 过期时间
+     */
+    private LocalDateTime expireTime;
+    /**
+     * 开始时间 (订单付款后开始的时间)
+     */
+    private LocalDateTime startTime;
+    /**
+     * 结束时间(成团时间/失败时间)
+     */
+    private LocalDateTime endTime;
+
+}

+ 25 - 6
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java

@@ -2,9 +2,10 @@ package cn.iocoder.yudao.module.promotion.api.coupon;
 
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
-import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
+import jakarta.validation.Valid;
 
-import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 优惠劵 API 接口
@@ -13,6 +14,15 @@ import javax.validation.Valid;
  */
 public interface CouponApi {
 
+    /**
+     * 获得用户的优惠劵列表
+     *
+     * @param userId 用户编号
+     * @param status 优惠劵状态
+     * @return 优惠劵列表
+     */
+    List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status);
+
     /**
      * 使用优惠劵
      *
@@ -28,11 +38,20 @@ public interface CouponApi {
     void returnUsedCoupon(Long id);
 
     /**
-     * 校验优惠劵
+     * 【管理员】给指定用户批量发送优惠券
+     *
+     * @param giveCoupons  key: 优惠劵模版编号,value:对应的数量
+     * @param userId      用户编号
+     * @return 优惠券编号列表
+     */
+    List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
+
+    /**
+     * 【管理员】作废指定用户的指定优惠劵
      *
-     * @param validReqDTO 校验请求
-     * @return 优惠劵
+     * @param giveCouponIds  赠送的优惠券编号
+     * @param userId         用户编号
      */
-    CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO);
+    void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
 
 }

+ 0 - 27
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java

@@ -1,27 +0,0 @@
-package cn.iocoder.yudao.module.promotion.api.coupon.dto;
-
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-
-/**
- * 优惠劵使用 Request DTO
- *
- * @author 芋道源码
- */
-@Data
-public class CouponValidReqDTO {
-
-    /**
-     * 优惠劵编号
-     */
-    @NotNull(message = "优惠劵编号不能为空")
-    private Long id;
-
-    /**
-     * 用户编号
-     */
-    @NotNull(message = "用户编号不能为空")
-    private Long userId;
-
-}

+ 40 - 12
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java

@@ -1,9 +1,14 @@
 package cn.iocoder.yudao.module.promotion.api.reward.dto;
 
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import lombok.Data;
 
+import java.io.Serializable;
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 满减送活动的匹配 Response DTO
@@ -21,6 +26,24 @@ public class RewardActivityMatchRespDTO {
      * 活动标题
      */
     private String name;
+    /**
+     * 状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endTime;
+    /**
+     * 备注
+     */
+    private String remark;
     /**
      * 条件类型
      *
@@ -28,21 +51,25 @@ public class RewardActivityMatchRespDTO {
      */
     private Integer conditionType;
     /**
-     * 优惠规则的数组
+     * 商品范围
+     *
+     * 枚举 {@link PromotionProductScopeEnum}
      */
-    private List<Rule> rules;
-
+    private Integer productScope;
     /**
      * 商品 SPU 编号的数组
      */
-    private List<Long> spuIds;
+    private List<Long> productScopeValues;
+    /**
+     * 优惠规则的数组
+     */
+    private List<Rule> rules;
 
-    // TODO 芋艿:后面 RewardActivityRespDTO 有了之后,Rule 可以放过去
     /**
      * 优惠规则
      */
     @Data
-    public static class Rule {
+    public static class Rule implements Serializable {
 
         /**
          * 优惠门槛
@@ -64,13 +91,14 @@ public class RewardActivityMatchRespDTO {
          */
         private Integer point;
         /**
-         * 赠送的优惠劵编号的数组
-         */
-        private List<Long> couponIds;
-        /**
-         * 赠送的优惠券数量的数组
+         * 赠送的优惠劵
+         *
+         * key: 优惠劵模版编号
+         * value:对应的优惠券数量
+         *
+         * 目的:用于订单支付后赠送优惠券
          */
-        private List<Integer> couponCounts;
+        private Map<Long, Integer> giveCouponTemplateCounts;
 
     }
 

+ 2 - 3
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java

@@ -20,8 +20,6 @@ public interface ErrorCodeConstants {
     ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1_013_002_000, "Banner 不存在");
 
     // ========== Coupon 相关 1-013-003-000 ============
-    ErrorCode COUPON_NO_MATCH_SPU = new ErrorCode(1_013_003_000, "优惠劵没有可使用的商品!");
-    ErrorCode COUPON_NO_MATCH_MIN_PRICE = new ErrorCode(1_013_003_001, "所结算的商品中未满足使用的金额");
 
     // ========== 优惠劵模板 1-013-004-000 ==========
     ErrorCode COUPON_TEMPLATE_NOT_EXISTS = new ErrorCode(1_013_004_000, "优惠劵模板不存在");
@@ -44,7 +42,8 @@ public interface ErrorCodeConstants {
     ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改");
     ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除");
     ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭");
-    ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1_013_006_005, "满减送活动已结束,不能关闭");
+    ErrorCode REWARD_ACTIVITY_SCOPE_ALL_EXISTS = new ErrorCode(1_013_006_005, "已存在商品范围为全场的满减送活动");
+    ErrorCode REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS = new ErrorCode(1_013_006_006, "存在商品类型参加了其它满减送活动");
 
     // ========== TODO 空着 1-013-007-000 ============
 

+ 16 - 4
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/common/PromotionProductScopeEnum.java

@@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 
 import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * 营销的商品范围枚举
@@ -15,10 +16,9 @@ import java.util.Arrays;
 @AllArgsConstructor
 public enum PromotionProductScopeEnum implements IntArrayValuable {
 
-    ALL(1, "通用券"), // 全部商品
-    SPU(2, "商品券"), // 指定商品
-    CATEGORY(3, "品类券"), // 指定品类
-    ;
+    ALL(1, "全部商品"),
+    SPU(2, "指定商品"),
+    CATEGORY(3, "指定品类");
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray();
 
@@ -36,4 +36,16 @@ public enum PromotionProductScopeEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
+    public static boolean isAll(Integer scope) {
+        return Objects.equals(scope, ALL.scope);
+    }
+
+    public static boolean isSpu(Integer scope) {
+        return Objects.equals(scope, SPU.scope);
+    }
+
+    public static boolean isCategory(Integer scope) {
+        return Objects.equals(scope, CATEGORY.scope);
+    }
+
 }

+ 1 - 2
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponStatusEnum.java

@@ -17,8 +17,7 @@ public enum CouponStatusEnum implements IntArrayValuable {
 
     UNUSED(1, "未使用"),
     USED(2, "已使用"),
-    EXPIRE(3, "已过期"),
-    ;
+    EXPIRE(3, "已过期");
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray();
 

+ 5 - 11
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java

@@ -1,20 +1,17 @@
 package cn.iocoder.yudao.module.promotion.api.combination;
 
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO;
+import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
 import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
-import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COMBINATION_RECORD_NOT_EXISTS;
-
 /**
  * 拼团活动 API 实现类
  *
@@ -38,12 +35,9 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
     }
 
     @Override
-    public boolean isCombinationRecordSuccess(Long userId, Long orderId) {
+    public CombinationRecordRespDTO getCombinationRecordByOrderId(Long userId, Long orderId) {
         CombinationRecordDO record = combinationRecordService.getCombinationRecord(userId, orderId);
-        if (record == null) {
-            throw exception(COMBINATION_RECORD_NOT_EXISTS);
-        }
-        return CombinationRecordStatusEnum.isSuccess(record.getStatus());
+        return BeanUtils.toBean(record, CombinationRecordRespDTO.class);
     }
 
     @Override

+ 16 - 7
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java

@@ -1,16 +1,16 @@
 package cn.iocoder.yudao.module.promotion.api.coupon;
 
 
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
-import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
-import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
-import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 优惠劵 API 实现类
@@ -24,6 +24,11 @@ public class CouponApiImpl implements CouponApi {
     @Resource
     private CouponService couponService;
 
+    @Override
+    public List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status) {
+        return BeanUtils.toBean(couponService.getCouponList(userId, status), CouponRespDTO.class);
+    }
+
     @Override
     public void useCoupon(CouponUseReqDTO useReqDTO) {
         couponService.useCoupon(useReqDTO.getId(), useReqDTO.getUserId(),
@@ -36,9 +41,13 @@ public class CouponApiImpl implements CouponApi {
     }
 
     @Override
-    public CouponRespDTO validateCoupon(CouponValidReqDTO validReqDTO) {
-        CouponDO coupon = couponService.validCoupon(validReqDTO.getId(), validReqDTO.getUserId());
-        return CouponConvert.INSTANCE.convert(coupon);
+    public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
+        return couponService.takeCouponsByAdmin(giveCoupons, userId);
+    }
+
+    @Override
+    public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
+        couponService.invalidateCouponsByAdmin(giveCouponIds, userId);
     }
 
 }

+ 22 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.promotion.controller.admin.combination;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
@@ -16,18 +17,20 @@ import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordSe
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import static cn.hutool.core.collection.CollectionUtil.newArrayList;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
 @Tag(name = "管理后台 - 拼团活动")
@@ -87,6 +90,23 @@ public class CombinationActivityController {
         return success(CombinationActivityConvert.INSTANCE.convert(activity, products));
     }
 
+    @GetMapping("/list-by-ids")
+    @Operation(summary = "获得拼团活动列表,基于活动编号数组")
+    @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
+    public CommonResult<List<CombinationActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
+        // 1. 获得开启的活动列表
+        List<CombinationActivityDO> activityList = combinationActivityService.getCombinationActivityListByIds(ids);
+        activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
+        if (CollUtil.isEmpty(activityList)) {
+            return success(Collections.emptyList());
+        }
+        // 2. 拼接返回
+        List<CombinationProductDO> productList = combinationActivityService.getCombinationProductListByActivityIds(
+                convertList(activityList, CombinationActivityDO::getId));
+        List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId));
+        return success(CombinationActivityConvert.INSTANCE.convertList(activityList, productList, spuList));
+    }
+
     @GetMapping("/page")
     @Operation(summary = "获得拼团活动分页")
     @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')")

+ 10 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java

@@ -27,4 +27,14 @@ public class CombinationActivityRespVO extends CombinationActivityBaseVO {
     @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED)
     private List<CombinationProductRespVO> products;
 
+    @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜")
+    private String spuName; // 从 SPU 的 name 读取
+    @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
+    private String picUrl; // 从 SPU 的 picUrl 读取
+    @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
+    private Integer marketPrice; // 从 SPU 的 marketPrice 读取
+
+    @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+    private Integer combinationPrice; // 从 products 获取最小 price 读取
+
 }

+ 5 - 6
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/RewardActivityController.java

@@ -2,23 +2,22 @@ package cn.iocoder.yudao.module.promotion.controller.admin.reward;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
-import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
-
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
 @Tag(name = "管理后台 - 满减送活动")
@@ -69,7 +68,7 @@ public class RewardActivityController {
     @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
     public CommonResult<RewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) {
         RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id);
-        return success(RewardActivityConvert.INSTANCE.convert(rewardActivity));
+        return success(BeanUtils.toBean(rewardActivity, RewardActivityRespVO.class));
     }
 
     @GetMapping("/page")
@@ -77,7 +76,7 @@ public class RewardActivityController {
     @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')")
     public CommonResult<PageResult<RewardActivityRespVO>> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) {
         PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(pageVO);
-        return success(RewardActivityConvert.INSTANCE.convertPage(pageResult));
+        return success(BeanUtils.toBean(pageResult, RewardActivityRespVO.class));
     }
 
 }

+ 22 - 21
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java

@@ -6,18 +6,17 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.Future;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
-import org.springframework.format.annotation.DateTimeFormat;
 
-import javax.validation.Valid;
-import javax.validation.constraints.AssertTrue;
-import javax.validation.constraints.Future;
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotNull;
 import java.time.LocalDateTime;
 import java.util.List;
-
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+import java.util.Map;
+import java.util.Objects;
 
 /**
 * 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用
@@ -32,12 +31,10 @@ public class RewardActivityBaseVO {
 
     @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @NotNull(message = "开始时间不能为空")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime startTime;
 
     @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @NotNull(message = "结束时间不能为空")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     @Future(message = "结束时间必须大于当前时间")
     private LocalDateTime endTime;
 
@@ -54,8 +51,8 @@ public class RewardActivityBaseVO {
     @InEnum(value = PromotionProductScopeEnum.class, message = "商品范围必须是 {value}")
     private Integer productScope;
 
-    @Schema(description = "商品 SPU 编号的数组", example = "1,2,3")
-    private List<Long> productSpuIds;
+    @Schema(description = "商品范围编号的数组", example = "[1, 3]")
+    private List<Long> productScopeValues;
 
     /**
      * 优惠规则的数组
@@ -76,24 +73,28 @@ public class RewardActivityBaseVO {
         private Integer discountPrice;
 
         @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+        @NotNull(message = "规则是否包邮不能为空")
         private Boolean freeDelivery;
 
         @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
-        @Min(value = 1L, message = "赠送的积分必须大于等于 1")
         private Integer point;
 
-        @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3")
-        private List<Long> couponIds;
-
-        @Schema(description = "赠送的优惠券数量的数组", example = "1,2,3")
-        private List<Integer> couponCounts;
+        @Schema(description = "赠送的优惠劵编号的数组")
+        private Map<Long, Integer> giveCouponTemplateCounts;
 
-        @AssertTrue(message = "优惠劵和数量必须一一对应")
+        @AssertTrue(message = "赠送的积分不能小于 0")
         @JsonIgnore
-        public boolean isCouponCountsValid() {
-            return CollUtil.size(couponCounts) == CollUtil.size(couponCounts);
+        public boolean isPointValid() {
+            return point == null || point >= 0;
         }
 
     }
 
+    @AssertTrue(message = "商品范围编号的数组不能为空")
+    @JsonIgnore
+    public boolean isProductScopeValuesValid() {
+        return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空
+                || CollUtil.isNotEmpty(productScopeValues);
+    }
+
 }

+ 48 - 20
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java

@@ -2,8 +2,11 @@ package cn.iocoder.yudao.module.promotion.controller.app.activity;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
+import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
@@ -11,7 +14,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.promotion.service.bargain.BargainActivityService;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
@@ -21,16 +24,15 @@ import cn.iocoder.yudao.module.promotion.service.seckill.SeckillActivityService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.*;
-import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -52,6 +54,9 @@ public class AppActivityController {
     @Resource
     private RewardActivityService rewardActivityService;
 
+    @Resource
+    private ProductSpuApi productSpuApi;
+
     @GetMapping("/list-by-spu-id")
     @Operation(summary = "获得单个商品,近期参与的每个活动")
     @Parameter(name = "spuId", description = "商品编号", required = true)
@@ -87,7 +92,7 @@ public class AppActivityController {
         // 4. 限时折扣活动
         getDiscountActivities(spuIds, now, activityList);
         // 5. 满减送活动
-        getRewardActivities(spuIds, now, activityList);
+        getRewardActivityList(spuIds, now, activityList);
         return activityList;
     }
 
@@ -144,28 +149,51 @@ public class AppActivityController {
                 item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime())));
     }
 
-    private void getRewardActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
-        // TODO @puhui999:有 3 范围,不只 spuId,还有 categoryId,全部
-        List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityBySpuIdsAndStatusAndDateTimeLt(
-                spuIds, PromotionActivityStatusEnum.RUN.getStatus(), now);
+    private void getRewardActivityList(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
+        // 1.1 获得所有的活动
+        List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(
+                CommonStatusEnum.ENABLE.getStatus(), now);
         if (CollUtil.isEmpty(rewardActivityList)) {
             return;
         }
+        // 1.2 获得所有的商品信息
+        List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
+        if (CollUtil.isEmpty(spuList)) {
+            return;
+        }
 
-        Map<Long, Optional<RewardActivityDO>> spuIdAndActivityMap = spuIds.stream()
-                .collect(Collectors.toMap(
-                        spuId -> spuId,
-                        spuId -> rewardActivityList.stream()
-                                .filter(activity -> activity.getProductSpuIds().contains(spuId))
-                                .max(Comparator.comparing(RewardActivityDO::getCreateTime))));
-        for (Long supId : spuIdAndActivityMap.keySet()) {
-            if (!spuIdAndActivityMap.get(supId).isPresent()) {
-                continue;
+        // 2. 构建活动
+        for (RewardActivityDO rewardActivity : rewardActivityList) {
+            // 情况一:所有商品都能参加
+            if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
+                buildAppActivityRespVO(rewardActivity, spuIds, activityList);
+            }
+            // 情况二:指定商品参加
+            if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
+                List<Long> fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id ->
+                        rewardActivity.getProductScopeValues().contains(id)).toList();
+                buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
             }
+            // 情况三:指定商品类型参加
+            if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
+                List<Long> fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues()
+                        .contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList();
+                buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
+            }
+        }
+    }
 
-            RewardActivityDO rewardActivityDO = spuIdAndActivityMap.get(supId).get();
-            activityList.add(new AppActivityRespVO(rewardActivityDO.getId(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
-                    rewardActivityDO.getName(), supId, rewardActivityDO.getStartTime(), rewardActivityDO.getEndTime()));
+    private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection<Long> spuIds,
+                                               List<AppActivityRespVO> activityList) {
+        for (Long spuId : spuIds) {
+            // 校验商品是否已经加入过活动
+            if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) &&
+                    ObjUtil.equal(appActivity.getSpuId(), spuId))) {
+                continue;
+            }
+            activityList.add(new AppActivityRespVO(rewardActivity.getId(),
+                    PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId,
+                    rewardActivity.getStartTime(), rewardActivity.getEndTime()));
         }
     }
 

+ 18 - 38
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java

@@ -14,24 +14,20 @@ import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivity
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import javax.annotation.Resource;
-import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
 @Tag(name = "用户 APP - 拼团活动")
@@ -40,45 +36,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
 @Validated
 public class AppCombinationActivityController {
 
-    /**
-     * {@link AppCombinationActivityRespVO} 缓存,通过它异步刷新 {@link #getCombinationActivityList0(Integer)} 所要的首页数据
-     */
-    private final LoadingCache<Integer, List<AppCombinationActivityRespVO>> combinationActivityListCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
-            new CacheLoader<Integer, List<AppCombinationActivityRespVO>>() {
-
-                @Override
-                public List<AppCombinationActivityRespVO> load(Integer count) {
-                    return getCombinationActivityList0(count);
-                }
-
-            });
-
     @Resource
     private CombinationActivityService activityService;
 
     @Resource
     private ProductSpuApi spuApi;
 
-    @GetMapping("/list")
-    @Operation(summary = "获得拼团活动列表", description = "用于小程序首页")
-    @Parameter(name = "count", description = "需要展示的数量", example = "6")
-    public CommonResult<List<AppCombinationActivityRespVO>> getCombinationActivityList(
-            @RequestParam(name = "count", defaultValue = "6") Integer count) {
-        return success(combinationActivityListCache.getUnchecked(count));
-    }
-
-    private List<AppCombinationActivityRespVO> getCombinationActivityList0(Integer count) {
-        List<CombinationActivityDO> activityList = activityService.getCombinationActivityListByCount(count);
-        if (CollUtil.isEmpty(activityList)) {
-            return Collections.emptyList();
-        }
-        // 拼接返回
-        List<CombinationProductDO> productList = activityService.getCombinationProductListByActivityIds(
-                convertList(activityList, CombinationActivityDO::getId));
-        List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId));
-        return CombinationActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList);
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得拼团活动分页")
     public CommonResult<PageResult<AppCombinationActivityRespVO>> getCombinationActivityPage(PageParam pageParam) {
@@ -93,6 +56,23 @@ public class AppCombinationActivityController {
         return success(CombinationActivityConvert.INSTANCE.convertAppPage(pageResult, productList, spuList));
     }
 
+    @GetMapping("/list-by-ids")
+    @Operation(summary = "获得拼团活动列表,基于活动编号数组")
+    @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
+    public CommonResult<List<AppCombinationActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
+        // 1. 获得开启的活动列表
+        List<CombinationActivityDO> activityList = activityService.getCombinationActivityListByIds(ids);
+        activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
+        if (CollUtil.isEmpty(activityList)) {
+            return success(Collections.emptyList());
+        }
+        // 2. 拼接返回
+        List<CombinationProductDO> productList = activityService.getCombinationProductListByActivityIds(
+                convertList(activityList, CombinationActivityDO::getId));
+        List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, CombinationActivityDO::getSpuId));
+        return success(CombinationActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList));
+    }
+
     @GetMapping("/get-detail")
     @Operation(summary = "获得拼团活动明细")
     @Parameter(name = "id", description = "活动编号", required = true, example = "1024")

+ 5 - 6
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java

@@ -19,15 +19,14 @@ public class AppCombinationActivityRespVO {
     @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
     private Long spuId;
 
+    @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个白菜")
+    private String spuName; // 从 SPU 的 name 读取
     @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
-    // 从 SPU 的 picUrl 读取
-    private String picUrl;
-
+    private String picUrl; // 从 SPU 的 picUrl 读取
     @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
-    // 从 SPU 的 marketPrice 读取
-    private Integer marketPrice;
+    private Integer marketPrice; // 从 SPU 的 marketPrice 读取
 
     @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
-    private Integer combinationPrice;
+    private Integer combinationPrice; // 从 products 获取最小 price 读取
 
 }

+ 5 - 12
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java

@@ -5,7 +5,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
-import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.*;
+import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO;
+import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO;
+import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponTakeReqVO;
 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
@@ -15,13 +17,12 @@ import cn.iocoder.yudao.module.promotion.service.coupon.CouponTemplateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.Collections;
-import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -56,14 +57,6 @@ public class AppCouponController {
         return success(canTakeAgain);
     }
 
-    @GetMapping("/match-list")
-    @Operation(summary = "获得匹配指定商品的优惠劵列表", description = "用于下单页,展示优惠劵列表")
-    public CommonResult<List<AppCouponMatchRespVO>> getMatchCouponList(AppCouponMatchReqVO matchReqVO) {
-        // todo: 优化:优惠金额倒序
-        List<CouponDO> list = couponService.getMatchCouponList(getLoginUserId(), matchReqVO);
-        return success(BeanUtils.toBean(list, AppCouponMatchRespVO.class));
-    }
-
     @GetMapping("/page")
     @Operation(summary = "我的优惠劵列表")
     @PreAuthenticated

+ 0 - 30
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-import java.util.List;
-
-@Schema(description = "用户 App - 优惠劵的匹配 Request VO")
-@Data
-public class AppCouponMatchReqVO {
-
-    @Schema(description = "商品金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @NotNull(message = "商品金额不能为空")
-    private Integer price;
-
-    @Schema(description = "商品 SPU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
-    @NotEmpty(message = "商品 SPU 编号不能为空")
-    private List<Long> spuIds;
-
-    @Schema(description = "商品 SKU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
-    @NotEmpty(message = "商品 SKU 编号不能为空")
-    private List<Long> skuIds;
-
-    @Schema(description = "分类编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[10, 20]")
-    @NotEmpty(message = "分类编号不能为空")
-    private List<Long> categoryIds;
-
-}

+ 0 - 16
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java

@@ -1,16 +0,0 @@
-package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-@Schema(description = "用户 App - 优惠劵 Response VO")
-@Data
-public class AppCouponMatchRespVO extends AppCouponRespVO {
-
-    @Schema(description = "是否匹配", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    private Boolean match;
-
-    @Schema(description = "匹配条件的提示", example = "所结算商品没有符合条件的商品")
-    private String description;
-
-}

+ 0 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import javax.validation.constraints.Min;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -42,7 +41,6 @@ public class AppCouponRespVO {
     private Integer discountPercent;
 
     @Schema(description = "优惠金额", example = "10")
-    @Min(value = 0, message = "优惠金额需要大于等于 0")
     private Integer discountPrice;
 
     @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用

+ 20 - 17
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
@@ -127,40 +128,42 @@ public interface CombinationActivityConvert {
                 .setSpuName(spu.getName()).setPicUrl(sku.getPicUrl());
     }
 
-    List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list);
-
-    default List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list,
-                                                              List<CombinationProductDO> productList,
-                                                              List<ProductSpuRespDTO> spuList) {
-        List<AppCombinationActivityRespVO> activityList = convertAppList(list);
+    default List<CombinationActivityRespVO> convertList(List<CombinationActivityDO> list,
+                                                        List<CombinationProductDO> productList,
+                                                        List<ProductSpuRespDTO> spuList) {
+        List<CombinationActivityRespVO> activityList = BeanUtils.toBean(list, CombinationActivityRespVO.class);
         Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
         Map<Long, List<CombinationProductDO>> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId);
         return CollectionUtils.convertList(activityList, item -> {
             // 设置 product 信息
             item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice));
             // 设置 SPU 信息
-            findAndThen(spuMap, item.getSpuId(), spu -> item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
+            findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
+                    .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
             return item;
         });
     }
 
-    PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result);
-
-    default PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result,
-                                                                    List<CombinationProductDO> productList,
-                                                                    List<ProductSpuRespDTO> spuList) {
-        PageResult<AppCombinationActivityRespVO> appPage = convertAppPage(result);
+    default List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list,
+                                                              List<CombinationProductDO> productList,
+                                                              List<ProductSpuRespDTO> spuList) {
+        List<AppCombinationActivityRespVO> activityList = BeanUtils.toBean(list, AppCombinationActivityRespVO.class);
         Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
         Map<Long, List<CombinationProductDO>> productMap = convertMultiMap(productList, CombinationProductDO::getActivityId);
-        List<AppCombinationActivityRespVO> list = CollectionUtils.convertList(appPage.getList(), item -> {
+        return CollectionUtils.convertList(activityList, item -> {
             // 设置 product 信息
             item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice));
             // 设置 SPU 信息
-            findAndThen(spuMap, item.getSpuId(), spu -> item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
+            findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
+                    .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
             return item;
         });
-        appPage.setList(list);
-        return appPage;
+    }
+
+    default PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result,
+                                                                    List<CombinationProductDO> productList,
+                                                                    List<ProductSpuRespDTO> spuList) {
+        return new PageResult<>(convertAppList(result.getList(), productList, spuList), result.getTotal());
     }
 
     AppCombinationActivityDetailRespVO convert2(CombinationActivityDO combinationActivity);

+ 0 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java

@@ -4,9 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
-import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchRespVO;
 import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO;
-import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
@@ -16,7 +14,6 @@ import org.mapstruct.factory.Mappers;
 
 import java.time.LocalDateTime;
 import java.util.Collection;
-import java.util.List;
 
 /**
  * 优惠劵 Convert

+ 0 - 29
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/reward/RewardActivityConvert.java

@@ -1,29 +0,0 @@
-package cn.iocoder.yudao.module.promotion.convert.reward;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityRespVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
-import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-
-/**
- * 满减送活动 Convert
- *
- * @author 芋道源码
- */
-@Mapper
-public interface RewardActivityConvert {
-
-    RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class);
-
-    RewardActivityDO convert(RewardActivityCreateReqVO bean);
-
-    RewardActivityDO convert(RewardActivityUpdateReqVO bean);
-
-    RewardActivityRespVO convert(RewardActivityDO bean);
-
-    PageResult<RewardActivityRespVO> convertPage(PageResult<RewardActivityDO> page);
-
-}

+ 0 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponDO.java

@@ -50,7 +50,6 @@ public class CouponDO extends BaseDO {
      *
      * 枚举 {@link CouponStatusEnum}
      */
-    // TODO 芋艿:已作废?
     private Integer status;
 
     // TODO 芋艿:发放 adminid?

+ 11 - 9
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/reward/RewardActivityDO.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.promotion.dal.dataobject.reward;
 
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -16,6 +16,7 @@ import lombok.EqualsAndHashCode;
 import java.io.Serializable;
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 满减送活动 DO
@@ -40,7 +41,7 @@ public class RewardActivityDO extends BaseDO {
     /**
      * 状态
      *
-     * 枚举 {@link PromotionActivityStatusEnum}
+     * 枚举 {@link CommonStatusEnum}
      */
     private Integer status;
     /**
@@ -71,7 +72,7 @@ public class RewardActivityDO extends BaseDO {
      * 商品 SPU 编号的数组
      */
     @TableField(typeHandler = LongListTypeHandler.class)
-    private List<Long> productSpuIds;
+    private List<Long> productScopeValues;
     /**
      * 优惠规则的数组
      */
@@ -104,13 +105,14 @@ public class RewardActivityDO extends BaseDO {
          */
         private Integer point;
         /**
-         * 赠送的优惠劵编号的数组
-         */
-        private List<Long> couponIds;
-        /**
-         * 赠送的优惠券数量的数组
+         * 赠送的优惠劵
+         *
+         * key: 优惠劵模版编号
+         * value:对应的优惠券数量
+         *
+         * 目的:用于订单支付后赠送优惠券
          */
-        private List<Integer> couponCounts;
+        private Map<Long, Integer> giveCouponTemplateCounts;
 
     }
 

+ 0 - 20
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java

@@ -1,13 +1,11 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.coupon;
 
 import cn.hutool.core.map.MapUtil;
-import cn.hutool.core.util.StrUtil;
 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.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.github.yulichang.toolkit.MPJWrappers;
 import org.apache.ibatis.annotations.Mapper;
@@ -16,8 +14,6 @@ import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
@@ -84,22 +80,6 @@ public interface CouponMapper extends BaseMapperX<CouponDO> {
         return convertMap(list, map -> MapUtil.getLong(map, templateIdAlias), map -> MapUtil.getInt(map, countAlias));
     }
 
-    default List<CouponDO> selectListByUserIdAndStatusAndUsePriceLeAndProductScope(
-            Long userId, Integer status, Integer usePrice, List<Long> spuIds, List<Long> categoryIds) {
-        Function<List<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
-                .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
-                .collect(Collectors.joining(" OR "));
-        return selectList(new LambdaQueryWrapperX<CouponDO>()
-                .eq(CouponDO::getUserId, userId)
-                .eq(CouponDO::getStatus, status)
-                .le(CouponDO::getUsePrice, usePrice) // 价格小于等于,满足价格使用条件
-                .and(w -> w.eq(CouponDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()) // 商品范围一:全部
-                        .or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.SPU.getScope()) // 商品范围二:满足指定商品
-                                .apply(productScopeValuesFindInSetFunc.apply(spuIds)))
-                        .or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope()) // 商品范围三:满足指定分类
-                                .apply(productScopeValuesFindInSetFunc.apply(categoryIds)))));
-    }
-
     default List<CouponDO> selectListByStatusAndValidEndTimeLe(Integer status, LocalDateTime validEndTime) {
         return selectList(new LambdaQueryWrapperX<CouponDO>()
                 .eq(CouponDO::getStatus, status)

+ 5 - 15
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java

@@ -30,19 +30,9 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
                 .orderByDesc(RewardActivityDO::getId));
     }
 
-    default List<RewardActivityDO> selectListByStatus(Collection<Integer> statuses) {
-        return selectList(RewardActivityDO::getStatus, statuses);
-    }
-
-    default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) {
-        return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
-                .eq(RewardActivityDO::getProductScope, productScope)
-                .eq(RewardActivityDO::getStatus, status));
-    }
-
     default List<RewardActivityDO> selectListBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
         Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
-                .map(id -> StrUtil.format("FIND_IN_SET({}, product_spu_ids) ", id))
+                .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
                 .collect(Collectors.joining(" OR "));
         return selectList(new QueryWrapper<RewardActivityDO>()
                 .eq("status", status)
@@ -53,16 +43,16 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
      * 获取指定活动编号的活动列表且
      * 开始时间和结束时间小于给定时间 dateTime 的活动列表
      *
-     * @param ids      活动编号
+     * @param status   状态
      * @param dateTime 指定日期
      * @return 活动列表
      */
-    default List<RewardActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
+    default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
         return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
-                .in(RewardActivityDO::getId, ids)
+                .eq(RewardActivityDO::getStatus, status)
                 .lt(RewardActivityDO::getStartTime, dateTime)
                 .gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
-                .orderByDesc(RewardActivityDO::getCreateTime)
+                .orderByAsc(RewardActivityDO::getStartTime)
         );
     }
 

+ 0 - 8
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java

@@ -100,14 +100,6 @@ public interface CombinationActivityService {
      */
     List<CombinationActivityDO> getCombinationActivityListByIds(Collection<Long> ids);
 
-    /**
-     * 获取正在进行的活动分页数据
-     *
-     * @param count 需要的数量
-     * @return 拼团活动分页
-     */
-    List<CombinationActivityDO> getCombinationActivityListByCount(Integer count);
-
     /**
      * 获取正在进行的活动分页数据
      *

+ 0 - 5
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java

@@ -225,11 +225,6 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
         return combinationActivityMapper.selectList(CombinationActivityDO::getId, ids);
     }
 
-    @Override
-    public List<CombinationActivityDO> getCombinationActivityListByCount(Integer count) {
-        return combinationActivityMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus(), count);
-    }
-
     @Override
     public PageResult<CombinationActivityDO> getCombinationActivityPage(PageParam pageParam) {
         return combinationActivityMapper.selectPage(pageParam, CommonStatusEnum.ENABLE.getStatus());

+ 9 - 4
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java

@@ -27,6 +27,9 @@ import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStat
 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum;
+import jakarta.annotation.Nullable;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
@@ -34,10 +37,11 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Nullable;
-import javax.annotation.Resource;
 import java.time.LocalDateTime;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -335,7 +339,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         List<CombinationRecordDO> headAndRecords = updateBatchCombinationRecords(headRecord,
                 CombinationRecordStatusEnum.FAILED);
         // 2. 订单取消
-        headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId()));
+        headAndRecords.forEach(item -> tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId(),
+                TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType()));
     }
 
     /**

+ 59 - 68
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java

@@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
-import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
@@ -18,34 +17,6 @@ import java.util.*;
  */
 public interface CouponService {
 
-    /**
-     * 校验优惠劵,包括状态、有限期
-     * <p>
-     * 1. 如果校验通过,则返回优惠劵信息
-     * 2. 如果校验不通过,则直接抛出业务异常
-     *
-     * @param id     优惠劵编号
-     * @param userId 用户编号
-     * @return 优惠劵信息
-     */
-    CouponDO validCoupon(Long id, Long userId);
-
-    /**
-     * 校验优惠劵,包括状态、有限期
-     *
-     * @param coupon 优惠劵
-     * @see #validCoupon(Long, Long) 逻辑相同,只是入参不同
-     */
-    void validCoupon(CouponDO coupon);
-
-    /**
-     * 获得优惠劵分页
-     *
-     * @param pageReqVO 分页查询
-     * @return 优惠劵分页
-     */
-    PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
-
     /**
      * 使用优惠劵
      *
@@ -69,42 +40,44 @@ public interface CouponService {
      */
     void deleteCoupon(Long id);
 
-    /**
-     * 获得用户的优惠劵列表
-     *
-     * @param userId 用户编号
-     * @param status 优惠劵状态
-     * @return 优惠劵列表
-     */
-    List<CouponDO> getCouponList(Long userId, Integer status);
-
-    /**
-     * 获得未使用的优惠劵数量
-     *
-     * @param userId 用户编号
-     * @return 未使用的优惠劵数量
-     */
-    Long getUnusedCouponCount(Long userId);
-
     /**
      * 领取优惠券
      *
      * @param templateId 优惠券模板编号
      * @param userIds    用户编号列表
      * @param takeType   领取方式
+     * @return key: userId, value: 优惠券编号列表
      */
-    void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
+    Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
 
     /**
      * 【管理员】给用户发送优惠券
      *
      * @param templateId 优惠券模板编号
      * @param userIds    用户编号列表
+     * @return key: userId, value: 优惠券编号列表
      */
-    default void takeCouponByAdmin(Long templateId, Set<Long> userIds) {
-        takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
+    default Map<Long, List<Long>> takeCouponByAdmin(Long templateId, Set<Long> userIds) {
+        return takeCoupon(templateId, userIds, CouponTakeTypeEnum.ADMIN);
     }
 
+    /**
+     * 【管理员】给指定用户批量发送优惠券
+     *
+     * @param giveCoupons  key: 优惠劵模版编号,value:对应的数量
+     * @param userId      用户编号
+     * @return 优惠券编号列表
+     */
+    List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId);
+
+    /**
+     * 【管理员】作废指定用户的指定优惠劵
+     *
+     * @param giveCouponIds  赠送的优惠券编号
+     * @param userId         用户编号
+     */
+    void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId);
+
     /**
      * 【会员】领取优惠券
      *
@@ -123,16 +96,38 @@ public interface CouponService {
     void takeCouponByRegister(Long userId);
 
     /**
-     * 获取会员领取指定优惠券的数量
+     * 过期优惠券
      *
-     * @param templateId 优惠券模板编号
-     * @param userId     用户编号
-     * @return 领取优惠券的数量
+     * @return 过期数量
      */
-    default Integer getTakeCount(Long templateId, Long userId) {
-        Map<Long, Integer> map = getTakeCountMapByTemplateIds(Collections.singleton(templateId), userId);
-        return MapUtil.getInt(map, templateId, 0);
-    }
+    int expireCoupon();
+
+    // ======================= 查询相关 =======================
+
+    /**
+     * 获得未使用的优惠劵数量
+     *
+     * @param userId 用户编号
+     * @return 未使用的优惠劵数量
+     */
+    Long getUnusedCouponCount(Long userId);
+
+    /**
+     * 获得优惠劵分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 优惠劵分页
+     */
+    PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO);
+
+    /**
+     * 获得用户的优惠劵列表
+     *
+     * @param userId 用户编号
+     * @param status 优惠劵状态
+     * @return 优惠劵列表
+     */
+    List<CouponDO> getCouponList(Long userId, Integer status);
 
     /**
      * 统计会员领取优惠券的数量
@@ -144,20 +139,16 @@ public interface CouponService {
     Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId);
 
     /**
-     * 获取用户匹配的优惠券列表
+     * 获取会员领取指定优惠券的数量
      *
+     * @param templateId 优惠券模板编号
      * @param userId     用户编号
-     * @param matchReqVO 匹配参数
-     * @return 优惠券列表
-     */
-    List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO);
-
-    /**
-     * 过期优惠券
-     *
-     * @return 过期数量
+     * @return 领取优惠券的数量
      */
-    int expireCoupon();
+    default Integer getTakeCount(Long templateId, Long userId) {
+        Map<Long, Integer> map = getTakeCountMapByTemplateIds(Collections.singleton(templateId), userId);
+        return MapUtil.getInt(map, templateId, 0);
+    }
 
     /**
      * 获取用户是否可以领取优惠券

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

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.service.coupon;
 import cn.hutool.core.collection.CollStreamUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
@@ -11,7 +12,6 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
-import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO;
 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
@@ -19,18 +19,19 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper;
 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 import static java.util.Arrays.asList;
 
@@ -54,18 +55,9 @@ public class CouponServiceImpl implements CouponService {
     private MemberUserApi memberUserApi;
 
     @Override
-    public CouponDO validCoupon(Long id, Long userId) {
-        CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId);
-        if (coupon == null) {
-            throw exception(COUPON_NOT_EXISTS);
-        }
-        validCoupon(coupon);
-        return coupon;
-    }
-
-    @Override
-    public void validCoupon(CouponDO coupon) {
+    public void useCoupon(Long id, Long userId, Long orderId) {
         // 校验状态
+        CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId);
         if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) {
             throw exception(COUPON_STATUS_NOT_UNUSED);
         }
@@ -73,26 +65,6 @@ public class CouponServiceImpl implements CouponService {
         if (!LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) {
             throw exception(COUPON_VALID_TIME_NOT_NOW);
         }
-    }
-
-    @Override
-    public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {
-        // 获得用户编号
-        if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
-            List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
-            if (CollUtil.isEmpty(users)) {
-                return PageResult.empty();
-            }
-            pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
-        }
-        // 分页查询
-        return couponMapper.selectPage(pageReqVO);
-    }
-
-    @Override
-    public void useCoupon(Long id, Long userId, Long orderId) {
-        // 校验优惠劵
-        validCoupon(id, userId);
 
         // 更新状态
         int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
@@ -146,25 +118,8 @@ public class CouponServiceImpl implements CouponService {
     }
 
     @Override
-    public List<CouponDO> getCouponList(Long userId, Integer status) {
-        return couponMapper.selectListByUserIdAndStatus(userId, status);
-    }
-
-    private CouponDO validateCouponExists(Long id) {
-        CouponDO coupon = couponMapper.selectById(id);
-        if (coupon == null) {
-            throw exception(COUPON_NOT_EXISTS);
-        }
-        return coupon;
-    }
-
-    @Override
-    public Long getUnusedCouponCount(Long userId) {
-        return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
-    }
-
-    @Override
-    public void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
+    @Transactional(rollbackFor = Exception.class)
+    public Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
         CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
         // 1. 过滤掉达到领取限制的用户
         removeTakeLimitUser(userIds, template);
@@ -172,37 +127,86 @@ public class CouponServiceImpl implements CouponService {
         validateCouponTemplateCanTake(template, userIds, takeType);
 
         // 3. 批量保存优惠劵
-        couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)));
+        List<CouponDO> couponList = convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId));
+        couponMapper.insertBatch(couponList);
 
-        // 3. 增加优惠劵模板的领取数量
+        // 4. 增加优惠劵模板的领取数量
         couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
+
+        return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void takeCouponByRegister(Long userId) {
-        List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER);
-        for (CouponTemplateDO template : templates) {
-            takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER);
+    public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
+        if (CollUtil.isEmpty(giveCoupons)) {
+            return Collections.emptyList();
+        }
+
+        List<Long> couponIds = new ArrayList<>();
+        // 循环发放
+        for (Map.Entry<Long, Integer> entry : giveCoupons.entrySet()) {
+            try {
+                for (int i = 0; i < entry.getValue(); i++) {
+                    Map<Long, List<Long>> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId),
+                            CouponTakeTypeEnum.ADMIN);
+                    findAndThen(userCouponIdsMap, userId, couponIds::addAll);
+                }
+            } catch (Exception e) {
+                log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败]", entry, e);
+            }
         }
+        return couponIds;
     }
 
     @Override
-    public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
-        if (CollUtil.isEmpty(templateIds)) {
-            return Collections.emptyMap();
+    public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
+        // 循环收回
+        for (Long couponId : giveCouponIds) {
+            try {
+                getSelf().invalidateCoupon(couponId, userId);
+            } catch (Exception e) {
+                log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e);
+            }
         }
-        return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
+    }
+
+    /**
+     * 【管理员】收回优惠券
+     *
+     * @param couponId 模版编号
+     * @param userId   用户编号
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void invalidateCoupon(Long couponId, Long userId) {
+        // 1.1 校验优惠券
+        CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId);
+        if (coupon == null) {
+            throw exception(COUPON_NOT_EXISTS);
+        }
+        // 1.2 校验模板
+        CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId());
+        if (couponTemplate == null) {
+            throw exception(COUPON_TEMPLATE_NOT_EXISTS);
+        }
+        // 1.3 校验优惠券是否已经使用,如若使用则先不管
+        if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) {
+            log.info("[invalidateCoupon][coupon({}) 已经使用,无法作废]", couponId);
+            return;
+        }
+
+        // 2.1 减少优惠劵模板的领取数量
+        couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1);
+        // 2.2 作废优惠劵
+        couponMapper.deleteById(couponId);
     }
 
     @Override
-    public List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
-        List<CouponDO> list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
-                CouponStatusEnum.UNUSED.getStatus(),
-                matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
-        // 兜底逻辑:如果 CouponExpireJob 未执行,status 未变成 EXPIRE ,但是 validEndTime 已经过期了,需要进行过滤
-        list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
-        return list;
+    @Transactional(rollbackFor = Exception.class)
+    public void takeCouponByRegister(Long userId) {
+        List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER);
+        for (CouponTemplateDO template : templates) {
+            takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER);
+        }
     }
 
     @Override
@@ -229,27 +233,6 @@ public class CouponServiceImpl implements CouponService {
         return count;
     }
 
-    @Override
-    public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
-        // 1. 未登录时,都显示可以领取
-        Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
-        if (userId == null) {
-            return userCanTakeMap;
-        }
-
-        // 2.1 过滤领取数量无限制的
-        Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
-        // 2.2 检查用户领取的数量是否超过限制
-        if (CollUtil.isNotEmpty(templateIds)) {
-            Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
-            for (CouponTemplateDO template : templates) {
-                Integer takeCount = couponTakeCountMap.get(template.getId());
-                userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
-            }
-        }
-        return userCanTakeMap;
-    }
-
     /**
      * 过期单个优惠劵
      *
@@ -321,11 +304,74 @@ public class CouponServiceImpl implements CouponService {
         userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
     }
 
+    //======================= 查询相关 =======================
+
+    @Override
+    public Long getUnusedCouponCount(Long userId) {
+        return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus());
+    }
+
+    @Override
+    public PageResult<CouponDO> getCouponPage(CouponPageReqVO pageReqVO) {
+        // 获得用户编号
+        if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
+            List<MemberUserRespDTO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
+            if (CollUtil.isEmpty(users)) {
+                return PageResult.empty();
+            }
+            pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId));
+        }
+        // 分页查询
+        return couponMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CouponDO> getCouponList(Long userId, Integer status) {
+        return couponMapper.selectListByUserIdAndStatus(userId, status);
+    }
+
+    @Override
+    public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
+        if (CollUtil.isEmpty(templateIds)) {
+            return Collections.emptyMap();
+        }
+        return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
+    }
+
+    @Override
+    public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
+        // 1. 未登录时,都显示可以领取
+        Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
+        if (userId == null) {
+            return userCanTakeMap;
+        }
+
+        // 2.1 过滤领取数量无限制的
+        Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
+        // 2.2 检查用户领取的数量是否超过限制
+        if (CollUtil.isNotEmpty(templateIds)) {
+            Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
+            for (CouponTemplateDO template : templates) {
+                Integer takeCount = couponTakeCountMap.get(template.getId());
+                userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
+            }
+        }
+        return userCanTakeMap;
+    }
+
     @Override
     public CouponDO getCoupon(Long userId, Long id) {
         return couponMapper.selectByIdAndUserId(id, userId);
     }
 
+    private CouponDO validateCouponExists(Long id) {
+        CouponDO coupon = couponMapper.selectById(id);
+        if (coupon == null) {
+            throw exception(COUPON_NOT_EXISTS);
+        }
+        return coupon;
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *
@@ -334,4 +380,5 @@ public class CouponServiceImpl implements CouponService {
     private CouponServiceImpl getSelf() {
         return SpringUtil.getBean(getClass());
     }
+
 }

+ 1 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java

@@ -75,11 +75,10 @@ public interface RewardActivityService {
     /**
      * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
      *
-     * @param spuIds   spu 编号
      * @param status   状态
      * @param dateTime 当前日期时间
      * @return 满减送活动列表
      */
-    List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
+    List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
 
 }

+ 59 - 71
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java

@@ -1,15 +1,18 @@
 package cn.iocoder.yudao.module.promotion.service.reward;
 
-import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
+import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
+import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
-import cn.iocoder.yudao.module.promotion.convert.reward.RewardActivityConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -17,13 +20,13 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
+import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
-import static java.util.Arrays.asList;
 
 /**
  * 满减送活动 Service 实现类
@@ -37,13 +40,20 @@ public class RewardActivityServiceImpl implements RewardActivityService {
     @Resource
     private RewardActivityMapper rewardActivityMapper;
 
+    @Resource
+    private ProductCategoryApi productCategoryApi;
+    @Resource
+    private ProductSpuApi productSpuApi;
+
     @Override
     public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) {
-        // 校验商品是否冲突
-        validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds());
+        // 1.1 校验商品范围
+        validateProductScope(createReqVO.getProductScope(), createReqVO.getProductScopeValues());
+        // 1.2 校验商品是否冲突
+        validateRewardActivitySpuConflicts(null, createReqVO);
 
-        // 插入
-        RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO)
+        // 2. 插入
+        RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class)
                 .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
         rewardActivityMapper.insert(rewardActivity);
         // 返回
@@ -52,16 +62,18 @@ public class RewardActivityServiceImpl implements RewardActivityService {
 
     @Override
     public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) {
-        // 校验存在
+        // 1.1 校验存在
         RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId());
-        if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢
+        if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能修改噢
             throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED);
         }
-        // 校验商品是否冲突
-        validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds());
+        // 1.2 校验商品范围
+        validateProductScope(updateReqVO.getProductScope(), updateReqVO.getProductScopeValues());
+        // 1.3 校验商品是否冲突
+        validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO);
 
-        // 更新
-        RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO)
+        // 2. 更新
+        RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class)
                 .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
         rewardActivityMapper.updateById(updateObj);
     }
@@ -70,15 +82,12 @@ public class RewardActivityServiceImpl implements RewardActivityService {
     public void closeRewardActivity(Long id) {
         // 校验存在
         RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
-        if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢
+        if (dbRewardActivity.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { // 已关闭的活动,不能关闭噢
             throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
         }
-        if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动,不能关闭噢
-            throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END);
-        }
 
         // 更新
-        RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
+        RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus());
         rewardActivityMapper.updateById(updateObj);
     }
 
@@ -86,7 +95,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
     public void deleteRewardActivity(Long id) {
         // 校验存在
         RewardActivityDO dbRewardActivity = validateRewardActivityExists(id);
-        if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢
+        if (dbRewardActivity.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())) { // 未关闭的活动,不能删除噢
             throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED);
         }
 
@@ -102,41 +111,39 @@ public class RewardActivityServiceImpl implements RewardActivityService {
         return activity;
     }
 
-    // TODO @芋艿:逻辑有问题,需要优化;要分成全场、和指定来校验;
-
     /**
      * 校验商品参加的活动是否冲突
      *
-     * @param id     活动编号
-     * @param spuIds 商品 SPU 编号数组
+     * @param id             活动编号
+     * @param rewardActivity 请求
      */
-    private void validateRewardActivitySpuConflicts(Long id, Collection<Long> spuIds) {
-        if (CollUtil.isEmpty(spuIds)) {
-            return;
-        }
-        // 查询商品参加的活动
-        List<RewardActivityDO> rewardActivityList = getRewardActivityListBySpuIds(spuIds,
-                asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus()));
+    private void validateRewardActivitySpuConflicts(Long id, RewardActivityBaseVO rewardActivity) {
+        List<RewardActivityDO> list = rewardActivityMapper.selectList(RewardActivityDO::getProductScope,
+                rewardActivity.getProductScope(), RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
         if (id != null) { // 排除自己这个活动
-            rewardActivityList.removeIf(activity -> id.equals(activity.getId()));
+            list.removeIf(activity -> id.equals(activity.getId()));
         }
-        // 如果非空,则说明冲突
-        if (CollUtil.isNotEmpty(rewardActivityList)) {
-            throw exception(REWARD_ACTIVITY_SPU_CONFLICTS);
+
+        // 情况一:全部商品参加
+        if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope()) && !list.isEmpty()) {
+            throw exception(REWARD_ACTIVITY_SCOPE_ALL_EXISTS);
+        }
+        if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ||  // 情况二:指定商品参加
+                PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {  // 情况三:指定商品类型参加
+            if (anyMatch(list, item -> !intersectionDistinct(item.getProductScopeValues(),
+                    rewardActivity.getProductScopeValues()).isEmpty())) {
+                throw exception(PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ?
+                        REWARD_ACTIVITY_SPU_CONFLICTS : REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS);
+            }
         }
     }
 
-    /**
-     * 获得商品参加的满减送活动的数组
-     *
-     * @param spuIds   商品 SPU 编号数组
-     * @param statuses 活动状态数组
-     * @return 商品参加的满减送活动的数组
-     */
-    private List<RewardActivityDO> getRewardActivityListBySpuIds(Collection<Long> spuIds,
-                                                                 Collection<Integer> statuses) {
-        List<RewardActivityDO> list = rewardActivityMapper.selectListByStatus(statuses);
-        return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds));
+    private void validateProductScope(Integer productScope, List<Long> productScopeValues) {
+        if (Objects.equals(PromotionProductScopeEnum.SPU.getScope(), productScope)) {
+            productSpuApi.validateSpuList(productScopeValues);
+        } else if (Objects.equals(PromotionProductScopeEnum.CATEGORY.getScope(), productScope)) {
+            productCategoryApi.validateCategoryList(productScopeValues);
+        }
     }
 
     @Override
@@ -151,32 +158,13 @@ public class RewardActivityServiceImpl implements RewardActivityService {
 
     @Override
     public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) {
-        // TODO 芋艿:待实现;先指定,然后再全局的;
-//        // 如果有全局活动,则直接选择它
-//        List<RewardActivityDO> allActivities = rewardActivityMapper.selectListByProductScopeAndStatus(
-//                PromotionProductScopeEnum.ALL.getScope(), PromotionActivityStatusEnum.RUN.getStatus());
-//        if (CollUtil.isNotEmpty(allActivities)) {
-//            return MapUtil.builder(allActivities.get(0), spuIds).build();
-//        }
-//
-//        // 查询某个活动参加的活动
-//        List<RewardActivityDO> productActivityList = getRewardActivityListBySpuIds(spuIds,
-//                singleton(PromotionActivityStatusEnum.RUN.getStatus()));
-//        return convertMap(productActivityList, activity -> activity,
-//                rewardActivityDO -> intersectionDistinct(rewardActivityDO.getProductSpuIds(), spuIds)); // 求交集返回
-        return null;
+        List<RewardActivityDO> list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus());
+        return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
     }
 
     @Override
-    public List<RewardActivityDO> getRewardActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
-        // 1. 查询出指定 spuId 的 spu 参加的活动
-        List<RewardActivityDO> rewardActivityList = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, status);
-        if (CollUtil.isEmpty(rewardActivityList)) {
-            return Collections.emptyList();
-        }
-
-        // 2. 查询活动详情
-        return rewardActivityMapper.selectListByIdsAndDateTimeLt(convertSet(rewardActivityList, RewardActivityDO::getId), dateTime);
+    public List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
+        return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
     }
 
 }

+ 68 - 61
yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java

@@ -1,21 +1,23 @@
 package cn.iocoder.yudao.module.promotion.service.reward;
 
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
-import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
+import jakarta.annotation.Resource;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.context.annotation.Import;
 
-import javax.annotation.Resource;
 import java.time.Duration;
+import java.util.List;
 import java.util.Set;
 
 import static cn.hutool.core.util.RandomUtil.randomEle;
@@ -27,15 +29,15 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS;
-import static java.util.Arrays.asList;
+import static com.google.common.primitives.Longs.asList;
 import static java.util.Collections.singletonList;
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
-* {@link RewardActivityServiceImpl} 的单元测试类
-*
-* @author 芋道源码
-*/
+ * {@link RewardActivityServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
 @Disabled // TODO 芋艿:后续 fix 补充的单测
 @Import(RewardActivityServiceImpl.class)
 public class RewardActivityServiceImplTest extends BaseDbUnitTest {
@@ -63,7 +65,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
         // 校验记录的属性是否正确
         RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId);
         assertPojoEquals(reqVO, rewardActivity, "rules");
-        assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus());
+        assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
         for (int i = 0; i < reqVO.getRules().size(); i++) {
             assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
         }
@@ -72,7 +74,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
     @Test
     public void testUpdateRewardActivity_success() {
         // mock 数据
-        RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()));
+        RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
         rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
         // 准备参数
         RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> {
@@ -88,7 +90,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
         // 校验是否更新正确
         RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的
         assertPojoEquals(reqVO, rewardActivity, "rules");
-        assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus());
+        assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
         for (int i = 0; i < reqVO.getRules().size(); i++) {
             assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i));
         }
@@ -97,7 +99,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
     @Test
     public void testCloseRewardActivity() {
         // mock 数据
-        RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()));
+        RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
         rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
         // 准备参数
         Long id = dbRewardActivity.getId();
@@ -106,7 +108,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
         rewardActivityService.closeRewardActivity(id);
         // 校验状态
         RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id);
-        assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus());
+        assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
     }
 
     @Test
@@ -121,15 +123,15 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
     @Test
     public void testDeleteRewardActivity_success() {
         // mock 数据
-        RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()));
+        RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
         rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据
         // 准备参数
         Long id = dbRewardActivity.getId();
 
         // 调用
         rewardActivityService.deleteRewardActivity(id);
-       // 校验数据不存在了
-       assertNull(rewardActivityMapper.selectById(id));
+        // 校验数据不存在了
+        assertNull(rewardActivityMapper.selectById(id));
     }
 
     @Test
@@ -143,77 +145,82 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
 
     @Test
     public void testGetRewardActivityPage() {
-       // mock 数据
-       RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
-           o.setName("芋艿");
-           o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
-       });
-       rewardActivityMapper.insert(dbRewardActivity);
-       // 测试 name 不匹配
-       rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
-       // 测试 status 不匹配
-       rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())));
-       // 准备参数
-       RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
-       reqVO.setName("芋艿");
-       reqVO.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
-
-       // 调用
-       PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);
-       // 断言
-       assertEquals(1, pageResult.getTotal());
-       assertEquals(1, pageResult.getList().size());
-       assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
+        // mock 数据
+        RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到
+            o.setName("芋艿");
+            o.setStatus(CommonStatusEnum.DISABLE.getStatus());
+        });
+        rewardActivityMapper.insert(dbRewardActivity);
+        // 测试 name 不匹配
+        rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆")));
+        // 测试 status 不匹配
+        rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
+        // 准备参数
+        RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO();
+        reqVO.setName("芋艿");
+        reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus());
+
+        // 调用
+        PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
     }
 
     @Test
     public void testGetRewardActivities_all() {
         // mock 数据
-        RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
+        RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
                 .setProductScope(PromotionProductScopeEnum.ALL.getScope()));
         rewardActivityMapper.insert(allActivity);
-        RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
-                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L)));
+        RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
         rewardActivityMapper.insert(productActivity);
         // 准备参数
         Set<Long> spuIds = asSet(1L, 2L);
 
         // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
-        //Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds);
+        List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
         // 断言
-        //assertEquals(matchRewardActivities.size(), 1);
-        //Map.Entry<RewardActivityDO, Set<Long>> next = matchRewardActivities.entrySet().iterator().next();
-        //assertPojoEquals(next.getKey(), allActivity);
-        //assertEquals(next.getValue(), spuIds);
+        assertEquals(matchRewardActivityList.size(), 1);
+        matchRewardActivityList.forEach((activity) -> {
+            if (activity.getId().equals(productActivity.getId())) {
+                assertPojoEquals(activity, productActivity);
+                assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
+            } else {
+                fail();
+            }
+        });
     }
 
     @Test
     public void testGetRewardActivities_product() {
         // mock 数据
-        RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
-                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L)));
+        RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
         rewardActivityMapper.insert(productActivity01);
-        RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus())
-                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(singletonList(3L)));
+        RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)));
         rewardActivityMapper.insert(productActivity02);
         // 准备参数
         Set<Long> spuIds = asSet(1L, 2L, 3L);
 
         // 调用  TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList
-        //Map<RewardActivityDO, Set<Long>> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds);
+        List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
         // 断言
-        //assertEquals(matchRewardActivities.size(), 2);
-        //matchRewardActivities.forEach((activity, activitySpuIds) -> {
-        //    if (activity.getId().equals(productActivity01.getId())) {
-        //        assertPojoEquals(activity, productActivity01);
-        //        assertEquals(activitySpuIds, asSet(1L, 2L));
-        //    } else if (activity.getId().equals(productActivity02.getId())) {
-        //        assertPojoEquals(activity, productActivity02);
-        //        assertEquals(activitySpuIds, asSet(3L));
-        //    } else {
-        //        fail();
-        //    }
-        //});
+        assertEquals(matchRewardActivityList.size(), 2);
+        matchRewardActivityList.forEach((activity) -> {
+            if (activity.getId().equals(productActivity01.getId())) {
+                assertPojoEquals(activity, productActivity01);
+                assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
+            } else if (activity.getId().equals(productActivity02.getId())) {
+                assertPojoEquals(activity, productActivity02);
+                assertEquals(activity.getProductScopeValues(), singletonList(3L));
+            } else {
+                fail();
+            }
+        });
     }
 
 }

+ 3 - 3
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java

@@ -28,13 +28,13 @@ public interface TradeOrderApi {
      */
     TradeOrderRespDTO getOrder(Long id);
 
-    // TODO 芋艿:需要优化下;
     /**
      * 取消支付订单
      *
-     * @param userId  用户编号
+     * @param userId 用户编号
      * @param orderId 订单编号
+     * @param cancelType 取消类型
      */
-    void cancelPaidOrder(Long userId, Long orderId);
+    void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
 
 }

+ 4 - 0
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java

@@ -35,6 +35,7 @@ public interface ErrorCodeConstants {
     ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
     ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
     ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
+    ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
 
     // ========== After Sale 模块 1-011-000-100 ==========
     ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@@ -50,6 +51,7 @@ public interface ErrorCodeConstants {
     ErrorCode AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND = new ErrorCode(1_011_000_110, "退款失败,售后单状态不是【待退款】");
     ErrorCode AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE_OR_BUYER_DELIVERY =
             new ErrorCode(1_011_000_111, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】或【商家待收货】");
+    ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS = new ErrorCode(1_011_000_112, "订单拼团中,无法申请售后");
 
     // ========== Cart 模块 1-011-002-000 ==========
     ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1_011_002_000, "购物车项不存在");
@@ -59,6 +61,8 @@ public interface ErrorCodeConstants {
     ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
     ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
     ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
+    ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
+    ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:{}」");
 
     // ========== 物流 Express 模块 1-011-004-000 ==========
     ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");

+ 2 - 1
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderCancelTypeEnum.java

@@ -17,7 +17,8 @@ public enum TradeOrderCancelTypeEnum implements IntArrayValuable {
 
     PAY_TIMEOUT(10, "超时未支付"),
     AFTER_SALE_CLOSE(20, "退款关闭"),
-    MEMBER_CANCEL(30, "买家取消");
+    MEMBER_CANCEL(30, "买家取消"),
+    COMBINATION_CLOSE(40, "拼团关闭");
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray();
 

+ 3 - 3
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java

@@ -4,10 +4,10 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
 
@@ -36,8 +36,8 @@ public class TradeOrderApiImpl implements TradeOrderApi {
     }
 
     @Override
-    public void cancelPaidOrder(Long userId, Long orderId) {
-        tradeOrderUpdateService.cancelPaidOrder(userId, orderId);
+    public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
+        tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelType);
     }
 
 }

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

@@ -3,7 +3,7 @@ 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.module.trade.controller.admin.aftersale.vo.log.AfterSaleLogRespVO;
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
 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.AppAfterSaleRespVO;
@@ -12,14 +12,11 @@ import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-
-import java.util.List;
-
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -35,6 +32,7 @@ public class AppAfterSaleController {
 
     @GetMapping(value = "/page")
     @Operation(summary = "获得售后分页")
+    @PreAuthenticated
     public CommonResult<PageResult<AppAfterSaleRespVO>> getAfterSalePage(PageParam pageParam) {
         return success(AfterSaleConvert.INSTANCE.convertPage02(
                 afterSaleService.getAfterSalePage(getLoginUserId(), pageParam)));
@@ -43,18 +41,21 @@ public class AppAfterSaleController {
     @GetMapping(value = "/get")
     @Operation(summary = "获得售后订单")
     @Parameter(name = "id", description = "售后编号", required = true, example = "1")
+    @PreAuthenticated
     public CommonResult<AppAfterSaleRespVO> getAfterSale(@RequestParam("id") Long id) {
         return success(AfterSaleConvert.INSTANCE.convert(afterSaleService.getAfterSale(getLoginUserId(), id)));
     }
 
     @PostMapping(value = "/create")
     @Operation(summary = "申请售后")
+    @PreAuthenticated
     public CommonResult<Long> createAfterSale(@RequestBody AppAfterSaleCreateReqVO createReqVO) {
         return success(afterSaleService.createAfterSale(getLoginUserId(), createReqVO));
     }
 
     @PutMapping(value = "/delivery")
     @Operation(summary = "退回货物")
+    @PreAuthenticated
     public CommonResult<Boolean> deliveryAfterSale(@RequestBody AppAfterSaleDeliveryReqVO deliveryReqVO) {
         afterSaleService.deliveryAfterSale(getLoginUserId(), deliveryReqVO);
         return success(true);
@@ -63,6 +64,7 @@ public class AppAfterSaleController {
     @DeleteMapping(value = "/cancel")
     @Operation(summary = "取消售后")
     @Parameter(name = "id", description = "售后编号", required = true, example = "1")
+    @PreAuthenticated
     public CommonResult<Boolean> cancelAfterSale(@RequestParam("id") Long id) {
         afterSaleService.cancelAfterSale(getLoginUserId(), id);
         return success(true);

+ 2 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/aftersale/AppAfterSaleLogController.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.controller.app.aftersale;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
 import cn.iocoder.yudao.module.trade.controller.app.aftersale.vo.log.AppAfterSaleLogRespVO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleLogDO;
 import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleLogService;
@@ -33,6 +34,7 @@ public class AppAfterSaleLogController {
     @GetMapping("/list")
     @Operation(summary = "获得售后日志列表")
     @Parameter(name = "afterSaleId", description = "售后编号", required = true, example = "1")
+    @PreAuthenticated
     public CommonResult<List<AppAfterSaleLogRespVO>> getAfterSaleLogList(
             @RequestParam("afterSaleId") Long afterSaleId) {
         List<AfterSaleLogDO> logs = afterSaleLogService.getAfterSaleLogList(afterSaleId);

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageRecordController.java

@@ -45,6 +45,7 @@ public class AppBrokerageRecordController {
 
     @GetMapping("/get-product-brokerage-price")
     @Operation(summary = "获得商品的分销金额")
+    @PreAuthenticated
     public CommonResult<AppBrokerageProductPriceRespVO> getProductBrokeragePrice(@RequestParam("spuId") Long spuId) {
         return success(brokerageRecordService.calculateProductBrokeragePrice(getLoginUserId(), spuId));
     }

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/AppBrokerageUserController.java

@@ -133,6 +133,7 @@ public class AppBrokerageUserController {
     @GetMapping("/get-rank-by-price")
     @Operation(summary = "获得分销用户排行(基于佣金)")
     @Parameter(name = "times", description = "时间段", required = true)
+    @PreAuthenticated
     public CommonResult<Integer> getRankByPrice(
             @RequestParam("times") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime[] times) {
         return success(brokerageRecordService.getUserRankByPrice(getLoginUserId(), times));

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java

@@ -44,7 +44,7 @@ public class AppBrokerageWithdrawCreateReqVO {
     private String name;
     @Schema(description = "提现银行", example = "1")
     @NotNull(message = "提现银行不能为空", groups = {Bank.class})
-    private Integer bankName;
+    private String bankName;
     @Schema(description = "开户地址", example = "海淀支行")
     private String bankAddress;
 

+ 0 - 27
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/delivery/AppDeliverConfigController.java

@@ -1,27 +0,0 @@
-package cn.iocoder.yudao.module.trade.controller.app.delivery;
-
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.module.trade.controller.app.delivery.vo.config.AppDeliveryConfigRespVO;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
-@Tag(name = "用户 App - 配送配置")
-@RestController
-@RequestMapping("/trade/delivery/config")
-@Validated
-public class AppDeliverConfigController {
-
-    // TODO @芋艿:这里后面干掉,合并到 AppTradeConfigController 中
-    @GetMapping("/get")
-    @Operation(summary = "获得配送配置")
-    public CommonResult<AppDeliveryConfigRespVO> getDeliveryConfig() {
-        return success(new AppDeliveryConfigRespVO().setPickUpEnable(true).setTencentLbsKey("123456"));
-    }
-
-}

+ 9 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java

@@ -80,6 +80,7 @@ public class AppTradeOrderController {
     @GetMapping("/get-detail")
     @Operation(summary = "获得交易订单")
     @Parameter(name = "id", description = "交易订单编号")
+    @PreAuthenticated
     public CommonResult<AppTradeOrderDetailRespVO> getOrder(@RequestParam("id") Long id) {
         // 查询订单
         TradeOrderDO order = tradeOrderQueryService.getOrder(getLoginUserId(), id);
@@ -99,6 +100,7 @@ public class AppTradeOrderController {
     @GetMapping("/get-express-track-list")
     @Operation(summary = "获得交易订单的物流轨迹")
     @Parameter(name = "id", description = "交易订单编号")
+    @PreAuthenticated
     public CommonResult<List<AppOrderExpressTrackRespDTO>> getOrderExpressTrackList(@RequestParam("id") Long id) {
         return success(TradeOrderConvert.INSTANCE.convertList02(
                 tradeOrderQueryService.getExpressTrackList(id, getLoginUserId())));
@@ -106,6 +108,7 @@ public class AppTradeOrderController {
 
     @GetMapping("/page")
     @Operation(summary = "获得交易订单分页")
+    @PreAuthenticated
     public CommonResult<PageResult<AppTradeOrderPageItemRespVO>> getOrderPage(AppTradeOrderPageReqVO reqVO) {
         // 查询订单
         PageResult<TradeOrderDO> pageResult = tradeOrderQueryService.getOrderPage(getLoginUserId(), reqVO);
@@ -118,6 +121,7 @@ public class AppTradeOrderController {
 
     @GetMapping("/get-count")
     @Operation(summary = "获得交易订单数量")
+    @PreAuthenticated
     public CommonResult<Map<String, Long>> getOrderCount() {
         Map<String, Long> orderCount = Maps.newLinkedHashMapWithExpectedSize(5);
         // 全部
@@ -142,6 +146,7 @@ public class AppTradeOrderController {
     @PutMapping("/receive")
     @Operation(summary = "确认交易订单收货")
     @Parameter(name = "id", description = "交易订单编号")
+    @PreAuthenticated
     public CommonResult<Boolean> receiveOrder(@RequestParam("id") Long id) {
         tradeOrderUpdateService.receiveOrderByMember(getLoginUserId(), id);
         return success(true);
@@ -150,6 +155,7 @@ public class AppTradeOrderController {
     @DeleteMapping("/cancel")
     @Operation(summary = "取消交易订单")
     @Parameter(name = "id", description = "交易订单编号")
+    @PreAuthenticated
     public CommonResult<Boolean> cancelOrder(@RequestParam("id") Long id) {
         tradeOrderUpdateService.cancelOrderByMember(getLoginUserId(), id);
         return success(true);
@@ -158,6 +164,7 @@ public class AppTradeOrderController {
     @DeleteMapping("/delete")
     @Operation(summary = "删除交易订单")
     @Parameter(name = "id", description = "交易订单编号")
+    @PreAuthenticated
     public CommonResult<Boolean> deleteOrder(@RequestParam("id") Long id) {
         tradeOrderUpdateService.deleteOrder(getLoginUserId(), id);
         return success(true);
@@ -168,6 +175,7 @@ public class AppTradeOrderController {
     @GetMapping("/item/get")
     @Operation(summary = "获得交易订单项")
     @Parameter(name = "id", description = "交易订单项编号")
+    @PreAuthenticated
     public CommonResult<AppTradeOrderItemRespVO> getOrderItem(@RequestParam("id") Long id) {
         TradeOrderItemDO item = tradeOrderQueryService.getOrderItem(getLoginUserId(), id);
         return success(TradeOrderConvert.INSTANCE.convert03(item));
@@ -175,6 +183,7 @@ public class AppTradeOrderController {
 
     @PostMapping("/item/create-comment")
     @Operation(summary = "创建交易订单项的评价")
+    @PreAuthenticated
     public CommonResult<Long> createOrderItemComment(@RequestBody AppTradeOrderItemCommentCreateReqVO createReqVO) {
         return success(tradeOrderUpdateService.createOrderItemCommentByMember(getLoginUserId(), createReqVO));
     }

+ 43 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java

@@ -6,7 +6,7 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Schema(description = "用户 App - 交易订单结算信息 Response VO")
@@ -19,6 +19,9 @@ public class AppTradeOrderSettlementRespVO {
     @Schema(description = "购物项数组", requiredMode = Schema.RequiredMode.REQUIRED)
     private List<Item> items;
 
+    @Schema(description = "优惠劵数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Coupon> coupons; // 可用 + 不可用
+
     @Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED)
     private Price price;
 
@@ -109,7 +112,6 @@ public class AppTradeOrderSettlementRespVO {
         private String mobile;
 
         @Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED)
-        @NotNull(message = "地区编号不能为空")
         private Long areaId;
         @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区")
         private String areaName;
@@ -122,4 +124,43 @@ public class AppTradeOrderSettlementRespVO {
 
     }
 
+    @Schema(description = "优惠劵信息")
+    @Data
+    public static class Coupon {
+
+        @Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Long id;
+
+        @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送")
+        private String name;
+
+        @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位:分;0 - 不限制
+        private Integer usePrice;
+
+        @Schema(description = "固定日期 - 生效开始时间")
+        private LocalDateTime validStartTime;
+
+        @Schema(description = "固定日期 - 生效结束时间")
+        private LocalDateTime validEndTime;
+
+        @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer discountType;
+
+        @Schema(description = "折扣百分比", example = "80") //  例如说,80% 为 80
+        private Integer discountPercent;
+
+        @Schema(description = "优惠金额", example = "10")
+        private Integer discountPrice;
+
+        @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用
+        private Integer discountLimitPrice;
+
+        @Schema(description = "是否可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+        private Boolean match;
+
+        @Schema(description = "不可用原因", example = "优惠劵已过期")
+        private String mismatchReason;
+
+    }
+
 }

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java

@@ -261,7 +261,7 @@ public interface TradeOrderConvert {
     default BrokerageAddReqBO convert(MemberUserRespDTO user, TradeOrderItemDO item,
                                       ProductSpuRespDTO spu, ProductSkuRespDTO sku) {
         BrokerageAddReqBO bo = new BrokerageAddReqBO().setBizId(String.valueOf(item.getId())).setSourceUserId(item.getUserId())
-                .setBasePrice(item.getPayPrice() * item.getCount())
+                .setBasePrice(item.getPayPrice())
                 .setTitle(StrUtil.format("{}成功购买{}", user.getNickname(), item.getSpuName()))
                 .setFirstFixedPrice(0).setSecondFixedPrice(0);
         if (BooleanUtil.isTrue(spu.getSubCommissionType())) {

+ 23 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.order;
 
 import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
@@ -12,10 +13,14 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
 import lombok.*;
 
 import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 交易订单 DO
@@ -291,6 +296,24 @@ public class TradeOrderDO extends BaseDO {
      */
     private Integer vipPrice;
 
+    /**
+     * 赠送的优惠劵
+     *
+     * key: 优惠劵模版编号
+     * value:对应的优惠券数量
+     *
+     * 目的:用于订单支付后赠送优惠券
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private Map<Long, Integer> giveCouponTemplateCounts;
+    /**
+     * 赠送的优惠劵编号
+     *
+     * 目的:用于后续取消或者售后订单时,需要扣减赠送
+     */
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> giveCouponIds;
+
     /**
      * 秒杀活动编号
      *

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

@@ -8,6 +8,9 @@ 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;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
+import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
+import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
+import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 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;
@@ -26,6 +29,7 @@ import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleTypeEnum;
 import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleWayEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import cn.iocoder.yudao.module.trade.framework.aftersale.core.annotations.AfterSaleLog;
 import cn.iocoder.yudao.module.trade.framework.aftersale.core.utils.AfterSaleLogUtils;
 import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
@@ -71,6 +75,8 @@ public class AfterSaleServiceImpl implements AfterSaleService {
 
     @Resource
     private PayRefundApi payRefundApi;
+    @Resource
+    private CombinationRecordApi combinationRecordApi;
 
     @Resource
     private TradeOrderProperties tradeOrderProperties;
@@ -148,6 +154,14 @@ public class AfterSaleServiceImpl implements AfterSaleService {
                 && !TradeOrderStatusEnum.haveDelivered(order.getStatus())) {
             throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_DELIVERED);
         }
+        // 如果是拼团订单,则进行中不允许售后
+        if (TradeOrderTypeEnum.isCombination(order.getType())) {
+            CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(
+                    order.getUserId(), order.getId());
+            if (combinationRecord != null && CombinationRecordStatusEnum.isInProgress(combinationRecord.getStatus())) {
+                throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS);
+            }
+        }
         return orderItem;
     }
 

+ 2 - 5
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java

@@ -192,6 +192,8 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
             Integer enabledCondition = tradeConfigService.getTradeConfig().getBrokerageEnabledCondition();
             if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格
                 brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
+            } else {
+                brokerageUser.setBrokerageEnabled(false).setBrokerageTime(LocalDateTime.now());
             }
             brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser));
         } else {
@@ -266,11 +268,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
             return false;
         }
 
-        // 校验分佣模式:仅可后台手动设置推广员
-        if (BrokerageEnabledConditionEnum.ADMIN.getCondition().equals(tradeConfig.getBrokerageEnabledCondition())) {
-            throw exception(BROKERAGE_BIND_CONDITION_ADMIN);
-        }
-
         // 校验分销关系绑定模式
         if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) {
             // 判断是否为新用户:注册时间在 30 秒内的,都算新用户

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

@@ -96,7 +96,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
         Map<String, Object> templateParams = MapUtil.<String, Object>builder()
                 .put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime()))
                 .put("price", MoneyUtils.fenToYuanStr(withdraw.getPrice()))
-                .put("reason", withdraw.getAuditReason())
+                .put("reason", auditReason)
                 .build();
         notifyMessageSendApi.sendSingleMessageToMember(new NotifySendSingleToUserReqDTO()
                 .setUserId(withdraw.getUserId()).setTemplateCode(templateCode).setTemplateParams(templateParams));

+ 15 - 6
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.trade.service.order;
 
-import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO;
@@ -10,8 +9,9 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle
 import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO;
 import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
+import jakarta.validation.constraints.NotNull;
 
-import javax.validation.constraints.NotNull;
+import java.util.List;
 
 /**
  * 交易订单【写】Service 接口
@@ -187,13 +187,22 @@ public interface TradeOrderUpdateService {
      */
     void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId);
 
-    // TODO 芋艿:拼团取消,不调这个接口哈;
     /**
      * 取消支付订单
      *
-     * @param userId  用户编号
-     * @param orderId 订单编号
+     * @param userId           用户编号
+     * @param orderId          订单编号
+     * @param cancelType       取消类型
      */
-    void cancelPaidOrder(Long userId, Long orderId);
+    void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
+
+    /**
+     * 更新下单赠送的优惠券编号到订单
+     *
+     * @param userId        用户编号
+     * @param orderId       订单编号
+     * @param giveCouponIds 赠送的优惠券编号列表
+     */
+    void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds);
 
 }

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

@@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
+import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
+import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
@@ -111,6 +113,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     private ProductCommentApi productCommentApi;
     @Resource
     public SocialClientApi socialClientApi;
+    @Resource
+    public PayRefundApi payRefundApi;
 
     @Resource
     private TradeOrderProperties tradeOrderProperties;
@@ -197,6 +201,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
         order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum));
         order.setUserIp(getClientIP()).setTerminal(getTerminal());
+        // 使用 + 赠送优惠券
+        order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts());
         // 支付 + 退款信息
         order.setAdjustPrice(0).setPayStatus(false);
         order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0);
@@ -854,15 +860,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void cancelPaidOrder(Long userId, Long orderId) {
-        // TODO @puhui999:需要校验状态;已支付的情况下,才可以。
+    public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) {
+        // 1.1 这里校验下 cancelType 只允许拼团关闭;
+        if (ObjUtil.notEqual(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType(), cancelType)) {
+            return;
+        }
+        // 1.2 检验订单存在
+        TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
+        if (order == null) {
+            throw exception(ORDER_NOT_FOUND);
+        }
+
+        // 1.3 校验订单是否支付
+        if (!order.getPayStatus()) {
+            throw exception(ORDER_CANCEL_PAID_FAIL, "已支付");
+        }
+        // 1.3 校验订单是否已退款
+        if (ObjUtil.equal(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
+            throw exception(ORDER_CANCEL_PAID_FAIL, "未退款");
+        }
+
+        // 2.1 取消订单
+        cancelOrder0(order, TradeOrderCancelTypeEnum.COMBINATION_CLOSE);
+        // 2.2 创建退款单
+        payRefundApi.createRefund(new PayRefundCreateReqDTO()
+                .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用
+                .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
+                .setMerchantRefundId(String.valueOf(order.getId()))
+                .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice())); // 价格信息
+    }
+
+    @Override
+    public void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds) {
+        // 1. 检验订单存在
         TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
         if (order == null) {
             throw exception(ORDER_NOT_FOUND);
         }
-        cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
 
-        // TODO @puhui999:需要退款
+        // 2. 更新订单赠送的优惠券编号列表
+        tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds));
     }
 
     /**

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

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.trade.service.order.handler;
 import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO;
+import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
+import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
@@ -84,7 +86,9 @@ public class TradeCombinationOrderHandler implements TradeOrderHandler {
             return;
         }
         // 校验订单拼团是否成功
-        if (!combinationRecordApi.isCombinationRecordSuccess(order.getUserId(), order.getId())) {
+        CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(order.getUserId(), order.getId());
+        Assert.notNull(combinationRecord, "订单({})对应的拼团记录不存在", order.getId());
+        if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) {
             throw exception(ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS);
         }
     }

+ 32 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java

@@ -1,12 +1,16 @@
 package cn.iocoder.yudao.module.trade.service.order.handler;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.List;
 
 /**
@@ -17,6 +21,12 @@ import java.util.List;
 @Component
 public class TradeCouponOrderHandler implements TradeOrderHandler {
 
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private TradeOrderUpdateService orderUpdateService;
+    @Resource
+    private TradeOrderQueryService orderQueryService;
+
     @Resource
     private CouponApi couponApi;
 
@@ -30,13 +40,31 @@ public class TradeCouponOrderHandler implements TradeOrderHandler {
                 .setOrderId(order.getId()));
     }
 
+    @Override
+    public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        if (CollUtil.isEmpty(order.getGiveCouponTemplateCounts())) {
+            return;
+        }
+        // 赠送优惠券
+        List<Long> couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponTemplateCounts(), order.getUserId());
+        if (CollUtil.isEmpty(couponIds)) {
+            return;
+        }
+        orderUpdateService.updateOrderGiveCouponIds(order.getUserId(), order.getId(), couponIds);
+    }
+
     @Override
     public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
-        if (order.getCouponId() == null || order.getCouponId() <= 0) {
+        // 情况一:退还订单使用的优惠券
+        if (order.getCouponId() != null && order.getCouponId() > 0) {
+            // 退回优惠劵
+            couponApi.returnUsedCoupon(order.getCouponId());
+        }
+        // 情况二:收回赠送的优惠券
+        if (CollUtil.isEmpty(order.getGiveCouponIds())) {
             return;
         }
-        // 退回优惠劵
-        couponApi.returnUsedCoupon(order.getCouponId());
+        couponApi.invalidateCouponsByAdmin(order.getGiveCouponIds(), order.getUserId());
     }
 
 }

+ 93 - 3
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java

@@ -5,7 +5,9 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import lombok.Data;
 
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 价格计算 Response BO
@@ -44,9 +46,13 @@ public class TradePriceCalculateRespBO {
     private List<Promotion> promotions;
 
     /**
-     * 优惠劵编号
+     * 使用的优惠劵编号
      */
     private Long couponId;
+    /**
+     * 用户的优惠劵列表(可用 + 不可用)
+     */
+    private List<Coupon> coupons;
 
     /**
      * 会员剩余积分
@@ -67,6 +73,21 @@ public class TradePriceCalculateRespBO {
      */
     private Long bargainActivityId;
 
+    /**
+     * 是否包邮
+     */
+    private Boolean freeDelivery;
+
+    /**
+     * 赠送的优惠劵
+     *
+     * key: 优惠劵模版编号
+     * value:对应的优惠券数量
+     *
+     * 目的:用于订单支付后赠送优惠券
+     */
+    private Map<Long, Integer> giveCouponTemplateCounts;
+
     /**
      * 订单价格
      */
@@ -213,8 +234,19 @@ public class TradePriceCalculateRespBO {
          */
         private Long categoryId;
 
+        // ========== 物流相关字段 =========
+
+        /**
+         * 配送方式数组
+         *
+         * 对应 DeliveryTypeEnum 枚举
+         */
+        private List<Integer> deliveryTypes;
+
         /**
-         * 运费模板 Id
+         * 物流配置模板编号
+         *
+         * 对应 TradeDeliveryExpressTemplateDO 的 id 编号
          */
         private Long deliveryTemplateId;
 
@@ -234,7 +266,7 @@ public class TradePriceCalculateRespBO {
         private List<ProductPropertyValueDetailRespDTO> properties;
 
         /**
-         * 使用的积分
+         * 赠送的积分
          */
         private Integer givePoint;
 
@@ -312,4 +344,62 @@ public class TradePriceCalculateRespBO {
 
     }
 
+    /**
+     * 优惠劵信息
+     */
+    @Data
+    public static class Coupon {
+
+        /**
+         * 优惠劵编号
+         */
+        private Long id;
+        /**
+         * 优惠劵名
+         */
+        private String name;
+
+        /**
+         * 是否设置满多少金额可用,单位:分
+         */
+        private Integer usePrice;
+
+        /**
+         * 生效开始时间
+         */
+        private LocalDateTime validStartTime;
+        /**
+         * 生效结束时间
+         */
+        private LocalDateTime validEndTime;
+
+        /**
+         * 优惠类型
+         */
+        private Integer discountType;
+        /**
+         * 折扣百分比
+         */
+        private Integer discountPercent;
+        /**
+         * 优惠金额,单位:分
+         */
+        private Integer discountPrice;
+        /**
+         * 折扣上限,单位:分
+         */
+        private Integer discountLimitPrice;
+
+        /**
+         * 是否匹配
+         */
+        private Boolean match;
+        /**
+         * 不匹配的原因
+         */
+        private String mismatchReason;
+
+    }
+
+
 }

+ 67 - 25
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java

@@ -1,29 +1,30 @@
 package cn.iocoder.yudao.module.trade.service.price.calculator;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
-import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import jakarta.annotation.Resource;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.List;
 import java.util.function.Predicate;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
+import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_CAN_NOT_USE;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER;
 
 /**
@@ -40,33 +41,37 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
 
     @Override
     public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
-        // 1.1 校验优惠劵
-        if (param.getCouponId() == null) {
-            return;
-        }
-        CouponRespDTO coupon = couponApi.validateCoupon(new CouponValidReqDTO()
-                .setId(param.getCouponId()).setUserId(param.getUserId()));
-        Assert.notNull(coupon, "校验通过的优惠劵({}),不能为空", param.getCouponId());
-        // 1.2 只有【普通】订单,才允许使用优惠劵
+        // 只有【普通】订单,才允许使用优惠劵
         if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
-            throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
+            if (param.getCouponId() != null) {
+                throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
+            }
+            return;
         }
 
-        // 2.1 获得匹配的商品 SKU 数组
-        List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
-        if (CollUtil.isEmpty(orderItems)) {
-            throw exception(COUPON_NO_MATCH_SPU);
+        // 1.1 加载用户的优惠劵列表
+        List<CouponRespDTO> coupons = couponApi.getCouponListByUserId(param.getUserId(), CouponStatusEnum.UNUSED.getStatus());
+        coupons.removeIf(coupon -> LocalDateTimeUtils.beforeNow(coupon.getValidEndTime()));
+        // 1.2 计算优惠劵的使用条件
+        result.setCoupons(calculateCoupons(coupons, result));
+
+        // 2. 校验优惠劵是否可用
+        if (param.getCouponId() == null) {
+            return;
         }
-        // 2.2 计算是否满足优惠劵的使用金额
-        Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
-        if (totalPayPrice < coupon.getUsePrice()) {
-            throw exception(COUPON_NO_MATCH_MIN_PRICE);
+        TradePriceCalculateRespBO.Coupon couponBO = CollUtil.findOne(result.getCoupons(), item -> item.getId().equals(param.getCouponId()));
+        CouponRespDTO coupon = CollUtil.findOne(coupons, item -> item.getId().equals(param.getCouponId()));
+        if (couponBO == null || coupon == null) {
+            throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, "优惠劵不存在");
+        }
+        if (Boolean.FALSE.equals(couponBO.getMatch())) {
+            throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, couponBO.getMismatchReason());
         }
 
         // 3.1 计算可以优惠的金额
+        List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
+        Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
         Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
-        Assert.isTrue(couponPrice < totalPayPrice,
-                "优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice);
         // 3.2 计算分摊的优惠金额
         List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);
 
@@ -74,7 +79,7 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
         result.setCouponId(param.getCouponId());
         // 4.2 记录优惠明细
         TradePriceCalculatorHelper.addPromotion(result, orderItems,
-                param.getCouponId(), coupon.getName(), PromotionTypeEnum.COUPON.getType(),
+                param.getCouponId(), couponBO.getName(), PromotionTypeEnum.COUPON.getType(),
                 StrUtil.format("优惠劵:省 {} 元", TradePriceCalculatorHelper.formatPrice(couponPrice)),
                 divideCouponPrices);
         // 4.3 更新 SKU 优惠金额
@@ -86,6 +91,43 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
         TradePriceCalculatorHelper.recountAllPrice(result);
     }
 
+    /**
+     * 计算用户的优惠劵列表(可用 + 不可用)
+     *
+     * @param coupons 优惠劵
+     * @param result 计算结果
+     * @return 优惠劵列表
+     */
+    private List<TradePriceCalculateRespBO.Coupon> calculateCoupons(List<CouponRespDTO> coupons,
+                                                                    TradePriceCalculateRespBO result) {
+        return convertList(coupons, coupon -> {
+            TradePriceCalculateRespBO.Coupon matchCoupon = BeanUtils.toBean(coupon, TradePriceCalculateRespBO.Coupon.class);
+            // 1.1 优惠劵未到使用时间
+            if (LocalDateTimeUtils.afterNow(coupon.getValidStartTime())) {
+                return matchCoupon.setMatch(false).setMismatchReason("优惠劵未到使用时间");
+            }
+            // 1.2 优惠劵没有匹配的商品
+            List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
+            if (CollUtil.isEmpty(orderItems)) {
+                return matchCoupon.setMatch(false).setMismatchReason("优惠劵没有匹配的商品");
+            }
+            // 1.3 差 %1$,.2f 元可用优惠劵
+            Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
+            if (totalPayPrice < coupon.getUsePrice()) {
+                return matchCoupon.setMatch(false)
+                        .setMismatchReason(String.format("差 %1$,.2f 元可用优惠劵", (coupon.getUsePrice() - totalPayPrice) / 100D));
+            }
+            // 1.4 优惠金额超过订单金额
+            Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
+            if (couponPrice >= totalPayPrice) {
+                return matchCoupon.setMatch(false).setMismatchReason("优惠金额超过订单金额");
+            }
+
+            // 2. 满足条件
+            return matchCoupon.setMatch(true);
+        });
+    }
+
     private Integer getCouponPrice(CouponRespDTO coupon, Integer totalPayPrice) {
         if (PromotionDiscountTypeEnum.PRICE.getType().equals(coupon.getDiscountType())) { // 减价
             return coupon.getDiscountPrice();

+ 16 - 6
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
 import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
@@ -17,11 +18,11 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -55,7 +56,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
         if (param.getDeliveryType() == null) {
             return;
         }
-        // TODO @puhui999:需要校验,是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈
+        // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈
+        if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) {
+            throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL);
+        }
+
         if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) {
             calculateByPickUp(param);
         } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) {
@@ -90,7 +95,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
             return;
         }
 
-        // 情况二:快递模版
+        // 情况二:活动包邮
+        if (Boolean.TRUE.equals(result.getFreeDelivery())) {
+            return;
+        }
+
+        // 情况三:快递模版
         // 2.1 过滤出已选中的商品 SKU
         List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
         Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
@@ -124,7 +134,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
         Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
         // 依次计算快递运费
         for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) {
-            Long templateId  = entry.getKey();
+            Long templateId = entry.getKey();
             List<OrderItem> orderItems = entry.getValue();
             DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId);
             if (templateBO == null) {
@@ -144,8 +154,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
     /**
      * 按配送方式来计算运费
      *
-     * @param orderItems SKU 商品项目
-     * @param chargeMode  配送计费方式
+     * @param orderItems     SKU 商品项目
+     * @param chargeMode     配送计费方式
      * @param templateCharge 快递运费配置
      */
     private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode,

+ 3 - 3
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java

@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -31,8 +32,7 @@ public class TradePriceCalculatorHelper {
                                                                List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) {
         // 创建 PriceCalculateRespDTO 对象
         TradePriceCalculateRespBO result = new TradePriceCalculateRespBO();
-        result.setType(getOrderType(param));
-        result.setPromotions(new ArrayList<>());
+        result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>());
 
         // 创建它的 OrderItem 属性
         result.setItems(new ArrayList<>(param.getItems().size()));
@@ -60,7 +60,7 @@ public class TradePriceCalculatorHelper {
                     .setWeight(sku.getWeight()).setVolume(sku.getVolume());
             // spu 信息
             orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
-                    .setDeliveryTemplateId(spu.getDeliveryTemplateId())
+                    .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId())
                     .setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
             if (StrUtil.isBlank(orderItem.getPicUrl())) {
                 orderItem.setPicUrl(spu.getPicUrl());

+ 77 - 11
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java

@@ -3,23 +3,30 @@ package cn.iocoder.yudao.module.trade.service.price.calculator;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import jakarta.annotation.Resource;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
 
+// TODO @puhui999:相关的单测,建议改一改
+
 /**
  * 满减送活动的 {@link TradePriceCalculator} 实现类
  *
@@ -52,7 +59,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
     private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result,
                            RewardActivityMatchRespDTO rewardActivity) {
         // 1.1 获得满减送的订单项(商品)列表
-        List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, rewardActivity);
+        List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchActivityOrderItems(result, rewardActivity);
         if (CollUtil.isEmpty(orderItems)) {
             return;
         }
@@ -61,7 +68,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
         if (rule == null) {
             TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems,
                     rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
-                    getRewardActivityNotMeetTip(rewardActivity));
+                    getRewardActivityNotMeetTip(rewardActivity, orderItems));
             return;
         }
 
@@ -84,6 +91,36 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
             TradePriceCalculatorHelper.recountPayPrice(orderItem);
         }
         TradePriceCalculatorHelper.recountAllPrice(result);
+
+        // 4.1 记录赠送的积分
+        if (rule.getPoint() != null && rule.getPoint() > 0) {
+            List<Integer> dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint());
+            for (int i = 0; i < orderItems.size(); i++) {
+                // 商品可能赠送了积分,所以这里要加上
+                TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i);
+                orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i));
+            }
+        }
+        // 4.2 记录订单是否包邮
+        if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
+            // 只要满足一个活动包邮那么这单就包邮
+            result.setFreeDelivery(true);
+        }
+        // 4.3 记录赠送的优惠券
+        if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
+            for (Map.Entry<Long, Integer> entry : rule.getGiveCouponTemplateCounts().entrySet()) {
+                Map<Long, Integer> giveCouponTemplateCounts = result.getGiveCouponTemplateCounts();
+                // TODO @puhui999:是不是有一种可能性,这个 key 没有,别的 key 有哈。
+                // TODO 这里还有一种简化的写法。就是下面,大概两行就可以啦
+//                result.getGiveCouponTemplateCounts().put(entry.getKey(),
+//                        result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue());
+                if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券
+                    result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts());
+                } else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量
+                    giveCouponTemplateCounts.put(entry.getKey(), giveCouponTemplateCounts.get(entry.getKey()) + entry.getValue());
+                }
+            }
+        }
     }
 
     /**
@@ -93,10 +130,23 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
      * @param rewardActivity 满减送活动
      * @return 订单项(商品)列表
      */
-    private List<TradePriceCalculateRespBO.OrderItem> filterMatchCouponOrderItems(TradePriceCalculateRespBO result,
-                                                                                  RewardActivityMatchRespDTO rewardActivity) {
-        return filterList(result.getItems(),
-                orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId()));
+    private List<TradePriceCalculateRespBO.OrderItem> filterMatchActivityOrderItems(TradePriceCalculateRespBO result,
+                                                                                    RewardActivityMatchRespDTO rewardActivity) {
+        // 情况一:全部商品都可以参与
+        if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
+            return result.getItems();
+        }
+        // 情况二:指定商品参与
+        if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
+            return filterList(result.getItems(),
+                    orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId()));
+        }
+        // 情况三:指定商品类型参与
+        if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
+            return filterList(result.getItems(),
+                    orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId()));
+        }
+        return List.of();
     }
 
     /**
@@ -129,14 +179,30 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
     }
 
     /**
-     * 获得满减送活动匹配时的提示
+     * 获得满减送活动匹配时的提示
      *
      * @param rewardActivity 满减送活动
      * @return 提示
      */
-    private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity) {
-        // TODO 芋艿:后面再想想;应该找第一个规则,算下还差多少即可。
-        return "TODO";
+    private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity,
+                                               List<TradePriceCalculateRespBO.OrderItem> orderItems) {
+        // 1. 计算数量和价格
+        Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems);
+        Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
+        assert count != null && price != null;
+
+        // 2. 构建不满足时的提示信息:按最低档规则算
+        String meetTip = "满减送:购满 {} {},可以减 {} 元";
+        List<RewardActivityMatchRespDTO.Rule> rules = new ArrayList<>(rewardActivity.getRules());
+        rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛升序
+        RewardActivityMatchRespDTO.Rule rule = rules.get(0);
+        if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) {
+            return StrUtil.format(meetTip, rule.getLimit(), "元", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
+        }
+        if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())) {
+            return StrUtil.format(meetTip, rule.getLimit(), "件", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
+        }
+        return StrUtil.EMPTY;
     }
 
 }

+ 8 - 3
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java

@@ -1,12 +1,13 @@
 package cn.iocoder.yudao.module.trade.service.price.calculator;
 
+import cn.hutool.core.collection.ListUtil;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
 import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
-import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
@@ -14,8 +15,10 @@ import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
+import java.time.Duration;
 import java.util.ArrayList;
 
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static java.util.Arrays.asList;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -69,8 +72,10 @@ public class TradeCouponPriceCalculatorTest extends BaseMockitoUnitTest {
         CouponRespDTO coupon = randomPojo(CouponRespDTO.class, o -> o.setId(1024L).setName("程序员节")
                 .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
                 .setUsePrice(350).setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType())
-                .setDiscountPercent(50).setDiscountLimitPrice(70));
-        when(couponApi.validateCoupon(eq(new CouponValidReqDTO().setId(1024L).setUserId(233L)))).thenReturn(coupon);
+                .setDiscountPercent(50).setDiscountLimitPrice(70))
+                .setValidStartTime(addTime(Duration.ofDays(1))).setValidEndTime(addTime(Duration.ofDays(2)));
+        when(couponApi.getCouponListByUserId(eq(233L), eq(CouponStatusEnum.UNUSED.getStatus())))
+                .thenReturn(ListUtil.toList(coupon));
 
         // 调用
         tradeCouponPriceCalculator.calculate(param, result);

+ 20 - 9
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
@@ -13,6 +14,7 @@ import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 
 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
@@ -47,7 +49,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
         TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
                 .setType(TradeOrderTypeEnum.NORMAL.getType())
                 .setPrice(new TradePriceCalculateRespBO.Price())
-                .setPromotions(new ArrayList<>())
+                .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>())
                 .setItems(asList(
                         new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
                                 .setPrice(100).setSpuId(1L),
@@ -60,16 +62,22 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
         TradePriceCalculatorHelper.recountPayPrice(result.getItems());
         TradePriceCalculatorHelper.recountAllPrice(result);
 
-        // mock 方法(限时折扣 DiscountActivity 信息)
+        // mock 方法(满减送 RewardActivity 信息)
         when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList(
                 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
-                        .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
-                        .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))),
+                        .setConditionType(PromotionConditionTypeEnum.PRICE.getType())
+                        .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
+                        .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70)
+                                .setFreeDelivery(false)))),
                 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号")
-                        .setSpuIds(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType())
-                        .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10),
-                                new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个
-                                new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100))))
+                        .setConditionType(PromotionConditionTypeEnum.COUNT.getType())
+                        .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
+                        .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10)
+                                        .setPoint(50).setFreeDelivery(false),
+                                new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60)
+                                        .setPoint(100).setFreeDelivery(false), // 最大可满足,因为是 4 个
+                                new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)
+                                        .setFreeDelivery(false))))
         ));
 
         // 调用
@@ -94,6 +102,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
         assertEquals(orderItem01.getCouponPrice(), 0);
         assertEquals(orderItem01.getPointPrice(), 0);
         assertEquals(orderItem01.getPayPrice(), 160);
+        assertEquals(orderItem01.getGivePoint(), 0);
         // 断言:SKU 2
         TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
         assertEquals(orderItem02.getSkuId(), 20L);
@@ -104,6 +113,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
         assertEquals(orderItem02.getCouponPrice(), 0);
         assertEquals(orderItem02.getPointPrice(), 0);
         assertEquals(orderItem02.getPayPrice(), 120);
+        assertEquals(orderItem02.getGivePoint(), 0);
         // 断言:SKU 3
         TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2);
         assertEquals(orderItem03.getSkuId(), 30L);
@@ -114,6 +124,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
         assertEquals(orderItem03.getCouponPrice(), 0);
         assertEquals(orderItem03.getPointPrice(), 0);
         assertEquals(orderItem03.getPayPrice(), 60);
+        assertEquals(orderItem03.getGivePoint(), 100);
         // 断言:Promotion 部分(第一个)
         assertEquals(result.getPromotions().size(), 2);
         TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
@@ -175,7 +186,7 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
         // mock 方法(限时折扣 DiscountActivity 信息)
         when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList(
                 randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
-                        .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
+                        .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
                         .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70))))
         ));
 

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql

@@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "trade_order"
     "give_point"              int      NULL,
     "refund_point"            int      NULL,
     "vip_price"               int      NULL,
+    "give_coupons_map"        varchar  NULL,
     "seckill_activity_id"     long     NULL,
     "bargain_activity_id"     long     NULL,
     "bargain_record_id"       long     NULL,

+ 2 - 10
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java

@@ -18,12 +18,12 @@ import cn.iocoder.yudao.module.member.service.user.MemberUserService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
@@ -76,14 +76,6 @@ public class MemberUserController {
         return success(true);
     }
 
-    @PutMapping("/update-balance")
-    @Operation(summary = "更新会员用户余额")
-    @PreAuthorize("@ss.hasPermission('member:user:update-balance')")
-    public CommonResult<Boolean> updateUserBalance(@Valid @RequestBody Long id) {
-        // todo @jason:增加一个【修改余额】
-        return success(true);
-    }
-
     @GetMapping("/get")
     @Operation(summary = "获得会员用户")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")

+ 2 - 3
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/wallet/PayWalletBizTypeEnum.java

@@ -18,9 +18,8 @@ public enum PayWalletBizTypeEnum implements IntArrayValuable {
     RECHARGE(1, "充值"),
     RECHARGE_REFUND(2, "充值退款"),
     PAYMENT(3, "支付"),
-    PAYMENT_REFUND(4, "支付退款");
-
-    // TODO 后续增加
+    PAYMENT_REFUND(4, "支付退款"),
+    UPDATE_BALANCE(5, "更新余额");
 
     /**
      * 业务分类

+ 22 - 3
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java

@@ -4,24 +4,26 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO;
+import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO;
 import cn.iocoder.yudao.module.pay.convert.wallet.PayWalletConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
+import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
 import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
 
 import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER;
+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.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND;
 
 @Tag(name = "管理后台 - 用户钱包")
 @RestController
@@ -49,4 +51,21 @@ public class PayWalletController {
         return success(PayWalletConvert.INSTANCE.convertPage(pageResult));
     }
 
+    @PutMapping("/update-balance")
+    @Operation(summary = "更新会员用户余额")
+    @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')")
+    public CommonResult<Boolean> updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) {
+        // 获得用户钱包
+        PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue());
+        if (wallet == null) {
+            log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO);
+            throw exception(WALLET_NOT_FOUND);
+        }
+
+        // 更新钱包余额
+        payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()),
+                PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance());
+        return success(true);
+    }
+
 }

+ 19 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 修改钱包余额 Request VO")
+@Data
+public class PayWalletUpdateBalanceReqVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+    @NotNull(message = "变动余额不能为空")
+    private Integer balance;
+
+}

+ 4 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java

@@ -12,12 +12,12 @@ import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -176,6 +176,9 @@ public class PayWalletServiceImpl implements  PayWalletService {
                 walletMapper.updateWhenRecharge(payWallet.getId(), price);
                 break;
             }
+            case UPDATE_BALANCE: // 更新余额
+                walletMapper.updateWhenRecharge(payWallet.getId(), price);
+                break;
             default: {
                 // TODO 其它类型待实现
                 throw new UnsupportedOperationException("待实现");

+ 12 - 2
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApi.java

@@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogCreateReqDTO;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogPageReqDTO;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogRespDTO;
-
-import javax.validation.Valid;
+import jakarta.validation.Valid;
+import org.springframework.scheduling.annotation.Async;
 
 /**
  * 操作日志 API 接口
@@ -21,6 +21,16 @@ public interface OperateLogApi {
      */
     void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO);
 
+    /**
+     * 【异步】创建操作日志
+     *
+     * @param createReqDTO 请求
+     */
+    @Async
+    default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) {
+        createOperateLog(createReqDTO);
+    }
+
     /**
      * 获取指定模块的指定数据的操作日志分页
      *

+ 1 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/logger/OperateLogApiImpl.java

@@ -8,12 +8,10 @@ import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogRespDTO;
 import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO;
 import cn.iocoder.yudao.module.system.service.logger.OperateLogService;
 import com.fhs.core.trans.anno.TransMethodResult;
-import org.springframework.scheduling.annotation.Async;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
-
 /**
  * 操作日志 API 实现类
  *
@@ -27,7 +25,6 @@ public class OperateLogApiImpl implements OperateLogApi {
     private OperateLogService operateLogService;
 
     @Override
-    @Async
     public void createOperateLog(OperateLogCreateReqDTO createReqDTO) {
         operateLogService.createOperateLog(createReqDTO);
     }

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