Browse Source

Merge remote-tracking branch 'origin/feature/mall_product' into feature/mall_product

# Conflicts:
#	yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
owen 1 year ago
parent
commit
74a5e40d8d
59 changed files with 1164 additions and 495 deletions
  1. 2 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  2. 1 1
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  3. 9 23
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
  4. 0 2
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/dto/CombinationRecordCreateReqDTO.java
  5. 1 1
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java
  6. 8 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/combination/CombinationRecordStatusEnum.java
  7. 15 19
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
  8. 21 8
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationActivityController.java
  9. 64 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationRecordController.java
  10. 4 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityBaseVO.java
  11. 53 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityPageItemRespVO.java
  12. 1 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityPageReqVO.java
  13. 0 20
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java
  14. 82 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordBaseVO.java
  15. 19 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordPageItemRespVO.java
  16. 30 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordReqPageVO.java
  17. 19 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordSummaryVO.java
  18. 12 10
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java
  19. 42 41
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.java
  20. 4 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/vo/AppActivityRespVO.java
  21. 22 17
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationActivityController.java
  22. 50 35
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java
  23. 5 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/vo/record/AppCombinationRecordSummaryRespVO.java
  24. 2 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/seckill/AppSeckillActivityController.java
  25. 74 20
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
  26. 1 13
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/combination/CombinationActivityDO.java
  27. 11 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/combination/CombinationRecordDO.java
  28. 22 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java
  29. 22 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationActivityMapper.java
  30. 48 29
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java
  31. 21 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
  32. 6 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityService.java
  33. 3 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityServiceImpl.java
  34. 16 7
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityService.java
  35. 9 6
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
  36. 69 28
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java
  37. 192 105
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  38. 5 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java
  39. 2 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
  40. 0 18
      yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImplTest.java
  41. 14 5
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApi.java
  42. 1 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  43. 1 1
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java
  44. 17 0
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderTypeEnum.java
  45. 13 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
  46. 0 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java
  47. 6 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
  48. 8 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderDO.java
  49. 8 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/redis/RedisKeyConstants.java
  50. 30 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java
  51. 19 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateService.java
  52. 37 15
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  53. 11 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBargainHandler.java
  54. 10 14
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationHandler.java
  55. 8 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeOrderHandler.java
  56. 10 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeSeckillHandler.java
  57. 0 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/bo/TradePriceCalculateReqBO.java
  58. 3 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeCombinationActivityPriceCalculator.java
  59. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java

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

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

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -56,7 +56,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
     }
 
     default Long selectCount() {
-        return selectCount(new QueryWrapper<T>());
+        return selectCount(new QueryWrapper<>());
     }
 
     default Long selectCount(String field, Object value) {

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

@@ -4,7 +4,6 @@ import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCr
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
 
 import javax.validation.Valid;
-import java.time.LocalDateTime;
 
 // TODO @芋艿:后面也再撸撸这几个接口
 
@@ -18,19 +17,21 @@ public interface CombinationRecordApi {
     /**
      * 校验是否满足拼团条件
      *
-     * @param activityId 活动编号
      * @param userId     用户编号
+     * @param activityId 活动编号
+     * @param headId     团长编号
      * @param skuId      sku 编号
      * @param count      数量
      */
-    void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count);
+    void validateCombinationRecord(Long userId, Long activityId, Long headId, Long skuId, Integer count);
 
     /**
      * 创建开团记录
      *
      * @param reqDTO 请求 DTO
+     * @return 开团记录编号
      */
-    void createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
+    Long createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
 
     /**
      * 查询拼团记录是否成功
@@ -41,14 +42,6 @@ public interface CombinationRecordApi {
      */
     boolean isCombinationRecordSuccess(Long userId, Long orderId);
 
-    /**
-     * 更新拼团状态为【成功】
-     *
-     * @param userId  用户编号
-     * @param orderId 订单编号
-     */
-    void updateRecordStatusToSuccess(Long userId, Long orderId);
-
     /**
      * 更新拼团状态为【失败】
      *
@@ -57,26 +50,19 @@ public interface CombinationRecordApi {
      */
     void updateRecordStatusToFailed(Long userId, Long orderId);
 
-    /**
-     * 更新拼团状态为 进行中
-     *
-     * @param userId    用户编号
-     * @param orderId   订单编号
-     * @param startTime 开始时间
-     */
-    void updateRecordStatusToInProgress(Long userId, Long orderId, LocalDateTime startTime);
-
     /**
      * 【下单前】校验是否满足拼团活动条件
      *
      * 如果校验失败,则抛出业务异常
      *
-     * @param activityId 活动编号
      * @param userId     用户编号
+     * @param activityId 活动编号
+     * @param headId     团长编号
      * @param skuId      sku 编号
      * @param count      数量
      * @return 拼团信息
      */
-    CombinationValidateJoinRespDTO validateJoinCombination(Long activityId, Long userId, Long skuId, Integer count);
+    CombinationValidateJoinRespDTO validateJoinCombination(Long userId, Long activityId, Long headId,
+                                                           Long skuId, Integer count);
 
 }

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

@@ -4,7 +4,6 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
-// TODO @芋艿:这块要在看看
 /**
  * 拼团记录的创建 Request DTO
  *
@@ -46,7 +45,6 @@ public class CombinationRecordCreateReqDTO {
     /**
      * 团长编号
      */
