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

This commit is contained in:
puhui999 2023-10-10 15:30:58 +08:00
parent fdbebc406c
commit bccd270fc7
6 changed files with 223 additions and 24 deletions

View File

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

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

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

View File

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

View File

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