Quellcode durchsuchen

!653 完善拼团活动下单流程
Merge pull request !653 from puhui999/feature/mall_product

芋道源码 vor 1 Jahr
Ursprung
Commit
a76760c419
30 geänderte Dateien mit 591 neuen und 235 gelöschten Zeilen
  1. 14 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
  2. 0 41
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordRespDTO.java
  3. 27 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationValidateJoinRespDTO.java
  4. 3 1
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
  5. 6 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
  6. 62 31
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
  7. 26 5
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java
  8. 60 57
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java
  9. 0 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/record/AppCombinationRecordDetailRespVO.java
  10. 2 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/record/AppCombinationRecordSummaryRespVO.java
  11. 5 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
  12. 7 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java
  13. 7 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationActivityMapper.java
  14. 31 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java
  15. 7 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
  16. 8 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityService.java
  17. 5 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityServiceImpl.java
  18. 17 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java
  19. 12 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
  20. 65 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java
  21. 79 48
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  22. 8 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java
  23. 5 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
  24. 1 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java
  25. 13 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
  26. 17 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
  27. 5 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  28. 45 33
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationHandler.java
  29. 53 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCombinationActivityPriceCalculator.java
  30. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculator.java

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

@@ -1,6 +1,7 @@
 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.CombinationValidateJoinRespDTO;
 
 import javax.validation.Valid;
 import java.time.LocalDateTime;
@@ -65,4 +66,17 @@ public interface CombinationRecordApi {
      */
     void updateRecordStatusToInProgress(Long userId, Long orderId, LocalDateTime startTime);
 
+    /**
+     * 【下单前】校验是否满足拼团活动条件
+     *
+     * 如果校验失败,则抛出业务异常
+     *
+     * @param activityId 活动编号
+     * @param userId     用户编号
+     * @param skuId      sku 编号
+     * @param count      数量
+     * @return 拼团信息
+     */
+    CombinationValidateJoinRespDTO validateJoinCombination(Long activityId, Long userId, Long skuId, Integer count);
+
 }

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

@@ -1,41 +0,0 @@
-package cn.iocoder.yudao.module.promotion.api.combination.dto;
-
-import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
-import lombok.Data;
-
-/**
- * 拼团记录 Response DTO
- *
- * @author HUIHUI
- */
-@Data
-public class CombinationRecordRespDTO {
-
-    /**
-     * 拼团活动编号
-     */
-    private Long activityId;
-    /**
-     * SPU 编号
-     */
-    private Long spuId;
-    /**
-     * SKU 编号
-     */
-    private Long skuId;
-    /**
-     * 用户编号
-     */
-    private Long userId;
-    /**
-     * 订单编号
-     */
-    private Long orderId;
-    /**
-     * 开团状态
-     *
-     * 枚举 {@link CombinationRecordStatusEnum}
-     */
-    private Integer status;
-
-}

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

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.promotion.api.combination.dto;
+
+import lombok.Data;
+
+/**
+ * 校验参与拼团 Response DTO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CombinationValidateJoinRespDTO {
+
+    /**
+     * 砍价活动编号
+     */
+    private Long activityId;
+    /**
+     * 砍价活动名称
+     */
+    private String name;
+
+    /**
+     * 拼团金额
+     */
+    private Integer combinationPrice;
+
+}

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

@@ -55,7 +55,7 @@ public interface ErrorCodeConstants {
     ErrorCode SECKILL_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_008_003, "秒杀活动已关闭,不能修改");
     ErrorCode SECKILL_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1_013_008_004, "秒杀活动未关闭或未结束,不能删除");
     ErrorCode SECKILL_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_008_005, "秒杀活动已关闭,不能重复关闭");
-    ErrorCode SECKILL_ACTIVITY_UPDATE_STOCK_FAIL = new ErrorCode(1_013_008_006, "秒杀失败,原因秒杀库存不足");
+    ErrorCode SECKILL_ACTIVITY_UPDATE_STOCK_FAIL = new ErrorCode(1_013_008_006, "秒杀失败,原因秒杀库存不足");
     ErrorCode SECKILL_JOIN_ACTIVITY_TIME_ERROR = new ErrorCode(1_013_008_007, "秒杀失败,原因:不在活动时间范围内");
     ErrorCode SECKILL_JOIN_ACTIVITY_STATUS_CLOSED = new ErrorCode(1_013_008_008, "秒杀失败,原因:秒杀活动已关闭");
     ErrorCode SECKILL_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED = new ErrorCode(1_013_008_009, "秒杀失败,原因:单次限购超出");
@@ -72,6 +72,8 @@ public interface ErrorCodeConstants {
     ErrorCode COMBINATION_ACTIVITY_STATUS_DISABLE_NOT_UPDATE = new ErrorCode(1_013_010_002, "拼团活动已关闭不能修改");
     ErrorCode COMBINATION_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1_013_010_003, "拼团活动未关闭或未结束,不能删除");
     ErrorCode COMBINATION_ACTIVITY_STATUS_DISABLE = new ErrorCode(1_013_010_004, "拼团失败,原因:拼团活动已关闭");
+    ErrorCode COMBINATION_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS = new ErrorCode(1_013_010_005, "拼团失败,原因:拼团活动商品不存在");
+    ErrorCode COMBINATION_ACTIVITY_UPDATE_STOCK_FAIL = new ErrorCode(1_013_010_006, "拼团失败,原因:拼团活动商品库存不足");
 
     // ========== 拼团记录 1-013-011-000 ==========
     ErrorCode COMBINATION_RECORD_NOT_EXISTS = new ErrorCode(1_013_011_000, "拼团不存在");

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

@@ -1,6 +1,7 @@
 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.CombinationValidateJoinRespDTO;
 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
 import org.springframework.stereotype.Service;
@@ -50,4 +51,9 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
                 userId, orderId, startTime);
     }
 
+    @Override
+    public CombinationValidateJoinRespDTO validateJoinCombination(Long activityId, Long userId, Long skuId, Integer count) {
+        return recordService.validateJoinCombination(activityId, userId, skuId, count);
+    }
+
 }

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

@@ -1,7 +1,16 @@
 package cn.iocoder.yudao.module.promotion.controller.app.activity;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 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.seckill.SeckillActivityDO;
