Explorar o código

Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/DiscountActivityApi.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/DiscountActivityApiImpl.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/discount/DiscountActivityConvert.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountProductMapper.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityService.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java
#	yudao-module-mall/yudao-module-promotion-biz/src/main/resources/mapper/discount/DiscountProductMapper.xml
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java
YunaiV hai 8 meses
pai
achega
20775791d9
Modificáronse 100 ficheiros con 1269 adicións e 1537 borrados
  1. 3 3
      pom.xml
  2. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  3. 3 1
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java
  4. 1 3
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java
  5. 2 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java
  6. 6 6
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java
  7. 0 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkService.java
  8. 0 33
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java
  9. 0 19
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkService.java
  10. 0 33
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java
  11. 6 6
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
  12. 4 9
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
  13. 6 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
  14. 2 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
  15. 9 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
  16. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
  17. 11 0
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java
  18. 11 0
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java
  19. 1 1
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
  20. 0 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
  21. 0 13
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  22. 8 5
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java
  23. 0 46
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm
  24. 0 124
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm
  25. 0 65
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm
  26. 0 85
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm
  27. 33 9
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm
  28. 0 45
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
  29. 0 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuDetailRespVO.java
  30. 0 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuRespVO.java
  31. 4 4
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
  32. 110 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java
  33. 9 9
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApi.java
  34. 0 27
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/dto/CouponValidReqDTO.java
  35. 3 3
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/DiscountActivityApi.java
  36. 11 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/dto/DiscountProductRespDTO.java
  37. 4 5
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApi.java
  38. 12 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java
  39. 0 2
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
  40. 4 9
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
  41. 6 9
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/coupon/CouponApiImpl.java
  42. 5 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/DiscountActivityApiImpl.java
  43. 3 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApiImpl.java
  44. 22 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java
  45. 10 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java
  46. 3 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java
  47. 23 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java
  48. 3 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java
  49. 19 153
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
  50. 18 38
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java
  51. 5 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java
  52. 5 12
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponController.java
  53. 0 30
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java
  54. 0 16
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java
  55. 0 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java
  56. 8 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/reward/vo/AppRewardActivityRespVO.java
  57. 21 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java
  58. 3 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java
  59. 20 17
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
  60. 0 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/coupon/CouponConvert.java
  61. 96 14
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/discount/DiscountActivityConvert.java
  62. 33 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java
  63. 7 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/discount/DiscountProductDO.java
  64. 7 32
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java
  65. 7 38
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationActivityMapper.java
  66. 0 20
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java
  67. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
  68. 12 19
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountProductMapper.java
  69. 20 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
  70. 11 35
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
  71. 5 9
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityService.java
  72. 5 14
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityServiceImpl.java
  73. 7 18
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java
  74. 6 20
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
  75. 0 30
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
  76. 4 31
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
  77. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
  78. 5 9
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityService.java
  79. 52 107
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java
  80. 5 5
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityService.java
  81. 65 11
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImpl.java
  82. 15 10
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java
  83. 14 17
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
  84. 0 25
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/util/PromotionUtils.java
  85. 0 5
      yudao-module-mall/yudao-module-promotion-biz/src/main/resources/mapper/discount/DiscountProductMapper.xml
  86. 86 94
      yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/reward/RewardActivityServiceImplTest.java
  87. 2 1
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  88. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java
  89. 5 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.http
  90. 10 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java
  91. 51 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java
  92. 56 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeProductSettlementRespVO.java
  93. 1 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java
  94. 15 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java
  95. 2 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  96. 5 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java
  97. 14 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceService.java
  98. 73 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java
  99. 64 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateRespBO.java
  100. 67 27
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java

+ 3 - 3
pom.xml

@@ -15,12 +15,12 @@
         <!-- 各种 module 拓展 -->
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
-        <module>yudao-module-member</module>
+<!--        <module>yudao-module-member</module>-->
 <!--        <module>yudao-module-bpm</module>-->
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
-        <module>yudao-module-pay</module>
-        <module>yudao-module-mall</module>
+<!--        <module>yudao-module-pay</module>-->
+<!--        <module>yudao-module-mall</module>-->
 <!--        <module>yudao-module-crm</module>-->
 <!--        <module>yudao-module-erp</module>-->
 <!--        <module>yudao-module-ai</module>-->

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

@@ -290,7 +290,7 @@ public class CollectionUtils {
         return valueFunc.apply(t);
     }
 
-    public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc,
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
                                                                      BinaryOperator<V> accumulator) {
         return getSumValue(from, valueFunc, accumulator, null);
     }

+ 3 - 1
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.tenant.core.job;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
@@ -44,7 +45,8 @@ public class TenantJobAspect {
             // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
             TenantUtils.execute(tenantId, () -> {
                 try {
-                    joinPoint.proceed();
+                    Object result = joinPoint.proceed();
+                    results.put(tenantId, StrUtil.toStringOrNull(result));
                 } catch (Throwable e) {
                     log.error("[execute][租户({}) 执行 Job 发生异常", tenantId, e);
                     results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));

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

@@ -11,7 +11,6 @@ import com.mzt.logapi.service.ILogRecordService;
 import jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
 
 import java.util.List;
 
@@ -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);

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

@@ -2,15 +2,10 @@ 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;
@@ -23,18 +18,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 @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 请求日志
      */
@@ -42,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);
     }
 

+ 6 - 6
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;
@@ -36,7 +36,7 @@ import java.time.temporal.ChronoUnit;
 import java.util.Iterator;
 import java.util.Map;
 
-import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*;
+import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
@@ -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 jakarta.annotation.Resource;
-import jakarta.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

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

@@ -5,7 +5,6 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.servlet.JakartaServletUtil;
-import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
 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;
@@ -14,6 +13,7 @@ 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;
@@ -40,12 +40,7 @@ import java.time.LocalDateTime;
 import java.util.Map;
 import java.util.Set;
 
-import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
-import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
-import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
-import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.METHOD_NOT_ALLOWED;
-import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_FOUND;
-import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
 
 /**
  * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
@@ -65,7 +60,7 @@ public class GlobalExceptionHandler {
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     private final String applicationName;
 
-    private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
+    private final ApiErrorLogApi apiErrorLogApi;
 
     /**
      * 处理所有异常,主要是提供给 Filter 使用
@@ -288,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);
         }

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
 import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
 import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
 import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
+import cn.iocoder.yudao.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer;
 import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
 import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
 import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
@@ -76,6 +77,11 @@ public class YudaoWebSocketAutoConfiguration {
         return new WebSocketSessionManagerImpl();
     }
 
+    @Bean
+    public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
+        return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
+    }
+
     // ==================== Sender 相关 ====================
 
     @Configuration

+ 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));
         // 获得金额

+ 11 - 0
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApi.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
 
 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);
+    }
+
 }

+ 11 - 0
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApi.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.api.logger;
 import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
 
 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);
+    }
+
 }

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

@@ -22,7 +22,7 @@ public interface ErrorCodeConstants {
     ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改");
     ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改");
     ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确");
-    ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在");
+    ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在,注意 Bean 默认首字母小写");
     ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口");
 
     // ========== API 错误日志 1-001-002-000 ==========

+ 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";
     }

+ 8 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java

@@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.quartz.SchedulerException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -91,13 +92,15 @@ public class JobServiceImpl implements JobService {
     }
 
     private void validateJobHandlerExists(String handlerName) {
-        Object handler = SpringUtil.getBean(handlerName);
-        if (handler == null) {
+        try {
+            Object handler = SpringUtil.getBean(handlerName);
+            assert handler != null;
+            if (!(handler instanceof JobHandler)) {
+                throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
+            }
+        } catch (NoSuchBeanDefinitionException e) {
             throw exception(JOB_HANDLER_BEAN_NOT_EXISTS);
         }
-        if (!(handler instanceof JobHandler)) {
-            throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
-        }
     }
 
     @Override

+ 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

+ 0 - 45
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java

@@ -4,10 +4,6 @@ import cn.hutool.core.collection.CollUtil;
 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.member.api.level.MemberLevelApi;
-import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
-import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
-import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuDetailRespVO;
 import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRespVO;
@@ -51,11 +47,6 @@ public class AppProductSpuController {
     @Resource
     private ProductBrowseHistoryService productBrowseHistoryService;
 
-    @Resource
-    private MemberLevelApi memberLevelApi;
-    @Resource
-    private MemberUserApi memberUserApi;
-
     @GetMapping("/list-by-ids")
     @Operation(summary = "获得商品 SPU 列表")
     @Parameter(name = "ids", description = "编号列表", required = true)
@@ -68,9 +59,6 @@ public class AppProductSpuController {
         // 拼接返回
         list.forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount()));
         List<AppProductSpuRespVO> voList = BeanUtils.toBean(list, AppProductSpuRespVO.class);
-        // 处理 vip 价格
-        MemberLevelRespDTO memberLevel = getMemberLevel();
-        voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
         return success(voList);
     }
 
@@ -85,9 +73,6 @@ public class AppProductSpuController {
         // 拼接返回
         pageResult.getList().forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount()));
         PageResult<AppProductSpuRespVO> voPageResult = BeanUtils.toBean(pageResult, AppProductSpuRespVO.class);
-        // 处理 vip 价格
-        MemberLevelRespDTO memberLevel = getMemberLevel();
-        voPageResult.getList().forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
         return success(voPageResult);
     }
 
@@ -115,37 +100,7 @@ public class AppProductSpuController {
         spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount());
         AppProductSpuDetailRespVO spuVO = BeanUtils.toBean(spu, AppProductSpuDetailRespVO.class)
                 .setSkus(BeanUtils.toBean(skus, AppProductSpuDetailRespVO.Sku.class));
-        // 处理 vip 价格
-        MemberLevelRespDTO memberLevel = getMemberLevel();
-        spuVO.setVipPrice(calculateVipPrice(spuVO.getPrice(), memberLevel));
         return success(spuVO);
     }
 
-    private MemberLevelRespDTO getMemberLevel() {
-        Long userId = getLoginUserId();
-        if (userId == null) {
-            return null;
-        }
-        MemberUserRespDTO user = memberUserApi.getUser(userId);
-        if (user.getLevelId() == null || user.getLevelId() <= 0) {
-            return null;
-        }
-        return memberLevelApi.getMemberLevel(user.getLevelId());
-    }
-
-    /**
-     * 计算会员 VIP 优惠价格
-     *
-     * @param price 原价
-     * @param memberLevel 会员等级
-     * @return 优惠价格
-     */
-    public Integer calculateVipPrice(Integer price, MemberLevelRespDTO memberLevel) {
-        if (memberLevel == null || memberLevel.getDiscountPercent() == null) {
-            return 0;
-        }
-        Integer newPrice = price * memberLevel.getDiscountPercent() / 100;
-        return price - newPrice;
-    }
-
 }

+ 0 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuDetailRespVO.java