-    @NotNull(message = "团长编号不能为空")
     private Long headId;
     /**
      * 拼团商品单价

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

@@ -80,7 +80,7 @@ public interface ErrorCodeConstants {
     ErrorCode COMBINATION_RECORD_EXISTS = new ErrorCode(1_013_011_001, "拼团失败,已参与过该拼团");
     ErrorCode COMBINATION_RECORD_HEAD_NOT_EXISTS = new ErrorCode(1_013_011_002, "拼团失败,父拼团不存在");
     ErrorCode COMBINATION_RECORD_USER_FULL = new ErrorCode(1_013_011_003, "拼团失败,拼团人数已满");
-    ErrorCode COMBINATION_RECORD_FAILED_HAVE_JOINED = new ErrorCode(1_013_011_004, "拼团失败,已参与其它拼团");
+    ErrorCode COMBINATION_RECORD_FAILED_HAVE_JOINED = new ErrorCode(1_013_011_004, "拼团失败,原因:存在该活动正在进行的拼团记录");
     ErrorCode COMBINATION_RECORD_FAILED_TIME_NOT_START = new ErrorCode(1_013_011_005, "拼团失败,活动未开始");
     ErrorCode COMBINATION_RECORD_FAILED_TIME_END = new ErrorCode(1_013_011_006, "拼团失败,活动已经结束");
     ErrorCode COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED = new ErrorCode(1_013_011_007, "拼团失败,原因:单次限购超出");

+ 8 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/combination/CombinationRecordStatusEnum.java

@@ -40,4 +40,12 @@ public enum CombinationRecordStatusEnum implements IntArrayValuable {
         return ObjectUtil.equal(status, SUCCESS.getStatus());
     }
 
+    public static boolean isInProgress(Integer status) {
+        return ObjectUtil.equal(status, IN_PROGRESS.getStatus());
+    }
+
+    public static boolean isFailed(Integer status) {
+        return ObjectUtil.equal(status, FAILED.getStatus());
+    }
+
 }

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

@@ -2,12 +2,15 @@ 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.dal.dataobject.combination.CombinationRecordDO;
 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
-import java.time.LocalDateTime;
+
+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 实现类
@@ -21,23 +24,22 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
     private CombinationRecordService recordService;
 
     @Override
-    public void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count) {
-        recordService.validateCombinationRecord(activityId, userId, skuId, count);
+    public void validateCombinationRecord(Long userId, Long activityId, Long headId, Long skuId, Integer count) {
+        recordService.validateCombinationRecord(userId, activityId, headId, skuId, count);
     }
 
     @Override
-    public void createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
-        recordService.createCombinationRecord(reqDTO);
+    public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
+        return recordService.createCombinationRecord(reqDTO);
     }
 
     @Override
     public boolean isCombinationRecordSuccess(Long userId, Long orderId) {
-        return CombinationRecordStatusEnum.isSuccess(recordService.getCombinationRecord(userId, orderId).getStatus());
-    }
-
-    @Override
-    public void updateRecordStatusToSuccess(Long userId, Long orderId) {
-        recordService.updateCombinationRecordStatusByUserIdAndOrderId(CombinationRecordStatusEnum.SUCCESS.getStatus(), userId, orderId);
+        CombinationRecordDO record = recordService.getCombinationRecord(userId, orderId);
+        if (record == null) {
+            throw exception(COMBINATION_RECORD_NOT_EXISTS);
+        }
+        return CombinationRecordStatusEnum.isSuccess(record.getStatus());
     }
 
     @Override
@@ -46,14 +48,8 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
     }
 
     @Override
-    public void updateRecordStatusToInProgress(Long userId, Long orderId, LocalDateTime startTime) {
-        recordService.updateRecordStatusAndStartTimeByUserIdAndOrderId(CombinationRecordStatusEnum.IN_PROGRESS.getStatus(),
-                userId, orderId, startTime);
-    }
-
-    @Override
-    public CombinationValidateJoinRespDTO validateJoinCombination(Long activityId, Long userId, Long skuId, Integer count) {
-        return recordService.validateJoinCombination(activityId, userId, skuId, count);
+    public CombinationValidateJoinRespDTO validateJoinCombination(Long userId, Long activityId, Long headId, Long skuId, Integer count) {
+        return recordService.validateJoinCombination(userId, activityId, headId, skuId, count);
     }
 
 }

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

@@ -5,14 +5,14 @@ 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;
 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
-import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityCreateReqVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityRespVO;
-import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.*;
 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.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
+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.tags.Tag;
@@ -23,6 +23,8 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.validation.Valid;
 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;
@@ -36,6 +38,8 @@ public class CombinationActivityController {
 
     @Resource
     private CombinationActivityService combinationActivityService;
+    @Resource
+    private CombinationRecordService combinationRecordService;
 
     @Resource
     private ProductSpuApi productSpuApi;
@@ -70,14 +74,14 @@ public class CombinationActivityController {
     @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')")
     public CommonResult<CombinationActivityRespVO> getCombinationActivity(@RequestParam("id") Long id) {
         CombinationActivityDO activity = combinationActivityService.getCombinationActivity(id);
-        List<CombinationProductDO> products = combinationActivityService.getCombinationProductsByActivityIds(newArrayList(id));
+        List<CombinationProductDO> products = combinationActivityService.getCombinationProductListByActivityIds(newArrayList(id));
         return success(CombinationActivityConvert.INSTANCE.convert(activity, products));
     }
 
     @GetMapping("/page")
     @Operation(summary = "获得拼团活动分页")
     @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')")
-    public CommonResult<PageResult<CombinationActivityRespVO>> getCombinationActivityPage(
+    public CommonResult<PageResult<CombinationActivityPageItemRespVO>> getCombinationActivityPage(
             @Valid CombinationActivityPageReqVO pageVO) {
         // 查询拼团活动
         PageResult<CombinationActivityDO> pageResult = combinationActivityService.getCombinationActivityPage(pageVO);
@@ -85,12 +89,21 @@ public class CombinationActivityController {
             return success(PageResult.empty(pageResult.getTotal()));
         }
 
+        // 统计数据
+        Set<Long> activityIds = convertSet(pageResult.getList(), CombinationActivityDO::getId);
+        Map<Long, Integer> groupCountMap = combinationRecordService.getCombinationRecordCountMapByActivity(
+                activityIds, null, CombinationRecordDO.HEAD_ID_GROUP);
+        Map<Long, Integer> groupSuccessCountMap = combinationRecordService.getCombinationRecordCountMapByActivity(
+                activityIds, CombinationRecordStatusEnum.SUCCESS.getStatus(), CombinationRecordDO.HEAD_ID_GROUP);
+        Map<Long, Integer> recordCountMap = combinationRecordService.getCombinationRecordCountMapByActivity(
+                activityIds, null, null);
         // 拼接数据
-        List<CombinationProductDO> products = combinationActivityService.getCombinationProductsByActivityIds(
+        List<CombinationProductDO> products = combinationActivityService.getCombinationProductListByActivityIds(
                 convertSet(pageResult.getList(), CombinationActivityDO::getId));
         List<ProductSpuRespDTO> spus = productSpuApi.getSpuList(
                 convertSet(pageResult.getList(), CombinationActivityDO::getSpuId));
-        return success(CombinationActivityConvert.INSTANCE.convertPage(pageResult, products, spus));
+        return success(CombinationActivityConvert.INSTANCE.convertPage(pageResult, products,
+                groupCountMap, groupSuccessCountMap, recordCountMap, spus));
     }
 
 }

+ 64 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationRecordController.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.combination;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordPageItemRespVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordSummaryVO;
+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.CombinationRecordDO;
+import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
+import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
+import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "管理后台 - 拼团记录")
+@RestController
+@RequestMapping("/promotion/combination-record")
+@Validated
+public class CombinationRecordController {
+
+    @Resource
+    private CombinationActivityService combinationActivityService;
+    @Resource
+    @Lazy
+    private CombinationRecordService combinationRecordService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获得拼团记录分页")
+    @PreAuthorize("@ss.hasPermission('promotion:combination-record:query')")
+    public CommonResult<PageResult<CombinationRecordPageItemRespVO>> getBargainRecordPage(@Valid CombinationRecordReqPageVO pageVO) {
+        PageResult<CombinationRecordDO> recordPage = combinationRecordService.getCombinationRecordPage(pageVO);
+        List<CombinationActivityDO> activities = combinationActivityService.getCombinationActivityListByIds(
+                convertSet(recordPage.getList(), CombinationRecordDO::getActivityId));
+        return success(CombinationActivityConvert.INSTANCE.convert(recordPage, activities));
+    }
+
+    @GetMapping("/get-summary")
+    @Operation(summary = "获得拼团记录的概要信息", description = "用于拼团记录页面展示")
+    @PreAuthorize("@ss.hasPermission('promotion:combination-record:query')")
+    public CommonResult<CombinationRecordSummaryVO> getCombinationRecordSummary() {
+        CombinationRecordSummaryVO summaryVO = new CombinationRecordSummaryVO();
+        summaryVO.setUserCount(combinationRecordService.getCombinationRecordCount(null, null)); // 获取所有拼团记录
+        summaryVO.setSuccessCount(combinationRecordService.getCombinationRecordCount( // 获取成团记录
+                CombinationRecordStatusEnum.SUCCESS.getStatus(), null));
+        summaryVO.setVirtualGroupCount(combinationRecordService.getCombinationRecordCount(null, Boolean.TRUE));// 获取虚拟成团记录
+        return success(summaryVO);
+    }
+
+}

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

@@ -48,6 +48,10 @@ public class CombinationActivityBaseVO {
     @NotNull(message = "开团人数不能为空")
     private Integer userSize;
 
+    @Schema(description = "虚拟成团", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+    @NotNull(message = "虚拟成团不能为空")
+    private Boolean virtualGroup;
+
     @Schema(description = "限制时长(小时)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     @NotNull(message = "限制时长不能为空")
     private Integer limitDuration;

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

@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity;
+
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.product.CombinationProductRespVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 拼团活动的分页项 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CombinationActivityPageItemRespVO extends CombinationActivityBaseVO {
+
+    @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22901")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+    @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    private Integer status;
+
+    @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<CombinationProductRespVO> products;
+
+    // ========== 商品字段 ==========
+
+    @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 name 读取
+            example = "618大促")
+    private String spuName;
+    @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 picUrl 读取
+            example = "https://www.iocoder.cn/xx.png")
+    private String picUrl;
+    @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 marketPrice 读取
+            example = "50")
+    private Integer marketPrice;
+
+    // ========== 统计字段 ==========
+
+    @Schema(description = "开团组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "33")
+    private Integer groupCount;
+
+    @Schema(description = "成团组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+    private Integer groupSuccessCount;
+
+    @Schema(description = "购买次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+    private Integer recordCount;
+
+}

+ 1 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/activity/CombinationActivityPageReqVO.java

@@ -15,8 +15,7 @@ public class CombinationActivityPageReqVO extends PageParam {
     @Schema(description = "拼团名称", example = "赵六")
     private String name;
 
-    @Schema(description = "活动状态:0开启 1关闭", example = "0")
+    @Schema(description = "活动状态", example = "0")
     private Integer status;
 
-
 }

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

@@ -6,8 +6,6 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -20,30 +18,12 @@ public class CombinationActivityRespVO extends CombinationActivityBaseVO {
     @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22901")
     private Long id;
 
-    @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618 大促")
-    private String spuName;
-
-    @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png")
-    private String picUrl;
-
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
     @Schema(description = "开团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
     private Integer userSize;
 
-    @Schema(description = "开团组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "33")
-    private Integer totalCount;
-
-    @Schema(description = "成团组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
-    private Integer successCount;
-
-    @Schema(description = "虚拟成团", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
-    private Integer virtualGroup;
-
-    @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
-    private Integer status;
-
     @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED)
     private List<CombinationProductRespVO> products;
 

+ 82 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordBaseVO.java

@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 拼团记录 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CombinationRecordBaseVO {
+
+    @Schema(description = "拼团记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "拼团活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long activityId;
+
+    @Schema(description = "团长编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long headId;
+
+    // ========== 用户相关 ==========
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9430")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "用户昵称", example = "老芋艿")
+    private String nickname;
+
+    @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xxx.jpg")
+    private String avatar;
+
+    // ========== 商品相关 ==========
+
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23622")
+    @NotNull(message = "商品 SPU 编号不能为空")
+    private Long spuId;
+
+    @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29950")
+    @NotNull(message = "商品 SKU 编号不能为空")
+    private Long skuId;
+
+    @Schema(description = "商品名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是大黄豆")
+    private String spuName;
+
+    @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    private String picUrl;
+
+    @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime expireTime;
+
+    @Schema(description = "可参团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private Integer userSize;
+
+    @Schema(description = "已参团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
+    private Integer userCount;
+
+    @Schema(description = "拼团状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status;
+
+    @Schema(description = "是否虚拟成团", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+    private Boolean virtualGroup;
+
+    @Schema(description = "开始时间 (订单付款后开始的时间)", requiredMode = Schema.RequiredMode.REQUIRED)
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime startTime;
+
+    @Schema(description = "结束时间(成团时间/失败时间)", requiredMode = Schema.RequiredMode.REQUIRED)
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime endTime;
+
+}

+ 19 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordPageItemRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod;
+
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityRespVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 拼团记录的分页项 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CombinationRecordPageItemRespVO extends CombinationRecordBaseVO {
+
+    // ========== 活动相关 ==========
+
+    private CombinationActivityRespVO activity;
+
+}

+ 30 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordReqPageVO.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.promotion.enums.bargain.BargainRecordStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 拼团记录分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CombinationRecordReqPageVO extends PageParam {
+
+    @Schema(description = "活动状态", example = "1")
+    @InEnum(BargainRecordStatusEnum.class)
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 19 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordSummaryVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 拼团记录信息统计 Response VO")
+@Data
+public class CombinationRecordSummaryVO {
+
+    @Schema(description = "所有拼团记录", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long userCount;
+
+    @Schema(description = "成团记录", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long successCount;
+
+    @Schema(description = "虚拟成团记录", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long virtualGroupCount;
+
+}

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

@@ -18,16 +18,6 @@ public class SeckillActivityRespVO extends SeckillActivityBaseVO {
     @Schema(description = "秒杀活动 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Long id;
 
-    @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 name 读取
-            example = "618大促")
-    private String spuName;
-    @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 picUrl 读取
-            example = "https://www.iocoder.cn/xx.png")
-    private String picUrl;
-    @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 marketPrice 读取
-            example = "50")
-    private Integer marketPrice;
-
     @Schema(description = "秒杀商品", requiredMode = Schema.RequiredMode.REQUIRED)
     private List<SeckillProductRespVO> products;
 
@@ -52,4 +42,16 @@ public class SeckillActivityRespVO extends SeckillActivityBaseVO {
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
+    // ========== 商品字段 ==========
+
+    @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 name 读取
+            example = "618大促")
+    private String spuName;
+    @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 picUrl 读取
+            example = "https://www.iocoder.cn/xx.png")
+    private String picUrl;
+    @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // 从 SPU 的 marketPrice 读取
+            example = "50")
+    private Integer marketPrice;
+
 }

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

@@ -2,6 +2,7 @@ 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.enums.CommonStatusEnum;
 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;
@@ -21,12 +22,10 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
 
 @Tag(name = "用户 APP - 营销活动") // 用于提供跨多个活动的 HTTP 接口
 @RestController
@@ -42,58 +41,60 @@ public class AppActivityController {
     private BargainActivityService bargainActivityService;
 
     @GetMapping("/list-by-spu-id")
-    @Operation(summary = "获得单个商品,近期参与的每个活动") // 每种活动,只返回一个
+    @Operation(summary = "获得单个商品,近期参与的每个活动")
     @Parameter(name = "spuId", description = "商品编号", required = true)
     public CommonResult<List<AppActivityRespVO>> getActivityListBySpuId(@RequestParam("spuId") Long spuId) {
-        return success(getAppActivityRespVOList(spuId));
+        // 每种活动,只返回一个
+        return success(getAppActivityRespVOList(Collections.singletonList(spuId)));
     }
 
     @GetMapping("/list-by-spu-ids")
-    @Operation(summary = "获得多个商品,近期参与的每个活动") // 每种活动,只返回一个;key 为 SPU 编号
+    @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());
         }
-
-        Map<Long, List<AppActivityRespVO>> map = new HashMap<>(spuIds.size());
-        spuIds.forEach(spuId -> {
-            map.put(spuId, getAppActivityRespVOList(spuId));
-        });
-        return success(map);
+        // 每种活动,只返回一个;key 为 SPU 编号
+        return success(convertMultiMap(getAppActivityRespVOList(spuIds), AppActivityRespVO::getSpuId));
     }
 
-    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()));
+    private List<AppActivityRespVO> getAppActivityRespVOList(Collection<Long> spuIds) {
+        if (CollUtil.isEmpty(spuIds)) {
+            return new ArrayList<>();
+        }
+        List<AppActivityRespVO> activityList = new ArrayList<>();
+        // 拼团活动
+        List<CombinationActivityDO> combinationActivities = combinationActivityService.getCombinationActivityBySpuIdsAndStatus(
+                spuIds, CommonStatusEnum.ENABLE.getStatus());
+        if (CollUtil.isNotEmpty(combinationActivities)) {
+            combinationActivities.forEach(item -> {
+                activityList.add(new AppActivityRespVO().setId(item.getId())
+                        .setType(PromotionTypeEnum.COMBINATION_ACTIVITY.getType()).setName(item.getName())
+                        .setSpuId(item.getSpuId()).setStartTime(item.getStartTime()).setEndTime(item.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()));
+        // 秒杀活动
+        List<SeckillActivityDO> seckillActivities = seckillActivityService.getSeckillActivityBySpuIdsAndStatus(
+                spuIds, CommonStatusEnum.ENABLE.getStatus());
+        if (CollUtil.isNotEmpty(seckillActivities)) {
+            seckillActivities.forEach(item -> {
+                activityList.add(new AppActivityRespVO().setId(item.getId())
+                        .setType(PromotionTypeEnum.SECKILL_ACTIVITY.getType()).setName(item.getName())
+                        .setSpuId(item.getSpuId()).setStartTime(item.getStartTime()).setEndTime(item.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()));
+        // 砍价活动
+        List<BargainActivityDO> bargainActivities = bargainActivityService.getBargainActivityBySpuIdsAndStatus(
+                spuIds, CommonStatusEnum.ENABLE.getStatus());
+        if (CollUtil.isNotEmpty(bargainActivities)) {
+            bargainActivities.forEach(item -> {
+                activityList.add(new AppActivityRespVO().setId(item.getId())
+                        .setType(PromotionTypeEnum.BARGAIN_ACTIVITY.getType()).setName(item.getName())
+                        .setSpuId(item.getSpuId()).setStartTime(item.getStartTime()).setEndTime(item.getEndTime()));
+            });
         }
-        return respList;
+        return activityList;
     }
 
 }

+ 4 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/vo/AppActivityRespVO.java

@@ -13,12 +13,14 @@ public class AppActivityRespVO {
     private Long id;
 
     @Schema(description = "活动类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    // 对应 PromotionTypeEnum 枚举
-    private Integer type;
+    private Integer type; // 对应 PromotionTypeEnum 枚举
 
     @Schema(description = "活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618 大促")
     private String name;
 
+    @Schema(description = "spu 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "618")
+    private Long spuId;
+
     @Schema(description = "活动开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime startTime;
 

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

@@ -30,7 +30,6 @@ 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;
@@ -56,6 +55,7 @@ public class AppCombinationActivityController {
 
     @Resource
     private CombinationActivityService activityService;
+
     @Resource
     private ProductSpuApi spuApi;
 
@@ -68,40 +68,45 @@ public class AppCombinationActivityController {
     }
 
     private List<AppCombinationActivityRespVO> getCombinationActivityList0(Integer count) {
-        List<CombinationActivityDO> list = activityService.getCombinationActivityListByCount(defaultIfNull(count, 6));
-        if (CollUtil.isEmpty(list)) {
+        List<CombinationActivityDO> activityList = activityService.getCombinationActivityListByCount(count);
+        if (CollUtil.isEmpty(activityList)) {
             return Collections.emptyList();
         }
         // 拼接返回
-        List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(list, CombinationActivityDO::getSpuId));
-        return CombinationActivityConvert.INSTANCE.convertAppList(list, spuList);
+        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) {
-        PageResult<CombinationActivityDO> result = activityService.getCombinationActivityPage(pageParam);
-        if (CollUtil.isEmpty(result.getList())) {
-            return success(PageResult.empty(result.getTotal()));
+        PageResult<CombinationActivityDO> pageResult = activityService.getCombinationActivityPage(pageParam);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty(pageResult.getTotal()));
         }
         // 拼接返回
-        List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(result.getList(), CombinationActivityDO::getSpuId));
-        return success(CombinationActivityConvert.INSTANCE.convertAppPage(result, spuList));
+        List<CombinationProductDO> productList = activityService.getCombinationProductListByActivityIds(
+                convertList(pageResult.getList(), CombinationActivityDO::getId));
+        List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(pageResult.getList(), CombinationActivityDO::getSpuId));
+        return success(CombinationActivityConvert.INSTANCE.convertAppPage(pageResult, productList, spuList));
     }
 
     @GetMapping("/get-detail")
     @Operation(summary = "获得拼团活动明细")
     @Parameter(name = "id", description = "活动编号", required = true, example = "1024")
     public CommonResult<AppCombinationActivityDetailRespVO> getCombinationActivityDetail(@RequestParam("id") Long id) {
-        // 1获取活动
-        CombinationActivityDO combinationActivity = activityService.getCombinationActivity(id);
-        if (combinationActivity == null
-                || ObjectUtil.equal(combinationActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
+        // 1. 获取活动
+        CombinationActivityDO activity = activityService.getCombinationActivity(id);
+        if (activity == null
+                || ObjectUtil.equal(activity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
             return success(null);
         }
-        // 2、获取活动商品
-        List<CombinationProductDO> products = activityService.getCombinationProductsByActivityId(combinationActivity.getId());
-        return success(CombinationActivityConvert.INSTANCE.convert3(combinationActivity, products));
+
+        // 2. 获取活动商品
+        List<CombinationProductDO> products = activityService.getCombinationProductsByActivityId(activity.getId());
+        return success(CombinationActivityConvert.INSTANCE.convert3(activity, products));
     }
 
 }

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

@@ -1,18 +1,19 @@
 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.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.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
+import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
 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.context.annotation.Lazy;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -23,6 +24,7 @@ import javax.annotation.Resource;
 import javax.validation.constraints.Max;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -36,24 +38,27 @@ public class AppCombinationRecordController {
 
     @Resource
     private CombinationRecordService combinationRecordService;
+    @Resource
+    @Lazy
+    private TradeOrderApi tradeOrderApi;
 
     @GetMapping("/get-summary")
     @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
-    // TODO 芋艿:增加 @Cache 缓存,1 分钟过期
     public CommonResult<AppCombinationRecordSummaryRespVO> getCombinationRecordSummary() {
-        // 获取所有拼团记录
-        Long count = combinationRecordService.getRecordsCount();
         AppCombinationRecordSummaryRespVO summary = new AppCombinationRecordSummaryRespVO();
-        if (count == null || count == 0L) {
+        // 1. 获得拼团记录数量
+        Long count = combinationRecordService.getCombinationRecordCount(null, null);
+        if (count == 0) {
             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));
+
+        // 2. 获得拼团记录头像
+        List<CombinationRecordDO> records = combinationRecordService.getLatestCombinationRecordList(
+                AppCombinationRecordSummaryRespVO.AVATAR_COUNT);
+        summary.setAvatars(convertList(records, CombinationRecordDO::getAvatar));
         return success(summary);
     }
 
@@ -61,7 +66,7 @@ public class AppCombinationRecordController {
     @Operation(summary = "获得最近 n 条拼团记录(团长发起的)")
     @Parameters({
             @Parameter(name = "activityId", description = "拼团活动编号"),
-            @Parameter(name = "status", description = "状态"),
+            @Parameter(name = "status", description = "拼团状态"), // 对应 CombinationRecordStatusEnum 枚举
             @Parameter(name = "count", description = "数量")
     })
     public CommonResult<List<AppCombinationRecordRespVO>> getHeadCombinationRecordList(
@@ -69,44 +74,54 @@ public class AppCombinationRecordController {
             @RequestParam("status") Integer status,
             @RequestParam(value = "count", defaultValue = "20") @Max(20) Integer count) {
         return success(CombinationActivityConvert.INSTANCE.convertList3(
-                combinationRecordService.getRecordListWithHead(activityId, status, count)));
+                combinationRecordService.getHeadCombinationRecordList(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);
+        // 1. 查找这条拼团记录
+        CombinationRecordDO record = combinationRecordService.getCombinationRecordById(id);
         if (record == null) {
             return success(null);
         }
 
-        AppCombinationRecordDetailRespVO detail = new AppCombinationRecordDetailRespVO();
-        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);
-            }
+        // 2. 查找该拼团的参团记录
+        CombinationRecordDO headRecord;
+        List<CombinationRecordDO> memberRecords;
+        if (Objects.equals(record.getHeadId(), CombinationRecordDO.HEAD_ID_GROUP)) { // 情况一:团长
+            headRecord = record;
+            memberRecords = combinationRecordService.getCombinationRecordListByHeadId(record.getId());
+        } else { // 情况二:团员
+            headRecord = combinationRecordService.getCombinationRecordById(record.getHeadId());
+            memberRecords = combinationRecordService.getCombinationRecordListByHeadId(headRecord.getId());
+        }
 
-            detail.setHeadRecord(CombinationActivityConvert.INSTANCE.convert(headRecord));
-            // 2.3、查找团员拼团记录
-            records = combinationRecordService.getRecordListByHeadId(headRecord.getId());
+        // 3. 拼接数据
+        return success(CombinationActivityConvert.INSTANCE.convert(getLoginUserId(), headRecord, memberRecords));
+    }
 
+    @GetMapping("/cancel")
+    @Operation(summary = "取消拼团")
+    @Parameter(name = "id", description = "拼团记录编号", required = true, example = "1024")
+    public CommonResult<Boolean> cancelCombinationRecord(@RequestParam("id") Long id) {
+        Long userId = getLoginUserId();
+        // 1、查找这条拼团记录
+        CombinationRecordDO record = combinationRecordService.getCombinationRecordByIdAndUser(userId, id);
+        if (record == null) {
+            return success(Boolean.FALSE);
+        }
+        // 1.1、需要先校验拼团记录未完成;
+        if (!CombinationRecordStatusEnum.isInProgress(record.getStatus())) {
+            return success(Boolean.FALSE);
         }
-        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);
+        // 2. 取消已支付的订单
+        tradeOrderApi.cancelPaidOrder(userId, record.getOrderId());
+        // 3. 取消拼团记录
+        combinationRecordService.cancelCombinationRecord(userId, record.getId(), record.getHeadId());
+        return success(Boolean.TRUE);
     }
 
 }

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

@@ -9,6 +9,11 @@ import java.util.List;
 @Data
 public class AppCombinationRecordSummaryRespVO {
 
+    /**
+     * 加载 {@link #avatars} 的数量
+     */
+    public static final Integer AVATAR_COUNT = 7;
+
     @Schema(description = "拼团用户数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long userCount;
 

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

@@ -86,7 +86,8 @@ public class AppSeckillActivityController {
 
         // 2.1 查询满足当前阶段的活动
         List<SeckillActivityDO> activityList = activityService.getSeckillActivityListByConfigIdAndStatus(config.getId(), CommonStatusEnum.ENABLE.getStatus());
-        List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityId(convertList(activityList, SeckillActivityDO::getId));
+        List<SeckillProductDO> productList = activityService.getSeckillProductListByActivityId(
+                convertList(activityList, SeckillActivityDO::getId));
         // 2.2 获取 spu 信息
         List<ProductSpuRespDTO> spuList = spuApi.getSpuList(convertList(activityList, SeckillActivityDO::getSpuId));
         return SeckillActivityConvert.INSTANCE.convert(config, activityList, productList, spuList);

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.promotion.convert.combination;
 
+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;
@@ -8,25 +9,30 @@ 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.controller.admin.combination.vo.activity.CombinationActivityCreateReqVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageItemRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityRespVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.product.CombinationProductBaseVO;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.product.CombinationProductRespVO;
+import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordPageItemRespVO;
 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.AppCombinationRecordDetailRespVO;
 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;
+import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 
 /**
@@ -53,20 +59,29 @@ public interface CombinationActivityConvert {
 
     List<CombinationActivityRespVO> convertList(List<CombinationActivityDO> list);
 
-    PageResult<CombinationActivityRespVO> convertPage(PageResult<CombinationActivityDO> page);
 
-    default PageResult<CombinationActivityRespVO> convertPage(PageResult<CombinationActivityDO> page,
-                                                              List<CombinationProductDO> productList,
-                                                              List<ProductSpuRespDTO> spuList) {
+    default PageResult<CombinationActivityPageItemRespVO> convertPage(PageResult<CombinationActivityDO> page,
+                                                                      List<CombinationProductDO> productList,
+                                                                      Map<Long, Integer> groupCountMap,
+                                                                      Map<Long, Integer> groupSuccessCountMap,
+                                                                      Map<Long, Integer> recordCountMap,
+                                                                      List<ProductSpuRespDTO> spuList) {
+        PageResult<CombinationActivityPageItemRespVO> pageResult = convertPage(page);
         Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
-        PageResult<CombinationActivityRespVO> pageResult = convertPage(page);
         pageResult.getList().forEach(item -> {
-            MapUtils.findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()));
+            MapUtils.findAndThen(spuMap, item.getSpuId(), spu -> item.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl())
+                    .setMarketPrice(spu.getMarketPrice()));
             item.setProducts(convertList2(productList));
+            // 设置统计字段
+            item.setGroupCount(groupCountMap.getOrDefault(item.getId(), 0))
+                    .setGroupSuccessCount(groupSuccessCountMap.getOrDefault(item.getId(), 0))
+                    .setRecordCount(recordCountMap.getOrDefault(item.getId(), 0));
         });
         return pageResult;
     }
 
+    PageResult<CombinationActivityPageItemRespVO> convertPage(PageResult<CombinationActivityDO> page);
+
     List<CombinationProductRespVO> convertList2(List<CombinationProductDO> productDOs);
 
     @Mappings({
@@ -97,23 +112,31 @@ public interface CombinationActivityConvert {
     default CombinationRecordDO convert(CombinationRecordCreateReqDTO reqDTO,
                                         CombinationActivityDO activity, MemberUserRespDTO user,
                                         ProductSpuRespDTO spu, ProductSkuRespDTO sku) {
-        return convert(reqDTO)
-                .setCount(reqDTO.getCount())
-                .setVirtualGroup(false)
+        return convert(reqDTO).setVirtualGroup(false)
+                .setStatus(CombinationRecordStatusEnum.IN_PROGRESS.getStatus()) // 创建后默认状态为进行中
+                .setStartTime(LocalDateTime.now()) // TODO @puhui999:想了下,这个 startTime 应该是团长的;
+                // TODO @puhui999:有团长的情况下,expireTime 应该是团长的;
                 .setExpireTime(activity.getStartTime().plusHours(activity.getLimitDuration()))
-                .setUserSize(activity.getUserSize())
-                .setNickname(user.getNickname())
-                .setAvatar(user.getAvatar())
-                .setSpuName(spu.getName())
-                .setPicUrl(sku.getPicUrl());
+                .setUserSize(activity.getUserSize()).setUserCount(1) // 默认就是 1 插入后会接着更新一次所有的拼团记录
+                // 用户信息
+                .setNickname(user.getNickname()).setAvatar(user.getAvatar())
+                // 商品信息
+                .setSpuName(spu.getName()).setPicUrl(sku.getPicUrl());
+
     }
 
     List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list);
 
-    default List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list, List<ProductSpuRespDTO> spuList) {
+    default List<AppCombinationActivityRespVO> convertAppList(List<CombinationActivityDO> list,
+                                                              List<CombinationProductDO> productList,
+                                                              List<ProductSpuRespDTO> spuList) {
         List<AppCombinationActivityRespVO> activityList = convertAppList(list);
         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()));
             return item;
         });
@@ -121,13 +144,17 @@ public interface CombinationActivityConvert {
 
     PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result);
 
-    default PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result, List<ProductSpuRespDTO> spuList) {
+    default PageResult<AppCombinationActivityRespVO> convertAppPage(PageResult<CombinationActivityDO> result,
+                                                                    List<CombinationProductDO> productList,
+                                                                    List<ProductSpuRespDTO> spuList) {
         PageResult<AppCombinationActivityRespVO> appPage = convertAppPage(result);
         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 -> {
-            findAndThen(spuMap, item.getSpuId(), spu -> {
-                item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice());
-            });
+            // 设置 product 信息
+            item.setCombinationPrice(getMinValue(productMap.get(item.getId()), CombinationProductDO::getCombinationPrice));
+            // 设置 SPU 信息
+            findAndThen(spuMap, item.getSpuId(), spu -> item.setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
             return item;
         });
         appPage.setList(list);
@@ -146,4 +173,31 @@ public interface CombinationActivityConvert {
 
     AppCombinationRecordRespVO convert(CombinationRecordDO record);
 
+    PageResult<CombinationRecordPageItemRespVO> convert(PageResult<CombinationRecordDO> result);
+
+    default PageResult<CombinationRecordPageItemRespVO> convert(PageResult<CombinationRecordDO> recordPage, List<CombinationActivityDO> activities) {
+        PageResult<CombinationRecordPageItemRespVO> result = convert(recordPage);
+        Map<Long, CombinationActivityDO> activityMap = convertMap(activities, CombinationActivityDO::getId);
+        result.setList(CollectionUtils.convertList(result.getList(), item -> {
+            findAndThen(activityMap, item.getActivityId(), activity -> {
+                item.setActivity(convert(activity));
+            });
+            return item;
+        }));
+        return result;
+    }
+
+
+    default AppCombinationRecordDetailRespVO convert(Long userId, CombinationRecordDO headRecord, List<CombinationRecordDO> memberRecords) {
+        AppCombinationRecordDetailRespVO respVO = new AppCombinationRecordDetailRespVO()
+                .setHeadRecord(convert(headRecord)).setMemberRecords(convertList3(memberRecords));
+        // 处理自己参与拼团的 orderId
+        CombinationRecordDO userRecord = CollectionUtils.findFirst(memberRecords, r -> ObjectUtil.equal(r.getUserId(), userId));
+        if (userRecord == null && ObjectUtil.equal(headRecord.getUserId(), userId)) {
+            userRecord = headRecord;
+        }
+        respVO.setOrderId(userRecord == null ? null : userRecord.getOrderId());
+        return respVO;
+    }
+
 }

+ 1 - 13
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/combination/CombinationActivityDO.java

@@ -59,22 +59,10 @@ public class CombinationActivityDO extends BaseDO {
      * 几人团
      */
     private Integer userSize;
-    /**
-     * 开团组数
-     */
-    private Integer totalCount;
-    /**
-     * 成团组数
-     */
-    private Integer successCount;
-    /**
-     * 参与人数
-     */
-    private Integer orderUserCount;
     /**
      * 虚拟成团
      */
-    private Integer virtualGroup;
+    private Boolean virtualGroup;
     /**
      * 活动状态
      *

+ 11 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/combination/CombinationRecordDO.java

@@ -28,6 +28,11 @@ import java.time.LocalDateTime;
 @AllArgsConstructor
 public class CombinationRecordDO extends BaseDO {
 
+    /**
+     * 团长编号 - 团长
+     */
+    public static final Long HEAD_ID_GROUP = 0L;
+
     /**
      * 编号,主键自增
      */
@@ -71,6 +76,7 @@ public class CombinationRecordDO extends BaseDO {
      * 用户编号
      */
     private Long userId;
+
     /**
      * 用户昵称
      */
@@ -84,6 +90,8 @@ public class CombinationRecordDO extends BaseDO {
      * 团长编号
      *
      * 关联 {@link CombinationRecordDO#getId()}
+     *
+     * 如果是团长,则它的值是 {@link #HEAD_ID_GROUP}
      */
     private Long headId;
     /**
@@ -108,6 +116,9 @@ public class CombinationRecordDO extends BaseDO {
     private Integer userCount;
     /**
      * 是否虚拟成团
+     *
+     * 默认为 false。
+     * 拼团过期都还没有成功,如果 {@link CombinationActivityDO#getVirtualGroup()} 为 true,则执行虚拟成团的逻辑,才会更新该字段为 true
      */
     private Boolean virtualGroup;
 

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

@@ -8,8 +8,10 @@ import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.Ba
 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
 
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -83,11 +85,25 @@ 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);
-    }
+    // TODO @puhui999:一个商品,在统一时间,不会参与多个活动;so 是不是不用 inner join 哈?
+    // PS:如果可以参与多个,其实可以这样写 select * from promotion_bargain_activity group by spu_id ORDER BY create_time DESC;通过 group 来过滤
+    /**
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     *
+     * @param spuIds spu 编号
+     * @param status 状态
+     * @return 砍价活动列表
+     */
+    @Select("SELECT p1.* " +
+            "FROM promotion_bargain_activity p1 " +
+            "INNER JOIN ( " +
+            "  SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
+            "  FROM promotion_bargain_activity " +
+            "  WHERE spu_id IN #{spuIds} " +
+            "  GROUP BY spu_id " +
+            ") p2 " +
+            "ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
+            "ORDER BY p1.create_time DESC;")
+    List<BargainActivityDO> selectListBySpuIds(Collection<Long> spuIds, Integer status);
 
 }

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

@@ -7,7 +7,10 @@ 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 org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -40,11 +43,24 @@ 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);
-    }
+    // TODO @puhui999:类似 BargainActivityMapper
+    /**
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     *
+     * @param spuIds spu 编号
+     * @param status 状态
+     * @return 拼团活动列表
+     */
+    @Select("SELECT p1.* " +
+            "FROM promotion_combination_activity p1 " +
+            "INNER JOIN ( " +
+            "  SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
+            "  FROM promotion_combination_activity " +
+            "  WHERE spu_id IN #{spuIds} " +
+            "  GROUP BY spu_id " +
+            ") p2 " +
+            "ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
+            "ORDER BY p1.create_time DESC;")
+    List<CombinationActivityDO> selectListBySpuIds(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status);
 
 }

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