+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.seckill.SeckillActivityService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -11,8 +20,11 @@ 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 javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
@@ -22,47 +34,66 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 @Validated
 public class AppActivityController {
 
-    // TODO @puhui999:可以实现下
+    @Resource
+    private CombinationActivityService combinationActivityService;
+    @Resource
+    private SeckillActivityService seckillActivityService;
+    @Resource
+    private BargainActivityService bargainActivityService;
+
     @GetMapping("/list-by-spu-id")
     @Operation(summary = "获得单个商品,近期参与的每个活动") // 每种活动,只返回一个
     @Parameter(name = "spuId", description = "商品编号", required = true)
     public CommonResult<List<AppActivityRespVO>> getActivityListBySpuId(@RequestParam("spuId") Long spuId) {
-        // TODO 芋艿,实现
-        List<AppActivityRespVO> randomList = new ArrayList<>();
-        Random random = new Random();
-        for (int i = 0; i < 3; i++) { // 生成5个随机对象
-            AppActivityRespVO vo = new AppActivityRespVO();
-            vo.setId(random.nextLong()); // 随机生成一个长整型 ID
-            vo.setType(i + 1); // 随机生成一个介于0到2之间的整数,对应枚举类型的三种类型之一
-            vo.setName(String.format("活动%d", random.nextInt(100))); // 随机生成一个类似于“活动XX”的活动名称,XX为0到99之间的随机整数
-            vo.setStartTime(LocalDateTime.now()); // 随机生成一个在过去的一年内的开始时间(以毫秒为单位)
-            vo.setEndTime(LocalDateTime.now()); // 随机生成一个在未来的一年内的结束时间(以毫秒为单位)
-            randomList.add(vo);
-        }
-        return success(randomList);
+        return success(getAppActivityRespVOList(spuId));
     }
 
-    // TODO @puhui999:可以实现下
     @GetMapping("/list-by-spu-ids")
     @Operation(summary = "获得多个商品,近期参与的每个活动") // 每种活动,只返回一个;key 为 SPU 编号
     @Parameter(name = "spuIds", description = "商品编号数组", required = true)
     public CommonResult<Map<Long, List<AppActivityRespVO>>> getActivityListBySpuIds(@RequestParam("spuIds") List<Long> spuIds) {
-        // TODO 芋艿,实现
-        List<AppActivityRespVO> randomList = new ArrayList<>();
-        Random random = new Random();
-        for (int i = 0; i < 5; i++) { // 生成5个随机对象
-            AppActivityRespVO vo = new AppActivityRespVO();
-            vo.setId(random.nextLong()); // 随机生成一个长整型 ID
-            vo.setType(random.nextInt(3)); // 随机生成一个介于0到2之间的整数,对应枚举类型的三种类型之一
-            vo.setName(String.format("活动%d", random.nextInt(100))); // 随机生成一个类似于“活动XX”的活动名称,XX为0到99之间的随机整数
-            vo.setStartTime(LocalDateTime.now()); // 随机生成一个在过去的一年内的开始时间(以毫秒为单位)
-            vo.setEndTime(LocalDateTime.now()); // 随机生成一个在未来的一年内的结束时间(以毫秒为单位)
-            randomList.add(vo);
+
+        if (CollUtil.isEmpty(spuIds)) {
+            return success(MapUtil.empty());
         }
-        Map<Long, List<AppActivityRespVO>> map = new HashMap<>();
-        map.put(109L, randomList);
-        map.put(2L, randomList);
+
+        Map<Long, List<AppActivityRespVO>> map = new HashMap<>(spuIds.size());
+        spuIds.forEach(spuId -> {
+            map.put(spuId, getAppActivityRespVOList(spuId));
+        });
         return success(map);
     }
 
+    private List<AppActivityRespVO> getAppActivityRespVOList(Long spuId) {
+        List<AppActivityRespVO> respList = new ArrayList<>();
+        CombinationActivityDO combination = combinationActivityService.getCombinationActivityBySpuId(spuId);
+        if (combination != null) {
+            respList.add(new AppActivityRespVO()
+                    .setId(combination.getId())
+                    .setType(PromotionTypeEnum.COMBINATION_ACTIVITY.getType())
+                    .setName(combination.getName())
+                    .setStartTime(combination.getStartTime())
+                    .setEndTime(combination.getEndTime()));
+        }
+        SeckillActivityDO seckill = seckillActivityService.getSeckillActivityBySpuId(spuId);
+        if (seckill != null) {
+            respList.add(new AppActivityRespVO()
+                    .setId(seckill.getId())
+                    .setType(PromotionTypeEnum.SECKILL_ACTIVITY.getType())
+                    .setName(seckill.getName())
+                    .setStartTime(seckill.getStartTime())
+                    .setEndTime(seckill.getEndTime()));
+        }
+        BargainActivityDO bargain = bargainActivityService.getBargainActivityBySpuId(spuId);
+        if (bargain != null) {
+            respList.add(new AppActivityRespVO()
+                    .setId(bargain.getId())
+                    .setType(PromotionTypeEnum.BARGAIN_ACTIVITY.getType())
+                    .setName(bargain.getName())
+                    .setStartTime(bargain.getStartTime())
+                    .setEndTime(bargain.getEndTime()));
+        }
+        return respList;
+    }
+
 }

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

@@ -14,6 +14,8 @@ 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;
@@ -24,10 +26,13 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
+import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
 
+import static cn.hutool.core.util.ObjectUtil.defaultIfNull;
 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 - 拼团活动")
@@ -36,24 +41,40 @@ 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;
 
-    // TODO 芋艿:增加 Spring Cache
     @GetMapping("/list")
     @Operation(summary = "获得拼团活动列表", description = "用于小程序首页")
     @Parameter(name = "count", description = "需要展示的数量", example = "6")
     public CommonResult<List<AppCombinationActivityRespVO>> getCombinationActivityList(
             @RequestParam(name = "count", defaultValue = "6") Integer count) {
-        List<CombinationActivityDO> list = activityService.getCombinationActivityListByCount(6);
+        return success(combinationActivityListCache.getUnchecked(count));
+    }
+
+    private List<AppCombinationActivityRespVO> getCombinationActivityList0(Integer count) {
+        List<CombinationActivityDO> list = activityService.getCombinationActivityListByCount(defaultIfNull(count, 6));
         if (CollUtil.isEmpty(list)) {
-            return success(Collections.emptyList());
+            return Collections.emptyList();
         }
         // 拼接返回
         List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(list, CombinationActivityDO::getSpuId));
-        return success(CombinationActivityConvert.INSTANCE.convertAppList(list, spuList));
+        return CombinationActivityConvert.INSTANCE.convertAppList(list, spuList);
     }
 
     @GetMapping("/page")