@@ -46,9 +46,6 @@ public class AppProductSpuDetailRespVO {
     @Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Integer marketPrice;
 
-    @Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级,计算出折扣后价格
-    private Integer vipPrice;
-
     @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
     private Integer stock;
 

+ 0 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuRespVO.java

@@ -38,9 +38,6 @@ public class AppProductSpuRespVO {
     @Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Integer marketPrice;
 
-    @Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级,计算出折扣后价格
-    private Integer vipPrice;
-
     @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
     private Integer stock;
 

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

@@ -2,8 +2,8 @@ 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 jakarta.validation.Valid;
 
 /**
@@ -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;
+
+}

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

@@ -2,7 +2,6 @@ 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 java.util.List;
@@ -15,6 +14,15 @@ import java.util.Map;
  */
 public interface CouponApi {
 
+    /**
+     * 获得用户的优惠劵列表
+     *
+     * @param userId 用户编号
+     * @param status 优惠劵状态
+     * @return 优惠劵列表
+     */
+    List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status);
+
     /**
      * 使用优惠劵
      *
@@ -29,14 +37,6 @@ public interface CouponApi {
      */
     void returnUsedCoupon(Long id);
 
-    /**
-     * 校验优惠劵
-     *
-     * @param validReqDTO 校验请求
-     * @return 优惠劵
-     */
-    CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO);
-
     /**
      * 【管理员】给指定用户批量发送优惠券
      *

+ 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 jakarta.validation.constraints.NotNull;
-
-/**
- * 优惠劵使用 Request DTO
- *
- * @author 芋道源码
- */
-@Data
-public class CouponValidReqDTO {
-
-    /**
-     * 优惠劵编号
-     */
-    @NotNull(message = "优惠劵编号不能为空")
-    private Long id;
-
-    /**
-     * 用户编号
-     */
-    @NotNull(message = "用户编号不能为空")
-    private Long userId;
-
-}

+ 3 - 3
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/DiscountActivityApi.java

@@ -13,11 +13,11 @@ import java.util.List;
 public interface DiscountActivityApi {
 
     /**
-     * 获得商品匹配的的限时折扣信息
+     * 获得 skuId 商品匹配的的限时折扣信息
      *
-     * @param spuIds 商品 spu 编号数组
+     * @param skuIds 商品 SKU 编号数组
      * @return 限时折扣信息
      */
-    List<DiscountProductRespDTO> getMatchDiscountProductList(Collection<Long> spuIds);
+    List<DiscountProductRespDTO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds);
 
 }

+ 11 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/dto/DiscountProductRespDTO.java

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.promotion.api.discount.dto;
 
 import lombok.Data;
 
+import java.time.LocalDateTime;
+
 /**
  * 限时折扣活动商品 Response DTO
  *
@@ -44,5 +46,14 @@ public class DiscountProductRespDTO {
      * 活动标题
      */
     private String activityName;
+    /**
+     * 活动开始时间点
+     */
+    private LocalDateTime activityStartTime;
+    /**
+     * 活动结束时间点
+     */
+    private LocalDateTime activityEndTime;
+
 
 }

+ 4 - 5
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApi.java

@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.promotion.api.reward;
 
 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
 
-import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -13,12 +13,11 @@ import java.util.List;
 public interface RewardActivityApi {
 
     /**
-     * 获得当前时间内开启的满减送活动
+     * 获得 spuId 商品匹配的的满减送活动列表
      *
-     * @param status   状态
-     * @param dateTime 当前时间,即筛选 <= dateTime 的满减送活动
+     * @param spuIds   SPU 编号
      * @return 满减送活动列表
      */
-    List<RewardActivityMatchRespDTO> getRewardActivityListByStatusAndNow(Integer status, LocalDateTime dateTime);
+    List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds);
 
 }

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

@@ -18,6 +18,11 @@ import java.util.Map;
 @Data
 public class RewardActivityMatchRespDTO {
 
+    /**
+     * 匹配的 SPU 数组
+     */
+    private List<Long> spuIds;
+
     /**
      * 活动编号,主键自增
      */
@@ -100,6 +105,13 @@ public class RewardActivityMatchRespDTO {
          */
         private Map<Long, Integer> giveCouponTemplateCounts;
 
+        /**
+         * 规则描述
+         *
+         * 通过 {@link #limit}、{@link #discountPrice} 等字段进行拼接
+         */
+        private String description;
+
     }
 
 }

+ 0 - 2
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, "优惠劵模板不存在");

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

