Bläddra i källkod

!657 营销活动:完成砍价、秒杀库存回滚。完成拼团过期和虚拟成团处理
Merge pull request !657 from puhui999/feature/mall_product

芋道源码 1 år sedan
förälder
incheckning
7181ccbcef
32 ändrade filer med 609 tillägg och 168 borttagningar
  1. 7 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  2. 3 2
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
  3. 16 7
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/seckill/SeckillActivityApi.java
  4. 2 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
  5. 7 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/seckill/SeckillActivityApiImpl.java
  6. 16 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationRecordController.java
  7. 21 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordReqPage2VO.java
  8. 5 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.http
  9. 10 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainRecordController.java
  10. 2 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java
  11. 32 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
  12. 18 16
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java
  13. 17 16
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationActivityMapper.java
  14. 36 7
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java
  15. 39 21
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
  16. 19 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillProductMapper.java
  17. 30 0
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/combination/CombinationRecordExpireJob.java
  18. 21 9
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityServiceImpl.java
  19. 10 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
  20. 20 3
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java
  21. 155 35
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  22. 14 5
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java
  23. 23 4
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
  24. 2 1
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
  25. 4 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
  26. 12 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderMapper.java
  27. 11 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryService.java
  28. 5 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java
  29. 2 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  30. 11 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBargainHandler.java
  31. 30 12
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationHandler.java
  32. 9 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeSeckillHandler.java

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

@@ -64,6 +64,13 @@ public class CollectionUtils {
         return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
     }
 
+    public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
+        return map.values()
+                .stream()
+                .flatMap(List::stream)
+                .collect(Collectors.toList());
+    }
+
     public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
         if (CollUtil.isEmpty(from)) {
             return new HashSet<>();

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.promotion.api.combination;
 
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
 
@@ -29,9 +30,9 @@ public interface CombinationRecordApi {
      * 创建开团记录
      *
      * @param reqDTO 请求 DTO
-     * @return 开团记录编号
+     * @return key 开团记录编号 value 团长编号
      */
-    Long createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
+    KeyValue<Long, Long> createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
 
     /**
      * 查询拼团记录是否成功

+ 16 - 7
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/seckill/SeckillActivityApi.java

@@ -10,13 +10,22 @@ import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinResp
 public interface SeckillActivityApi {
 
     /**
-     * 更新秒杀库存
+     * 更新秒杀库存(减少)
      *
-     * @param id 活动编号
-     * @param skuId      sku 编号
-     * @param count      数量
+     * @param id    活动编号
+     * @param skuId sku 编号
+     * @param count 数量(正数)
+     */
+    void updateSeckillStockDecr(Long id, Long skuId, Integer count);
+
+    /**
+     * 更新秒杀库存(增加)
+     *
+     * @param id    活动编号
+     * @param skuId sku 编号
+     * @param count 数量(正数)
      */
-    void updateSeckillStock(Long id, Long skuId, Integer count);
+    void updateSeckillStockIncr(Long id, Long skuId, Integer count);
 
     /**
      * 【下单前】校验是否参与秒杀活动
@@ -24,8 +33,8 @@ public interface SeckillActivityApi {
      * 如果校验失败,则抛出业务异常
      *
      * @param activityId 活动编号
-     * @param skuId SKU 编号
-     * @param count 数量
+     * @param skuId      SKU 编号
+     * @param count      数量
      * @return 秒杀信息
      */
     SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.promotion.api.combination;
 
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
@@ -29,7 +30,7 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
     }
 
     @Override
-    public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
+    public KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
         return recordService.createCombinationRecord(reqDTO);
     }
 

+ 7 - 2
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/seckill/SeckillActivityApiImpl.java