@@ -1,11 +1,20 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.combination;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 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.recrod.CombinationRecordReqPageVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 拼团记录 Mapper
@@ -20,34 +29,18 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
                 CombinationRecordDO::getOrderId, orderId);
     }
 
-    default List<CombinationRecordDO> selectListByUserId(Long userId) {
-        return selectList(CombinationRecordDO::getUserId, userId);
-    }
-
-    default List<CombinationRecordDO> selectListByUserIdAndStatus(Long userId, Integer status) {
-        return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
-                .eq(CombinationRecordDO::getUserId, userId)
-                .eq(CombinationRecordDO::getStatus, status));
-    }
-
     /**
      * 查询拼团记录
      *
      * @param headId 团长编号
      * @return 拼团记录
      */
-    default CombinationRecordDO selectOneByHeadId(Long headId, Integer status) {
+    default CombinationRecordDO selectByHeadId(Long headId, Integer status) {
         return selectOne(new LambdaQueryWrapperX<CombinationRecordDO>()
                 .eq(CombinationRecordDO::getId, headId)
                 .eq(CombinationRecordDO::getStatus, status));
     }
 
-    default List<CombinationRecordDO> selectListByHeadIdAndStatus(Long headId, Integer status) {
-        return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
-                .eq(CombinationRecordDO::getHeadId, headId)
-                .eq(CombinationRecordDO::getStatus, status));
-    }
-
     /**
      * 查询拼团记录
      *
@@ -69,25 +62,51 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
      */
     default List<CombinationRecordDO> selectLatestList(int count) {
         return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
-                .orderByDesc(CombinationRecordDO::getCreateTime)
+                .orderByDesc(CombinationRecordDO::getId)
                 .last("LIMIT " + count));
     }
 