@@ -1,19 +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 static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COMBINATION_RECORD_NOT_EXISTS;
-
 /**
  * 拼团活动 API 实现类
  *
@@ -37,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

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

@@ -1,11 +1,9 @@
 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;
@@ -26,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(),
@@ -37,12 +40,6 @@ public class CouponApiImpl implements CouponApi {
         couponService.returnUsedCoupon(id);
     }
 
-    @Override
-    public CouponRespDTO validateCoupon(CouponValidReqDTO validReqDTO) {
-        CouponDO coupon = couponService.validCoupon(validReqDTO.getId(), validReqDTO.getUserId());
-        return CouponConvert.INSTANCE.convert(coupon);
-    }
-
     @Override
     public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
         return couponService.takeCouponsByAdmin(giveCoupons, userId);

+ 5 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/discount/DiscountActivityApiImpl.java

@@ -1,7 +1,8 @@
 package cn.iocoder.yudao.module.promotion.api.discount;
 
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
-import cn.iocoder.yudao.module.promotion.convert.discount.DiscountActivityConvert;
+import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
 import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityService;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
@@ -23,8 +24,9 @@ public class DiscountActivityApiImpl implements DiscountActivityApi {
     private DiscountActivityService discountActivityService;
 
     @Override
-    public List<DiscountProductRespDTO> getMatchDiscountProductList(Collection<Long> spuIds) {
-        return DiscountActivityConvert.INSTANCE.convertList02(discountActivityService.getMatchDiscountProductList(spuIds));
+    public List<DiscountProductRespDTO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds) {
+        List<DiscountProductDO> list = discountActivityService.getMatchDiscountProductListBySkuIds(skuIds);
+        return BeanUtils.toBean(list, DiscountProductRespDTO.class);
     }
 
 }

+ 3 - 6
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/reward/RewardActivityApiImpl.java

@@ -1,14 +1,12 @@
 package cn.iocoder.yudao.module.promotion.api.reward;
 
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
-import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -24,9 +22,8 @@ public class RewardActivityApiImpl implements RewardActivityApi {
     private RewardActivityService rewardActivityService;
 
     @Override
-    public List<RewardActivityMatchRespDTO> getRewardActivityListByStatusAndNow(Integer status, LocalDateTime dateTime) {
-        List<RewardActivityDO> list = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(status, dateTime);
-        return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
+    public List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds) {
+        return rewardActivityService.getMatchRewardActivityListBySpuIds(spuIds);
     }
 
 }

+ 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 jakarta.annotation.Resource;
-import jakarta.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 读取
+
 }

+ 3 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java

@@ -9,12 +9,12 @@ import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityType
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import org.springframework.format.annotation.DateTimeFormat;
-
 import jakarta.validation.constraints.AssertTrue;
 import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Objects;

+ 23 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/SeckillActivityController.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.promotion.controller.admin.seckill;
 
 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;
@@ -13,15 +14,17 @@ 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 jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import jakarta.annotation.Resource;
-import jakarta.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.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
 @Tag(name = "管理后台 - 秒杀活动")
@@ -89,11 +92,28 @@ public class SeckillActivityController {
         }
 
         // 拼接数据
-        List<SeckillProductDO> products = seckillActivityService.getSeckillProductListByActivityId(
+        List<SeckillProductDO> products = seckillActivityService.getSeckillProductListByActivityIds(
                 convertSet(pageResult.getList(), SeckillActivityDO::getId));
         List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(
                 convertSet(pageResult.getList(), SeckillActivityDO::getSpuId));
         return success(SeckillActivityConvert.INSTANCE.convertPage(pageResult, products, spuList));
     }
 
+    @GetMapping("/list-by-ids")
+    @Operation(summary = "获得秒杀活动列表,基于活动编号数组")
+    @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
+    public CommonResult<List<SeckillActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
+        // 1. 获得开启的活动列表
+        List<SeckillActivityDO> activityList = seckillActivityService.getSeckillActivityListByIds(ids);
+        activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
+        if (CollUtil.isEmpty(activityList)) {
+            return success(Collections.emptyList());
+        }
+        // 2. 拼接返回
+        List<SeckillProductDO> productList = seckillActivityService.getSeckillProductListByActivityIds(
+                convertList(activityList, SeckillActivityDO::getId));
+        List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId));
+        return success(SeckillActivityConvert.INSTANCE.convertList(activityList, productList, spuList));
+    }
+
 }

+ 3 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java

@@ -54,4 +54,7 @@ public class SeckillActivityRespVO extends SeckillActivityBaseVO {
             example = "50")
     private Integer marketPrice;
 
+    @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+    private Integer seckillPrice; // 从 products 获取最小 price 读取
+
 }

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

@@ -1,25 +1,13 @@
 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;
-import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO;
-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.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;
-import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityService;
-import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
 import cn.iocoder.yudao.module.promotion.service.seckill.SeckillActivityService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -31,11 +19,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.time.LocalDateTime;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 
 @Tag(name = "用户 APP - 营销活动") // 用于提供跨多个活动的 HTTP 接口
 @RestController
@@ -49,152 +36,31 @@ public class AppActivityController {
     private SeckillActivityService seckillActivityService;
     @Resource
     private BargainActivityService bargainActivityService;
-    @Resource
-    private DiscountActivityService discountActivityService;
-    @Resource
-    private RewardActivityService rewardActivityService;
-
-    @Resource
-    private ProductSpuApi productSpuApi;
 
     @GetMapping("/list-by-spu-id")
-    @Operation(summary = "获得单个商品,近期参与的每个活动")
+    @Operation(summary = "获得单个商品,进行中的拼团、秒杀、砍价活动信息", description = "每种活动,只返回一个")
     @Parameter(name = "spuId", description = "商品编号", required = true)
     public CommonResult<List<AppActivityRespVO>> getActivityListBySpuId(@RequestParam("spuId") Long spuId) {
-        // 每种活动,只返回一个
-        return success(getAppActivityList(Collections.singletonList(spuId)));
-    }
-
-    @GetMapping("/list-by-spu-ids")
-    @Operation(summary = "获得多个商品,近期参与的每个活动")
-    @Parameter(name = "spuIds", description = "商品编号数组", required = true)
-    public CommonResult<Map<Long, List<AppActivityRespVO>>> getActivityListBySpuIds(@RequestParam("spuIds") List<Long> spuIds) {
-        if (CollUtil.isEmpty(spuIds)) {
-            return success(MapUtil.empty());
-        }
-        // 每种活动,只返回一个;key 为 SPU 编号
-        return success(convertMultiMap(getAppActivityList(spuIds), AppActivityRespVO::getSpuId));
-    }
-
-    private List<AppActivityRespVO> getAppActivityList(Collection<Long> spuIds) {
-        if (CollUtil.isEmpty(spuIds)) {
-            return new ArrayList<>();
-        }
-        // 获取开启的且开始的且没有结束的活动
-        List<AppActivityRespVO> activityList = new ArrayList<>();
-        LocalDateTime now = LocalDateTime.now();
+        List<AppActivityRespVO> activityVOList = new ArrayList<>();
         // 1. 拼团活动
-        getCombinationActivities(spuIds, now, activityList);
-        // 2. 秒杀活动
-        getSeckillActivities(spuIds, now, activityList);
-        // 3. 砍价活动
-        getBargainActivities(spuIds, now, activityList);
-        // 4. 限时折扣活动
-        getDiscountActivities(spuIds, now, activityList);
-        // 5. 满减送活动
-        getRewardActivityList(spuIds, now, activityList);
-        return activityList;
-    }
-
-    private void getCombinationActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
-        List<CombinationActivityDO> combinationActivities = combinationActivityService.getCombinationActivityBySpuIdsAndStatusAndDateTimeLt(
-                spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
-        if (CollUtil.isEmpty(combinationActivities)) {
-            return;
-        }
-
-        combinationActivities.forEach(item -> {
-            activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.COMBINATION_ACTIVITY.getType(),
-                    item.getName(), item.getSpuId(), item.getStartTime(), item.getEndTime()));
-        });
-    }
-
-    private void getSeckillActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
-        List<SeckillActivityDO> seckillActivities = seckillActivityService.getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(
-                spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
-        if (CollUtil.isEmpty(seckillActivities)) {
-            return;
+        CombinationActivityDO combinationActivity = combinationActivityService.getMatchCombinationActivityBySpuId(spuId);
+        if (combinationActivity != null) {
+            activityVOList.add(new AppActivityRespVO(combinationActivity.getId(), PromotionTypeEnum.COMBINATION_ACTIVITY.getType(),
+                    combinationActivity.getName(), combinationActivity.getSpuId(), combinationActivity.getStartTime(), combinationActivity.getEndTime()));
         }
-
-        seckillActivities.forEach(item -> {
-            activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.SECKILL_ACTIVITY.getType(),
-                    item.getName(), item.getSpuId(), item.getStartTime(), item.getEndTime()));
-        });
-    }
-
-    private void getBargainActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
-        List<BargainActivityDO> bargainActivities = bargainActivityService.getBargainActivityBySpuIdsAndStatusAndDateTimeLt(
-                spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
-        if (CollUtil.isNotEmpty(bargainActivities)) {
-            return;
-        }
-
-        bargainActivities.forEach(item -> {
-            activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.BARGAIN_ACTIVITY.getType(),
-                    item.getName(), item.getSpuId(), item.getStartTime(), item.getEndTime()));
-        });
-    }
-
-    private void getDiscountActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
-        List<DiscountActivityDO> discountActivities = discountActivityService.getDiscountActivityBySpuIdsAndStatusAndDateTimeLt(
-                spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
-        if (CollUtil.isEmpty(discountActivities)) {
-            return;
-        }
-
-        List<DiscountProductDO> products = discountActivityService.getDiscountProductsByActivityId(
-                convertSet(discountActivities, DiscountActivityDO::getId));
-        Map<Long, Long> productMap = convertMap(products, DiscountProductDO::getActivityId, DiscountProductDO::getSpuId);
-        discountActivities.forEach(item -> activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType(),
-                item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime())));
-    }
-
-    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;
-        }
-
-        // 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);
-            }
+        // 2. 秒杀活动
+        SeckillActivityDO seckillActivity = seckillActivityService.getMatchSeckillActivityBySpuId(spuId);
+        if (seckillActivity != null) {
+            activityVOList.add(new AppActivityRespVO(seckillActivity.getId(), PromotionTypeEnum.SECKILL_ACTIVITY.getType(),
+                    seckillActivity.getName(), seckillActivity.getSpuId(), seckillActivity.getStartTime(), seckillActivity.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()));
+        // 3. 砍价活动
+        BargainActivityDO bargainActivity = bargainActivityService.getMatchBargainActivityBySpuId(spuId);
+        if (bargainActivity != null) {
+            activityVOList.add(new AppActivityRespVO(bargainActivity.getId(), PromotionTypeEnum.BARGAIN_ACTIVITY.getType(),
+                    bargainActivity.getName(), bargainActivity.getSpuId(), bargainActivity.getStartTime(), bargainActivity.getEndTime()));
         }
+        return success(activityVOList);
     }
 
 }

+ 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 jakarta.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 jakarta.annotation.Resource;
-import jakarta.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 jakarta.validation.constraints.NotEmpty;
-import jakarta.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 jakarta.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 使用

+ 8 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/reward/vo/AppRewardActivityRespVO.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivi
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Schema(description = "用户 App - 满减送活动 Response VO")
@@ -19,6 +20,12 @@ public class AppRewardActivityRespVO {
     @Schema(description = "活动标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "满啦满啦")
     private String name;
 
+    @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime startTime;
+
+    @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime endTime;
+
     @Schema(description = "条件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Integer conditionType;
 
@@ -26,7 +33,7 @@ public class AppRewardActivityRespVO {
     private Integer productScope;
 
     @Schema(description = "商品 SPU 编号的数组", example = "1,2,3")
-    private List<Long> productSpuIds;
+    private List<Long> productScopeValues;
 
     @Schema(description = "优惠规则的数组")
     private List<RewardActivityBaseVO.Rule> rules;

+ 21 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java

@@ -23,6 +23,7 @@ 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.context.annotation.Lazy;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -30,11 +31,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import jakarta.annotation.Resource;
 import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.Collections;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -86,7 +87,7 @@ public class AppSeckillActivityController {
 
         // 2.1 查询满足当前阶段的活动
         List<SeckillActivityDO> activityList = activityService.getSeckillActivityListByConfigIdAndStatus(config.getId(), CommonStatusEnum.ENABLE.getStatus());
-        List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityId(
+        List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityIds(
                 convertList(activityList, SeckillActivityDO::getId));
         // 2.2 获取 spu 信息
         List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId));
@@ -101,7 +102,7 @@ public class AppSeckillActivityController {
         if (CollUtil.isEmpty(pageResult.getList())) {
             return success(PageResult.empty(pageResult.getTotal()));
         }
-        List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityId(
+        List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityIds(
                 convertList(pageResult.getList(), SeckillActivityDO::getId));
 
         // 2. 拼接数据
@@ -149,4 +150,21 @@ public class AppSeckillActivityController {
         return success(SeckillActivityConvert.INSTANCE.convert3(activity, productList, startTime, endTime));
     }
 
+    @GetMapping("/list-by-ids")
+    @Operation(summary = "获得拼团活动列表,基于活动编号数组")
+    @Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
+    public CommonResult<List<AppSeckillActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
+        // 1. 获得开启的活动列表
+        List<SeckillActivityDO> activityList = activityService.getSeckillActivityListByIds(ids);
+        activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
+        if (CollUtil.isEmpty(activityList)) {
+            return success(Collections.emptyList());
+        }
+        // 2. 拼接返回
+        List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityIds(
+                convertList(activityList, SeckillActivityDO::getId));
+        List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId));
+        return success(SeckillActivityConvert.INSTANCE.convertAppList(activityList, productList, spuList));
+    }
+
 }

+ 3 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java

@@ -16,6 +16,9 @@ public class AppSeckillActivityRespVO {
     @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, // 从 SPU 的 picUrl 读取
             example = "https://www.iocoder.cn/xx.png")
     private String picUrl;

+ 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

+ 96 - 14
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/discount/DiscountActivityConvert.java

@@ -1,18 +1,19 @@
 package cn.iocoder.yudao.module.promotion.convert.discount;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
-import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityRespVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityUpdateReqVO;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
+import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.*;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 限时折扣活动 Convert
@@ -31,24 +32,105 @@ public interface DiscountActivityConvert {
     DiscountActivityRespVO convert(DiscountActivityDO bean);
 
     List<DiscountActivityRespVO> convertList(List<DiscountActivityDO> list);
-
     List<DiscountActivityBaseVO.Product> convertList2(List<DiscountProductDO> list);
-
-    List<DiscountProductRespDTO> convertList02(List<DiscountProductDO> list);
-
     PageResult<DiscountActivityRespVO> convertPage(PageResult<DiscountActivityDO> page);
 
     default PageResult<DiscountActivityRespVO> convertPage(PageResult<DiscountActivityDO> page,
-                                                           List<DiscountProductDO> discountProductDOList) {
+                                                           List<DiscountProductDO> discountProductDOList,
+                                                           List<ProductSpuRespDTO> spuList) {
         PageResult<DiscountActivityRespVO> pageResult = convertPage(page);
-        pageResult.getList().forEach(item -> item.setProducts(convertList2(discountProductDOList)));
+
+        // 拼接商品 TODO @zhangshuai:类似空行的问题,也可以看看
+        Map<Long, DiscountProductDO> discountActivityMap = CollectionUtils.convertMap(discountProductDOList, DiscountProductDO::getActivityId);
+        Map<Long, ProductSpuRespDTO> spuMap = CollectionUtils.convertMap(spuList, ProductSpuRespDTO::getId);
+        pageResult.getList().forEach(item -> {
+            item.setProducts(convertList2(discountProductDOList));
+            item.setSpuId(discountActivityMap.get(item.getId())==null?null: discountActivityMap.get(item.getId()).getSpuId());
+            if (item.getSpuId() != null) {
+                MapUtils.findAndThen(spuMap, item.getSpuId(),
+                        spu -> item.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
+            }
+
+        });
         return pageResult;
     }
 
     DiscountProductDO convert(DiscountActivityBaseVO.Product bean);
 
-    default DiscountActivityRespVO convert(DiscountActivityDO activity, List<DiscountProductDO> products) {
-        return BeanUtils.toBean(activity, DiscountActivityRespVO.class).setProducts(convertList2(products));
+    default DiscountActivityDetailRespVO convert(DiscountActivityDO activity, List<DiscountProductDO> products){
+        if ( activity == null && products == null ) {
+            return null;
+        }
+
+        DiscountActivityDetailRespVO discountActivityDetailRespVO = new DiscountActivityDetailRespVO();
+
+        if ( activity != null ) {
+            discountActivityDetailRespVO.setName( activity.getName() );
+            discountActivityDetailRespVO.setStartTime( activity.getStartTime() );
+            discountActivityDetailRespVO.setEndTime( activity.getEndTime() );
+            discountActivityDetailRespVO.setRemark( activity.getRemark() );
+            discountActivityDetailRespVO.setId( activity.getId() );
+            discountActivityDetailRespVO.setStatus( activity.getStatus() );
+            discountActivityDetailRespVO.setCreateTime( activity.getCreateTime() );
+        }
+        if (!products.isEmpty()) {
+            discountActivityDetailRespVO.setSpuId(products.get(0).getSpuId());
+        }
+        discountActivityDetailRespVO.setProducts( convertList2( products ) );
+
+        return discountActivityDetailRespVO;
+    }
+
+    // =========== 比较是否相等 ==========
+    /**
+     * 比较两个限时折扣商品是否相等
+     *
+     * @param productDO 数据库中的商品
+     * @param productVO 前端传入的商品
+     * @return 是否匹配
+     */
+    @SuppressWarnings("DuplicatedCode")
+    default boolean isEquals(DiscountProductDO productDO, DiscountActivityBaseVO.Product productVO) {
+        if (ObjectUtil.notEqual(productDO.getSpuId(), productVO.getSpuId())
+                || ObjectUtil.notEqual(productDO.getSkuId(), productVO.getSkuId())
+                || ObjectUtil.notEqual(productDO.getDiscountType(), productVO.getDiscountType())) {
+            return false;
+        }
+        if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PRICE.getType())) {
+            return ObjectUtil.equal(productDO.getDiscountPrice(), productVO.getDiscountPrice());
+        }
+        if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PERCENT.getType())) {
+            return ObjectUtil.equal(productDO.getDiscountPercent(), productVO.getDiscountPercent());
+        }
+        return true;
     }
 
+    /**
+     * 比较两个限时折扣商品是否相等
+     * 注意,比较时忽略 id 编号
+     *
+     * @param productDO 商品 1
+     * @param productVO 商品 2
+     * @return 是否匹配
+     */
+    @SuppressWarnings("DuplicatedCode")
+    default boolean isEquals(DiscountProductDO productDO, DiscountProductDO productVO) {
+        if (ObjectUtil.notEqual(productDO.getSpuId(), productVO.getSpuId())
+                || ObjectUtil.notEqual(productDO.getSkuId(), productVO.getSkuId())
+                || ObjectUtil.notEqual(productDO.getDiscountType(), productVO.getDiscountType())
+                || ObjectUtil.notEqual(productDO.getActivityEndTime(), productVO.getActivityEndTime())
+                || ObjectUtil.notEqual(productDO.getActivityStartTime(), productVO.getActivityStartTime())
+                || ObjectUtil.notEqual(productDO.getActivityStatus(), productVO.getActivityStatus())) {
+            return false;
+        }
+        if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PRICE.getType())) {
+            return ObjectUtil.equal(productDO.getDiscountPrice(), productVO.getDiscountPrice());
+        }
+        if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PERCENT.getType())) {
+            return ObjectUtil.equal(productDO.getDiscountPercent(), productVO.getDiscountPercent());
+        }
+        return true;
+    }
+
+
 }

+ 33 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/seckill/SeckillActivityConvert.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.convert.seckill;
 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.product.api.spu.dto.ProductSpuRespDTO;
 import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO;
@@ -87,6 +88,38 @@ public interface SeckillActivityConvert {
         return CollectionUtils.convertList(products, item -> convert(activity, item).setActivityStatus(activity.getStatus()));
     }
 