@@ -75,7 +96,7 @@ public class AppCombinationActivityController {
         // 1、获取活动
         CombinationActivityDO combinationActivity = activityService.getCombinationActivity(id);
         if (combinationActivity == null
-            || ObjectUtil.equal(combinationActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
+                || ObjectUtil.equal(combinationActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
             return success(null);
         }
         // 2、获取活动商品

+ 60 - 57
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java

@@ -1,12 +1,17 @@
 package cn.iocoder.yudao.module.promotion.controller.app.combination;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordDetailRespVO;
 import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordRespVO;
 import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordSummaryRespVO;
+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.service.combination.CombinationRecordService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -14,13 +19,14 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.annotation.Resource;
 import javax.validation.constraints.Max;
-import java.time.Duration;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
+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.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "用户 APP - 拼团活动")
 @RestController
@@ -28,81 +34,78 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 @Validated
 public class AppCombinationRecordController {
 
+    @Resource
+    private CombinationRecordService combinationRecordService;
+
     @GetMapping("/get-summary")
     @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
     // TODO 芋艿:增加 @Cache 缓存,1 分钟过期
     public CommonResult<AppCombinationRecordSummaryRespVO> getCombinationRecordSummary() {
+        // 获取所有拼团记录
+        Long count = combinationRecordService.getRecordsCount();
         AppCombinationRecordSummaryRespVO summary = new AppCombinationRecordSummaryRespVO();
-        summary.setUserCount(1024);
-        summary.setAvatars(new ArrayList<>());
-        summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLjFK35Wvia9lJKHoXfQuHhk0qZbvpPNxrAiaEKF7aL2k4I8kuqrdTWwliamdPHeyAA7DjAg725X2GIQ/132");
-        summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTK1pXgdj5DvBMwrbe8v3tFibSWeQATEsAibt3fllD8XwJ460P2r6KS3WCQvDefuv1bVpDhNCle6CTCA/132");
-        summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTL7KRGHBE62N0awFyBesmmxiaCicf1fJ7E7UCh6zA8GWlT1QC1zT01gG4OxI7BWDESkdPZ5o7tno4hA/132");
-        summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/ouwtwJycbic2JrCoZjETict0klxd1uRuicRneKk00ewMcCClxVcVHQT91Sh9MJGtwibf1fOicD1WpwSP4icJM6eQq1AA/132");
-        summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/RpUrhwens58qc99OcGs993xL4M5QPOe05ekqF9Eia440kRicAlicicIdQWicHBmy2bzLgHzHguWEzHHxnIgeictL7bLA/132");
-        summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/S4tfqmxc8GZGsKc1K4mnhpvtG16gtMrLnTQfDibhr7jJich9LRI5RQKZDoqEjZM3azMib5nic7F4ZXKMEgYyLO08KA/132");
-        summary.getAvatars().add("https://static.iocoder.cn/mall/132.jpeg");
+        if (count == null || count == 0L) {
+            summary.setAvatars(Collections.emptyList());
+            summary.setUserCount(count);
+            return success(summary);
+        }
+
+        summary.setUserCount(count);
+        // TODO 只返回最近的 7 个
+        int num = 7;
+        summary.setAvatars(convertList(combinationRecordService.getLatestRecordList(num), CombinationRecordDO::getAvatar));
         return success(summary);
     }
 
     @GetMapping("/get-head-list")
     @Operation(summary = "获得最近 n 条拼团记录(团长发起的)")
-    // TODO @芋艿:注解要补全
+    @Parameters({
+            @Parameter(name = "activityId", description = "拼团活动编号"),
+            @Parameter(name = "status", description = "状态"),
+            @Parameter(name = "count", description = "数量")
+    })
     public CommonResult<List<AppCombinationRecordRespVO>> getHeadCombinationRecordList(
             @RequestParam(value = "activityId", required = false) Long activityId,
             @RequestParam("status") Integer status,
             @RequestParam(value = "count", defaultValue = "20") @Max(20) Integer count) {
-        List<AppCombinationRecordRespVO> list = new ArrayList<>();
-        for (int i = 1; i <= count; i++) {
-            AppCombinationRecordRespVO record = new AppCombinationRecordRespVO();
-            record.setId((long) i);
-            record.setNickname("用户" + i);
-            record.setAvatar("头像" + i);
-            record.setExpireTime(LocalDateTime.now());
-            record.setUserSize(10);
-            record.setUserCount(i);
-            record.setPicUrl("https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg");
-            record.setActivityId(1L);
-            record.setSpuName("活动:" + i);
-            list.add(record);
-        }
-        return success(list);
+        return success(CombinationActivityConvert.INSTANCE.convertList3(
+                combinationRecordService.getRecordListWithHead(activityId, status, count)));
     }
 
     @GetMapping("/get-detail")
     @Operation(summary = "获得拼团记录明细")
     @Parameter(name = "id", description = "拼团记录编号", required = true, example = "1024")
     public CommonResult<AppCombinationRecordDetailRespVO> getCombinationRecordDetail(@RequestParam("id") Long id) {
+        // 1、查询这条记录
+        CombinationRecordDO record = combinationRecordService.getRecordById(id);
+        if (record == null) {
+            return success(null);
+        }
+
         AppCombinationRecordDetailRespVO detail = new AppCombinationRecordDetailRespVO();
-        // 团长
-        AppCombinationRecordRespVO headRecord = new AppCombinationRecordRespVO();
-        headRecord.setId(1L);
-        headRecord.setNickname("用户" + 1);
-        headRecord.setAvatar("头像" + 1);
-        headRecord.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofDays(1)));
-        headRecord.setUserSize(10);
-        headRecord.setUserCount(3);
-        headRecord.setStatus(1);
-        headRecord.setActivityId(10L);
-        headRecord.setPicUrl("https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg");
-        headRecord.setCombinationPrice(100);
-        detail.setHeadRecord(headRecord);
-        // 团员
-        List<AppCombinationRecordRespVO> list = new ArrayList<>();
-        for (int i = 1; i <= 2; i++) {
-            AppCombinationRecordRespVO record = new AppCombinationRecordRespVO();
-            record.setId((long) i);
-            record.setNickname("用户" + i);
-            record.setAvatar("头像" + i);
-            record.setExpireTime(LocalDateTime.now());
-            record.setUserSize(10);
-            record.setUserCount(i);
-            record.setStatus(1);
-            list.add(record);
+        List<CombinationRecordDO> records;
+        // 2、判断是否为团长
+        if (record.getHeadId() == null) {
+            detail.setHeadRecord(CombinationActivityConvert.INSTANCE.convert(record));
+            // 2.1、查找团员拼团记录
+            records = combinationRecordService.getRecordListByHeadId(record.getId());
+        } else {
+            // 2.2、查找团长拼团记录
+            CombinationRecordDO headRecord = combinationRecordService.getRecordById(record.getHeadId());
+            if (headRecord == null) {
+                return success(null);
+            }
+
+            detail.setHeadRecord(CombinationActivityConvert.INSTANCE.convert(headRecord));
+            // 2.3、查找团员拼团记录
+            records = combinationRecordService.getRecordListByHeadId(headRecord.getId());
+
         }
-        detail.setMemberRecords(list);
-        // 订单编号
-        detail.setOrderId(100L);
+        detail.setMemberRecords(CombinationActivityConvert.INSTANCE.convertList3(records));
+
+        // 3、获取当前用户参团记录订单编号
+        CombinationRecordDO userRecord = CollectionUtils.findFirst(records, r -> ObjectUtil.equal(r.getUserId(), getLoginUserId()));
+        detail.setOrderId(userRecord == null ? null : userRecord.getOrderId()); // 如果没参团,返回 null
         return success(detail);
     }
 

+ 0 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/record/AppCombinationRecordDetailRespVO.java

@@ -16,7 +16,6 @@ public class AppCombinationRecordDetailRespVO {
     private List<AppCombinationRecordRespVO> memberRecords;
 
     @Schema(description = "当前用户参团记录对应的订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
-    // 如果没参团,返回 null
     private Long orderId;
 
 }

+ 2 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/record/AppCombinationRecordSummaryRespVO.java

@@ -10,9 +10,9 @@ import java.util.List;
 public class AppCombinationRecordSummaryRespVO {
 
     @Schema(description = "拼团用户数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
-    private Integer userCount;
+    private Long userCount;
 
-    @Schema(description = "拼团用户头像列表", requiredMode = Schema.RequiredMode.REQUIRED) // 只返回最近的 7 个
+    @Schema(description = "拼团用户头像列表", requiredMode = Schema.RequiredMode.REQUIRED)
     private List<String> avatars;
 
 }

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

@@ -7,7 +7,6 @@ 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;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
-import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityCreateReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO;
@@ -15,6 +14,7 @@ import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.product
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.product.CombinationProductRespVO;
 import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.activity.AppCombinationActivityDetailRespVO;
 import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.activity.AppCombinationActivityRespVO;
+import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordRespVO;
 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.dal.dataobject.combination.CombinationRecordDO;
@@ -108,8 +108,6 @@ public interface CombinationActivityConvert {
                 .setPicUrl(sku.getPicUrl());
     }
 
-    List<CombinationRecordRespDTO> convert(List<CombinationRecordDO> bean);
-
     List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list);
 
     default List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list, List<ProductSpuRespDTO> spuList) {
@@ -144,4 +142,8 @@ public interface CombinationActivityConvert {
         return convert2(combinationActivity).setProducts(convertList1(products));
     }
 
+    List<AppCombinationRecordRespVO> convertList3(List<CombinationRecordDO> records);
+
+    AppCombinationRecordRespVO convert(CombinationRecordDO record);
+
 }

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

@@ -83,4 +83,11 @@ public interface BargainActivityMapper extends BaseMapperX<BargainActivityDO> {
                 .last("LIMIT " + count));
     }
 
+    default BargainActivityDO selectOne(Long spuId) {
+        return selectOne(new LambdaQueryWrapperX<BargainActivityDO>()
+                        .eq(BargainActivityDO::getSpuId, spuId)
+                        .orderByDesc(BargainActivityDO::getCreateTime)
+                , false);
+    }
+
 }

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

