diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java index 0a89482c4..7da5cb9c7 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java +++ b/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 List mergeValuesFromMap(Map> map) { + return map.values() + .stream() + .flatMap(List::stream) + .collect(Collectors.toList()); + } + public static Set convertSet(Collection from, Function func) { if (CollUtil.isEmpty(from)) { return new HashSet<>(); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java index 067669a00..03cac269f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java +++ b/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 convertVirtualGroupList(List virtualGroupHeadRecords) { + List 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; + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java index dd8747677..bb97054e4 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java +++ b/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 selectListByHeadIdAndStatusAndExpireTimeLt(Long headId, Integer status, LocalDateTime dateTime) { + return selectList(new LambdaQueryWrapperX() + .eq(CombinationRecordDO::getHeadId, headId) + .eq(CombinationRecordDO::getStatus, status) + .lt(CombinationRecordDO::getExpireTime, dateTime)); + } + + default List selectListByHeadIds(Collection headIds) { + return selectList(new LambdaQueryWrapperX().in(CombinationRecordDO::getHeadId, headIds)); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/combination/CombinationRecordExpireJob.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/combination/CombinationRecordExpireJob.java new file mode 100644 index 000000000..2ee6e8b63 --- /dev/null +++ b/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 keyValue = combinationRecordService.expireCombinationRecord(); + return StrUtil.format("过期拼团 {} 个, 虚拟成团 {} 个", keyValue.getKey(), keyValue.getValue()); + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java index 309047470..12d51d338 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java +++ b/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 expireCombinationRecord(); } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java index 0033d143b..c1cb539ac 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java +++ b/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 recordList = recordMapper.selectListByUserIdAndActivityId(userId, activityId); + List 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 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 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 getLatestCombinationRecordList(int count) { - return recordMapper.selectLatestList(count); + return combinationRecordMapper.selectLatestList(count); } @Override public List 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 getCombinationRecordListByHeadId(Long headId) { - return recordMapper.selectList(CombinationRecordDO::getHeadId, headId); + return combinationRecordMapper.selectList(CombinationRecordDO::getHeadId, headId); } @Override public PageResult getCombinationRecordPage(CombinationRecordReqPageVO pageVO) { - return recordMapper.selectPage(pageVO); + return combinationRecordMapper.selectPage(pageVO); } @Override public Map getCombinationRecordCountMapByActivity(Collection 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 updateRecords = new ArrayList<>(); @@ -315,7 +322,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService { }); } else { // 情况二:团员 // 团长 - CombinationRecordDO recordHead = recordMapper.selectById(headId); + CombinationRecordDO recordHead = combinationRecordMapper.selectById(headId); // 团员 List 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 expireCombinationRecord() { + // 1。获取所有正在进行中的过期的父拼团 + List headExpireRecords = combinationRecordMapper.selectListByHeadIdAndStatusAndExpireTimeLt( + CombinationRecordDO.HEAD_ID_GROUP, CombinationRecordStatusEnum.IN_PROGRESS.getStatus(), LocalDateTime.now()); + if (CollUtil.isEmpty(headExpireRecords)) { + return new KeyValue<>(0, 0); + } + + // 2.获取拼团活动 + List combinationActivities = combinationActivityService.getCombinationActivityListByIds( + convertSet(headExpireRecords, CombinationRecordDO::getActivityId)); + Map activityMap = convertMap(combinationActivities, CombinationActivityDO::getId); + + // 3.校验是否虚拟成团 + List virtualGroupHeadRecords = new ArrayList<>(); // 虚拟成团 + for (Iterator 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 headExpireRecords) { + if (CollUtil.isEmpty(headExpireRecords)) { + return; + } + + // 1.更新拼团记录 + List 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 virtualGroupHeadRecords) { + if (CollUtil.isEmpty(virtualGroupHeadRecords)) { + return; + } + + // 1.团员补齐 + combinationRecordMapper.insertBatch(CombinationActivityConvert.INSTANCE.convertVirtualGroupList(virtualGroupHeadRecords)); + // 2.更新拼团记录 + updateBatchCombinationRecords(virtualGroupHeadRecords, CombinationRecordStatusEnum.SUCCESS); + } + + private List updateBatchCombinationRecords(List headRecords, CombinationRecordStatusEnum status) { + // 1. 查询团成员 + List records = combinationRecordMapper.selectListByHeadIds( + convertSet(headRecords, CombinationRecordDO::getId)); + if (CollUtil.isEmpty(records)) { + return null; + } + Map> recordsMap = convertMultiMap(records, CombinationRecordDO::getHeadId); + headRecords.forEach(item -> { + recordsMap.get(item.getId()).add(item); // 把团长加进团里 + }); + // 2.批量更新拼团记录 status 和 失败/成团时间 + List headsAndRecords = mergeValuesFromMap(recordsMap); + List 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()); } }