+    default List<SeckillActivityRespVO> convertList(List<SeckillActivityDO> list,
+                                                        List<SeckillProductDO> productList,
+                                                        List<ProductSpuRespDTO> spuList) {
+        List<SeckillActivityRespVO> activityList = BeanUtils.toBean(list, SeckillActivityRespVO.class);
+        Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
+        Map<Long, List<SeckillProductDO>> productMap = convertMultiMap(productList, SeckillProductDO::getActivityId);
+        return CollectionUtils.convertList(activityList, item -> {
+            // 设置 product 信息
+            item.setSeckillPrice(getMinValue(productMap.get(item.getId()), SeckillProductDO::getSeckillPrice));
+            // 设置 SPU 信息
+            findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
+                    .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
+            return item;
+        });
+    }
+
+    default List<AppSeckillActivityRespVO> convertAppList(List<SeckillActivityDO> list,
+                                                              List<SeckillProductDO> productList,
+                                                              List<ProductSpuRespDTO> spuList) {
+        List<AppSeckillActivityRespVO> activityList = BeanUtils.toBean(list, AppSeckillActivityRespVO.class);
+        Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
+        Map<Long, List<SeckillProductDO>> productMap = convertMultiMap(productList, SeckillProductDO::getActivityId);
+        return CollectionUtils.convertList(activityList, item -> {
+            // 设置 product 信息
+            item.setSeckillPrice(getMinValue(productMap.get(item.getId()), SeckillProductDO::getSeckillPrice));
+            // 设置 SPU 信息
+            findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName())
+                    .setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
+            return item;
+        });
+    }
+
     List<SeckillProductRespVO> convertList2(List<SeckillProductDO> list);
 
     List<AppSeckillActivityRespVO> convertList3(List<SeckillActivityDO> activityList);

+ 7 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/discount/DiscountProductDO.java

@@ -66,10 +66,16 @@ public class DiscountProductDO extends BaseDO {
      */
     private Integer discountPrice;
 
+    /**
+     * 活动标题
+     *
+     * 冗余 {@link DiscountActivityDO#getName()}
+     */
+    private String activityName;
     /**
      * 活动状态
      *
-     * 关联 {@link DiscountActivityDO#getStatus()}
+     * 冗余 {@link DiscountActivityDO#getStatus()}
      */
     private Integer activityStatus;
     /**

+ 7 - 32
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java

@@ -6,14 +6,11 @@ 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.bargain.vo.activity.BargainActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.time.LocalDateTime;
-import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 
 /**
  * 砍价活动 Mapper
@@ -86,35 +83,13 @@ public interface BargainActivityMapper extends BaseMapperX<BargainActivityDO> {
                 .last("LIMIT " + count));
     }
 
-    /**
-     * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
-     *
-     * @param spuIds spu 编号
-     * @param status 状态
-     * @return 包含 spuId 和 activityId 的 map 对象列表
-     */
-    default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
-        return selectMaps(new QueryWrapper<BargainActivityDO>()
-                .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
-                .in("spu_id", spuIds)
-                .eq("status", status)
-                .groupBy("spu_id"));
-    }
-
-    /**
-     * 获取指定活动编号的活动列表且
-     * 开始时间和结束时间小于给定时间 dateTime 的活动列表
-     *
-     * @param ids      活动编号
-     * @param dateTime 指定日期
-     * @return 活动列表
-     */
-    default List<BargainActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
-        return selectList(new LambdaQueryWrapperX<BargainActivityDO>()
-                .in(BargainActivityDO::getId, ids)
-                .lt(BargainActivityDO::getStartTime, dateTime)
-                .gt(BargainActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
-                .orderByDesc(BargainActivityDO::getCreateTime));
+    default BargainActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
+        LocalDateTime now = LocalDateTime.now();
+        return selectOne(new LambdaQueryWrapperX<BargainActivityDO>()
+                .eq(BargainActivityDO::getSpuId, spuId)
+                .eq(BargainActivityDO::getStatus, status)
+                .lt(BargainActivityDO::getStartTime, now)
+                .gt(BargainActivityDO::getEndTime, now)); // 开始时间 < now < 结束时间,也就是说获取指定时间段的活动
     }
 
 }

+ 7 - 38
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationActivityMapper.java

@@ -6,14 +6,10 @@ 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.combination.vo.activity.CombinationActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
 
 import java.time.LocalDateTime;
-import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 
 /**
  * 拼团活动 Mapper
@@ -39,40 +35,13 @@ public interface CombinationActivityMapper extends BaseMapperX<CombinationActivi
                 .eq(CombinationActivityDO::getStatus, status));
     }
 
-    default List<CombinationActivityDO> selectListByStatus(Integer status, Integer count) {
-        return selectList(new LambdaQueryWrapperX<CombinationActivityDO>()
+    default CombinationActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
+        LocalDateTime now = LocalDateTime.now();
+        return selectOne(new LambdaQueryWrapperX<CombinationActivityDO>()
+                .eq(CombinationActivityDO::getSpuId, spuId)
                 .eq(CombinationActivityDO::getStatus, status)
-                .last("LIMIT " + count));
+                .lt(CombinationActivityDO::getStartTime, now)
+                .gt(CombinationActivityDO::getEndTime, now)); // 开始时间 < now < 结束时间,也就是说获取指定时间段的活动
     }
 
-    /**
-     * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
-     * @param spuIds spu 编号
-     * @param status 状态
-     * @return 包含 spuId 和 activityId 的 map 对象列表
-     */
-    default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
-        return selectMaps(new QueryWrapper<CombinationActivityDO>()
-                .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
-                .in("spu_id", spuIds)
-                .eq("status", status)
-                .groupBy("spu_id"));
-    }
-
-    /**
-     * 获取指定活动编号的活动列表且
-     * 开始时间和结束时间小于给定时间 dateTime 的活动列表
-     *
-     * @param ids      活动编号
-     * @param dateTime 指定日期
-     * @return 活动列表
-     */
-    default List<CombinationActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
-        return selectList(new LambdaQueryWrapperX<CombinationActivityDO>()
-                .in(CombinationActivityDO::getId, ids)
-                .lt(CombinationActivityDO::getStartTime, dateTime)
-                .gt(CombinationActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
-                .orderByDesc(CombinationActivityDO::getCreateTime));
-    }
-
-}
+}

+ 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)

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java

@@ -70,7 +70,7 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
                             .in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致
                             .and(ww -> ww.gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now())  // 3.1 未过期
                                     .or().eq(CouponTemplateDO::getValidityType, CouponTemplateValidityTypeEnum.TERM.getType())) // 3.2 领取之后
-                            .apply(" (take_count < total_count OR total_count = -1 )"); // 4. 剩余数量大于 0,或者无限领取
+                            .apply(" (take_count < total_count OR total_count = -1)"); // 4. 剩余数量大于 0,或者无限领取
         }
         return canTakeConsumer;
     }

+ 12 - 19
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountProductMapper.java

@@ -4,9 +4,9 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -19,13 +19,21 @@ import java.util.Map;
 @Mapper
 public interface DiscountProductMapper extends BaseMapperX<DiscountProductDO> {
 
-
     default List<DiscountProductDO> selectListByActivityId(Long activityId) {
         return selectList(DiscountProductDO::getActivityId, activityId);
     }
 
-    default List<DiscountProductDO> selectListByActivityId(Collection<Long> activityIds) {
-        return selectList(DiscountProductDO::getActivityId, activityIds);
+    default List<DiscountProductDO> selectListBySkuIds(Collection<Long> skuIds) {
+        return selectList(DiscountProductDO::getSkuId, skuIds);
+    }
+
+    default List<DiscountProductDO> selectListBySkuIdsAndStatusAndNow(Collection<Long> skuIds, Integer status) {
+        LocalDateTime now = LocalDateTime.now();
+        return selectList(new LambdaQueryWrapperX<DiscountProductDO>()
+                .in(DiscountProductDO::getSkuId, skuIds)
+                .eq(DiscountProductDO::getActivityStatus,status)
+                .lt(DiscountProductDO::getActivityStartTime, now)
+                .gt(DiscountProductDO::getActivityEndTime, now));
     }
 
     /**
@@ -43,19 +51,4 @@ public interface DiscountProductMapper extends BaseMapperX<DiscountProductDO> {
                 .groupBy("spu_id"));
     }
 
-    default List<DiscountProductDO> selectListBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
-        return selectList(new LambdaQueryWrapperX<DiscountProductDO>()
-                .in(DiscountProductDO::getSpuId, spuIds)
-                .eq(DiscountProductDO::getActivityStatus, status));
-    }
-
-    default void updateByActivityId(DiscountProductDO discountProductDO) {
-        update(discountProductDO, new LambdaUpdateWrapper<DiscountProductDO>()
-                .eq(DiscountProductDO::getActivityId, discountProductDO.getActivityId()));
-    }
-
-    default void deleteByActivityId(Long activityId) {
-        delete(DiscountProductDO::getActivityId, activityId);
-    }
-
 }

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

@@ -1,14 +1,19 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.reward;
 
+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.reward.vo.RewardActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * 满减送活动 Mapper
@@ -25,12 +30,23 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
                 .orderByDesc(RewardActivityDO::getId));
     }
 
-    default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
+    default List<RewardActivityDO> selectListBySpuIdAndStatusAndNow(Collection<Long> spuIds,
+                                                                    Collection<Long> categoryIds,
+                                                                    Integer status) {
+        LocalDateTime now = LocalDateTime.now();
+        Function<Collection<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<RewardActivityDO>()
                 .eq(RewardActivityDO::getStatus, status)
-                // 开始时间 < 指定时间(dateTime) < 结束时间,也就是说获取指定时间段的活动
-                .lt(RewardActivityDO::getStartTime, dateTime).gt(RewardActivityDO::getEndTime, dateTime)
-                .orderByAsc(RewardActivityDO::getStartTime)
+                .lt(RewardActivityDO::getStartTime, now)
+                .gt(RewardActivityDO::getEndTime, now)
+                .and(i -> i.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.SPU.getScope())
+                            .and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(spuIds)))
+                        .or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()))
+                        .or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope())
+                                .and(i2 -> i2.apply(productScopeValuesFindInSetFunc.apply(categoryIds)))))
+                .orderByDesc(RewardActivityDO::getId)
         );
     }
 

+ 11 - 35
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java

@@ -8,15 +8,11 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
 
 import java.time.LocalDateTime;
-import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 
 /**
  * 秒杀活动 Mapper
@@ -51,7 +47,7 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
         Assert.isTrue(count > 0);
         return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
                 .eq(SeckillActivityDO::getId, id)
-                .gt(SeckillActivityDO::getStock, count)
+                .ge(SeckillActivityDO::getStock, count)
                 .setSql("stock = stock - " + count));
     }
 
@@ -69,41 +65,21 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
                 .setSql("stock = stock + " + count));
     }
 
-    default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
+    default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status, LocalDateTime dateTime) {
         return selectPage(pageReqVO, new LambdaQueryWrapperX<SeckillActivityDO>()
                 .eqIfPresent(SeckillActivityDO::getStatus, status)
+                .lt(SeckillActivityDO::getStartTime, dateTime)
+                .gt(SeckillActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
                 .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
     }
 
-    /**
-     * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
-     *
-     * @param spuIds spu 编号
-     * @param status 状态
-     * @return 包含 spuId 和 activityId 的 map 对象列表
-     */
-    default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
-        return selectMaps(new QueryWrapper<SeckillActivityDO>()
-                .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
-                .in("spu_id", spuIds)
-                .eq("status", status)
-                .groupBy("spu_id"));
-    }
-
-    /**
-     * 获取指定活动编号的活动列表且
-     * 开始时间和结束时间小于给定时间 dateTime 的活动列表
-     *
-     * @param ids      活动编号
-     * @param dateTime 指定日期
-     * @return 活动列表
-     */
-    default List<SeckillActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
-        return selectList(new LambdaQueryWrapperX<SeckillActivityDO>()
-                .in(SeckillActivityDO::getId, ids)
-                .lt(SeckillActivityDO::getStartTime, dateTime)
-                .gt(SeckillActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
-                .orderByDesc(SeckillActivityDO::getCreateTime));
+    default SeckillActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
+        LocalDateTime now = LocalDateTime.now();
+        return selectOne(new LambdaQueryWrapperX<SeckillActivityDO>()
+                .eq(SeckillActivityDO::getSpuId, spuId)
+                .eq(SeckillActivityDO::getStatus, status)
+                .lt(SeckillActivityDO::getStartTime, now)
+                .gt(SeckillActivityDO::getEndTime, now)); // 开始时间 < now < 结束时间,也就是说获取指定时间段的活动
     }
 
 }