-    /**
-     * 获得最近 count 条拼团记录(团长发起的)
-     *
-     * @param activityId 拼团活动编号
-     * @param status     记录状态
-     * @param count      数量
-     * @return 拼团记录列表
-     */
-    default List<CombinationRecordDO> selectList(Long activityId, Integer status, Integer count) {
+    default List<CombinationRecordDO> selectListByActivityIdAndStatusAndHeadId(Long activityId, Integer status,
+                                                                               Long headId, 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)
+                .eq(CombinationRecordDO::getHeadId, headId)
+                .orderByDesc(CombinationRecordDO::getId)
                 .last("LIMIT " + count));
     }
 
+    default Map<Long, Integer> selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(Collection<Long> activityIds,
+                                                                                             Integer status, Long headId) {
+        // SQL count 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<CombinationRecordDO>()
+                .select("COUNT(DISTINCT(user_id)) AS recordCount, activity_id AS activityId")
+                .in("activity_id", activityIds)
+                .eq(status != null, "status", status)
+                .eq(headId != null, "head_id", headId)
+                .groupBy("activity_id"));
+        if (CollUtil.isEmpty(result)) {
+            return Collections.emptyMap();
+        }
+        // 转换数据
+        return CollectionUtils.convertMap(result,
+                record -> MapUtil.getLong(record, "activityId"),
+                record -> MapUtil.getInt(record, "recordCount"));
+    }
+
+    default PageResult<CombinationRecordDO> selectPage(CombinationRecordReqPageVO pageVO) {
+        return selectPage(pageVO, new LambdaQueryWrapperX<CombinationRecordDO>()
+                .eqIfPresent(CombinationRecordDO::getStatus, pageVO.getStatus())
+                .betweenIfPresent(CombinationRecordDO::getCreateTime, pageVO.getCreateTime()));
+    }
+
+    // TODO @puhui999:这个最好把 headId 也作为一个参数;因为有个要求 userCount,它要 DISTINCT 下;整体可以参考 selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId
+    default Long selectCountByHeadAndStatusAndVirtualGroup(Integer status, Boolean virtualGroup) {
+        return selectCount(new LambdaQueryWrapperX<CombinationRecordDO>()
+                .eq(status != null || virtualGroup != null,
+                        CombinationRecordDO::getHeadId, CombinationRecordDO.HEAD_ID_GROUP) // 统计团信息则指定团长
+                .eqIfPresent(CombinationRecordDO::getStatus, status)
+                .eqIfPresent(CombinationRecordDO::getVirtualGroup, virtualGroup));
+    }
+
 }

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