@@ -40,4 +40,11 @@ public interface CombinationActivityMapper extends BaseMapperX<CombinationActivi
                 .last("LIMIT " + count));
     }
 
+    default CombinationActivityDO selectOne(Long spuId) {
+        return selectOne(new LambdaQueryWrapperX<CombinationActivityDO>()
+                        .eq(CombinationActivityDO::getSpuId, spuId)
+                        .orderByDesc(CombinationActivityDO::getCreateTime)
+                , false);
+    }
+
 }

+ 31 - 4
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java

@@ -29,6 +29,7 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
                 .eq(CombinationRecordDO::getUserId, userId)
                 .eq(CombinationRecordDO::getStatus, status));
     }
+
     /**
      * 查询拼团记录
      *
@@ -47,10 +48,6 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
                 .eq(CombinationRecordDO::getStatus, status));
     }
 
-    default List<CombinationRecordDO> selectListByStatus(Integer status) {
-        return selectList(CombinationRecordDO::getStatus, status);
-    }
-
     /**
      * 查询拼团记录
      *
@@ -63,4 +60,34 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
                 .eq(CombinationRecordDO::getUserId, userId)
                 .eq(CombinationRecordDO::getActivityId, activityId));
     }
+
+    /**
+     * 获取最近的 count 条数据
+     *
+     * @param count 数量
+     * @return 拼团记录列表
+     */
+    default List<CombinationRecordDO> selectLatestList(int count) {
+        return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
+                .orderByDesc(CombinationRecordDO::getCreateTime)
+                .last("LIMIT " + count));
+    }
+
+    /**
+     * 获得最近 count 条拼团记录(团长发起的)
+     *
+     * @param activityId 拼团活动编号
+     * @param status     记录状态
+     * @param count      数量
+     * @return 拼团记录列表
+     */
+    default List<CombinationRecordDO> selectList(Long activityId, Integer status, Integer count) {
+        return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
+                .eqIfPresent(CombinationRecordDO::getActivityId, activityId)
+                .eqIfPresent(CombinationRecordDO::getStatus, status)
+                .eq(CombinationRecordDO::getHeadId, null) // TODO 团长的 headId 是不是 null 还是自己的记录编号来着?
+                .orderByDesc(CombinationRecordDO::getCreateTime)
+                .last("LIMIT " + count));
+    }
+
 }

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