+ 5 - 9
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityService.java

@@ -6,10 +6,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.Ba
 import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.BargainActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.BargainActivityUpdateReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
-
 import jakarta.validation.Valid;
-import java.time.LocalDateTime;
-import java.util.Collection;
+
 import java.util.List;
 import java.util.Set;
 
@@ -108,13 +106,11 @@ public interface BargainActivityService {
     List<BargainActivityDO> getBargainActivityListByCount(Integer count);
 
     /**
-     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     * 获得 SPU 进行中的砍价活动
      *
-     * @param spuIds   spu 编号
-     * @param status   状态
-     * @param dateTime 日期时间
-     * @return 砍价活动列表
+     * @param spuId SPU 编号数组
+     * @return 砍价活动
      */
-    List<BargainActivityDO> getBargainActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
+    BargainActivityDO getMatchBargainActivityBySpuId(Long spuId);
 
 }

+ 5 - 14
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityServiceImpl.java

@@ -1,7 +1,5 @@
 package cn.iocoder.yudao.module.promotion.service.bargain;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -15,17 +13,17 @@ import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.Ba
 import cn.iocoder.yudao.module.promotion.convert.bargain.BargainActivityConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.bargain.BargainActivityMapper;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
 import java.time.LocalDateTime;
-import java.util.*;
+import java.util.List;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 
@@ -194,15 +192,8 @@ public class BargainActivityServiceImpl implements BargainActivityService {
     }
 
     @Override
-    public List<BargainActivityDO> getBargainActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
-        // 1. 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
-        List<Map<String, Object>> spuIdAndActivityIdMaps = bargainActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
-        if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
-            return Collections.emptyList();
-        }
-        // 2. 查询活动详情
-        return bargainActivityMapper.selectListByIdsAndDateTimeLt(
-                convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
+    public BargainActivityDO getMatchBargainActivityBySpuId(Long spuId) {
+        return bargainActivityMapper.selectBySpuIdAndStatusAndNow(spuId, CommonStatusEnum.ENABLE.getStatus());
     }
 
 }

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

@@ -7,9 +7,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activit
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
-
 import jakarta.validation.Valid;
-import java.time.LocalDateTime;
+
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -100,14 +99,6 @@ public interface CombinationActivityService {
      */
     List<CombinationActivityDO> getCombinationActivityListByIds(Collection<Long> ids);
 
-    /**
-     * 获取正在进行的活动分页数据
-     *
-     * @param count 需要的数量
-     * @return 拼团活动分页
-     */
-    List<CombinationActivityDO> getCombinationActivityListByCount(Integer count);
-
     /**
      * 获取正在进行的活动分页数据
      *
@@ -117,22 +108,20 @@ public interface CombinationActivityService {
     PageResult<CombinationActivityDO> getCombinationActivityPage(PageParam pageParam);
 
     /**
-     * 获取指定活动、指定 sku 编号的商品
+     * 获取指定活动、指定 SKU 编号的商品
      *
      * @param activityId 活动编号
-     * @param skuId      sku 编号
+     * @param skuId      SKU 编号
      * @return 活动商品信息
      */
     CombinationProductDO selectByActivityIdAndSkuId(Long activityId, Long skuId);
 
     /**
-     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     * 获得 SPU 进行中的拼团活动
      *
-     * @param spuIds   spu 编号
-     * @param status   状态
-     * @param dateTime 日期时间
-     * @return 拼团活动列表
+     * @param spuId SPU 编号数组
+     * @return 拼团活动
      */
-    List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
+    CombinationActivityDO getMatchCombinationActivityBySpuId(Long spuId);
 
 }

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

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.promotion.service.combination;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -20,19 +19,18 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationA
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationActivityMapper;
 import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationProductMapper;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
-import java.time.LocalDateTime;
 import java.util.Collection;
-import java.util.Collections;
 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.*;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
@@ -178,7 +176,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
             combinationProductMapper.updateBatch(diffList.get(1));
         }
         if (CollUtil.isNotEmpty(diffList.get(2))) {
-            combinationProductMapper.deleteBatchIds(CollectionUtils.convertList(diffList.get(2), CombinationProductDO::getId));
+            combinationProductMapper.deleteByIds(CollectionUtils.convertList(diffList.get(2), CombinationProductDO::getId));
         }
     }
 
@@ -225,11 +223,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());
@@ -243,15 +236,8 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
     }
 
     @Override
-    public List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
-        // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
-        List<Map<String, Object>> spuIdAndActivityIdMaps = combinationActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
-        if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
-            return Collections.emptyList();
-        }
-        // 2.查询活动详情
-        return combinationActivityMapper.selectListByIdsAndDateTimeLt(
-                convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
+    public CombinationActivityDO getMatchCombinationActivityBySpuId(Long spuId) {
+        return combinationActivityMapper.selectBySpuIdAndStatusAndNow(spuId, CommonStatusEnum.ENABLE.getStatus());
     }
 
 }

+ 0 - 30
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,26 +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);
-
     /**
      * 使用优惠劵
      *
@@ -171,15 +150,6 @@ public interface CouponService {
         return MapUtil.getInt(map, templateId, 0);
     }
 
-    /**
-     * 获取用户匹配的优惠券列表
-     *
-     * @param userId     用户编号
-     * @param matchReqVO 匹配参数
-     * @return 优惠券列表
-     */
-    List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO);
-
     /**
      * 获取用户是否可以领取优惠券
      *

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

@@ -12,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;
@@ -56,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);
         }
@@ -75,12 +65,6 @@ public class CouponServiceImpl implements CouponService {
         if (!LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) {
             throw exception(COUPON_VALID_TIME_NOT_NOW);
         }
-    }
-
-    @Override
-    public void useCoupon(Long id, Long userId, Long orderId) {
-        // 校验优惠劵
-        validCoupon(id, userId);
 
         // 更新状态
         int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
@@ -284,9 +268,8 @@ public class CouponServiceImpl implements CouponService {
         if (couponTemplate == null) {
             throw exception(COUPON_TEMPLATE_NOT_EXISTS);
         }
-        // 校验剩余数量(仅在 CouponTakeTypeEnum.USER 用户领取时)
-        if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeCount())
-                && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
+        // 校验剩余数量
+        if (couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
             throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
         }
         // 校验"固定日期"的有效期类型是否过期
@@ -355,16 +338,6 @@ public class CouponServiceImpl implements CouponService {
         return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
     }
 
-    @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;
-    }
-
     @Override
     public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
         // 1. 未登录时,都显示可以领取

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java

@@ -12,10 +12,10 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponTemplateMapper;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;

+ 5 - 9
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityService.java

@@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
 import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
 import jakarta.validation.Valid;
 
-import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 
@@ -24,10 +23,10 @@ public interface DiscountActivityService {
      *
      * 注意,匹配的条件,仅仅是日期符合,并且处于开启状态
      *
-     * @param spuIds SKU 编号数组
+     * @param skuIds SKU 编号数组
      * @return 匹配的限时折扣商品
      */
-    List<DiscountProductDO> getMatchDiscountProductList(Collection<Long> spuIds);
+    List<DiscountProductDO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds);
 
     /**
      * 创建限时折扣活动
@@ -91,14 +90,11 @@ public interface DiscountActivityService {
     List<DiscountProductDO> getDiscountProductsByActivityId(Collection<Long> activityIds);
 
     /**
-     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     * 获取指定 SPU 编号最近参加的活动,每个 spuId 只返回一条记录
      *
-     * @param spuIds   spu 编号
-     * @param status   状态
-     * @param dateTime 当前日期时间
+     * @param spuIds   SPU 编号数组
      * @return 折扣活动列表
      */
-    List<DiscountActivityDO> getDiscountActivityBySpuIdsAndStatusAndDateTimeLt(
-            Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
+    List<DiscountActivityDO> getDiscountActivityListBySpuIds(Collection<Long> spuIds);
 
 }

+ 52 - 107
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java

@@ -1,14 +1,11 @@
 package cn.iocoder.yudao.module.promotion.service.discount;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.map.MapUtil;
-import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
-import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO;
@@ -18,7 +15,6 @@ 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.mysql.discount.DiscountActivityMapper;
 import cn.iocoder.yudao.module.promotion.dal.mysql.discount.DiscountProductMapper;