@@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppS
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -56,11 +58,24 @@ 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);
-    }
+    // TODO @puhui999:类似 BargainActivityMapper
+    /**
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     *
+     * @param spuIds spu 编号
+     * @param status 状态
+     * @return 秒杀活动列表
+     */
+    @Select("SELECT p1.* " +
+            "FROM promotion_seckill_activity p1 " +
+            "INNER JOIN ( " +
+            "  SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
+            "  FROM promotion_seckill_activity " +
+            "  WHERE spu_id IN #{spuIds} " +
+            "  GROUP BY spu_id " +
+            ") p2 " +
+            "ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
+            "ORDER BY p1.create_time DESC;")
+    List<SeckillActivityDO> selectListBySpuIds(Collection<Long> spuIds, Integer status);
 
 }

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

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.Ba
 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
 
 import javax.validation.Valid;
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
@@ -99,11 +100,12 @@ public interface BargainActivityService {
     List<BargainActivityDO> getBargainActivityListByCount(Integer count);
 
     /**
-     * 获取指定 spu 编号的活动
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
      *
-     * @param spuId spu 编号
-     * @return 砍价活动
+     * @param spuIds spu 编号
+     * @param status 状态
+     * @return 砍价活动列表
      */
-    BargainActivityDO getBargainActivityBySpuId(Long spuId);
+    List<BargainActivityDO> getBargainActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status);
 
 }

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

@@ -20,6 +20,7 @@ import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
@@ -176,8 +177,8 @@ public class BargainActivityServiceImpl implements BargainActivityService {
     }
 
     @Override
-    public BargainActivityDO getBargainActivityBySpuId(Long spuId) {
-        return bargainActivityMapper.selectOne(spuId);
+    public List<BargainActivityDO> getBargainActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
+        return bargainActivityMapper.selectListBySpuIds(spuIds, status);
     }
 
 }

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

@@ -73,7 +73,7 @@ public interface CombinationActivityService {
      * @return 拼团活动的商品列表
      */
     default List<CombinationProductDO> getCombinationProductsByActivityId(Long activityId) {
-        return getCombinationProductsByActivityIds(Collections.singletonList(activityId));
+        return getCombinationProductListByActivityIds(Collections.singletonList(activityId));
     }
 
     /**
@@ -82,7 +82,15 @@ public interface CombinationActivityService {
      * @param activityIds 拼团活动 ids
      * @return 拼团活动的商品列表
      */
-    List<CombinationProductDO> getCombinationProductsByActivityIds(Collection<Long> activityIds);
+    List<CombinationProductDO> getCombinationProductListByActivityIds(Collection<Long> activityIds);
+
+    /**
+     * 获得拼团活动列表
+     *
+     * @param ids 拼团活动 ids
+     * @return 拼团活动的列表
+     */
+    List<CombinationActivityDO> getCombinationActivityListByIds(Collection<Long> ids);
 
     /**
      * 获取正在进行的活动分页数据
@@ -101,7 +109,7 @@ public interface CombinationActivityService {
     PageResult<CombinationActivityDO> getCombinationActivityPage(PageParam pageParam);
 
     /**
-     * 获取指定活动指定 sku 编号的商品
+     * 获取指定活动指定 sku 编号的商品
      *
      * @param activityId 活动编号
      * @param skuId      sku 编号
@@ -110,11 +118,12 @@ public interface CombinationActivityService {
     CombinationProductDO selectByActivityIdAndSkuId(Long activityId, Long skuId);
 
     /**
-     * 获取指定 spu 编号的活动
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
      *
-     * @param spuId spu 编号
-     * @return 拼团活动
+     * @param spuIds spu 编号
+     * @param status 状态
+     * @return 拼团活动列表
      */
-    CombinationActivityDO getCombinationActivityBySpuId(Long spuId);
+    List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status);
 
 }

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

@@ -65,13 +65,11 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
 
         // 插入拼团活动
         CombinationActivityDO activity = CombinationActivityConvert.INSTANCE.convert(createReqVO)
-                .setStatus(CommonStatusEnum.ENABLE.getStatus())
-                .setTotalCount(0).setSuccessCount(0).setOrderUserCount(0).setVirtualGroup(0);
+                .setStatus(CommonStatusEnum.ENABLE.getStatus());
         combinationActivityMapper.insert(activity);
         // 插入商品
         List<CombinationProductDO> products = CombinationActivityConvert.INSTANCE.convertList(createReqVO.getProducts(), activity);
         combinationProductMapper.insertBatch(products);
-        // 返回
         return activity.getId();
     }
 
@@ -202,10 +200,15 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
     }
 
     @Override
-    public List<CombinationProductDO> getCombinationProductsByActivityIds(Collection<Long> activityIds) {
+    public List<CombinationProductDO> getCombinationProductListByActivityIds(Collection<Long> activityIds) {
         return combinationProductMapper.selectListByActivityIds(activityIds);
     }
 
+    @Override
+    public List<CombinationActivityDO> getCombinationActivityListByIds(Collection<Long> ids) {
+        return combinationActivityMapper.selectList(CombinationActivityDO::getId, ids);
+    }
+
     @Override
     public List<CombinationActivityDO> getCombinationActivityListByCount(Integer count) {
         return combinationActivityMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus(), count);
@@ -224,8 +227,8 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
     }
 
     @Override
-    public CombinationActivityDO getCombinationActivityBySpuId(Long spuId) {
-        return combinationActivityMapper.selectOne(spuId);
+    public List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
+        return combinationActivityMapper.selectListBySpuIds(spuIds, status);
     }
 
 }

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

@@ -1,14 +1,18 @@
 package cn.iocoder.yudao.module.promotion.service.combination;
 
 import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
 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;
+import javax.annotation.Nullable;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 拼团记录 Service 接口
@@ -27,40 +31,34 @@ public interface CombinationRecordService {
     void updateCombinationRecordStatusByUserIdAndOrderId(Integer status, Long userId, Long orderId);
 
     /**
-     * 校验是否满足拼团条件
-     * 如果不满足,会抛出异常
+     * 【下单前】校验是否满足拼团活动条件
+     *
+     * 如果校验失败,则抛出业务异常
      *
-     * @param activityId 活动编号
      * @param userId     用户编号
+     * @param activityId 活动编号
+     * @param headId     团长编号
      * @param skuId      sku 编号
      * @param count      数量
-     * @return 返回拼团活动和拼团活动商品
+     * @return 拼团信息
      */
-    KeyValue<CombinationActivityDO, CombinationProductDO> validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count);
+    KeyValue<CombinationActivityDO, CombinationProductDO> validateCombinationRecord(Long userId, Long activityId, Long headId,
+                                                                                    Long skuId, Integer count);
 
     /**
      * 创建拼团记录
      *
      * @param reqDTO 创建信息
+     * @return 开团记录编号
      */
-    void createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
+    Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
 
     /**
-     * 更新拼团状态和开始时间
-     *
-     * @param status    状态
-     * @param userId    用户编号
-     * @param orderId   订单编号
-     * @param startTime 开始时间
-     */
-    void updateRecordStatusAndStartTimeByUserIdAndOrderId(Integer status, Long userId, Long orderId, LocalDateTime startTime);
-
-    /**
-     * 获得拼团状态
+     * 获得拼团记录
      *
      * @param userId  用户编号
      * @param orderId 订单编号
-     * @return 拼团状态
+     * @return 拼团记录
      */
     CombinationRecordDO getCombinationRecord(Long userId, Long orderId);
 
@@ -71,27 +69,30 @@ public interface CombinationRecordService {
      * @param activityId 活动 id
      * @return 拼团记录列表
      */
-    List<CombinationRecordDO> getRecordListByUserIdAndActivityId(Long userId, Long activityId);
+    List<CombinationRecordDO> getCombinationRecordListByUserIdAndActivityId(Long userId, Long activityId);
 
     /**
      * 【下单前】校验是否满足拼团活动条件
      *
      * 如果校验失败,则抛出业务异常
      *
-     * @param activityId 活动编号
      * @param userId     用户编号
+     * @param activityId 活动编号
+     * @param headId     团长编号
      * @param skuId      sku 编号
      * @param count      数量
      * @return 拼团信息
      */
-    CombinationValidateJoinRespDTO validateJoinCombination(Long activityId, Long userId, Long skuId, Integer count);
+    CombinationValidateJoinRespDTO validateJoinCombination(Long userId, Long activityId, Long headId, Long skuId, Integer count);
 
     /**
-     * 获取所有拼团记录数
+     * 获取拼团记录数
      *
+     * @param status       状态-允许为空
+     * @param virtualGroup 是否虚拟成团-允许为空
      * @return 记录数
      */
-    Long getRecordsCount();
+    Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup);
 
     /**
      * 获取最近的 count 条拼团记录
@@ -99,7 +100,7 @@ public interface CombinationRecordService {
      * @param count 限制数量
      * @return 拼团记录列表
      */
-    List<CombinationRecordDO> getLatestRecordList(int count);
+    List<CombinationRecordDO> getLatestCombinationRecordList(int count);
 
     /**
      * 获得最近 n 条拼团记录(团长发起的)
@@ -109,7 +110,7 @@ public interface CombinationRecordService {
      * @param count      数量
      * @return 拼团记录列表
      */
-    List<CombinationRecordDO> getRecordListWithHead(Long activityId, Integer status, Integer count);
+    List<CombinationRecordDO> getHeadCombinationRecordList(Long activityId, Integer status, Integer count);
 
     /**
      * 获取指定编号的拼团记录
@@ -117,7 +118,7 @@ public interface CombinationRecordService {
      * @param id 拼团记录编号
      * @return 拼团记录
      */
-    CombinationRecordDO getRecordById(Long id);
+    CombinationRecordDO getCombinationRecordById(Long id);
 
     /**
      * 获取指定团长编号的拼团记录
@@ -125,6 +126,46 @@ public interface CombinationRecordService {
      * @param headId 团长编号
      * @return 拼团记录列表
      */
-    List<CombinationRecordDO> getRecordListByHeadId(Long headId);
+    List<CombinationRecordDO> getCombinationRecordListByHeadId(Long headId);
+
+    /**
+     * 获取拼团记录分页数据
+     *
+     * @param pageVO 分页请求
+     * @return 拼团记录分页数据
+     */
+    PageResult<CombinationRecordDO> getCombinationRecordPage(CombinationRecordReqPageVO pageVO);
+
+    /**
+     * 【拼团活动】获得拼团记录数量 Map
+     *
+     * @param activityIds 活动记录编号数组
+     * @param status      拼团状态,允许空
+     * @param headId      团长编号,允许空。目的 headId 设置为 {@link CombinationRecordDO#HEAD_ID_GROUP} 时,可以设置
+     * @return 拼团记录数量 Map
+     */
+    Map<Long, Integer> getCombinationRecordCountMapByActivity(Collection<Long> activityIds,
+                                                              @Nullable Integer status,
+                                                              @Nullable Long headId);
+
+
+    /**
+     * 获取拼团记录
+     *
+     * @param userId 用户编号
+     * @param id     拼团记录编号
+     * @return 拼团记录
+     */
+    CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id);
+
+    /**
+     * 取消拼团
+     *
+     * @param userId 用户编号
+     * @param id     拼团记录编号
+     * @param headId 团长编号
+     */
+    void cancelCombinationRecord(Long userId, Long id, Long headId);
+
 
 }

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

@@ -1,9 +1,10 @@
 package cn.iocoder.yudao.module.promotion.service.combination;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.api.sku.ProductSkuApi;
@@ -12,6 +13,7 @@ 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.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
 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;
@@ -19,18 +21,20 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationR
 import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecordMapper;
 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
-import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import javax.annotation.Nullable;
 import javax.annotation.Resource;
-import java.time.LocalDateTime;
-import java.util.List;
+import java.util.*;
 
 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.findFirst;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.afterNow;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeNow;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 
 // TODO 芋艿:等拼团记录做完,完整 review 下
@@ -61,6 +65,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     @Resource
     private TradeOrderApi tradeOrderApi;
 