@@ -18,8 +18,13 @@ public class SeckillActivityApiImpl implements SeckillActivityApi {
     private SeckillActivityService activityService;
 
     @Override
-    public void updateSeckillStock(Long id, Long skuId, Integer count) {
-        activityService.updateSeckillStock(id, skuId, count);
+    public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
+        activityService.updateSeckillStockDecr(id, skuId, count);
+    }
+
+    @Override
+    public void updateSeckillStockIncr(Long id, Long skuId, Integer count) {
+        activityService.updateSeckillStockIncr(id, skuId, count);
     }
 
     @Override

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

@@ -3,6 +3,7 @@ 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.CombinationRecordReqPage2VO;
 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;
@@ -49,15 +50,27 @@ public class CombinationRecordController {
         return success(CombinationActivityConvert.INSTANCE.convert(recordPage, activities));
     }
 
+    @GetMapping("/page-by-headId")
+    @Operation(summary = "获得拼团记录分页")
+    @PreAuthorize("@ss.hasPermission('promotion:combination-record:query')")
+    public CommonResult<PageResult<CombinationRecordPageItemRespVO>> getBargainRecordPage(@Valid CombinationRecordReqPage2VO pageVO) {
+        // 包含团长和团员的分页记录
+        PageResult<CombinationRecordDO> recordPage = combinationRecordService.getCombinationRecordPage2(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.setUserCount(combinationRecordService.getCombinationRecordCount(null, null, null)); // 获取拼团用户参与数量
         summaryVO.setSuccessCount(combinationRecordService.getCombinationRecordCount( // 获取成团记录
-                CombinationRecordStatusEnum.SUCCESS.getStatus(), null));
-        summaryVO.setVirtualGroupCount(combinationRecordService.getCombinationRecordCount(null, Boolean.TRUE));// 获取虚拟成团记录
+                CombinationRecordStatusEnum.SUCCESS.getStatus(), null, CombinationRecordDO.HEAD_ID_GROUP));
+        summaryVO.setVirtualGroupCount(combinationRecordService.getCombinationRecordCount(// 获取虚拟成团记录
+                null, Boolean.TRUE, CombinationRecordDO.HEAD_ID_GROUP));
         return success(summaryVO);
     }
 

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

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 拼团记录分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CombinationRecordReqPage2VO extends PageParam {
+
+    @Schema(description = "团长编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "团长编号不能为空")
+    private Long headId;
+
+}

+ 5 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.http

@@ -0,0 +1,5 @@
+### /promotion/activity/list-by-spu-ids 获得多个商品,近期参与的每个活动
+GET {{appApi}}/promotion/activity/list-by-spu-ids?spuIds=222&spuIds=633
+Authorization: Bearer {{appToken}}
+Content-Type: application/json
+tenant-id: {{appTenentId}}

+ 10 - 4
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainRecordController.java

@@ -27,6 +27,7 @@ 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.*;
 
@@ -46,18 +47,23 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
 public class AppBargainRecordController {
 
     @Resource
+    private BargainHelpService bargainHelpService;
+    @Resource
+    @Lazy
     private BargainRecordService bargainRecordService;
     @Resource
+    @Lazy
     private BargainActivityService bargainActivityService;
-    @Resource
-    private BargainHelpService bargainHelpService;
+
 
     @Resource
+    private TradeOrderApi tradeOrderApi;
+    @Resource
+    @Lazy
     private MemberUserApi memberUserApi;
     @Resource
+    @Lazy
     private ProductSpuApi productSpuApi;
-    @Resource
-    private TradeOrderApi tradeOrderApi;
 
     @GetMapping("/get-summary")
     @Operation(summary = "获得砍价记录的概要信息", description = "用于小程序首页")

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

@@ -46,8 +46,8 @@ public class AppCombinationRecordController {
     @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
     public CommonResult<AppCombinationRecordSummaryRespVO> getCombinationRecordSummary() {
         AppCombinationRecordSummaryRespVO summary = new AppCombinationRecordSummaryRespVO();
-        // 1. 获得拼团记录数量
-        Long count = combinationRecordService.getCombinationRecordCount(null, null);
+        // 1. 获得拼团参与用户数量
+        Long count = combinationRecordService.getCombinationRecordCount(null, null, null);
         if (count == 0) {
             summary.setAvatars(Collections.emptyList());
             summary.setUserCount(count);

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

@@ -28,7 +28,7 @@ import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
-import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -114,9 +114,6 @@ public interface CombinationActivityConvert {
                                         ProductSpuRespDTO spu, ProductSkuRespDTO sku) {
         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()).setUserCount(1) // 默认就是 1 插入后会接着更新一次所有的拼团记录
                 // 用户信息
                 .setNickname(user.getNickname()).setAvatar(user.getAvatar())
@@ -200,4 +197,35 @@ public interface CombinationActivityConvert {
         return respVO;
     }
 
+    /**
+     * 转换生成虚拟成团虚拟记录
+     *
+     * @param virtualGroupHeadRecords 虚拟成团团长记录列表
+     * @return 虚拟记录列表
+     */
+    default List<CombinationRecordDO> convertVirtualGroupList(List<CombinationRecordDO> virtualGroupHeadRecords) {
+        List<CombinationRecordDO> createRecords = new ArrayList<>();
+        virtualGroupHeadRecords.forEach(headRecord -> {
+            // 计算需要创建的虚拟成团记录数量
+            int count = headRecord.getUserSize() - headRecord.getUserCount();
+            for (int i = 0; i < count; i++) {
+                // 基础信息和团长保持一致
+                CombinationRecordDO newRecord = new CombinationRecordDO().setActivityId(headRecord.getActivityId())
+                        .setCombinationPrice(headRecord.getCombinationPrice()).setSpuId(headRecord.getSpuId()).setSpuName(headRecord.getSpuName())
+                        .setPicUrl(headRecord.getPicUrl()).setSkuId(headRecord.getSkuId()).setHeadId(headRecord.getId())
+                        .setStatus(headRecord.getStatus()) // 状态保持和创建时一致,创建完成后会接着处理
+                        .setVirtualGroup(headRecord.getVirtualGroup()).setExpireTime(headRecord.getExpireTime())
+                        .setStartTime(headRecord.getStartTime()).setUserSize(headRecord.getUserSize()).setUserCount(headRecord.getUserCount());
+                // 虚拟信息
+                newRecord.setCount(0);
+                newRecord.setUserId(0L);
+                newRecord.setNickname("");
+                newRecord.setAvatar("");
+                newRecord.setOrderId(0L);
+                createRecords.add(newRecord);
+            }
+        });
+        return createRecords;
+    }
+
 }

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

@@ -6,13 +6,15 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.BargainActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Param;
 
 import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 砍价活动 Mapper
@@ -85,25 +87,25 @@ public interface BargainActivityMapper extends BaseMapperX<BargainActivityDO> {
                 .last("LIMIT " + count));
     }
 
-    // TODO @puhui999:一个商品,在统一时间,不会参与多个活动;so 是不是不用 inner join 哈?
-    // PS:如果可以参与多个,其实可以这样写 select * from promotion_bargain_activity group by spu_id ORDER BY create_time DESC;通过 group 来过滤
     /**
-     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
      *
      * @param spuIds spu 编号
      * @param status 状态
-     * @return 砍价活动列表
+     * @return 包含 spuId 和 activityId 的 map 对象列表
      */
-    @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);
+    default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
+        return selectMaps(new QueryWrapper<BargainActivityDO>()
+                .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
+                .in("spu_id", spuIds)
+                .eq("status", status)
+                .groupBy("spu_id"));
+    }
+
+    default List<BargainActivityDO> selectListByIds(Collection<Long> ids) {
+        return selectList(new LambdaQueryWrapperX<BargainActivityDO>()
+                .in(BargainActivityDO::getId, ids)
+                .orderByDesc(BargainActivityDO::getCreateTime));
+    }
 
 }

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

@@ -6,12 +6,13 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
-import org.apache.ibatis.annotations.Select;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 拼团活动 Mapper
@@ -43,24 +44,24 @@ public interface CombinationActivityMapper extends BaseMapperX<CombinationActivi
                 .last("LIMIT " + count));
     }
 
-    // TODO @puhui999:类似 BargainActivityMapper
     /**
-     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
-     *
+     * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
      * @param spuIds spu 编号
      * @param status 状态
-     * @return 拼团活动列表
+     * @return 包含 spuId 和 activityId 的 map 对象列表
      */
-    @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);
+    default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
+        return selectMaps(new QueryWrapper<CombinationActivityDO>()
+                .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
+                .in("spu_id", spuIds)
+                .eq("status", status)
+                .groupBy("spu_id"));
+    }
+
+    default List<CombinationActivityDO> selectListByIds(Collection<Long> ids) {
+        return selectList(new LambdaQueryWrapperX<CombinationActivityDO>()
+                .in(CombinationActivityDO::getId, ids)
+                .orderByDesc(CombinationActivityDO::getCreateTime));
+    }
 
 }

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

@@ -6,11 +6,13 @@ 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.CombinationRecordReqPage2VO;
 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.time.LocalDateTime;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -100,13 +102,40 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
                 .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));
+    default PageResult<CombinationRecordDO> selectPage(CombinationRecordReqPage2VO pageVO) {
+        return selectPage(pageVO, new LambdaQueryWrapperX<CombinationRecordDO>()
+                .eq(CombinationRecordDO::getId, pageVO.getHeadId())
+                .or()
+                .eq(CombinationRecordDO::getHeadId, pageVO.getHeadId()));
+    }
+
+    /**
+     * 查询指定条件的记录数
+     * 如果参数都为 null 时则查询用户拼团记录(DISTINCT 去重),也就是说查询会员表中的用户有多少人参与过拼团活动每个人只统计一次
+     *
+     * @param status       状态,可为 null
+     * @param virtualGroup 是否虚拟成团,可为 null
+     * @param headId       团长编号,可为 null
+     * @return 记录数
+     */
+    default Long selectCountByHeadAndStatusAndVirtualGroup(Integer status, Boolean virtualGroup, Long headId) {
+        return selectCount(new QueryWrapper<CombinationRecordDO>()
+                .select(status == null && virtualGroup == null && headId == null, "DISTINCT (user_id)")
+                .eq(status != null, "status", status)
+                .eq(virtualGroup != null, "virtual_group", virtualGroup)
+                .eq(headId != null, "head_id", headId)
+                .groupBy("user_id"));
+    }
+
+    default List<CombinationRecordDO> selectListByHeadIdAndStatusAndExpireTimeLt(Long headId, Integer status, LocalDateTime dateTime) {
+        return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
+                .eq(CombinationRecordDO::getHeadId, headId)
+                .eq(CombinationRecordDO::getStatus, status)
+                .lt(CombinationRecordDO::getExpireTime, dateTime));
+    }
+
+    default List<CombinationRecordDO> selectListByHeadIds(Collection<Long> headIds) {
+        return selectList(new LambdaQueryWrapperX<CombinationRecordDO>().in(CombinationRecordDO::getHeadId, headIds));
     }
 
 }

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity;
 
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
@@ -7,12 +8,14 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 秒杀活动 Mapper
@@ -37,18 +40,32 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
     }
 
     /**
-     * 更新活动库存
+     * 更新活动库存(减少)
      *
      * @param id    活动编号
-     * @param count 扣减的库存数量
+     * @param count 扣减的库存数量(正数)
      * @return 影响的行数
      */
-    default int updateStock(Long id, int count) {
+    default int updateStockDecr(Long id, int count) {
+        Assert.isTrue(count > 0);
         return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
                 .eq(SeckillActivityDO::getId, id)
-                .gt(SeckillActivityDO::getTotalStock, 0)
-                .setSql("stock = stock + " + count)
-                .setSql("total_stock = total_stock - " + count));
+                .gt(SeckillActivityDO::getStock, count)
+                .setSql("stock = stock - " + count));
+    }
+
+    /**
+     * 更新活动库存(增加)
+     *
+     * @param id    活动编号
+     * @param count 增加的库存数量(正数)
+     * @return 影响的行数
+     */
+    default int updateStockIncr(Long id, int count) {
+        Assert.isTrue(count > 0);
+        return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
+                .eq(SeckillActivityDO::getId, id)
+                .setSql("stock = stock + " + count));
     }
 
     default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
@@ -58,24 +75,25 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
                 .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
     }
 
-    // TODO @puhui999:类似 BargainActivityMapper
     /**
-     * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
+     * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
      *
      * @param spuIds spu 编号
      * @param status 状态
-     * @return 秒杀活动列表
+     * @return 包含 spuId 和 activityId 的 map 对象列表
      */
-    @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);
+    default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
+        return selectMaps(new QueryWrapper<SeckillActivityDO>()
+                .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
+                .in("spu_id", spuIds)
+                .eq("status", status)
+                .groupBy("spu_id"));
+    }
+
+    default List<SeckillActivityDO> selectListByIds(Collection<Long> ids) {
+        return selectList(new LambdaQueryWrapperX<SeckillActivityDO>()
+                .in(SeckillActivityDO::getId, ids)
+                .orderByDesc(SeckillActivityDO::getCreateTime));
+    }
 
 }

+ 19 - 3
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillProductMapper.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity;
 
+import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -30,17 +31,32 @@ public interface SeckillProductMapper extends BaseMapperX<SeckillProductDO> {
     }
 
     /**
-     * 更新活动库存
+     * 更新活动库存(减少)
      *
      * @param id    活动编号
-     * @param count 扣减的库存数量
+     * @param count 扣减的库存数量(减少库存)
      * @return 影响的行数
      */
-    default int updateStock(Long id, int count) {
+    default int updateStockDecr(Long id, int count) {
+        Assert.isTrue(count > 0);
         return update(null, new LambdaUpdateWrapper<SeckillProductDO>()
                 .eq(SeckillProductDO::getId, id)
                 .gt(SeckillProductDO::getStock, count)
                 .setSql("stock = stock - " + count));
     }
 
+    /**
+     * 更新活动库存(增加)
+     *
+     * @param id    活动编号
+     * @param count 需要增加的库存(增加库存)
+     * @return 影响的行数
+     */
+    default int updateStockIncr(Long id, int count) {
+        Assert.isTrue(count > 0);
+        return update(null, new LambdaUpdateWrapper<SeckillProductDO>()
+                .eq(SeckillProductDO::getId, id)
+                .setSql("stock = stock + " + count));
+    }
+
 }

+ 30 - 0
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/combination/CombinationRecordExpireJob.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.promotion.job.combination;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 拼团过期 Job
+ *
+ * @author HUIHUI
+ */
+@Component
+public class CombinationRecordExpireJob implements JobHandler {
+
+    @Resource
+    private CombinationRecordService combinationRecordService;
+
+    @Override
+    @TenantJob
+    public String execute(String param) throws Exception {
+        KeyValue<Integer, Integer> keyValue = combinationRecordService.expireCombinationRecord();
+        return StrUtil.format("过期拼团 {} 个, 虚拟成团 {} 个", keyValue.getKey(), keyValue.getValue());
+    }
+
+}

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

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.promotion.service.bargain;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -20,12 +22,11 @@ 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;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
 
@@ -84,11 +85,16 @@ public class BargainActivityServiceImpl implements BargainActivityService {
 
     @Override
     public void updateBargainActivityStock(Long id, Integer count) {
-        // 更新库存。如果更新失败,则抛出异常
-        int updateCount = bargainActivityMapper.updateStock(id, count);
-        if (updateCount == 0) {
-            throw exception(BARGAIN_ACTIVITY_STOCK_NOT_ENOUGH);
+        if (count < 0) {
+            // 更新库存。如果更新失败,则抛出异常
+            int updateCount = bargainActivityMapper.updateStock(id, count);
+            if (updateCount == 0) {
+                throw exception(BARGAIN_ACTIVITY_STOCK_NOT_ENOUGH);
+            }
+        } else if (count > 0) {
+            bargainActivityMapper.updateStock(id, count);
         }
+
     }
 
     private void validateBargainConflict(Long spuId, Long activityId) {
@@ -139,7 +145,7 @@ public class BargainActivityServiceImpl implements BargainActivityService {
 
     @Override
     public List<BargainActivityDO> getBargainActivityList(Set<Long> ids) {
-         return bargainActivityMapper.selectBatchIds(ids);
+        return bargainActivityMapper.selectBatchIds(ids);
     }
 
     @Override
@@ -178,7 +184,13 @@ public class BargainActivityServiceImpl implements BargainActivityService {
 
     @Override
     public List<BargainActivityDO> getBargainActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
-        return bargainActivityMapper.selectListBySpuIds(spuIds, status);
+        // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
+        List<Map<String, Object>> spuIdAndActivityIdMaps = bargainActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
+        if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
+            return Collections.emptyList();
+        }
+        // 2.查询活动详情
+        return bargainActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
     }
 
 }

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

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.promotion.service.combination;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -25,12 +26,12 @@ import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
@@ -228,7 +229,13 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
 
     @Override
     public List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
-        return combinationActivityMapper.selectListBySpuIds(spuIds, status);
+        // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
+        List<Map<String, Object>> spuIdAndActivityIdMaps = combinationActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
+        if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
+            return Collections.emptyList();
+        }
+        // 2.查询活动详情
+        return combinationActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
     }
 
 }

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

@@ -4,6 +4,7 @@ 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.CombinationRecordReqPage2VO;
 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;
@@ -49,9 +50,9 @@ public interface CombinationRecordService {
      * 创建拼团记录
      *
      * @param reqDTO 创建信息
-     * @return 开团记录编号
+     * @return key 开团记录编号 value 团长编号
      */
-    Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
+    KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
 
     /**
      * 获得拼团记录
@@ -90,9 +91,10 @@ public interface CombinationRecordService {
      *
      * @param status       状态-允许为空
      * @param virtualGroup 是否虚拟成团-允许为空
+     * @param headId       团长编号,允许空。目的 headId 设置为 {@link CombinationRecordDO#HEAD_ID_GROUP} 时,可以设置
      * @return 记录数
      */
-    Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup);
+    Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup, @Nullable Long headId);
 
     /**
      * 获取最近的 count 条拼团记录
@@ -136,6 +138,15 @@ public interface CombinationRecordService {
      */
     PageResult<CombinationRecordDO> getCombinationRecordPage(CombinationRecordReqPageVO pageVO);
 
+    /**
+     * 获取拼团记录分页数据(通过团长查询)
+     *
+     * @param pageVO 分页请求
+     * @return 拼团记录分页数据(包括团长的)
+     */
+    PageResult<CombinationRecordDO> getCombinationRecordPage2(CombinationRecordReqPage2VO pageVO);
+
+
     /**
      * 【拼团活动】获得拼团记录数量 Map
      *
@@ -167,5 +178,11 @@ public interface CombinationRecordService {
      */
     void cancelCombinationRecord(Long userId, Long id, Long headId);
 
+    /**
+     * 处理过期拼团
+     *
+     * @return key 过期拼团数量, value 虚拟成团数量
+     */
+    KeyValue<Integer, Integer> expireCombinationRecord();
 
 }

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

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.promotion.service.combination;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -13,6 +14,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.CombinationRecordReqPage2VO;
 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;
@@ -22,17 +24,18 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecord
 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
 import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Nullable;
 import javax.annotation.Resource;
+import java.time.LocalDateTime;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-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.collection.CollectionUtils.*;
 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.*;
@@ -52,7 +55,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     @Lazy
     private CombinationActivityService combinationActivityService;
     @Resource
-    private CombinationRecordMapper recordMapper;
+    private CombinationRecordMapper combinationRecordMapper;
 
     @Resource
     private MemberUserApi memberUserApi;
@@ -62,6 +65,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     @Resource
     @Lazy
     private ProductSkuApi productSkuApi;
+
     @Resource
     private TradeOrderApi tradeOrderApi;
 
@@ -74,12 +78,12 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
 
         // 更新状态
         record.setStatus(status);
-        recordMapper.updateById(record);
+        combinationRecordMapper.updateById(record);
     }
 
     private CombinationRecordDO validateCombinationRecord(Long userId, Long orderId) {
         // 校验拼团是否存在
-        CombinationRecordDO recordDO = recordMapper.selectByUserIdAndOrderId(userId, orderId);
+        CombinationRecordDO recordDO = combinationRecordMapper.selectByUserIdAndOrderId(userId, orderId);
         if (recordDO == null) {
             throw exception(COMBINATION_RECORD_NOT_EXISTS);
         }
@@ -108,7 +112,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         // 2. 父拼团是否存在,是否已经满了
         if (headId != null) {
             // 2.1. 查询进行中的父拼团
-            CombinationRecordDO record = recordMapper.selectByHeadId(headId, CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
+            CombinationRecordDO record = combinationRecordMapper.selectByHeadId(headId, CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
             if (record == null) {
                 throw exception(COMBINATION_RECORD_HEAD_NOT_EXISTS);
             }
@@ -143,7 +147,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         }
 
         // 6.1 校验是否有拼团记录
-        List<CombinationRecordDO> recordList = recordMapper.selectListByUserIdAndActivityId(userId, activityId);
+        List<CombinationRecordDO> recordList = combinationRecordMapper.selectListByUserIdAndActivityId(userId, activityId);
         recordList.removeIf(record -> CombinationRecordStatusEnum.isFailed(record.getStatus())); // 取消的订单,不算数
         if (CollUtil.isEmpty(recordList)) { // 如果为空,说明可以参与,直接返回
             return new KeyValue<>(activity, product);
@@ -164,32 +168,36 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
-        // 1. 校验拼团活动
+    public KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
+        // 1.校验拼团活动
         KeyValue<CombinationActivityDO, CombinationProductDO> keyValue = validateCombinationRecord(reqDTO.getUserId(),
                 reqDTO.getActivityId(), reqDTO.getHeadId(), reqDTO.getSkuId(), reqDTO.getCount());
 
-        // 2.1 组合数据创建拼团记录
+        // 2.组合数据创建拼团记录
         MemberUserRespDTO user = memberUserApi.getUser(reqDTO.getUserId());
         ProductSpuRespDTO spu = productSpuApi.getSpu(reqDTO.getSpuId());
         ProductSkuRespDTO sku = productSkuApi.getSku(reqDTO.getSkuId());
         CombinationRecordDO record = CombinationActivityConvert.INSTANCE.convert(reqDTO, keyValue.getKey(), user, spu, sku);
-        // 2.2 如果是团长需要设置 headId 为 CombinationRecordDO#HEAD_ID_GROUP
+        // 2.1.如果是团长需要设置 headId 为 CombinationRecordDO#HEAD_ID_GROUP
         if (record.getHeadId() == null) {
-            record.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
+            record.setStartTime(LocalDateTime.now())
+                    .setExpireTime(keyValue.getKey().getStartTime().plusHours(keyValue.getKey().getLimitDuration()))
+                    .setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
+        } else {
+            // 2.2.有团长的情况下需要设置开始时间和过期时间为团长的
+            CombinationRecordDO headRecord = combinationRecordMapper.selectByHeadId(record.getHeadId(),
+                    CombinationRecordStatusEnum.IN_PROGRESS.getStatus()); // 查询进行中的父拼团
+            record.setStartTime(headRecord.getStartTime()).setExpireTime(headRecord.getExpireTime());
         }
-        recordMapper.insert(record);
+        combinationRecordMapper.insert(record);
 
         if (ObjUtil.equal(CombinationRecordDO.HEAD_ID_GROUP, record.getHeadId())) {
-            return record.getId();
+            return new KeyValue<>(record.getId(), record.getHeadId());
         }
 
-        // TODO @puhui:是不是这里的更新,放到 order 模块那;支付完成后;
-        // 4、更新拼团相关信息到订单
-        tradeOrderApi.updateOrderCombinationInfo(record.getOrderId(), record.getActivityId(), record.getId(), record.getHeadId());
-        // 4、更新拼团记录
+        // 3、更新拼团记录
         updateCombinationRecordWhenCreate(reqDTO.getHeadId(), keyValue.getKey());
-        return record.getId();
+        return new KeyValue<>(record.getId(), record.getHeadId());
     }
 
     /**
@@ -204,31 +212,33 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         if (CollUtil.isEmpty(records)) {
             return;
         }
-        CombinationRecordDO headRecord = recordMapper.selectById(headId);
+        CombinationRecordDO headRecord = combinationRecordMapper.selectById(headId);
 
         // 2. 批量更新记录
         List<CombinationRecordDO> updateRecords = new ArrayList<>();
         records.add(headRecord); // 加入团长,团长也需要更新
         boolean isFull = records.size() >= activity.getUserSize();
+        LocalDateTime now = LocalDateTime.now();
         records.forEach(item -> {
             CombinationRecordDO updateRecord = new CombinationRecordDO();
             updateRecord.setId(item.getId()).setUserCount(records.size());
             if (isFull) {
                 updateRecord.setStatus(CombinationRecordStatusEnum.SUCCESS.getStatus());
+                updateRecord.setEndTime(now);
             }
             updateRecords.add(updateRecord);
         });
-        recordMapper.updateBatch(updateRecords);
+        combinationRecordMapper.updateBatch(updateRecords);
     }
 
     @Override
     public CombinationRecordDO getCombinationRecord(Long userId, Long orderId) {
-        return recordMapper.selectByUserIdAndOrderId(userId, orderId);
+        return combinationRecordMapper.selectByUserIdAndOrderId(userId, orderId);
     }
 
     @Override
     public List<CombinationRecordDO> getCombinationRecordListByUserIdAndActivityId(Long userId, Long activityId) {
-        return recordMapper.selectListByUserIdAndActivityId(userId, activityId);
+        return combinationRecordMapper.selectListByUserIdAndActivityId(userId, activityId);
     }
 
     @Override
@@ -241,52 +251,57 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     }
 
     @Override
-    public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup) {
-        return recordMapper.selectCountByHeadAndStatusAndVirtualGroup(status, virtualGroup);
+    public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup, @Nullable Long headId) {
+        return combinationRecordMapper.selectCountByHeadAndStatusAndVirtualGroup(status, virtualGroup, headId);
     }
 
     @Override
     public List<CombinationRecordDO> getLatestCombinationRecordList(int count) {
-        return recordMapper.selectLatestList(count);
+        return combinationRecordMapper.selectLatestList(count);
     }
 
     @Override
     public List<CombinationRecordDO> getHeadCombinationRecordList(Long activityId, Integer status, Integer count) {
-        return recordMapper.selectListByActivityIdAndStatusAndHeadId(activityId, status,
+        return combinationRecordMapper.selectListByActivityIdAndStatusAndHeadId(activityId, status,
                 CombinationRecordDO.HEAD_ID_GROUP, count);
     }
 
     @Override
     public CombinationRecordDO getCombinationRecordById(Long id) {
-        return recordMapper.selectById(id);
+        return combinationRecordMapper.selectById(id);
     }
 
     @Override
     public List<CombinationRecordDO> getCombinationRecordListByHeadId(Long headId) {
-        return recordMapper.selectList(CombinationRecordDO::getHeadId, headId);
+        return combinationRecordMapper.selectList(CombinationRecordDO::getHeadId, headId);
     }
 
     @Override
     public PageResult<CombinationRecordDO> getCombinationRecordPage(CombinationRecordReqPageVO pageVO) {
-        return recordMapper.selectPage(pageVO);
+        return combinationRecordMapper.selectPage(pageVO);
+    }
+
+    @Override
+    public PageResult<CombinationRecordDO> getCombinationRecordPage2(CombinationRecordReqPage2VO pageVO) {
+        return combinationRecordMapper.selectPage(pageVO);
     }
 
     @Override
     public Map<Long, Integer> getCombinationRecordCountMapByActivity(Collection<Long> activityIds,
                                                                      @Nullable Integer status, @Nullable Long headId) {
-        return recordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId);
+        return combinationRecordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId);
     }
 
     @Override
     public CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id) {
-        return recordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
+        return combinationRecordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void cancelCombinationRecord(Long userId, Long id, Long headId) {
         // 删除记录
-        recordMapper.deleteById(id);
+        combinationRecordMapper.deleteById(id);
 
         // 需要更新的记录
         List<CombinationRecordDO> updateRecords = new ArrayList<>();
@@ -313,7 +328,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
             });
         } else { // 情况二:团员
             // 团长
-            CombinationRecordDO recordHead = recordMapper.selectById(headId);
+            CombinationRecordDO recordHead = combinationRecordMapper.selectById(headId);
             // 团员
             List<CombinationRecordDO> records = getCombinationRecordListByHeadId(headId);
             if (CollUtil.isEmpty(records)) {
@@ -329,7 +344,112 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         }
 
         // 更新拼团记录
-        recordMapper.updateBatch(updateRecords);
+        combinationRecordMapper.updateBatch(updateRecords);
+    }
+
+    @Override
+    public KeyValue<Integer, Integer> expireCombinationRecord() {
+        // 1。获取所有正在进行中的过期的父拼团
+        List<CombinationRecordDO> headExpireRecords = combinationRecordMapper.selectListByHeadIdAndStatusAndExpireTimeLt(
+                CombinationRecordDO.HEAD_ID_GROUP, CombinationRecordStatusEnum.IN_PROGRESS.getStatus(), LocalDateTime.now());
+        if (CollUtil.isEmpty(headExpireRecords)) {
+            return new KeyValue<>(0, 0);
+        }
+
+        // 2.获取拼团活动
+        List<CombinationActivityDO> combinationActivities = combinationActivityService.getCombinationActivityListByIds(
+                convertSet(headExpireRecords, CombinationRecordDO::getActivityId));
+        Map<Long, CombinationActivityDO> activityMap = convertMap(combinationActivities, CombinationActivityDO::getId);
+
+        // 3.校验是否虚拟成团
+        List<CombinationRecordDO> virtualGroupHeadRecords = new ArrayList<>(); // 虚拟成团
+        for (Iterator<CombinationRecordDO> iterator = headExpireRecords.iterator(); iterator.hasNext(); ) {
+            CombinationRecordDO record = iterator.next();
+            // 3.1 不匹配,则直接跳过
+            CombinationActivityDO activityDO = activityMap.get(record.getActivityId());
+            if (activityDO == null || !activityDO.getVirtualGroup()) { // 取不到活动的或者不是虚拟拼团的
+                continue;
+            }
+            // 3.2 匹配,则移除,添加到虚拟成团中,并结束寻找
+            virtualGroupHeadRecords.add(record);
+            iterator.remove();
+            break;
+        }
+
+        // 4.处理过期的拼团
+        getSelf().handleExpireRecord(headExpireRecords);
+        // 5.虚拟成团
+        getSelf().handleVirtualGroupRecord(virtualGroupHeadRecords);
+
+        return new KeyValue<>(headExpireRecords.size(), virtualGroupHeadRecords.size());
+    }
+
+    @Async
+    protected void handleExpireRecord(List<CombinationRecordDO> headExpireRecords) {
+        if (CollUtil.isEmpty(headExpireRecords)) {
+            return;
+        }
+
+        // 1.更新拼团记录
+        List<CombinationRecordDO> headsAndRecords = updateBatchCombinationRecords(headExpireRecords,
+                CombinationRecordStatusEnum.FAILED);
+        if (headsAndRecords == null) {
+            return;
+        }
+
+        // 2.订单取消 TODO 以现在的取消回滚逻辑好像只能循环了
+        headsAndRecords.forEach(item -> {
+            tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId());
+        });
+
+    }
+
+    @Async
+    protected void handleVirtualGroupRecord(List<CombinationRecordDO> virtualGroupHeadRecords) {
+        if (CollUtil.isEmpty(virtualGroupHeadRecords)) {
+            return;
+        }
+
+        // 1.团员补齐
+        combinationRecordMapper.insertBatch(CombinationActivityConvert.INSTANCE.convertVirtualGroupList(virtualGroupHeadRecords));
+        // 2.更新拼团记录
+        updateBatchCombinationRecords(virtualGroupHeadRecords, CombinationRecordStatusEnum.SUCCESS);
+    }
+
+    private List<CombinationRecordDO> updateBatchCombinationRecords(List<CombinationRecordDO> headRecords, CombinationRecordStatusEnum status) {
+        // 1. 查询团成员
+        List<CombinationRecordDO> records = combinationRecordMapper.selectListByHeadIds(
+                convertSet(headRecords, CombinationRecordDO::getId));
+        if (CollUtil.isEmpty(records)) {
+            return null;
+        }
+        Map<Long, List<CombinationRecordDO>> recordsMap = convertMultiMap(records, CombinationRecordDO::getHeadId);
+        headRecords.forEach(item -> {
+            recordsMap.get(item.getId()).add(item); // 把团长加进团里
+        });
+        // 2.批量更新拼团记录 status 和 失败/成团时间
+        List<CombinationRecordDO> headsAndRecords = mergeValuesFromMap(recordsMap);
+        List<CombinationRecordDO> updateRecords = new ArrayList<>(headsAndRecords.size());
+        LocalDateTime now = LocalDateTime.now();
+        headsAndRecords.forEach(item -> {
+            CombinationRecordDO record = new CombinationRecordDO().setId(item.getId())
+                    .setStatus(status.getStatus()).setEndTime(now);
+            if (CombinationRecordStatusEnum.isSuccess(status.getStatus())) { // 虚拟成团完事更改状态成功后还需要把参与人数修改为成团需要人数
+                record.setUserCount(record.getUserSize());
+            }
+            updateRecords.add(record);
+        });
+        combinationRecordMapper.updateBatch(updateRecords);
+        return headsAndRecords;
+    }
+
+    /**
+     * 获得自身的代理对象,解决 AOP 生效问题
+     *
+     * @return 自己
+     */
+    private CombinationRecordServiceImpl getSelf() {
+        return SpringUtil.getBean(getClass());
     }
 
 }

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

@@ -36,13 +36,22 @@ public interface SeckillActivityService {
     void updateSeckillActivity(@Valid SeckillActivityUpdateReqVO updateReqVO);
 
     /**
-     * 更新秒杀库存
+     * 更新秒杀库存(减少)
      *
      * @param id    活动编号
      * @param skuId sku 编号
-     * @param count 数量
+     * @param count 数量(正数)
      */
-    void updateSeckillStock(Long id, Long skuId, Integer count);
+    void updateSeckillStockDecr(Long id, Long skuId, Integer count);
+
+    /**
+     * 更新秒杀库存(增加)
+     *
+     * @param id    活动编号
+     * @param skuId sku 编号
+     * @param count 数量(正数)
+     */
+    void updateSeckillStockIncr(Long id, Long skuId, Integer count);
 
     /**
      * 关闭秒杀活动
@@ -113,8 +122,8 @@ public interface SeckillActivityService {
      * 如果校验失败,则抛出业务异常
      *
      * @param activityId 活动编号
-     * @param skuId SKU 编号
-     * @param count 数量
+     * @param skuId      SKU 编号
+     * @param count      数量
      * @return 秒杀信息
      */
     SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);

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

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.promotion.service.seckill;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -27,6 +29,7 @@ import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -154,7 +157,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void updateSeckillStock(Long id, Long skuId, Integer count) {
+    public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
         // 1.1 校验活动库存是否充足
         SeckillActivityDO seckillActivity = validateSeckillActivityExists(id);
         if (count > seckillActivity.getTotalStock()) {
@@ -167,18 +170,28 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
         }
 
         // 2.1 更新活动商品库存
-        int updateCount = seckillProductMapper.updateStock(product.getId(), count);
+        int updateCount = seckillProductMapper.updateStockDecr(product.getId(), count);
         if (updateCount == 0) {
             throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
         }
 
         // 2.2 更新活动库存
-        updateCount = seckillActivityMapper.updateStock(seckillActivity.getId(), count);
+        updateCount = seckillActivityMapper.updateStockDecr(seckillActivity.getId(), count);
         if (updateCount == 0) {
             throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
         }
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSeckillStockIncr(Long id, Long skuId, Integer count) {
+        SeckillProductDO product = seckillProductMapper.selectByActivityIdAndSkuId(id, skuId);
+        // 更新活动商品库存
+        seckillProductMapper.updateStockIncr(product.getId(), count);
+        // 更新活动库存
+        seckillActivityMapper.updateStockIncr(id, count);
+    }
+
     /**
      * 更新秒杀商品
      *
@@ -312,7 +325,13 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
 
     @Override
     public List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
-        return seckillActivityMapper.selectListBySpuIds(spuIds, status);
+        // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
+        List<Map<String, Object>> spuIdAndActivityIdMaps = seckillActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
+        if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
+            return Collections.emptyList();
+        }
+        // 2.查询活动详情
+        return seckillActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
     }
 
 }

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

@@ -33,7 +33,8 @@ 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, "交易订单修改收货地址失败,原因:订单已发货");
+    ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
+    ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
 
     // ========== After Sale 模块 1-011-000-100 ==========
     ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");

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

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderSummaryRespDTO;
 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -22,10 +23,11 @@ import java.util.List;
 @Validated
 public class TradeOrderApiImpl implements TradeOrderApi {
 
-    @Resource
-    private TradeOrderQueryService tradeOrderQueryService;
     @Resource
     private TradeOrderUpdateService tradeOrderUpdateService;
+    @Resource
+    @Lazy
+    private TradeOrderQueryService tradeOrderQueryService;
 
     @Override
     public List<TradeOrderRespDTO> getOrderList(Collection<Long> ids) {

+ 12 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderMapper.java

@@ -110,4 +110,16 @@ public interface TradeOrderMapper extends BaseMapperX<TradeOrderDO> {
         return selectOne(TradeOrderDO::getPickUpVerifyCode, pickUpVerifyCode);
     }
 
+    default TradeOrderDO selectByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status) {
+        return selectOne(new LambdaQueryWrapperX<TradeOrderDO>()
+                .and(q -> q.eq(TradeOrderDO::getUserId, userId)
+                        .eq(TradeOrderDO::getStatus, status))
+                .and(q -> q.eq(TradeOrderDO::getCombinationActivityId, activityId)
+                        .or()
+                        .eq(TradeOrderDO::getSeckillActivityId, activityId)
+                        .or()
+                        .eq(TradeOrderDO::getBargainActivityId, activityId))
+        );
+    }
+
 }

+ 11 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryService.java

@@ -40,6 +40,16 @@ public interface TradeOrderQueryService {
      */
     TradeOrderDO getOrder(Long userId, Long id);
 
+    /**
+     * 获得指定用户,指定活动,指定状态的交易订单
+     *
+     * @param userId     用户编号
+     * @param activityId 活动编号
+     * @param status     订单状态
+     * @return 交易订单
+     */
+    TradeOrderDO getActivityOrderByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status);
+
     /**
      * 获得订单列表
      *
@@ -95,7 +105,7 @@ public interface TradeOrderQueryService {
     /**
      * 【会员】在指定秒杀活动下,用户购买的商品数量
      *
-     * @param userId 用户编号
+     * @param userId     用户编号
      * @param activityId 活动编号
      * @return 秒杀商品数量
      */

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

@@ -72,6 +72,11 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
         return order;
     }
 
+    @Override
+    public TradeOrderDO getActivityOrderByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status) {
+        return tradeOrderMapper.selectByUserIdAndActivityIdAndStatus(userId, activityId, status);
+    }
+
     @Override
     public List<TradeOrderDO> getOrderList(Collection<Long> ids) {
         if (CollUtil.isEmpty(ids)) {

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

@@ -742,9 +742,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     public void updateOrderAddress(TradeOrderUpdateAddressReqVO reqVO) {
         // 校验交易订单
         TradeOrderDO order = validateOrderExists(reqVO.getId());
-        // 发货后,不允许修改;
-        // TODO @puhui999:只有待发货,可以执行 update
-        if (TradeOrderStatusEnum.isDelivered(order.getStatus())) {
+        // 只有待发货状态才可以修改订单收货地址;
+        if (!TradeOrderStatusEnum.isUndelivered(order.getStatus())) {
             throw exception(ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED);
         }
 

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.trade.service.order.handler;
 
+import cn.hutool.core.lang.Assert;
 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;
@@ -28,6 +29,8 @@ public class TradeBargainHandler implements TradeOrderHandler {
         if (TradeOrderTypeEnum.isBargain(order.getType())) {
             return;
         }
+        // 明确校验一下
+        Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
 
         // 扣减砍价活动的库存
         bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(),
@@ -39,6 +42,8 @@ public class TradeBargainHandler implements TradeOrderHandler {
         if (TradeOrderTypeEnum.isBargain(order.getType())) {
             return;
         }
+        // 明确校验一下
+        Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
 
         // 记录砍价记录对应的订单编号
         bargainRecordApi.updateBargainRecordOrderId(order.getBargainRecordId(), order.getId());
@@ -49,7 +54,12 @@ public class TradeBargainHandler implements TradeOrderHandler {
         if (TradeOrderTypeEnum.isBargain(order.getType())) {
             return;
         }
-        // TODO 芋艿:取消订单时,需要增加库存
+        // 明确校验一下
+        Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
+
+        // 恢复砍价活动的库存
+        bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(),
+                orderItems.get(0).getCount());
     }
 
 }

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

@@ -1,16 +1,24 @@
 package cn.iocoder.yudao.module.trade.service.order.handler;
 
 import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
+import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_CREATE_FAIL_EXIST_UNPAID;
+
 /**
  * 拼团订单 handler 接口实现类
  *
@@ -20,6 +28,14 @@ import java.util.List;
 public class TradeCombinationHandler implements TradeOrderHandler {
 
     @Resource
+    @Lazy
+    private TradeOrderUpdateService orderUpdateService;
+    @Resource
+    @Lazy
+    private TradeOrderQueryService orderQueryService;
+
+    @Resource
+    @Lazy
     private CombinationRecordApi combinationRecordApi;
 
     @Override
@@ -34,28 +50,30 @@ public class TradeCombinationHandler implements TradeOrderHandler {
         TradeOrderItemDO item = orderItems.get(0);
         combinationRecordApi.validateCombinationRecord(order.getUserId(), order.getCombinationActivityId(),
                 order.getCombinationHeadId(), item.getSkuId(), item.getCount());
-        // TODO @puhui999:这里还要限制下,是不是已经 createOrder;就是还没支付的时候,重复下单了;需要校验下;不然的话,一个拼团可以下多个单子了;
+        // 校验该用户是否存在未支付的拼团活动订单;就是还没支付的时候,重复下单了;需要校验下;不然的话,一个拼团可以下多个单子了;
+        TradeOrderDO activityOrder = orderQueryService.getActivityOrderByUserIdAndActivityIdAndStatus(
+                order.getUserId(), order.getCombinationActivityId(), TradeOrderStatusEnum.UNPAID.getStatus());
+        if (activityOrder != null) {
+            throw exception(ORDER_CREATE_FAIL_EXIST_UNPAID);
+        }
     }
 
     @Override
     public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
-        // 如果不是拼团订单则结束
+        // 1.如果不是拼团订单则结束
         if (TradeOrderTypeEnum.isCombination(order.getType())) {
             return;
         }
         Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品");
 
-        // 获取商品信息
+        // 2.获取商品信息
         TradeOrderItemDO item = orderItems.get(0);
-        // 创建拼团记录
-        combinationRecordApi.createCombinationRecord(TradeOrderConvert.INSTANCE.convert(order, item));
-    }
-
-    @Override
-    public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
-        if (TradeOrderTypeEnum.isCombination(order.getType())) {
-            return;
-        }
+        // 2.1.创建拼团记录
+        KeyValue<Long, Long> recordIdAndHeadId = combinationRecordApi.createCombinationRecord(
+                TradeOrderConvert.INSTANCE.convert(order, item));
+        // 3.更新拼团相关信息到订单
+        orderUpdateService.updateOrderCombinationInfo(order.getId(), order.getCombinationActivityId(),
+                recordIdAndHeadId.getKey(), recordIdAndHeadId.getValue());
     }
 
 }

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.trade.service.order.handler;
 
+import cn.hutool.core.lang.Assert;
 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;
@@ -25,9 +26,11 @@ public class TradeSeckillHandler implements TradeOrderHandler {
         if (TradeOrderTypeEnum.isSeckill(order.getType())) {
             return;
         }
+        // 明确校验一下
+        Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品");
 
         // 扣减秒杀活动的库存
-        seckillActivityApi.updateSeckillStock(order.getSeckillActivityId(),
+        seckillActivityApi.updateSeckillStockDecr(order.getSeckillActivityId(),
                 orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
     }
 
@@ -36,7 +39,12 @@ public class TradeSeckillHandler implements TradeOrderHandler {
         if (TradeOrderTypeEnum.isSeckill(order.getType())) {
             return;
         }
+        // 明确校验一下
+        Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品");
 
+        // 恢复秒杀活动的库存
+        seckillActivityApi.updateSeckillStockIncr(order.getSeckillActivityId(),
+                orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
     }
 
 }