-import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -30,11 +26,9 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
-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.*;
-import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
-import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 
 /**
@@ -50,26 +44,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
     private DiscountActivityMapper discountActivityMapper;
     @Resource
     private DiscountProductMapper discountProductMapper;
-    @Resource
-    private ProductSkuApi productSkuApi;
 
     @Override
-    public List<DiscountProductDO> getMatchDiscountProductList(Collection<Long> spuIds) {
-        // 1.1 查询出 spu 对应的开启的限时折扣商品
-        List<DiscountProductDO> productList = discountProductMapper.selectListBySpuIdsAndStatus(spuIds,
-                CommonStatusEnum.ENABLE.getStatus());
-        if (CollUtil.isEmpty(productList)) {
-            return Collections.emptyList();
-        }
-        // 1.2 查询出符合的限时折扣活动
-        List<DiscountActivityDO> activityList = discountActivityMapper.selectListByIdsAndDateTimeLt(
-                convertSet(productList, DiscountProductDO::getActivityId), LocalDateTime.now());
-        if (CollUtil.isEmpty(productList)) {
-            return Collections.emptyList();
-        }
-
-        // 2. 获得这些活动的商品列表
-        return discountProductMapper.selectListByActivityId(convertList(activityList, DiscountActivityDO::getId));
+    public List<DiscountProductDO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds) {
+        return discountProductMapper.selectListBySkuIdsAndStatusAndNow(skuIds, CommonStatusEnum.ENABLE.getStatus());
     }
 
     @Override
@@ -77,8 +55,6 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
     public Long createDiscountActivity(DiscountActivityCreateReqVO createReqVO) {
         // 校验商品是否冲突
         validateDiscountActivityProductConflicts(null, createReqVO.getProducts());
-        // 校验商品是否存在
-        validateProductExists(createReqVO.getProducts());
 
         // 插入活动
         DiscountActivityDO discountActivity = DiscountActivityConvert.INSTANCE.convert(createReqVO)
@@ -86,10 +62,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
         discountActivityMapper.insert(discountActivity);
         // 插入商品
         List<DiscountProductDO> discountProducts = BeanUtils.toBean(createReqVO.getProducts(), DiscountProductDO.class,
-                product -> product.setActivityId(discountActivity.getId()).setActivityStatus(discountActivity.getStatus())
+                product -> product.setActivityId(discountActivity.getId())
+                        .setActivityName(discountActivity.getName()).setActivityStatus(discountActivity.getStatus())
                         .setActivityStartTime(createReqVO.getStartTime()).setActivityEndTime(createReqVO.getEndTime()));
         discountProductMapper.insertBatch(discountProducts);
-        // 返回
         return discountActivity.getId();
     }
 
@@ -103,40 +79,36 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
         }
         // 校验商品是否冲突
         validateDiscountActivityProductConflicts(updateReqVO.getId(), updateReqVO.getProducts());
-        // 校验商品是否存在
-        validateProductExists(updateReqVO.getProducts());
 
         // 更新活动
-        DiscountActivityDO updateObj = DiscountActivityConvert.INSTANCE.convert(updateReqVO)
-                .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
+        DiscountActivityDO updateObj = DiscountActivityConvert.INSTANCE.convert(updateReqVO);
         discountActivityMapper.updateById(updateObj);
         // 更新商品
-        updateDiscountProduct(updateObj, updateReqVO.getProducts());
+        updateDiscountProduct(updateReqVO);
     }
 
-    private void updateDiscountProduct(DiscountActivityDO activity, List<DiscountActivityCreateReqVO.Product> products) {
-        // 第一步,对比新老数据,获得添加、修改、删除的列表
-        List<DiscountProductDO> newList = BeanUtils.toBean(products, DiscountProductDO.class,
-                product -> product.setActivityId(activity.getId()).setActivityStatus(activity.getStatus())
-                        .setActivityStartTime(activity.getStartTime()).setActivityEndTime(activity.getEndTime()));
-        List<DiscountProductDO> oldList = discountProductMapper.selectListByActivityId(activity.getId());
-        List<List<DiscountProductDO>> diffList = CollectionUtils.diffList(oldList, newList, (oldVal, newVal) -> {
-            boolean same = ObjectUtil.equal(oldVal.getSkuId(), newVal.getSkuId());
-            if (same) {
-                newVal.setId(oldVal.getId());
-            }
-            return same;
-        });
-
-        // 第二步,批量添加、修改、删除
-        if (CollUtil.isNotEmpty(diffList.get(0))) {
-            discountProductMapper.insertBatch(diffList.get(0));
+    private void updateDiscountProduct(DiscountActivityUpdateReqVO updateReqVO) {
+        // TODO @zhangshuai:这里的逻辑,可以优化下哈;参考 CombinationActivityServiceImpl 的 updateCombinationProduct,主要是 CollectionUtils.diffList 的使用哈;
+        //  然后原先是使用 DiscountActivityConvert.INSTANCE.isEquals 对比,现在看看是不是简化就基于 skuId 对比就完事了;之前写的太精细,意义不大;
+        List<DiscountProductDO> dbDiscountProducts = discountProductMapper.selectListByActivityId(updateReqVO.getId());
+        // 计算要删除的记录
+        List<Long> deleteIds = convertList(dbDiscountProducts, DiscountProductDO::getId,
+                discountProductDO -> updateReqVO.getProducts().stream()
+                        .noneMatch(product -> DiscountActivityConvert.INSTANCE.isEquals(discountProductDO, product)));
+        if (CollUtil.isNotEmpty(deleteIds)) {
+            discountProductMapper.deleteByIds(deleteIds);
         }
-        if (CollUtil.isNotEmpty(diffList.get(1))) {
-            discountProductMapper.updateBatch(diffList.get(1));
-        }
-        if (CollUtil.isNotEmpty(diffList.get(2))) {
-            discountProductMapper.deleteBatchIds(convertList(diffList.get(2), DiscountProductDO::getId));
+        // 计算新增的记录
+        List<DiscountProductDO> newDiscountProducts = convertList(updateReqVO.getProducts(),
+                product -> DiscountActivityConvert.INSTANCE.convert(product)
+                        .setActivityId(updateReqVO.getId())
+                        .setActivityName(updateReqVO.getName())
+                        .setActivityStartTime(updateReqVO.getStartTime())
+                        .setActivityEndTime(updateReqVO.getEndTime()));
+        newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch(
+                dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的
+        if (CollectionUtil.isNotEmpty(newDiscountProducts)) {
+            discountProductMapper.insertBatch(newDiscountProducts);
         }
     }
 
@@ -147,44 +119,20 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
      * @param products 商品列表
      */
     private void validateDiscountActivityProductConflicts(Long id, List<DiscountActivityBaseVO.Product> products) {
-        // 1.1 查询所有开启的折扣活动
-        List<DiscountActivityDO> activityList = discountActivityMapper.selectList(DiscountActivityDO::getStatus,
-                CommonStatusEnum.ENABLE.getStatus());
-        if (id != null) { // 时排除自己
-            activityList.removeIf(item -> ObjectUtil.equal(item.getId(), id));
+        if (CollUtil.isEmpty(products)) {
+            return;
+        }
+        // 查询商品参加的活动
+        List<DiscountProductDO> list = discountProductMapper.selectListByActivityId(id);
+        List<DiscountProductDO> matchDiscountProductList = discountProductMapper.selectListBySkuIds(
+                convertSet(list, DiscountProductDO::getSkuId));
+        if (id != null) { // 排除自己这个活动
+            matchDiscountProductList.removeIf(product -> id.equals(product.getActivityId()));
+        }
+        // 如果非空,则说明冲突
+        if (CollUtil.isNotEmpty(matchDiscountProductList)) {
+            throw exception(DISCOUNT_ACTIVITY_SPU_CONFLICTS);
         }
-        // 1.2 查询活动下的所有商品
-        List<DiscountProductDO> productList = discountProductMapper.selectListByActivityId(
-                convertList(activityList, DiscountActivityDO::getId));
-        Map<Long, List<DiscountProductDO>> productListMap = convertMultiMap(productList, DiscountProductDO::getActivityId);
-
-        // 2. 校验商品是否冲突
-        activityList.forEach(item -> {
-            findAndThen(productListMap, item.getId(), discountProducts -> {
-                if (!intersectionDistinct(convertList(discountProducts, DiscountProductDO::getSpuId),
-                        convertList(products, DiscountActivityBaseVO.Product::getSpuId)).isEmpty()) {
-                    throw exception(DISCOUNT_ACTIVITY_SPU_CONFLICTS, item.getName());
-                }
-            });
-        });
-    }
-
-    /**
-     * 校验活动商品是否都存在
-     *
-     * @param products 活动商品
-     */
-    private void validateProductExists(List<DiscountActivityBaseVO.Product> products) {
-        // 1.获得商品所有的 sku
-        List<ProductSkuRespDTO> skus = productSkuApi.getSkuListBySpuId(
-                convertList(products, DiscountActivityBaseVO.Product::getSpuId));
-        Map<Long, ProductSkuRespDTO> skuMap = convertMap(skus, ProductSkuRespDTO::getId);
-        // 2. 校验商品 sku 都存在
-        products.forEach(product -> {
-            if (!skuMap.containsKey(product.getSkuId())) {
-                throw exception(SKU_NOT_EXISTS);
-            }
-        });
     }
 
     @Override
@@ -195,11 +143,9 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
             throw exception(DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
         }
 
-        // 更新活动状态
-        discountActivityMapper.updateById(new DiscountActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus()));
-        // 更新活动商品状态
-        discountProductMapper.updateByActivityId(new DiscountProductDO().setActivityId(id).setActivityStatus(
-                CommonStatusEnum.DISABLE.getStatus()));
+        // 更新
+        DiscountActivityDO updateObj = new DiscountActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus());
+        discountActivityMapper.updateById(updateObj);
     }
 
     @Override
@@ -210,10 +156,8 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
             throw exception(DISCOUNT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED);
         }
 
-        // 删除活动
+        // 删除
         discountActivityMapper.deleteById(id);
-        // 删除活动商品
-        discountProductMapper.deleteByActivityId(id);
     }
 
     private DiscountActivityDO validateDiscountActivityExists(Long id) {
@@ -241,20 +185,21 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
 
     @Override
     public List<DiscountProductDO> getDiscountProductsByActivityId(Collection<Long> activityIds) {
-        return discountProductMapper.selectList(DiscountProductDO::getActivityId, activityIds);
+        return discountProductMapper.selectList("activity_id", activityIds);
     }
 
     @Override
-    public List<DiscountActivityDO> getDiscountActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
+    public List<DiscountActivityDO> getDiscountActivityListBySpuIds(Collection<Long> spuIds) {
         // 1. 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
-        List<Map<String, Object>> spuIdAndActivityIdMaps = discountProductMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
+        List<Map<String, Object>> spuIdAndActivityIdMaps = discountProductMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(
+                spuIds, CommonStatusEnum.ENABLE.getStatus());
         if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
             return Collections.emptyList();
         }
 
         // 2. 查询活动详情
         return discountActivityMapper.selectListByIdsAndDateTimeLt(
-                convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
+                convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), LocalDateTime.now());
     }
 
 }

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

@@ -1,13 +1,14 @@
 package cn.iocoder.yudao.module.promotion.service.reward;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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 jakarta.validation.Valid;
 
-import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -63,12 +64,11 @@ public interface RewardActivityService {
     PageResult<RewardActivityDO> getRewardActivityPage(RewardActivityPageReqVO pageReqVO);
 
     /**
-     * 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
+     * 获得 spuId 商品匹配的的满减送活动列表
      *
-     * @param status   状态
-     * @param dateTime 当前日期时间
+     * @param spuIds   SPU 编号数组
      * @return 满减送活动列表
      */
-    List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
+    List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds);
 
 }

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

@@ -1,31 +1,33 @@
 package cn.iocoder.yudao.module.promotion.service.reward;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
 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.product.api.spu.dto.ProductSpuRespDTO;
+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.dal.dataobject.reward.RewardActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
-import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 
 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.*;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 
 /**
@@ -52,9 +54,9 @@ public class RewardActivityServiceImpl implements RewardActivityService {
         // 1.2 校验商品是否冲突
         validateRewardActivitySpuConflicts(null, createReqVO);
 
-        // 2. 插入
+        // 插入
         RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class)
-                .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
+                .setStatus(CommonStatusEnum.ENABLE.getStatus());
         rewardActivityMapper.insert(rewardActivity);
         // 返回
         return rewardActivity.getId();
@@ -73,8 +75,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
         validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO);
 
         // 2. 更新
-        RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class)
-                .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
+        RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class);
         rewardActivityMapper.updateById(updateObj);
     }
 
@@ -195,8 +196,61 @@ public class RewardActivityServiceImpl implements RewardActivityService {
     }
 
     @Override
-    public List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
-        return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
+    public List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds) {
+        // 1. 查询商品分类
+        List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
+        if (CollUtil.isEmpty(spuList)) {
+            return Collections.emptyList();
+        }
+        Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
+
+        // 2. 查询出指定 spuId 的 spu 参加的活动
+        List<RewardActivityDO> activityList = rewardActivityMapper.selectListBySpuIdAndStatusAndNow(
+                spuIds, convertSet(spuList, ProductSpuRespDTO::getCategoryId), CommonStatusEnum.ENABLE.getStatus());
+        if (CollUtil.isEmpty(activityList)) {
+            return Collections.emptyList();
+        }
+
+        // 3. 转换成 Response DTO
+        return BeanUtils.toBean(activityList, RewardActivityMatchRespDTO.class, activityDTO -> {
+            // 3.1 设置对应匹配的 spuIds
+            activityDTO.setSpuIds(new ArrayList<>());
+            for (Long spuId : spuIds) {
+                if (PromotionProductScopeEnum.isAll(activityDTO.getProductScope())) {
+                    activityDTO.getSpuIds().add(spuId);
+                } else if (PromotionProductScopeEnum.isSpu(activityDTO.getProductScope())) {
+                    if (CollUtil.contains(activityDTO.getProductScopeValues(), spuId)) {
+                        activityDTO.getSpuIds().add(spuId);
+                    }
+                } else if (PromotionProductScopeEnum.isCategory(activityDTO.getProductScope())) {
+                    ProductSpuRespDTO spu = spuMap.get(spuId);
+                    if (spu != null && CollUtil.contains(activityDTO.getProductScopeValues(), spu.getCategoryId())) {
+                        activityDTO.getSpuIds().add(spuId);
+                    }
+                }
+            }
+
+            // 3.2 设置每个 Rule 的描述
+            activityDTO.getRules().forEach(rule -> {
+                String description = "";
+                if (PromotionConditionTypeEnum.PRICE.getType().equals(activityDTO.getConditionType())) {
+                    description += StrUtil.format("满 {} 元", MoneyUtils.fenToYuanStr(rule.getLimit()));
+                } else {
+                    description += StrUtil.format("满 {} 件", rule.getLimit());
+                }
+                if (rule.getDiscountPrice() != null) {
+                    description += StrUtil.format("减 {}", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
+                } else if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
+                    description += "包邮";
+                } else if (rule.getPoint() != null && rule.getPoint() > 0) {
+                    description += StrUtil.format("增 {} 积分", rule.getPoint());
+                } else if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
+                    description += StrUtil.format("送 {} 张优惠券",
+                            getSumValue(rule.getGiveCouponTemplateCounts().values(), count -> count, Integer::sum));
+                }
+                rule.setDescription(description);
+            });
+        });
     }
 
 }

+ 15 - 10
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java

@@ -8,9 +8,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.Se
 import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
-
 import jakarta.validation.Valid;
-import java.time.LocalDateTime;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -98,7 +97,7 @@ public interface SeckillActivityService {
      * @param activityIds 活动编号
      * @return 活动商品列表
      */