+    // TODO @芋艿:在详细预览下;
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updateCombinationRecordStatusByUserIdAndOrderId(Integer status, Long userId, Long orderId) {
@@ -72,30 +77,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         recordMapper.updateById(record);
     }
 
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void updateRecordStatusAndStartTimeByUserIdAndOrderId(Integer status, Long userId, Long orderId, LocalDateTime startTime) {
-        CombinationRecordDO record = validateCombinationRecord(userId, orderId);
-        // 更新状态
-        record.setStatus(status);
-        // 更新开始时间
-        record.setStartTime(startTime);
-        recordMapper.updateById(record);
-
-        // 更新拼团参入人数
-        List<CombinationRecordDO> records = recordMapper.selectListByHeadIdAndStatus(record.getHeadId(), status);
-        if (CollUtil.isNotEmpty(records)) {
-            records.forEach(item -> {
-                item.setUserCount(records.size());
-                // 校验拼团是否满足要求
-                if (ObjectUtil.equal(records.size(), record.getUserSize())) {
-                    item.setStatus(CombinationRecordStatusEnum.SUCCESS.getStatus());
-                }
-            });
-        }
-        recordMapper.updateBatch(records);
-    }
-
     private CombinationRecordDO validateCombinationRecord(Long userId, Long orderId) {
         // 校验拼团是否存在
         CombinationRecordDO recordDO = recordMapper.selectByUserIdAndOrderId(userId, orderId);
@@ -108,141 +89,247 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     // TODO @芋艿:在详细预览下;
     @Override
     public KeyValue<CombinationActivityDO, CombinationProductDO> validateCombinationRecord(
-            Long activityId, Long userId, Long skuId, Integer count) {
-        // 1.1 校验拼团活动是否存在
+            Long userId, Long activityId, Long headId, Long skuId, Integer count) {
+        // 1. 校验拼团活动是否存在
         CombinationActivityDO activity = combinationActivityService.validateCombinationActivityExists(activityId);
-        // 1.2 校验活动是否开启
-        if (ObjectUtil.equal(activity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
+        // 1.1 校验活动是否开启
+        if (ObjUtil.equal(activity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
             throw exception(COMBINATION_ACTIVITY_STATUS_DISABLE);
         }
-        // 2 校验是否超出单次限购数量
+        // 1.2. 校验活动开始时间
+        if (afterNow(activity.getStartTime())) {
+            throw exception(COMBINATION_RECORD_FAILED_TIME_NOT_START);
+        }
+        // 1.3 校验是否超出单次限购数量
         if (count > activity.getSingleLimitCount()) {
             throw exception(COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED);
         }
-        // 2.1、校验活动商品是否存在
+
+        // 2. 父拼团是否存在,是否已经满了
+        if (headId != null) {
+            // 2.1. 查询进行中的父拼团
+            CombinationRecordDO record = recordMapper.selectByHeadId(headId, CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
+            if (record == null) {
+                throw exception(COMBINATION_RECORD_HEAD_NOT_EXISTS);
+            }
+            // 2.2. 校验拼团是否已满
+            if (ObjUtil.equal(record.getUserCount(), record.getUserSize())) {
+                throw exception(COMBINATION_RECORD_USER_FULL);
+            }
+            // 2.3 校验拼团是否过期(有父拼团的时候只校验父拼团的过期时间)
+            if (beforeNow(record.getExpireTime())) {
+                throw exception(COMBINATION_RECORD_FAILED_TIME_END);
+            }
+        } else {
+            // 3. 校验当前活动是否结束(自己是父拼团的时候才校验活动是否结束)
+            if (beforeNow(activity.getEndTime())) {
+                throw exception(COMBINATION_RECORD_FAILED_TIME_END);
+            }
+        }
+
+        // 4.1 校验活动商品是否存在
         CombinationProductDO product = combinationActivityService.selectByActivityIdAndSkuId(activityId, skuId);
         if (product == null) {
             throw exception(COMBINATION_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS);
         }
-        // 2.2、校验 sku 是否存在
+        // 4.2 校验 sku 是否存在
         ProductSkuRespDTO sku = productSkuApi.getSku(skuId);
         if (sku == null) {
             throw exception(COMBINATION_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS);
         }
-        // 2.3、 校验库存是否充足
+        // 4.3 校验库存是否充足
         if (count > sku.getStock()) {
             throw exception(COMBINATION_ACTIVITY_UPDATE_STOCK_FAIL);
         }
-        // 3、校验是否有拼团记录
-        List<CombinationRecordDO> recordList = getRecordListByUserIdAndActivityId(userId, activityId);
-        if (CollUtil.isEmpty(recordList)) {
+
+        // 6.1 校验是否有拼团记录
+        List<CombinationRecordDO> recordList = recordMapper.selectListByUserIdAndActivityId(userId, activityId);
+        recordList.removeIf(record -> CombinationRecordStatusEnum.isFailed(record.getStatus())); // 取消的订单,不算数
+        if (CollUtil.isEmpty(recordList)) { // 如果为空,说明可以参与,直接返回
             return new KeyValue<>(activity, product);
         }
-        // 4、校验是否超出总限购数量
-        Integer sumValue = getSumValue(convertList(recordList, CombinationRecordDO::getCount,
-                item -> ObjectUtil.equals(item.getStatus(), CombinationRecordStatusEnum.SUCCESS.getStatus())), i -> i, Integer::sum);
-        if ((sumValue + count) > activity.getTotalLimitCount()) {
-            throw exception(COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED);
-        }
-        // 5、校验拼团记录是否存在未支付的订单(如果存在未支付的订单则不允许发起新的拼团)
-        CombinationRecordDO record = findFirst(recordList, item -> ObjectUtil.equals(item.getStatus(), null));
-        if (record == null) {
-            return new KeyValue<>(activity, product);
+        // 6.2 校验用户是否有该活动正在进行的拼团
+        CombinationRecordDO inProgressRecord = findFirst(recordList,
+                record -> CombinationRecordStatusEnum.isInProgress(record.getStatus()));
+        if (inProgressRecord != null) {
+            throw exception(COMBINATION_RECORD_FAILED_HAVE_JOINED);
         }
-        // 5.1、查询关联的订单是否已经支付
-        // 当前 activityId 已经有未支付的订单,不允许在发起新的;要么支付,要么去掉先;
-        // TODO 芋艿:看看是不是可以删除掉;
-        Integer orderStatus = tradeOrderApi.getOrderStatus(record.getOrderId());
-        if (ObjectUtil.equal(orderStatus, TradeOrderStatusEnum.UNPAID.getStatus())) {
-            throw exception(COMBINATION_RECORD_FAILED_ORDER_STATUS_UNPAID);
+        // 6.3 校验是否超出总限购数量
+        Integer sumValue = getSumValue(recordList, CombinationRecordDO::getCount, Integer::sum);
+        if (sumValue != null && sumValue + count > activity.getTotalLimitCount()) {
+            throw exception(COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED);
         }
-
         return new KeyValue<>(activity, product);
     }
 
-    // TODO 芋艿:在详细 review 下;
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
-        // 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、校验活动是否开启
-        if (LocalDateTime.now().isAfter(activity.getStartTime())) {
-            throw exception(COMBINATION_RECORD_FAILED_TIME_NOT_START);
-        }
-        // 4、校验当前活动是否过期
-        if (LocalDateTime.now().isAfter(activity.getEndTime())) {
-            throw exception(COMBINATION_RECORD_FAILED_TIME_END);
-        }
-        // 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);
-            }
-        }
+    public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
+        // 1. 校验拼团活动
+        KeyValue<CombinationActivityDO, CombinationProductDO> keyValue = validateCombinationRecord(reqDTO.getUserId(),
+                reqDTO.getActivityId(), reqDTO.getHeadId(), reqDTO.getSkuId(), reqDTO.getCount());
 
-        // 6. 创建拼团记录
+        // 2.1 组合数据创建拼团记录
         MemberUserRespDTO user = memberUserApi.getUser(reqDTO.getUserId());
         ProductSpuRespDTO spu = productSpuApi.getSpu(reqDTO.getSpuId());
         ProductSkuRespDTO sku = productSkuApi.getSku(reqDTO.getSkuId());
-        recordMapper.insert(CombinationActivityConvert.INSTANCE.convert(reqDTO, activity, user, spu, sku));
+        CombinationRecordDO record = CombinationActivityConvert.INSTANCE.convert(reqDTO, keyValue.getKey(), user, spu, sku);
+        // 2.2 如果是团长需要设置 headId 为 CombinationRecordDO#HEAD_ID_GROUP
+        if (record.getHeadId() == null) {
+            record.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
+        }
+        recordMapper.insert(record);
+
+        if (ObjUtil.equal(CombinationRecordDO.HEAD_ID_GROUP, record.getHeadId())) {
+            return record.getId();
+        }
+
+        // TODO @puhui:是不是这里的更新,放到 order 模块那;支付完成后;
+        // 4、更新拼团相关信息到订单
+        tradeOrderApi.updateOrderCombinationInfo(record.getOrderId(), record.getActivityId(), record.getId(), record.getHeadId());
+        // 4、更新拼团记录
+        updateCombinationRecordWhenCreate(reqDTO.getHeadId(), keyValue.getKey());
+        return record.getId();
+    }
+
+    /**
+     * 当新增拼团时,更新拼团记录的进展
+     *
+     * @param headId   团长编号
+     * @param activity 活动
+     */
+    private void updateCombinationRecordWhenCreate(Long headId, CombinationActivityDO activity) {
+        // 1. 团长 + 团员
+        List<CombinationRecordDO> records = getCombinationRecordListByHeadId(headId);
+        if (CollUtil.isEmpty(records)) {
+            return;
+        }
+        CombinationRecordDO headRecord = recordMapper.selectById(headId);
+
+        // 2. 批量更新记录
+        List<CombinationRecordDO> updateRecords = new ArrayList<>();
+        records.add(headRecord); // 加入团长,团长也需要更新
+        boolean isFull = records.size() >= activity.getUserSize();
+        records.forEach(item -> {
+            CombinationRecordDO updateRecord = new CombinationRecordDO();
+            updateRecord.setId(item.getId()).setUserCount(records.size());
+            if (isFull) {
+                updateRecord.setStatus(CombinationRecordStatusEnum.SUCCESS.getStatus());
+            }
+            updateRecords.add(updateRecord);
+        });
+        recordMapper.updateBatch(updateRecords);
     }
 
     @Override
     public CombinationRecordDO getCombinationRecord(Long userId, Long orderId) {
-        return validateCombinationRecord(userId, orderId);
+        return recordMapper.selectByUserIdAndOrderId(userId, orderId);
     }
 
     @Override
-    public List<CombinationRecordDO> getRecordListByUserIdAndActivityId(Long userId, Long activityId) {
+    public List<CombinationRecordDO> getCombinationRecordListByUserIdAndActivityId(Long userId, Long activityId) {
         return recordMapper.selectListByUserIdAndActivityId(userId, activityId);
     }
 
     @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());
+    public CombinationValidateJoinRespDTO validateJoinCombination(Long userId, Long activityId, Long headId,
+                                                                  Long skuId, Integer count) {
+        KeyValue<CombinationActivityDO, CombinationProductDO> keyValue = validateCombinationRecord(userId, activityId,
+                headId, skuId, count);
+        return new CombinationValidateJoinRespDTO().setActivityId(keyValue.getKey().getId())
+                .setName(keyValue.getKey().getName()).setCombinationPrice(keyValue.getValue().getCombinationPrice());
     }
 
     @Override
-    public Long getRecordsCount() {
-        return recordMapper.selectCount();
+    public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup) {
+        return recordMapper.selectCountByHeadAndStatusAndVirtualGroup(status, virtualGroup);
     }
 
     @Override
-    public List<CombinationRecordDO> getLatestRecordList(int count) {
+    public List<CombinationRecordDO> getLatestCombinationRecordList(int count) {
         return recordMapper.selectLatestList(count);
     }
 
     @Override
-    public List<CombinationRecordDO> getRecordListWithHead(Long activityId, Integer status, Integer count) {
-        return recordMapper.selectList(activityId, status, count);
+    public List<CombinationRecordDO> getHeadCombinationRecordList(Long activityId, Integer status, Integer count) {
+        return recordMapper.selectListByActivityIdAndStatusAndHeadId(activityId, status,
+                CombinationRecordDO.HEAD_ID_GROUP, count);
     }
 
     @Override
-    public CombinationRecordDO getRecordById(Long id) {
+    public CombinationRecordDO getCombinationRecordById(Long id) {
         return recordMapper.selectById(id);
     }
 
     @Override
-    public List<CombinationRecordDO> getRecordListByHeadId(Long headId) {
+    public List<CombinationRecordDO> getCombinationRecordListByHeadId(Long headId) {
         return recordMapper.selectList(CombinationRecordDO::getHeadId, headId);
     }
 
+    @Override
+    public PageResult<CombinationRecordDO> getCombinationRecordPage(CombinationRecordReqPageVO pageVO) {
+        return recordMapper.selectPage(pageVO);
+    }
+
+    @Override
+    public Map<Long, Integer> getCombinationRecordCountMapByActivity(Collection<Long> activityIds,
+                                                                     @Nullable Integer status, @Nullable Long headId) {
+        return recordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId);
+    }
+
+    @Override
+    public CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id) {
+        return recordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void cancelCombinationRecord(Long userId, Long id, Long headId) {
+        // 删除记录
+        recordMapper.deleteById(id);
+
+        // 需要更新的记录
+        List<CombinationRecordDO> updateRecords = new ArrayList<>();
+        // 如果它是团长,则顺序(下单时间)继承
+        if (Objects.equals(headId, CombinationRecordDO.HEAD_ID_GROUP)) { // 情况一:团长
+            // 团员
+            List<CombinationRecordDO> list = getCombinationRecordListByHeadId(id);
+            if (CollUtil.isEmpty(list)) {
+                return;
+            }
+            // 按照创建时间升序排序
+            list.sort(Comparator.comparing(CombinationRecordDO::getCreateTime)); // 影响原 list
+            CombinationRecordDO newHead = list.get(0); // 新团长继位
+            list.forEach(item -> {
+                CombinationRecordDO recordDO = new CombinationRecordDO();
+                recordDO.setId(item.getId());
+                if (ObjUtil.equal(item.getId(), newHead.getId())) { // 新团长
+                    recordDO.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
+                } else {
+                    recordDO.setHeadId(newHead.getId());
+                }
+                recordDO.setUserCount(list.size());
+                updateRecords.add(recordDO);
+            });
+        } else { // 情况二:团员
+            // 团长
+            CombinationRecordDO recordHead = recordMapper.selectById(headId);
+            // 团员
+            List<CombinationRecordDO> records = getCombinationRecordListByHeadId(headId);
+            if (CollUtil.isEmpty(records)) {
+                return;
+            }
+            records.add(recordHead); // 加入团长,团长数据也需要更新
+            records.forEach(item -> {
+                CombinationRecordDO recordDO = new CombinationRecordDO();
+                recordDO.setId(item.getId());
+                recordDO.setUserCount(records.size());
+                updateRecords.add(recordDO);
+            });
+        }
+
+        // 更新拼团记录
+        recordMapper.updateBatch(updateRecords);
+    }
+
 }

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

@@ -120,11 +120,12 @@ public interface SeckillActivityService {
     SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);
 
     /**
-     * 获取指定 spu 编号的活动
+     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
      *
-     * @param spuId spu 编号
-     * @return 秒杀活动
+     * @param spuIds spu 编号
+     * @param status 状态
+     * @return 秒杀活动列表
      */
-    SeckillActivityDO getSeckillActivityBySpuId(Long spuId);
+    List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status);
 
 }

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

@@ -311,8 +311,8 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
     }
 
     @Override
-    public SeckillActivityDO getSeckillActivityBySpuId(Long spuId) {
-        return seckillActivityMapper.selectOne(spuId);
+    public List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
+        return seckillActivityMapper.selectListBySpuIds(spuIds, status);
     }
 
 }

+ 0 - 18
yudao-module-mall/yudao-module-promotion-biz/src/test/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImplTest.java

@@ -111,9 +111,6 @@ public class CombinationActivityServiceImplTest extends BaseDbUnitTest {
             o.setStartTime(null);
             o.setEndTime(null);
             o.setUserSize(null);
-            o.setTotalCount(null);
-            o.setSuccessCount(null);
-            o.setOrderUserCount(null);
             o.setVirtualGroup(null);
             o.setStatus(null);
             o.setLimitDuration(null);
@@ -134,12 +131,6 @@ public class CombinationActivityServiceImplTest extends BaseDbUnitTest {
         combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setEndTime(null)));
         // 测试 userSize 不匹配
         combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setUserSize(null)));
-        // 测试 totalNum 不匹配
-        combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setTotalCount(null)));
-        // 测试 successNum 不匹配
-        combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSuccessCount(null)));
-        // 测试 orderUserCount 不匹配
-        combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setOrderUserCount(null)));
         // 测试 virtualGroup 不匹配
         combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setVirtualGroup(null)));
         // 测试 status 不匹配