@@ -56,4 +56,11 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
                 .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
     }
 
+    default SeckillActivityDO selectOne(Long spuId) {
+        return selectOne(new LambdaQueryWrapperX<SeckillActivityDO>()
+                        .eq(SeckillActivityDO::getSpuId, spuId)
+                        .orderByDesc(SeckillActivityDO::getCreateTime)
+                , false);
+    }
+
 }

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

@@ -98,4 +98,12 @@ public interface BargainActivityService {
      */
     List<BargainActivityDO> getBargainActivityListByCount(Integer count);
 
+    /**
+     * 获取指定 spu 编号的活动
+     *
+     * @param spuId spu 编号
+     * @return 砍价活动
+     */
+    BargainActivityDO getBargainActivityBySpuId(Long spuId);
+
 }

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

@@ -175,4 +175,9 @@ public class BargainActivityServiceImpl implements BargainActivityService {
         return bargainActivityMapper.selectList(count, CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now());
     }
 
+    @Override
+    public BargainActivityDO getBargainActivityBySpuId(Long spuId) {
+        return bargainActivityMapper.selectOne(spuId);
+    }
+
 }

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

@@ -100,4 +100,21 @@ public interface CombinationActivityService {
      */
     PageResult<CombinationActivityDO> getCombinationActivityPage(PageParam pageParam);
 
+    /**
+     * 获取指定活动指定 sku 编号的商品
+     *
+     * @param activityId 活动编号
+     * @param skuId      sku 编号
+     * @return 活动商品信息
+     */
+    CombinationProductDO selectByActivityIdAndSkuId(Long activityId, Long skuId);
+
+    /**
+     * 获取指定 spu 编号的活动
+     *
+     * @param spuId spu 编号
+     * @return 拼团活动
+     */
+    CombinationActivityDO getCombinationActivityBySpuId(Long spuId);
+
 }

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

@@ -216,4 +216,16 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
         return combinationActivityMapper.selectPage(pageParam, CommonStatusEnum.ENABLE.getStatus());
     }
 
+    @Override
+    public CombinationProductDO selectByActivityIdAndSkuId(Long activityId, Long skuId) {
+        return combinationProductMapper.selectOne(
+                CombinationProductDO::getActivityId, activityId,
+                CombinationProductDO::getSkuId, skuId);
+    }
+
+    @Override
+    public CombinationActivityDO getCombinationActivityBySpuId(Long spuId) {
+        return combinationActivityMapper.selectOne(spuId);
+    }
+
 }

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

@@ -1,6 +1,10 @@
 package cn.iocoder.yudao.module.promotion.service.combination;
 
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
+import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
+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.dal.dataobject.combination.CombinationRecordDO;
 
 import java.time.LocalDateTime;
@@ -16,8 +20,8 @@ public interface CombinationRecordService {
     /**
      * 更新拼团状态
      *
-     * @param status 状态
-     * @param userId 用户编号
+     * @param status  状态
+     * @param userId  用户编号
      * @param orderId 订单编号
      */
     void updateCombinationRecordStatusByUserIdAndOrderId(Integer status, Long userId, Long orderId);
@@ -30,8 +34,9 @@ public interface CombinationRecordService {
      * @param userId     用户编号
      * @param skuId      sku 编号
      * @param count      数量
+     * @return 返回拼团活动和拼团活动商品
      */
-    void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count);
+    KeyValue<CombinationActivityDO, CombinationProductDO> validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count);
 
     /**
      * 创建拼团记录
@@ -43,9 +48,9 @@ public interface CombinationRecordService {
     /**
      * 更新拼团状态和开始时间
      *
-     * @param status 状态
-     * @param userId 用户编号
-     * @param orderId 订单编号
+     * @param status    状态
+     * @param userId    用户编号
+     * @param orderId   订单编号
      * @param startTime 开始时间
      */
     void updateRecordStatusAndStartTimeByUserIdAndOrderId(Integer status, Long userId, Long orderId, LocalDateTime startTime);
@@ -68,4 +73,58 @@ public interface CombinationRecordService {
      */
     List<CombinationRecordDO> getRecordListByUserIdAndActivityId(Long userId, Long activityId);
 
+    /**
+     * 【下单前】校验是否满足拼团活动条件
+     *
+     * 如果校验失败,则抛出业务异常
+     *
+     * @param activityId 活动编号
+     * @param userId     用户编号
+     * @param skuId      sku 编号
+     * @param count      数量
+     * @return 拼团信息
+     */
+    CombinationValidateJoinRespDTO validateJoinCombination(Long activityId, Long userId, Long skuId, Integer count);
+
+    /**
+     * 获取所有拼团记录数
+     *
+     * @return 记录数
+     */
+    Long getRecordsCount();
+
+    /**
+     * 获取最近的 count 条拼团记录
+     *
+     * @param count 限制数量
+     * @return 拼团记录列表
+     */
+    List<CombinationRecordDO> getLatestRecordList(int count);
+
+    /**
+     * 获得最近 n 条拼团记录(团长发起的)
+     *
+     * @param activityId 拼团活动编号
+     * @param status     状态
+     * @param count      数量
+     * @return 拼团记录列表
+     */
+    List<CombinationRecordDO> getRecordListWithHead(Long activityId, Integer status, Integer count);
+
+    /**
+     * 获取指定编号的拼团记录
+     *
+     * @param id 拼团记录编号
+     * @return 拼团记录
+     */
+    CombinationRecordDO getRecordById(Long id);
+
+    /**
+     * 获取指定团长编号的拼团记录
+     *
+     * @param headId 团长编号
+     * @return 拼团记录列表
+     */
+    List<CombinationRecordDO> getRecordListByHeadId(Long headId);
+
 }

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

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.promotion.service.combination;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
@@ -10,8 +11,10 @@ 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.combination.dto.CombinationRecordCreateReqDTO;
+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.CombinationActivityDO;
+import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecordMapper;
 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