-    List<SeckillProductDO> getSeckillProductListByActivityId(Collection<Long> activityIds);
+    List<SeckillProductDO> getSeckillProductListByActivityIds(Collection<Long> activityIds);
 
     /**
      * 通过活动时段编号获取指定 status 的秒杀活动
@@ -110,7 +109,7 @@ public interface SeckillActivityService {
     List<SeckillActivityDO> getSeckillActivityListByConfigIdAndStatus(Long configId, Integer status);
 
     /**
-     * 通过活动时段获取秒杀活动
+     * 通过活动时段获取开始的秒杀活动
      *
      * @param pageReqVO 请求
      * @return 秒杀活动列表
@@ -130,13 +129,19 @@ public interface SeckillActivityService {
     SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);
 
     /**
-     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     * 获得 SPU 进行中的秒杀活动
      *
-     * @param spuIds   spu 编号
-     * @param status   状态
-     * @param dateTime 日期时间
-     * @return 秒杀活动列表
+     * @param spuId SPU 编号数组
+     * @return 秒杀活动
+     */
+    SeckillActivityDO getMatchSeckillActivityBySpuId(Long spuId);
+
+    /**
+     * 获得拼团活动列表
+     *
+     * @param ids 拼团活动编号数组
+     * @return 拼团活动的列表
      */
-    List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
+    List<SeckillActivityDO> getSeckillActivityListByIds(Collection<Long> ids);
 
 }

+ 14 - 17
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java

@@ -1,8 +1,6 @@
 package cn.iocoder.yudao.module.promotion.service.seckill;
 
-import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
-import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -23,14 +21,13 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillConfigDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillActivityMapper;
 import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillProductMapper;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -56,8 +53,10 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
     private SeckillActivityMapper seckillActivityMapper;
     @Resource
     private SeckillProductMapper seckillProductMapper;
+
     @Resource
     private SeckillConfigService seckillConfigService;
+
     @Resource
     private ProductSpuApi productSpuApi;
     @Resource
@@ -219,7 +218,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
             seckillProductMapper.updateBatch(diffList.get(1));
         }
         if (isNotEmpty(diffList.get(2))) {
-            seckillProductMapper.deleteBatchIds(convertList(diffList.get(2), SeckillProductDO::getId));
+            seckillProductMapper.deleteByIds(convertList(diffList.get(2), SeckillProductDO::getId));
         }
     }
 
@@ -249,7 +248,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
         seckillActivityMapper.deleteById(id);
         // 删除活动商品
         List<SeckillProductDO> products = seckillProductMapper.selectListByActivityId(id);
-        seckillProductMapper.deleteBatchIds(convertSet(products, SeckillProductDO::getId));
+        seckillProductMapper.deleteByIds(convertSet(products, SeckillProductDO::getId));
     }
 
     private SeckillActivityDO validateSeckillActivityExists(Long id) {
@@ -276,7 +275,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
     }
 
     @Override
-    public List<SeckillProductDO> getSeckillProductListByActivityId(Collection<Long> activityIds) {
+    public List<SeckillProductDO> getSeckillProductListByActivityIds(Collection<Long> activityIds) {
         return seckillProductMapper.selectListByActivityId(activityIds);
     }
 
@@ -289,7 +288,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
 
     @Override
     public PageResult<SeckillActivityDO> getSeckillActivityAppPageByConfigId(AppSeckillActivityPageReqVO pageReqVO) {
-        return seckillActivityMapper.selectPage(pageReqVO, CommonStatusEnum.ENABLE.getStatus());
+        return seckillActivityMapper.selectPage(pageReqVO, CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now());
     }
 
     @Override
@@ -325,15 +324,13 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
     }
 
     @Override
-    public List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
-        // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
-        List<Map<String, Object>> spuIdAndActivityIdMaps = seckillActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
-        if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
-            return Collections.emptyList();
-        }
-        // 2.查询活动详情
-        return seckillActivityMapper.selectListByIdsAndDateTimeLt(
-                convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
+    public SeckillActivityDO getMatchSeckillActivityBySpuId(Long spuId) {
+        return seckillActivityMapper.selectBySpuIdAndStatusAndNow(spuId, CommonStatusEnum.ENABLE.getStatus());
+    }
+
+    @Override
+    public List<SeckillActivityDO> getSeckillActivityListByIds(Collection<Long> ids) {
+        return seckillActivityMapper.selectBatchIds(ids);
     }
 
 }

+ 0 - 25
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/util/PromotionUtils.java

@@ -1,25 +0,0 @@
-package cn.iocoder.yudao.module.promotion.util;
-
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
-
-import java.time.LocalDateTime;
-
-/**
- * 活动工具类
- *
- * @author 芋道源码
- */
-public class PromotionUtils {
-
-    /**
-     * 根据时间,计算活动状态
-     *
-     * @param endTime 结束时间
-     * @return 活动状态
-     */
-    public static Integer calculateActivityStatus(LocalDateTime endTime) {
-        return LocalDateTimeUtils.beforeNow(endTime) ? CommonStatusEnum.DISABLE.getStatus() : CommonStatusEnum.ENABLE.getStatus();
-    }
-
-}

+ 0 - 5
yudao-module-mall/yudao-module-promotion-biz/src/main/resources/mapper/discount/DiscountProductMapper.xml

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.promotion.dal.mysql.discount.DiscountProductMapper">
-
-</mapper>

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

@@ -18,15 +18,8 @@ import org.mockito.InjectMocks;
 import org.mockito.Mock;
 
 import java.time.Duration;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
 
-import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
 import static cn.hutool.core.util.RandomUtil.randomEle;
-import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
@@ -34,8 +27,6 @@ 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 com.google.common.primitives.Longs.asList;
-import static java.util.Collections.singletonList;
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
@@ -176,90 +167,91 @@ public class RewardActivityServiceImplTest extends BaseMockitoUnitTest {
         assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
     }
 
-    @Test
-    public void testGetRewardActivities_all() {
-        LocalDateTime now = LocalDateTime.now();
-        // mock 数据
-        RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
-                .setProductScope(PromotionProductScopeEnum.ALL.getScope()).setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
-        rewardActivityMapper.insert(allActivity);
-        RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
-                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
-                .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
-        rewardActivityMapper.insert(productActivity);
-        // 准备参数
-        Set<Long> spuIds = asSet(1L, 2L);
-
-        // 调用
-        List<RewardActivityDO> activityList = rewardActivityServiceImpl.getRewardActivityListByStatusAndDateTimeLt(
-                CommonStatusEnum.ENABLE.getStatus(), now);
-        List<RewardActivityDO> matchRewardActivityList = filterMatchActivity(spuIds, activityList);
-        // 断言
-        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() {
-        LocalDateTime now = LocalDateTime.now();
-        // mock 数据
-        RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
-                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
-                .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
-        rewardActivityMapper.insert(productActivity01);
-        RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
-                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
-                .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
-        rewardActivityMapper.insert(productActivity02);
-        // 准备参数
-        Set<Long> spuIds = asSet(1L, 2L, 3L);
-
-        List<RewardActivityDO> activityList = rewardActivityServiceImpl.getRewardActivityListByStatusAndDateTimeLt(
-                CommonStatusEnum.ENABLE.getStatus(), now);
-        List<RewardActivityDO> matchRewardActivityList = filterMatchActivity(spuIds, activityList);
-        // 断言
-        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();
-            }
-        });
-    }
-
-    /**
-     * 获得满减送的订单项(商品)列表
-     *
-     * @param spuIds       商品编号
-     * @param activityList 活动列表
-     * @return 订单项(商品)列表
-     */
-    private List<RewardActivityDO> filterMatchActivity(Collection<Long> spuIds, List<RewardActivityDO> activityList) {
-        List<RewardActivityDO> resultActivityList = new ArrayList<>();
-        for (RewardActivityDO activity : activityList) {
-            // 情况一:全部商品都可以参与
-            if (PromotionProductScopeEnum.isAll(activity.getProductScope())) {
-                resultActivityList.add(activity);
-            }
-            // 情况二:指定商品参与
-            if (PromotionProductScopeEnum.isSpu(activity.getProductScope()) &&
-                    !intersectionDistinct(activity.getProductScopeValues(), spuIds).isEmpty()) {
-                resultActivityList.add(activity);
-            }
-        }
-        return resultActivityList;
-    }
+    // TODO 芋艿:后续完善单测
+//    @Test
+//    public void testGetRewardActivities_all() {
+//        LocalDateTime now = LocalDateTime.now();
+//        // mock 数据
+//        RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+//                .setProductScope(PromotionProductScopeEnum.ALL.getScope()).setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
+//        rewardActivityMapper.insert(allActivity);
+//        RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+//                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
+//                .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
+//        rewardActivityMapper.insert(productActivity);
+//        // 准备参数
+//        Set<Long> spuIds = asSet(1L, 2L);
+//
+//        // 调用
+//        List<RewardActivityDO> activityList = rewardActivityServiceImpl.getRewardActivityListByStatusAndDateTimeLt(
+//                CommonStatusEnum.ENABLE.getStatus(), now);
+//        List<RewardActivityDO> matchRewardActivityList = filterMatchActivity(spuIds, activityList);
+//        // 断言
+//        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() {
+//        LocalDateTime now = LocalDateTime.now();
+//        // mock 数据
+//        RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+//                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
+//                .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
+//        rewardActivityMapper.insert(productActivity01);
+//        RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
+//                .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
+//                .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
+//        rewardActivityMapper.insert(productActivity02);
+//        // 准备参数
+//        Set<Long> spuIds = asSet(1L, 2L, 3L);
+//
+//        List<RewardActivityDO> activityList = rewardActivityServiceImpl.getRewardActivityListByStatusAndDateTimeLt(
+//                CommonStatusEnum.ENABLE.getStatus(), now);
+//        List<RewardActivityDO> matchRewardActivityList = filterMatchActivity(spuIds, activityList);
+//        // 断言
+//        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();
+//            }
+//        });
+//    }
+//
+//    /**
+//     * 获得满减送的订单项(商品)列表
+//     *
+//     * @param spuIds       商品编号
+//     * @param activityList 活动列表
+//     * @return 订单项(商品)列表
+//     */
+//    private List<RewardActivityDO> filterMatchActivity(Collection<Long> spuIds, List<RewardActivityDO> activityList) {
+//        List<RewardActivityDO> resultActivityList = new ArrayList<>();
+//        for (RewardActivityDO activity : activityList) {
+//            // 情况一:全部商品都可以参与
+//            if (PromotionProductScopeEnum.isAll(activity.getProductScope())) {
+//                resultActivityList.add(activity);
+//            }
+//            // 情况二:指定商品参与
+//            if (PromotionProductScopeEnum.isSpu(activity.getProductScope()) &&
+//                    !intersectionDistinct(activity.getProductScopeValues(), spuIds).isEmpty()) {
+//                resultActivityList.add(activity);
+//            }
+//        }
+//        return resultActivityList;
+//    }
 
 }

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

@@ -51,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, "购物车项不存在");
@@ -61,7 +62,7 @@ public interface ErrorCodeConstants {
     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_PRICE_TOO_MUCH = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:优惠金额超过订单金额");
+    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, "快递公司不存在");

+ 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;
 

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

@@ -62,3 +62,8 @@ tenant-id: {{appTenentId}}
 GET {{appApi}}/trade/order/get-express-track-list?id=70
 Authorization: Bearer {{appToken}}
 tenant-id: {{appTenentId}}
+
+### /trade-order/settlement-product 获得商品结算信息
+GET {{appApi}}/trade/order/settlement-product?spuIds=633
+Authorization: Bearer {{appToken}}
+tenant-id: {{appTenentId}}

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