@@ -173,9 +164,6 @@ public class CombinationActivityServiceImplTest extends BaseDbUnitTest {
             o.setStartTime(null);
             o.setEndTime(null);
             o.setUserSize(null);
-            o.setTotalCount(null);
-            o.setSuccessCount(null);
-            o.setOrderUserCount(null);
             o.setVirtualGroup(null);
             o.setStatus(null);
             o.setLimitDuration(null);
@@ -196,12 +184,6 @@ public class CombinationActivityServiceImplTest extends BaseDbUnitTest {
         combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setEndTime(null)));
         // 测试 userSize 不匹配
         combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setUserSize(null)));
-        // 测试 totalNum 不匹配
-        combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setTotalCount(null)));
-        // 测试 successNum 不匹配
-        combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSuccessCount(null)));
-        // 测试 orderUserCount 不匹配
-        combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setOrderUserCount(null)));
         // 测试 virtualGroup 不匹配
         combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setVirtualGroup(null)));
         // 测试 status 不匹配

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

@@ -28,13 +28,22 @@ public interface TradeOrderApi {
      */
     TradeOrderRespDTO getOrder(Long id);
 
-    // TODO 芋艿:看看是不是可以删除掉;
     /**
-     * 获取订单状态
+     * 更新拼团相关信息到订单
      *
-     * @param id 订单编号
-     * @return 订单状态
+     * @param orderId             订单编号
+     * @param activityId          拼团活动编号
+     * @param combinationRecordId 拼团记录编号
+     * @param headId              团长编号
+     */
+    void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId);
+
+    /**
+     * 取消支付订单
+     *
+     * @param userId  用户编号
+     * @param orderId 订单编号
      */
-    Integer getOrderStatus(Long id);
+    void cancelPaidOrder(Long userId, Long orderId);
 
 }

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

@@ -33,6 +33,7 @@ public interface ErrorCodeConstants {
     ErrorCode ORDER_UPDATE_PRICE_FAIL_PRICE_ERROR = new ErrorCode(1_011_000_028, "支付订单调价失败,原因:调整后支付价格不能小于 0.01 元");
     ErrorCode ORDER_DELETE_FAIL_STATUS_NOT_CANCEL = new ErrorCode(1_011_000_029, "交易订单删除失败,订单不是【已取消】状态");
     ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
+    ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单已发货");
 
     // ========== After Sale 模块 1-011-000-100 ==========
     ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");

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

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

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.trade.enums.order;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
@@ -37,4 +38,20 @@ public enum TradeOrderTypeEnum implements IntArrayValuable {
         return ARRAYS;
     }
 
+    public static boolean isNormal(Integer type) {
+        return ObjectUtil.equal(type, NORMAL.getType());
+    }
+
+    public static boolean isSeckill(Integer type) {
+        return ObjectUtil.equal(type, SECKILL.getType());
+    }
+
+    public static boolean isBargain(Integer type) {
+        return ObjectUtil.equal(type, BARGAIN.getType());
+    }
+
+    public static boolean isCombination(Integer type) {
+        return ObjectUtil.equal(type, COMBINATION.getType());
+    }
+
 }

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

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -25,6 +26,8 @@ public class TradeOrderApiImpl implements TradeOrderApi {
 
     @Resource
     private TradeOrderQueryService tradeOrderQueryService;
+    @Resource
+    private TradeOrderUpdateService tradeOrderUpdateService;
 
     @Override
     public List<TradeOrderRespDTO> getOrderList(Collection<Long> ids) {
@@ -36,6 +39,16 @@ public class TradeOrderApiImpl implements TradeOrderApi {
         return TradeOrderConvert.INSTANCE.convert(tradeOrderQueryService.getOrder(id));
     }
 
+    @Override
+    public void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId) {
+        tradeOrderUpdateService.updateOrderCombinationInfo(orderId, activityId, combinationRecordId, headId);
+    }
+
+    @Override
+    public void cancelPaidOrder(Long userId, Long orderId) {
+        tradeOrderUpdateService.cancelPaidOrder(userId, orderId);
+    }
+
     @Override
     public Integer getOrderStatus(Long id) {
         TradeOrderDO order = tradeOrderQueryService.getOrder(id);

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

@@ -52,7 +52,6 @@ public class AppTradeOrderSettlementReqVO {
     private Long seckillActivityId;
 
     // ========== 拼团活动相关字段 ==========
-    // TODO @puhui999:是不是拼团记录的编号哈?
     @Schema(description = "拼团活动编号", example = "1024")
     private Long combinationActivityId;
 

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

@@ -207,12 +207,16 @@ public interface TradeOrderConvert {
     default TradePriceCalculateReqBO convert(Long userId, AppTradeOrderSettlementReqVO settlementReqVO,
                                              List<CartDO> cartList) {
         TradePriceCalculateReqBO reqBO = new TradePriceCalculateReqBO().setUserId(userId)
+                .setItems(new ArrayList<>(settlementReqVO.getItems().size()))
                 .setCouponId(settlementReqVO.getCouponId()).setPointStatus(settlementReqVO.getPointStatus())
+                // 物流信息
                 .setDeliveryType(settlementReqVO.getDeliveryType()).setAddressId(settlementReqVO.getAddressId())
                 .setPickUpStoreId(settlementReqVO.getPickUpStoreId())
-                .setItems(new ArrayList<>(settlementReqVO.getItems().size()))
+                // 各种活动
                 .setSeckillActivityId(settlementReqVO.getSeckillActivityId())
-                .setBargainRecordId(settlementReqVO.getBargainRecordId());
+                .setBargainRecordId(settlementReqVO.getBargainRecordId())
+                .setCombinationActivityId(settlementReqVO.getCombinationActivityId())
+                .setCombinationHeadId(settlementReqVO.getCombinationHeadId());
         // 商品项的构建
         Map<Long, CartDO> cartMap = convertMap(cartList, CartDO::getId);
         for (AppTradeOrderSettlementReqVO.Item item : settlementReqVO.getItems()) {

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

@@ -318,10 +318,16 @@ public class TradeOrderDO extends BaseDO {
      */
     private Long combinationActivityId;
     /**
-     * 团长编号
+     * 拼团团长编号
      *
-     * 关联 CombinationRecordDO 的 id 字段
+     * 关联 CombinationRecordDO 的 headId 字段
      */
     private Long combinationHeadId;
+    /**
+     * 拼团记录编号
+     *
+     * 关联 CombinationRecordDO 的 id 字段
+     */
+    private Long combinationRecordId;
 
 }

+ 8 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/redis/RedisKeyConstants.java

@@ -15,4 +15,12 @@ public interface RedisKeyConstants {
      */
     String TRADE_NO = "trade_no:";
 
+    /**
+     * 交易序号的缓存
+     *
+     * KEY 格式:express_track:{code-logisticsNo-receiverMobile}
+     * VALUE 数据格式 String, 物流信息集合
+     */
+    String EXPRESS_TRACK = "express_track";
+
 }

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

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.trade.service.order;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
@@ -13,11 +14,13 @@ 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.dal.mysql.order.TradeOrderItemMapper;
 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
+import cn.iocoder.yudao.module.trade.dal.redis.RedisKeyConstants;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
 import cn.iocoder.yudao.module.trade.framework.delivery.core.client.ExpressClientFactory;
 import cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO;
 import cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO;
 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -141,7 +144,6 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
         return tradeOrderItemMapper.selectProductSumByOrderId(convertSet(orders, TradeOrderDO::getId));
     }
 
-    // TODO @puhui999:可以加个 spring 缓存,30 分钟;主要考虑及时性要求不高,但是每次调用需要钱;
     /**
      * 获得订单的物流轨迹
      *
@@ -149,19 +151,34 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
      * @return 物流轨迹
      */
     private List<ExpressTrackRespDTO> getExpressTrackList(TradeOrderDO order) {
-        // 查询物流公司
         if (order.getLogisticsId() == null) {
             return Collections.emptyList();
         }
+        // 查询物流公司
         DeliveryExpressDO express = deliveryExpressService.getDeliveryExpress(order.getLogisticsId());
         if (express == null) {
             throw exception(EXPRESS_NOT_EXISTS);
         }
+        // 查询物流轨迹
+        return getSelf().getExpressTrackList(express.getCode(), order.getLogisticsNo(), order.getReceiverMobile());
+    }
 
+    /**
+     * 查询物流轨迹
+     * 加个 spring 缓存,30 分钟;主要考虑及时性要求不高,但是每次调用需要钱;TODO @艿艿:这个时间不会搞了。。。交给你了哈哈哈
+     *
+     * @param code           快递公司编码
+     * @param logisticsNo    发货快递单号
+     * @param receiverMobile 收、寄件人的电话号码
+     * @return 物流轨迹
+     */
+    @Cacheable(cacheNames = RedisKeyConstants.EXPRESS_TRACK, key = "#code + '-' + #logisticsNo + '-' + #receiverMobile",
+            condition = "#result != null")
+    public List<ExpressTrackRespDTO> getExpressTrackList(String code, String logisticsNo, String receiverMobile) {
         // 查询物流轨迹
         return expressClientFactory.getDefaultExpressClient().getExpressTrackList(
-                new ExpressTrackQueryReqDTO().setExpressCode(express.getCode()).setLogisticsNo(order.getLogisticsNo())
-                        .setPhone(order.getReceiverMobile()));
+                new ExpressTrackQueryReqDTO().setExpressCode(code).setLogisticsNo(logisticsNo)
+                        .setPhone(receiverMobile));
     }
 
 
@@ -190,4 +207,13 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
         return tradeOrderItemMapper.selectListByOrderId(orderIds);
     }
 
+    /**
+     * 获得自身的代理对象,解决 AOP 生效问题
+     *
+     * @return 自己
+     */
+    private TradeOrderQueryServiceImpl getSelf() {
+        return SpringUtil.getBean(getClass());
+    }
+
 }

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

@@ -170,4 +170,23 @@ public interface TradeOrderUpdateService {
      */
     int createOrderItemCommentBySystem();
 
+    /**
+     * 更新拼团相关信息到订单
+     *
+     * @param orderId             订单编号
+     * @param activityId          拼团活动编号
+     * @param combinationRecordId 拼团记录编号
+     * @param headId              团长编号
+     */
+    // TODO 芋艿:再 review 拼团
+    void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId);
+
+    /**
+     * 取消支付订单
+     *
+     * @param userId  用户编号
+     * @param orderId 订单编号
+     */
+    void cancelPaidOrder(Long userId, Long orderId);
+
 }

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

@@ -250,7 +250,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     /**
      * 订单创建前,执行前置逻辑
      *
-     * @param order 订单
+     * @param order      订单
      * @param orderItems 订单项
      */
     private void beforeCreateTradeOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
@@ -267,9 +267,9 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
      * <p>
      * 例如说:优惠劵的扣减、积分的扣减、支付单的创建等等
      *
-     * @param order           订单
-     * @param orderItems      订单项
-     * @param createReqVO     创建订单请求
+     * @param order       订单
+     * @param orderItems  订单项
+     * @param createReqVO 创建订单请求
      */
     private void afterCreateTradeOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems,
                                        AppTradeOrderCreateReqVO createReqVO) {
@@ -331,7 +331,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         }
 
         // 3、订单支付成功后
-        tradeOrderHandlers.forEach(handler -> handler.afterPayOrder(order));
+        List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(id);
+        tradeOrderHandlers.forEach(handler -> handler.afterPayOrder(order, orderItems));
 
         // 4.1 增加用户积分(赠送)
         addUserPoint(order.getUserId(), order.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE, order.getId());
@@ -624,12 +625,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
             throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
         }
 
-        // 2. TODO 活动相关库存回滚需要活动 id,活动 id 怎么获取?app 端能否传过来;回复:从订单里拿呀
-        tradeOrderHandlers.forEach(handler -> handler.cancelOrder());
-
-        // 3. 回滚库存
         List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(id);
+        // 3. 回滚库存
         productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(orderItems));
+        // 3.1、 活动相关的回滚
+        tradeOrderHandlers.forEach(handler -> handler.cancelOrder(order, orderItems));
 
         // 4. 回滚优惠券
         if (order.getCouponId() != null && order.getCouponId() > 0) {
@@ -738,19 +738,21 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     }
 
     @Override
-    @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_UP_ADDRESS)
+    @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_UPDATE_ADDRESS)
     public void updateOrderAddress(TradeOrderUpdateAddressReqVO reqVO) {
         // 校验交易订单
         TradeOrderDO order = validateOrderExists(reqVO.getId());
-        // TODO @puhui999:是否需要校验订单是否发货
-        // TODO 发货后是否支持修改收货地址
+        // 发货后,不允许修改;
+        // TODO @puhui999:只有待发货,可以执行 update
+        if (TradeOrderStatusEnum.isDelivered(order.getStatus())) {
+            throw exception(ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED);
+        }
 
         // 更新
         tradeOrderMapper.updateById(TradeOrderConvert.INSTANCE.convert(reqVO));
 
         // 记录订单日志
-        TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus(),
-                MapUtil.<String, Object>builder().put("orderId", order.getId()).build());
+        TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus());
     }
 
     @Override
@@ -806,10 +808,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         // 2.2 如果全部退款,则进行取消订单
         getSelf().cancelOrderByAfterSale(order, orderRefundPrice);
 
-        // TODO @puhui999:活动相关的回滚
 
         // 3. 回滚库存
         productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(Collections.singletonList(orderItem)));
+        // 3.1、 活动相关的回滚
+        tradeOrderHandlers.forEach(handler -> handler.cancelOrder(order, Collections.singletonList(orderItem)));
 
         // 4.1 回滚积分:扣减用户积分(赠送的)
         reduceUserPoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.AFTER_SALE_DEDUCT_GIVE, orderItem.getAfterSaleId());
@@ -909,6 +912,25 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         return count;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId) {
+        tradeOrderMapper.updateById(
+                new TradeOrderDO().setId(orderId).setCombinationActivityId(activityId)
+                        .setCombinationRecordId(combinationRecordId).setCombinationHeadId(headId));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void cancelPaidOrder(Long userId, Long orderId) {
+        TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
+        if (order == null) {
+            throw exception(ORDER_NOT_FOUND);
+        }
+
+        cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
+    }
+
     /**
      * 创建单个订单的评论
      *

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.trade.service.order.handler;
 
-import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.module.promotion.api.bargain.BargainActivityApi;
 import cn.iocoder.yudao.module.promotion.api.bargain.BargainRecordApi;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
@@ -26,9 +25,10 @@ public class TradeBargainHandler implements TradeOrderHandler {
 
     @Override
     public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
-        if (ObjectUtil.notEqual(TradeOrderTypeEnum.BARGAIN.getType(), order.getType())) {
+        if (TradeOrderTypeEnum.isBargain(order.getType())) {
             return;
         }
+
         // 扣减砍价活动的库存
         bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(),
                 -orderItems.get(0).getCount());
@@ -36,13 +36,20 @@ public class TradeBargainHandler implements TradeOrderHandler {
 
     @Override
     public void afterOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
-        if (ObjectUtil.notEqual(TradeOrderTypeEnum.BARGAIN.getType(), order.getType())) {
+        if (TradeOrderTypeEnum.isBargain(order.getType())) {
             return;
         }
+
         // 记录砍价记录对应的订单编号
         bargainRecordApi.updateBargainRecordOrderId(order.getBargainRecordId(), order.getId());
     }
 
-    // TODO 芋艿:取消订单时,需要增加库存
+    @Override
+    public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        if (TradeOrderTypeEnum.isBargain(order.getType())) {
+            return;
+        }
+        // TODO 芋艿:取消订单时,需要增加库存
+    }
 
 }

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

@@ -1,7 +1,6 @@
 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;
@@ -26,40 +25,37 @@ public class TradeCombinationHandler implements TradeOrderHandler {
     @Override
     public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
         // 如果不是拼团订单则结束
-        if (ObjectUtil.notEqual(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
+        if (TradeOrderTypeEnum.isCombination(order.getType())) {
             return;
         }
         Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品");
 
-        // 获取商品信息
-        TradeOrderItemDO item = orderItems.get(0);
         // 校验是否满足拼团活动相关限制
-        combinationRecordApi.validateCombinationRecord(order.getCombinationActivityId(), order.getUserId(), item.getSkuId(), item.getCount());
+        TradeOrderItemDO item = orderItems.get(0);
+        combinationRecordApi.validateCombinationRecord(order.getUserId(), order.getCombinationActivityId(),
+                order.getCombinationHeadId(), item.getSkuId(), item.getCount());
+        // TODO @puhui999:这里还要限制下,是不是已经 createOrder;就是还没支付的时候,重复下单了;需要校验下;不然的话,一个拼团可以下多个单子了;
     }
 
     @Override
-    public void afterOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+    public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
         // 如果不是拼团订单则结束
-        if (ObjectUtil.notEqual(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
+        if (TradeOrderTypeEnum.isCombination(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())) {
+    public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        if (TradeOrderTypeEnum.isCombination(order.getType())) {
             return;
         }
-
-        // 更新拼团状态 TODO puhui999:订单支付失败或订单支付过期删除这条拼团记录
-        combinationRecordApi.updateRecordStatusToInProgress(order.getUserId(), order.getId(), order.getPayTime());
     }
 
 }

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

@@ -33,12 +33,18 @@ public interface TradeOrderHandler {
      * 支付订单后
      *
      * @param order 订单
+     * @param orderItems 订单项
      */
-    default void afterPayOrder(TradeOrderDO order) {}
+    default void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+    }
 
     /**
      * 订单取消
+     *
+     * @param order 订单
+     * @param orderItems 订单项
      */
-    default void cancelOrder() {}
+    default void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+    }
 
 }

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.trade.service.order.handler;
 
-import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
@@ -23,12 +22,21 @@ public class TradeSeckillHandler implements TradeOrderHandler {
 
     @Override
     public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
-        if (ObjectUtil.notEqual(TradeOrderTypeEnum.SECKILL.getType(), order.getType())) {
+        if (TradeOrderTypeEnum.isSeckill(order.getType())) {
             return;
         }
+
         // 扣减秒杀活动的库存
         seckillActivityApi.updateSeckillStock(order.getSeckillActivityId(),
                 orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
     }
 
+    @Override
+    public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
+        if (TradeOrderTypeEnum.isSeckill(order.getType())) {
+            return;
+        }
+
+    }
+
 }

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

@@ -68,7 +68,6 @@ public class TradePriceCalculateReqBO {
     private Long seckillActivityId;
 
     // ========== 拼团活动相关字段 ==========
-    // TODO @puhui999:是不是拼团记录的编号哈?
     /**
      * 拼团活动编号
      */

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

@@ -12,6 +12,8 @@ import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
 
+// TODO @puhui999:单测可以后补下
+
 /**
  * 拼团活动的 {@link TradePriceCalculator} 实现类
  *
@@ -30,12 +32,11 @@ public class TradeCombinationActivityPriceCalculator implements TradePriceCalcul
         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(),
+                param.getUserId(), param.getCombinationActivityId(), param.getCombinationHeadId(),
                 orderItem.getSkuId(), orderItem.getCount());
 
         // 3.1 记录优惠明细

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java

@@ -176,7 +176,7 @@ public class DeptServiceImpl implements DeptService {
     }
 
     @Override
-    @DataPermission(enable = false) // 禁用数据权限,避免简历不正确的缓存
+    @DataPermission(enable = false) // 禁用数据权限,避免建立不正确的缓存
     @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, key = "#id")
     public Set<Long> getChildDeptIdListFromCache(Long id) {
         List<DeptDO> children = getChildDeptList(id);