@@ -62,35 +65,35 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     @Transactional(rollbackFor = Exception.class)
     public void updateCombinationRecordStatusByUserIdAndOrderId(Integer status, Long userId, Long orderId) {
         // 校验拼团是否存在
-        CombinationRecordDO recordDO = validateCombinationRecord(userId, orderId);
+        CombinationRecordDO record = validateCombinationRecord(userId, orderId);
 
         // 更新状态
-        recordDO.setStatus(status);
-        recordMapper.updateById(recordDO);
+        record.setStatus(status);
+        recordMapper.updateById(record);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updateRecordStatusAndStartTimeByUserIdAndOrderId(Integer status, Long userId, Long orderId, LocalDateTime startTime) {
-        CombinationRecordDO recordDO = validateCombinationRecord(userId, orderId);
+        CombinationRecordDO record = validateCombinationRecord(userId, orderId);
         // 更新状态
-        recordDO.setStatus(status);
+        record.setStatus(status);
         // 更新开始时间
-        recordDO.setStartTime(startTime);
-        recordMapper.updateById(recordDO);
+        record.setStartTime(startTime);
+        recordMapper.updateById(record);
 
         // 更新拼团参入人数
-        List<CombinationRecordDO> recordDOs = recordMapper.selectListByHeadIdAndStatus(recordDO.getHeadId(), status);
-        if (CollUtil.isNotEmpty(recordDOs)) {
-            recordDOs.forEach(item -> {
-                item.setUserCount(recordDOs.size());
+        List<CombinationRecordDO> records = recordMapper.selectListByHeadIdAndStatus(record.getHeadId(), status);
+        if (CollUtil.isNotEmpty(records)) {
+            records.forEach(item -> {
+                item.setUserCount(records.size());
                 // 校验拼团是否满足要求
-                if (ObjectUtil.equal(recordDOs.size(), recordDO.getUserSize())) {
+                if (ObjectUtil.equal(records.size(), record.getUserSize())) {
                     item.setStatus(CombinationRecordStatusEnum.SUCCESS.getStatus());
                 }
             });
         }
-        recordMapper.updateBatch(recordDOs);
+        recordMapper.updateBatch(records);
     }
 
     private CombinationRecordDO validateCombinationRecord(Long userId, Long orderId) {
@@ -104,7 +107,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
 
     // TODO @芋艿:在详细预览下;
     @Override
-    public void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count) {
+    public KeyValue<CombinationActivityDO, CombinationProductDO> validateCombinationRecord(
+            Long activityId, Long userId, Long skuId, Integer count) {
         // 1.1 校验拼团活动是否存在
         CombinationActivityDO activity = combinationActivityService.validateCombinationActivityExists(activityId);
         // 1.2 校验活动是否开启
@@ -115,10 +119,24 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         if (count > activity.getSingleLimitCount()) {
             throw exception(COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED);
         }
+        // 2.1、校验活动商品是否存在
+        CombinationProductDO product = combinationActivityService.selectByActivityIdAndSkuId(activityId, skuId);
+        if (product == null) {
+            throw exception(COMBINATION_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS);
+        }
+        // 2.2、校验 sku 是否存在
+        ProductSkuRespDTO sku = productSkuApi.getSku(skuId);
+        if (sku == null) {
+            throw exception(COMBINATION_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS);
+        }
+        // 2.3、 校验库存是否充足
+        if (count > sku.getStock()) {
+            throw exception(COMBINATION_ACTIVITY_UPDATE_STOCK_FAIL);
+        }
         // 3、校验是否有拼团记录
         List<CombinationRecordDO> recordList = getRecordListByUserIdAndActivityId(userId, activityId);
         if (CollUtil.isEmpty(recordList)) {
-            return;
+            return new KeyValue<>(activity, product);
         }
         // 4、校验是否超出总限购数量
         Integer sumValue = getSumValue(convertList(recordList, CombinationRecordDO::getCount,
@@ -129,7 +147,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         // 5、校验拼团记录是否存在未支付的订单(如果存在未支付的订单则不允许发起新的拼团)
         CombinationRecordDO record = findFirst(recordList, item -> ObjectUtil.equals(item.getStatus(), null));
         if (record == null) {
-            return;
+            return new KeyValue<>(activity, product);
         }
         // 5.1、查询关联的订单是否已经支付
         // 当前 activityId 已经有未支付的订单,不允许在发起新的;要么支付,要么去掉先;
@@ -138,57 +156,45 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         if (ObjectUtil.equal(orderStatus, TradeOrderStatusEnum.UNPAID.getStatus())) {
             throw exception(COMBINATION_RECORD_FAILED_ORDER_STATUS_UNPAID);
         }
+
+        return new KeyValue<>(activity, product);
     }
 
     // TODO 芋艿:在详细 review 下;
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
-
-        // 1.1、 校验拼团活动
-        CombinationActivityDO activity = combinationActivityService.validateCombinationActivityExists(reqDTO.getActivityId());
-        // 1.2 校验是否超出单次限购数量
-        if (reqDTO.getCount() > activity.getSingleLimitCount()) {
-            throw exception(COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED);
-        }
-        // 1.3、校验是否有拼团记录
-        List<CombinationRecordDO> records = getRecordListByUserIdAndActivityId(reqDTO.getUserId(), reqDTO.getActivityId());
-        if (CollUtil.isEmpty(records)) {
-            return;
-        }
-        // 1.4、校验是否超出总限购数量
-        Integer sumValue = getSumValue(convertList(records, CombinationRecordDO::getCount,
-                item -> ObjectUtil.equals(item.getStatus(), CombinationRecordStatusEnum.SUCCESS.getStatus())), i -> i, Integer::sum);
-        if ((sumValue + reqDTO.getCount()) > activity.getTotalLimitCount()) {
-            throw exception(COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED);
-        }
-        // 2、 校验用户是否参加了其它拼团
+        // 1、校验拼团活动
+        KeyValue<CombinationActivityDO, CombinationProductDO> keyValue = validateCombinationRecord(
+                reqDTO.getActivityId(), reqDTO.getUserId(), reqDTO.getSkuId(), reqDTO.getCount());
+        CombinationActivityDO activity = keyValue.getKey();
+        // 2、校验用户是否参加了其它拼团
         List<CombinationRecordDO> recordDOList = recordMapper.selectListByUserIdAndStatus(reqDTO.getUserId(), CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
         if (CollUtil.isNotEmpty(recordDOList)) {
             throw exception(COMBINATION_RECORD_FAILED_HAVE_JOINED);
         }
-        // 3、 校验活动是否开启
+        // 3、校验活动是否开启
         if (LocalDateTime.now().isAfter(activity.getStartTime())) {
             throw exception(COMBINATION_RECORD_FAILED_TIME_NOT_START);
         }
-        // 4、 校验当前活动是否过期
+        // 4、校验当前活动是否过期
         if (LocalDateTime.now().isAfter(activity.getEndTime())) {
             throw exception(COMBINATION_RECORD_FAILED_TIME_END);
         }
-        // 5、 父拼团是否存在,是否已经满了
+        // 5、父拼团是否存在,是否已经满了
         if (reqDTO.getHeadId() != null) {
-            // 查询进行中的父拼团
+            // 5.1、查询进行中的父拼团
             CombinationRecordDO record = recordMapper.selectOneByHeadId(reqDTO.getHeadId(), CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
             if (record == null) {
                 throw exception(COMBINATION_RECORD_HEAD_NOT_EXISTS);
             }
-            // 校验拼团是否满足要求
+            // 5.2、校验拼团是否满足要求
             if (ObjectUtil.equal(record.getUserCount(), record.getUserSize())) {
                 throw exception(COMBINATION_RECORD_USER_FULL);
             }
         }
 
-        // 2. 创建拼团记录
+        // 6. 创建拼团记录
         MemberUserRespDTO user = memberUserApi.getUser(reqDTO.getUserId());
         ProductSpuRespDTO spu = productSpuApi.getSpu(reqDTO.getSpuId());
         ProductSkuRespDTO sku = productSkuApi.getSku(reqDTO.getSkuId());
@@ -205,13 +211,38 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         return recordMapper.selectListByUserIdAndActivityId(userId, activityId);
     }
 
-    /**
-     * APP 端获取开团记录
-     *
-     * @return 开团记录
-     */
-    public List<CombinationRecordDO> getRecordListByStatus(Integer status) {
-        return recordMapper.selectListByStatus(status);
+    @Override
+    public CombinationValidateJoinRespDTO validateJoinCombination(Long activityId, Long userId, Long skuId, Integer count) {
+        KeyValue<CombinationActivityDO, CombinationProductDO> keyValue = validateCombinationRecord(activityId, userId, skuId, count);
+        return new CombinationValidateJoinRespDTO()
+                .setActivityId(keyValue.getKey().getId())
+                .setName(keyValue.getKey().getName())
+                .setCombinationPrice(keyValue.getValue().getCombinationPrice());
+    }
+
+    @Override
+    public Long getRecordsCount() {
+        return recordMapper.selectCount();
+    }
+
+    @Override
+    public List<CombinationRecordDO> getLatestRecordList(int count) {
+        return recordMapper.selectLatestList(count);
+    }
+
+    @Override
+    public List<CombinationRecordDO> getRecordListWithHead(Long activityId, Integer status, Integer count) {
+        return recordMapper.selectList(activityId, status, count);
+    }
+
+    @Override
+    public CombinationRecordDO getRecordById(Long id) {
+        return recordMapper.selectById(id);
+    }
+
+    @Override
+    public List<CombinationRecordDO> getRecordListByHeadId(Long headId) {
+        return recordMapper.selectList(CombinationRecordDO::getHeadId, headId);
     }
 
 }

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

@@ -119,4 +119,12 @@ public interface SeckillActivityService {
      */
     SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);
 
+    /**
+     * 获取指定 spu 编号的活动
+     *
+     * @param spuId spu 编号
+     * @return 秒杀活动
+     */
+    SeckillActivityDO getSeckillActivityBySpuId(Long spuId);
+
 }

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

@@ -310,4 +310,9 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
         return SeckillActivityConvert.INSTANCE.convert02(activity, product);
     }
 
+    @Override
+    public SeckillActivityDO getSeckillActivityBySpuId(Long spuId) {
+        return seckillActivityMapper.selectOne(spuId);
+    }
+
 }

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

@@ -16,6 +16,7 @@ public enum TradeOrderOperateTypeEnum {
     MEMBER_CREATE(1, "用户下单"),
     ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,实际支付金额为 {newPayPrice} 元"),
     MEMBER_PAY(10, "用户付款成功"),
+    SYSTEM_UP_ADDRESS(11, "订单 {orderId} 收货地址修改"),
     ADMIN_DELIVERY(20, "已发货,快递公司:{deliveryName},快递单号:{logisticsNo}"),
     MEMBER_RECEIVE(30, "用户已收货"),
     SYSTEM_RECEIVE(31, "到期未收货,系统自动确认收货"),

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

@@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.product.api.property.dto.ProductPropertyValueDeta
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
+import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
 import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
 import cn.iocoder.yudao.module.trade.controller.admin.base.member.user.MemberUserRespVO;
 import cn.iocoder.yudao.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO;
@@ -265,4 +266,16 @@ public interface TradeOrderConvert {
     @Named("convertList04")
     List<TradeOrderRespDTO> convertList04(List<TradeOrderDO> list);
 
+    @Mappings({
+            @Mapping(target = "activityId", source = "order.combinationActivityId"),
+            @Mapping(target = "spuId", source = "item.spuId"),
+            @Mapping(target = "skuId", source = "item.skuId"),
+            @Mapping(target = "count", source = "item.count"),
+            @Mapping(target = "orderId", source = "order.id"),
+            @Mapping(target = "userId", source = "order.userId"),
+            @Mapping(target = "headId", source = "order.combinationHeadId"),
+            @Mapping(target = "combinationPrice", source = "item.payPrice"),
+    })
+    CombinationRecordCreateReqDTO convert(TradeOrderDO order, TradeOrderItemDO item);
+
 }

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

@@ -7,7 +7,10 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO;
 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
-import cn.iocoder.yudao.module.trade.enums.order.*;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
@@ -308,4 +311,17 @@ public class TradeOrderDO extends BaseDO {
      */
     private Long bargainRecordId;
 
+    /**
+     * 拼团活动编号
+     *
+     * 关联 CombinationActivityDO 的 id 字段
+     */
+    private Long combinationActivityId;
+    /**
+     * 团长编号
+     *
+     * 关联 CombinationRecordDO 的 id 字段
+     */
+    private Long combinationHeadId;
+
 }

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

@@ -738,16 +738,19 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     }
 
     @Override
+    @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_UP_ADDRESS)
     public void updateOrderAddress(TradeOrderUpdateAddressReqVO reqVO) {
         // 校验交易订单
-        validateOrderExists(reqVO.getId());
+        TradeOrderDO order = validateOrderExists(reqVO.getId());
         // TODO @puhui999:是否需要校验订单是否发货
         // TODO 发货后是否支持修改收货地址
 
         // 更新
         tradeOrderMapper.updateById(TradeOrderConvert.INSTANCE.convert(reqVO));
 
-        // TODO @puhui999:操作日志
+        // 记录订单日志
+        TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus(),
+                MapUtil.<String, Object>builder().put("orderId", order.getId()).build());
     }
 
     @Override

+ 45 - 33
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationHandler.java

@@ -1,9 +1,16 @@
 package cn.iocoder.yudao.module.trade.service.order.handler;
 
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
+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;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
+import java.util.List;
 
 /**
  * 拼团订单 handler 接口实现类
@@ -16,38 +23,43 @@ public class TradeCombinationHandler implements TradeOrderHandler {
     @Resource
     private CombinationRecordApi combinationRecordApi;
 
-//    @Override
-//    public void beforeOrderCreate(TradeBeforeOrderCreateReqBO reqBO) {
-//        // 如果不是拼团订单则结束
-//        if (ObjectUtil.notEqual(TradeOrderTypeEnum.COMBINATION.getType(), reqBO.getOrderType())) {
-//            return;
-//        }
-//
-//        // 获取商品信息
-//        TradeBeforeOrderCreateReqBO.Item item = reqBO.getItems().get(0);
-//        // 校验是否满足拼团活动相关限制
-//        combinationRecordApi.validateCombinationRecord(reqBO.getCombinationActivityId(), reqBO.getUserId(), item.getSkuId(), item.getCount());
-//    }
-
-//    @Override
-//    public void afterOrderCreate(TradeAfterOrderCreateReqBO reqBO) {
-//        if (reqBO.getCombinationActivityId() == null) {
-//            return;
-//        }
-//
-//        // 创建砍价记录
-//        combinationRecordApi.createCombinationRecord(TradeOrderConvert.INSTANCE.convert(reqBO));
-//    }
-
-//    @Override
-//    public void afterPayOrder(TradeAfterPayOrderReqBO reqBO) {
-//        // 如果不是拼团订单则结束
-//        if (ObjectUtil.notEqual(TradeOrderTypeEnum.COMBINATION.getType(), reqBO.getOrderType())) {
-//            return;
-//        }
-//
-//        // 更新拼团状态 TODO puhui999:订单支付失败或订单支付过期删除这条拼团记录
-//        combinationRecordApi.updateRecordStatusToInProgress(reqBO.getUserId(), reqBO.getOrderId(), reqBO.getPayTime());
-//    }
+    @Override
+    public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        // 如果不是拼团订单则结束
+        if (ObjectUtil.notEqual(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
+            return;
+        }
+        Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品");
+
+        // 获取商品信息
+        TradeOrderItemDO item = orderItems.get(0);
+        // 校验是否满足拼团活动相关限制
+        combinationRecordApi.validateCombinationRecord(order.getCombinationActivityId(), order.getUserId(), item.getSkuId(), item.getCount());
+    }
+
+    @Override
+    public void afterOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        // 如果不是拼团订单则结束
+        if (ObjectUtil.notEqual(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
+            return;
+        }
+        Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品");
+
+        // 获取商品信息
+        TradeOrderItemDO item = orderItems.get(0);
+        // 创建砍价记录
+        combinationRecordApi.createCombinationRecord(TradeOrderConvert.INSTANCE.convert(order, item));
+    }
+
+    @Override
+    public void afterPayOrder(TradeOrderDO order) {
+        // 如果不是拼团订单则结束
+        if (ObjectUtil.notEqual(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
+            return;
+        }
+
+        // 更新拼团状态 TODO puhui999:订单支付失败或订单支付过期删除这条拼团记录
+        combinationRecordApi.updateRecordStatusToInProgress(order.getUserId(), order.getId(), order.getPayTime());
+    }
 
 }

+ 53 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCombinationActivityPriceCalculator.java

@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.trade.service.price.calculator;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
+import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
+import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
+import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 拼团活动的 {@link TradePriceCalculator} 实现类
+ *
+ * @author HUIHUI
+ */
+@Component
+@Order(TradePriceCalculator.ORDER_COMBINATION_ACTIVITY)
+public class TradeCombinationActivityPriceCalculator implements TradePriceCalculator {
+
+    @Resource
+    private CombinationRecordApi combinationRecordApi;
+
+    @Override
+    public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
+        // 1. 判断订单类型和是否具有拼团活动编号
+        if (param.getCombinationActivityId() == null) {
+            return;
+        }
+
+        Assert.isTrue(param.getItems().size() == 1, "拼团时,只允许选择一个商品");
+        // 2. 校验是否可以参与拼团
+        TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0);
+        CombinationValidateJoinRespDTO combinationActivity = combinationRecordApi.validateJoinCombination(
+                param.getCombinationActivityId(), param.getUserId(),
+                orderItem.getSkuId(), orderItem.getCount());
+
+        // 3.1 记录优惠明细
+        Integer discountPrice = orderItem.getPayPrice() - combinationActivity.getCombinationPrice() * orderItem.getCount();
+        TradePriceCalculatorHelper.addPromotion(result, orderItem,
+                param.getCombinationActivityId(), combinationActivity.getName(), PromotionTypeEnum.COMBINATION_ACTIVITY.getType(),
+                StrUtil.format("拼团活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)),
+                discountPrice);
+        // 3.2 更新 SKU 优惠金额
+        orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice);
+        TradePriceCalculatorHelper.recountPayPrice(orderItem);
+        TradePriceCalculatorHelper.recountAllPrice(result);
+    }
+
+}

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculator.java

@@ -17,6 +17,7 @@ public interface TradePriceCalculator {
 
     int ORDER_SECKILL_ACTIVITY = 8;
     int ORDER_BARGAIN_ACTIVITY = 8;
+    int ORDER_COMBINATION_ACTIVITY = 8;
 
     int ORDER_DISCOUNT_ACTIVITY = 10;
     int ORDER_REWARD_ACTIVITY = 20;