@@ -17,6 +17,7 @@ import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import cn.iocoder.yudao.module.trade.service.price.TradePriceService;
 import com.google.common.collect.Maps;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -47,9 +48,10 @@ public class AppTradeOrderController {
     private TradeOrderQueryService tradeOrderQueryService;
     @Resource
     private DeliveryExpressService deliveryExpressService;
-
     @Resource
     private AfterSaleService afterSaleService;
+    @Resource
+    private TradePriceService priceService;
 
     @Resource
     private TradeOrderProperties tradeOrderProperties;
@@ -61,6 +63,13 @@ public class AppTradeOrderController {
         return success(tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO));
     }
 
+    @GetMapping("/settlement-product")
+    @Operation(summary = "获得商品结算信息", description = "用于商品列表、商品详情,获得参与活动后的价格信息")
+    @Parameter(name = "spuIds", description = "商品 SPU 编号数组")
+    public CommonResult<List<AppTradeProductSettlementRespVO>> settlementProduct(@RequestParam("spuIds") List<Long> spuIds) {
+        return success(priceService.calculateProductPrice(getLoginUserId(), spuIds));
+    }
+
     @PostMapping("/create")
     @Operation(summary = "创建订单")
     @PreAuthenticated

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

@@ -1,12 +1,13 @@
 package cn.iocoder.yudao.module.trade.controller.app.order.vo;
 
 import cn.iocoder.yudao.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
 import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Schema(description = "用户 App - 交易订单结算信息 Response VO")
@@ -19,6 +20,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;
 
@@ -31,6 +35,13 @@ public class AppTradeOrderSettlementRespVO {
     @Schema(description = "总积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
     private Integer totalPoint;
 
+    /**
+     * 营销活动数组
+     *
+     * 只对应 {@link TradePriceCalculateRespBO.Price#items} 商品匹配的活动
+     */
+    private List<TradePriceCalculateRespBO.Promotion> promotions;
+
     @Schema(description = "购物项")
     @Data
     public static class Item {
@@ -109,7 +120,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 +132,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;
+
+    }
+
 }

+ 56 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeProductSettlementRespVO.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.trade.controller.app.order.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "用户 App - 商品结算信息 Response VO")
+@Data
+public class AppTradeProductSettlementRespVO {
+
+    @Schema(description = "SPU 商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long spuId;
+
+    @Schema(description = "SKU 价格信息数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private List<Sku> skus;
+
+    @Schema(description = "满减送活动信息", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private RewardActivity rewardActivity;
+
+    @Schema(description = "满减送活动信息")
+    @Data
+    public static class RewardActivity {
+
+        @Schema(description = "满减活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Long id;
+
+        @Schema(description = "优惠规则描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "满 0.5 元减 0.3")
+        private List<String> ruleDescriptions;
+
+    }
+
+    @Schema(description = "SKU 价格信息")
+    @Data
+    public static class Sku implements Serializable {
+
+        @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Long id;
+
+        @Schema(description = "支付价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer payPrice; // 优惠后价格
+
+        @Schema(description = "营销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer promotionType; // 对应 PromotionTypeEnum 枚举
+
+        @Schema(description = "营销编号", requiredMode = Schema.RequiredMode.REQUIRED)
+        private Long promotionId; // 目前只有限时折扣活动的编号
+
+        @Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
+        private LocalDateTime promotionEndTime;
+
+    }
+
+}

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

@@ -46,8 +46,7 @@ public interface AfterSaleConvert {
             @Mapping(source = "afterSale.refundPrice", target = "price"),
             @Mapping(source = "orderProperties.payAppKey", target = "appKey")
     })
-    PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale,
-                                  TradeOrderProperties orderProperties);
+    PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale, TradeOrderProperties orderProperties);
 
     MemberUserRespVO convert(MemberUserRespDTO bean);
 

+ 15 - 1
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;
     }
 
@@ -372,7 +386,7 @@ public class AfterSaleServiceImpl implements AfterSaleService {
             public void afterCommit() {
                 // 创建退款单
                 PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
-                        .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
+                        .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));;
                 Long payRefundId = payRefundApi.createRefund(createReqDTO);
                 // 更新售后单的退款单号
                 tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));

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

@@ -166,7 +166,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         TradePriceCalculateReqBO calculateReqBO = TradeOrderConvert.INSTANCE.convert(userId, settlementReqVO, cartList);
         calculateReqBO.getItems().forEach(item -> Assert.isTrue(item.getSelected(), // 防御性编程,保证都是选中的
                 "商品({}) 未设置为选中", item.getSkuId()));
-        return tradePriceService.calculatePrice(calculateReqBO);
+        return tradePriceService.calculateOrderPrice(calculateReqBO);
     }
 
     @Override
@@ -887,7 +887,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
                 .setAppKey(tradeOrderProperties.getPayAppKey()).setUserIp(getClientIP()) // 支付应用
                 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
                 .setMerchantRefundId(String.valueOf(order.getId()))
-                .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice()));// 价格信息
+                .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice())); // 价格信息
     }
 
     @Override

+ 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);
         }
     }

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

@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.trade.service.price;
 
+import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeProductSettlementRespVO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
-
 import jakarta.validation.Valid;
 
+import java.util.List;
+
 /**
  * 价格计算 Service 接口
  *
@@ -13,11 +15,20 @@ import jakarta.validation.Valid;
 public interface TradePriceService {
 
     /**
-     * 价格计算
+     * 【订单】价格计算
      *
      * @param calculateReqDTO 计算信息
      * @return 计算结果
      */
-    TradePriceCalculateRespBO calculatePrice(@Valid TradePriceCalculateReqBO calculateReqDTO);
+    TradePriceCalculateRespBO calculateOrderPrice(@Valid TradePriceCalculateReqBO calculateReqDTO);
+
+    /**
+     * 【商品】价格计算,用于商品列表、商品详情
+     *
+     * @param userId 用户编号,允许为空
+     * @param spuIds 商品 SPU 编号数组
+     * @return 计算结果
+     */
+    List<AppTradeProductSettlementRespVO> calculateProductPrice(Long userId, List<Long> spuIds);
 
 }

+ 73 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java

@@ -1,24 +1,32 @@
 package cn.iocoder.yudao.module.trade.service.price;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 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.api.discount.DiscountActivityApi;
+import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
+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.PromotionTypeEnum;
+import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeProductSettlementRespVO;
 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.calculator.TradeDiscountActivityPriceCalculator;
 import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculator;
 import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import jakarta.annotation.Resource;
 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.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_PAY_PRICE_ILLEGAL;
@@ -37,12 +45,19 @@ public class TradePriceServiceImpl implements TradePriceService {
     private ProductSkuApi productSkuApi;
     @Resource
     private ProductSpuApi productSpuApi;
+    @Resource
+    private DiscountActivityApi discountActivityApi;
+    @Resource
+    private RewardActivityApi rewardActivityApi;
 
     @Resource
     private List<TradePriceCalculator> priceCalculators;
 
+    @Resource
+    private TradeDiscountActivityPriceCalculator discountActivityPriceCalculator;
+
     @Override
-    public TradePriceCalculateRespBO calculatePrice(TradePriceCalculateReqBO calculateReqBO) {
+    public TradePriceCalculateRespBO calculateOrderPrice(TradePriceCalculateReqBO calculateReqBO) {
         // 1.1 获得商品 SKU 数组
         List<ProductSkuRespDTO> skuList = checkSkuList(calculateReqBO);
         // 1.2 获得商品 SPU 数组
@@ -85,4 +100,58 @@ public class TradePriceServiceImpl implements TradePriceService {
         return productSpuApi.validateSpuList(convertSet(skuList, ProductSkuRespDTO::getSpuId));
     }
 
+    @Override
+    public List<AppTradeProductSettlementRespVO> calculateProductPrice(Long userId, List<Long> spuIds) {
+        // 1.1 获得 SPU 与 SKU 的映射
+        List<ProductSkuRespDTO> allSkuList = productSkuApi.getSkuListBySpuId(spuIds);
+        Map<Long, List<ProductSkuRespDTO>> spuIdAndSkuListMap = convertMultiMap(allSkuList, ProductSkuRespDTO::getSpuId);
+        // 1.2 获得会员等级
+        MemberLevelRespDTO level = discountActivityPriceCalculator.getMemberLevel(userId);
+        // 1.3 获得限时折扣活动
+        Map<Long, DiscountProductRespDTO> skuIdAndDiscountMap = convertMap(
+                discountActivityApi.getMatchDiscountProductListBySkuIds(convertSet(allSkuList, ProductSkuRespDTO::getId)),
+                DiscountProductRespDTO::getSkuId);
+        // 1.4 获得满减送活动
+       List<RewardActivityMatchRespDTO> rewardActivityMap = rewardActivityApi.getMatchRewardActivityListBySpuIds(spuIds);
+
+        // 2. 价格计算
+        return convertList(spuIds, spuId -> {
+            AppTradeProductSettlementRespVO spuVO = new AppTradeProductSettlementRespVO().setSpuId(spuId);
+            // 2.1 优惠价格
+            List<ProductSkuRespDTO> skuList = spuIdAndSkuListMap.get(spuId);
+            List<AppTradeProductSettlementRespVO.Sku> skuVOList = convertList(skuList, sku -> {
+                AppTradeProductSettlementRespVO.Sku skuVO = new AppTradeProductSettlementRespVO.Sku()
+                        .setId(sku.getId()).setPayPrice(sku.getPrice());
+                TradePriceCalculateRespBO.OrderItem orderItem = new TradePriceCalculateRespBO.OrderItem()
+                        .setPayPrice(sku.getPrice()).setCount(1);
+                // 计算限时折扣的优惠价格
+                DiscountProductRespDTO discountProduct = skuIdAndDiscountMap.get(sku.getId());
+                Integer discountPrice = discountActivityPriceCalculator.calculateActivityPrice(discountProduct, orderItem);
+                // 计算 VIP 优惠金额
+                Integer vipPrice = discountActivityPriceCalculator.calculateVipPrice(level, orderItem);
+                if (discountPrice <= 0 && vipPrice <= 0) {
+                    return skuVO;
+                }
+                // 选择一个大的优惠
+                if (discountPrice > vipPrice) {
+                    return skuVO.setPayPrice(sku.getPrice() - discountPrice)
+                            .setPromotionType(PromotionTypeEnum.DISCOUNT_ACTIVITY.getType())
+                            .setPromotionId(discountProduct.getId()).setPromotionEndTime(discountProduct.getActivityEndTime());
+                } else {
+                    return skuVO.setPayPrice(sku.getPrice() - vipPrice)
+                            .setPromotionType(PromotionTypeEnum.MEMBER_LEVEL.getType());
+                }
+            });
+            spuVO.setSkus(skuVOList);
+            // 2.2 满减送活动
+            RewardActivityMatchRespDTO rewardActivity = CollUtil.findOne(rewardActivityMap,
+                    activity -> CollUtil.contains(activity.getProductScopeValues(), spuId));
+            if (rewardActivity != null) {
+                spuVO.setRewardActivity(new AppTradeProductSettlementRespVO.RewardActivity().setId(rewardActivity.getId())
+                        .setRuleDescriptions(convertList(rewardActivity.getRules(), RewardActivityMatchRespDTO.Rule::getDescription)));
+            }
+            return spuVO;
+        });
+    }
+
 }

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

@@ -5,6 +5,7 @@ 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;
 
@@ -45,9 +46,13 @@ public class TradePriceCalculateRespBO {
     private List<Promotion> promotions;
 
     /**
-     * 优惠劵编号
+     * 使用的优惠劵编号
      */
     private Long couponId;
+    /**
+     * 用户的优惠劵列表(可用 + 不可用)
+     */
+    private List<Coupon> coupons;
 
     /**
      * 会员剩余积分
@@ -339,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 - 27
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCouponPriceCalculator.java

@@ -1,31 +1,31 @@
 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 jakarta.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;
-import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH;
 
 /**
  * 优惠劵的 {@link TradePriceCalculator} 实现类
@@ -41,34 +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);
-        if (couponPrice <= totalPayPrice) {
-            throw exception(PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH);
-        }
         // 3.2 计算分摊的优惠金额
         List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);
 
@@ -76,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 优惠金额
@@ -88,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();

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio