浏览代码

营销活动:完成砍价、秒杀库存回滚。完成拼团过期和虚拟成团处理

puhui999 1 年之前
父节点
当前提交
bccd270fc7

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

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

@@ -28,6 +28,7 @@ import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -196,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;
+    }
+
 }

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

@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationR
 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;
@@ -117,4 +118,15 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
                 .eq(headId != null, "head_id", headId));
     }
 
+    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));
+    }
+
 }

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

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

@@ -168,5 +168,11 @@ public interface CombinationRecordService {
      */
     void cancelCombinationRecord(Long userId, Long id, Long headId);
 
+    /**
+     * 处理过期拼团
+     *
+     * @return key 过期拼团数量, value 虚拟成团数量
+     */
+    KeyValue<Integer, Integer> expireCombinationRecord();
 
 }

+ 136 - 24
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;
@@ -20,7 +21,9 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationP
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecordMapper;
 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
+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;
@@ -31,8 +34,7 @@ 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 +54,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     @Lazy
     private CombinationActivityService combinationActivityService;
     @Resource
-    private CombinationRecordMapper recordMapper;
+    private CombinationRecordMapper combinationRecordMapper;
 
     @Resource
     private MemberUserApi memberUserApi;
@@ -63,6 +65,9 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     @Lazy
     private ProductSkuApi productSkuApi;
 
+    @Resource
+    private TradeOrderApi tradeOrderApi;
+
     // TODO @芋艿:在详细预览下;
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -72,12 +77,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);
         }
@@ -106,7 +111,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);
             }
@@ -141,7 +146,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);
@@ -179,11 +184,11 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
                     .setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
         } else {
             // 2.2.有团长的情况下需要设置开始时间和过期时间为团长的
-            CombinationRecordDO headRecord = recordMapper.selectByHeadId(record.getHeadId(),
+            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 new KeyValue<>(record.getId(), record.getHeadId());
@@ -206,31 +211,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
@@ -244,51 +251,51 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
 
     @Override
     public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup, @Nullable Long headId) {
-        return recordMapper.selectCountByHeadAndStatusAndVirtualGroup(status, virtualGroup, 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 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<>();
@@ -315,7 +322,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)) {
@@ -331,7 +338,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());
     }
 
 }