订单:完善拼团活动部分 TODO

This commit is contained in:
puhui999 2023-09-30 11:27:24 +08:00
parent 5ecd0d94eb
commit 0b35d4a7e3
16 changed files with 179 additions and 102 deletions

View File

@ -93,4 +93,44 @@ public class LocalDateTimeUtils {
LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
}
/**
* 构建日期时间 TODO 后面有需要的话再继续扩展
*
* @author HUIHUI
*/
public static class BuilderDateTime {
/**
* 日期2023-10-01
*/
private String localDate;
/**
* 时间10:01:00
*/
private String localTime;
public BuilderDateTime() {
}
public BuilderDateTime withDate(String date) {
this.localDate = date;
return this;
}
public BuilderDateTime withDate(LocalDateTime date) {
this.localDate = LocalDateTimeUtil.format(date, "yyyy-MM-dd");
return this;
}
public BuilderDateTime withTime(String time) {
this.localTime = time;
return this;
}
public LocalDateTime build() {
return LocalDateTimeUtil.parse(this.localDate + " " + this.localTime, "yyyy-MM-dd HH:mm:ss");
}
}
}

View File

@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.product.dal.mysql.comment.ProductCommentMapper;
import cn.iocoder.yudao.module.product.enums.comment.ProductCommentScoresEnum;
import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
@ -48,8 +47,6 @@ public class ProductCommentServiceImplTest extends BaseDbUnitTest {
@Lazy
private ProductCommentServiceImpl productCommentService;
@MockBean
private TradeOrderApi tradeOrderApi;
@MockBean
private ProductSpuService productSpuService;
@MockBean

View File

@ -7,14 +7,4 @@ package cn.iocoder.yudao.module.promotion.api.combination;
*/
public interface CombinationActivityApi {
/**
* 校验是否满足拼团条件
*
* @param activityId 活动编号
* @param userId 用户编号
* @param skuId sku 编号
* @param count 数量
*/
void validateCombination(Long activityId, Long userId, Long skuId, Integer count);
}

View File

@ -14,6 +14,16 @@ import java.time.LocalDateTime;
*/
public interface CombinationRecordApi {
/**
* 校验是否满足拼团条件
*
* @param activityId 活动编号
* @param userId 用户编号
* @param skuId sku 编号
* @param count 数量
*/
void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count);
/**
* 创建开团记录
*

View File

@ -75,9 +75,10 @@ public interface ErrorCodeConstants {
ErrorCode COMBINATION_RECORD_HEAD_NOT_EXISTS = new ErrorCode(1013011002, "拼团失败,父拼团不存在");
ErrorCode COMBINATION_RECORD_USER_FULL = new ErrorCode(1013011003, "拼团失败,拼团人数已满");
ErrorCode COMBINATION_RECORD_FAILED_HAVE_JOINED = new ErrorCode(1013011004, "拼团失败,已参与其它拼团");
ErrorCode COMBINATION_RECORD_FAILED_TIME_END = new ErrorCode(1013011005, "拼团失败,活动已经结束");
ErrorCode COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED = new ErrorCode(1013011006, "拼团失败,原因:单次限购超出");
ErrorCode COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED = new ErrorCode(1013011007, "拼团失败,原因:超出总购买次数");
ErrorCode COMBINATION_RECORD_FAILED_TIME_NOT_START = new ErrorCode(1013011005, "拼团失败,活动未开始");
ErrorCode COMBINATION_RECORD_FAILED_TIME_END = new ErrorCode(1013011006, "拼团失败,活动已经结束");
ErrorCode COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED = new ErrorCode(1013011007, "拼团失败,原因:单次限购超出");
ErrorCode COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED = new ErrorCode(1013011008, "拼团失败,原因:超出总购买次数");
// ========== 砍价活动 1013012000 ==========
ErrorCode BARGAIN_ACTIVITY_NOT_EXISTS = new ErrorCode(1013012000, "砍价活动不存在");

View File

@ -1,10 +1,7 @@
package cn.iocoder.yudao.module.promotion.api.combination;
import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 拼团活动 Api 接口实现类
*
@ -13,12 +10,4 @@ import javax.annotation.Resource;
@Service
public class CombinationActivityApiImpl implements CombinationActivityApi {
@Resource
private CombinationActivityService activityService;
@Override
public void validateCombination(Long activityId, Long userId, Long skuId, Integer count) {
activityService.validateCombination(activityId, userId, skuId, count);
}
}

View File

@ -19,6 +19,11 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
@Resource
private CombinationRecordService recordService;
@Override
public void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count) {
recordService.validateCombinationRecord(activityId, userId, skuId, count);
}
@Override
public void createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
recordService.createCombinationRecord(reqDTO);

View File

@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.promotion.convert.seckill.seckillactivity;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.DictTypeConstants;
@ -129,11 +129,14 @@ public interface SeckillActivityConvert {
default AppSeckillActivityDetailRespVO convert3(SeckillActivityDO seckillActivity, List<SeckillProductDO> products, SeckillConfigDO filteredConfig) {
return convert2(seckillActivity)
.setProducts(convertList1(products))
// TODO @puhui999要不要在里面 default 一个方法处理这个事件简洁一点
.setStartTime(LocalDateTimeUtil.parse(LocalDateTimeUtil.format(seckillActivity.getStartTime(), "yyyy-MM-dd") + " " + filteredConfig.getStartTime(),
"yyyy-MM-dd HH:mm:ss")) // 活动开始日期和时段结合
.setEndTime(LocalDateTimeUtil.parse(LocalDateTimeUtil.format(seckillActivity.getEndTime(), "yyyy-MM-dd") + " " + filteredConfig.getEndTime(),
"yyyy-MM-dd HH:mm:ss")); // 活动结束日期和时段结合
.setStartTime(new LocalDateTimeUtils.BuilderDateTime()
.withDate(seckillActivity.getStartTime())
.withTime(filteredConfig.getStartTime())
.build())// 活动开始日期和时段结合
.setEndTime(new LocalDateTimeUtils.BuilderDateTime()
.withDate(seckillActivity.getEndTime())
.withTime(filteredConfig.getEndTime())
.build()); // 活动结束日期和时段结合
}
List<SeckillActivityProductRespDTO> convertList4(List<SeckillProductDO> seckillActivityProductList);

View File

@ -20,6 +20,10 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
CombinationRecordDO::getOrderId, orderId);
}
default List<CombinationRecordDO> selectListByUserId(Long userId) {
return selectList(CombinationRecordDO::getUserId, userId);
}
default List<CombinationRecordDO> selectListByUserIdAndStatus(Long userId, Integer status) {
return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
.eq(CombinationRecordDO::getUserId, userId)

View File

@ -84,17 +84,6 @@ public interface CombinationActivityService {
*/
List<CombinationProductDO> getCombinationProductsByActivityIds(Collection<Long> activityIds);
/**
* 校验是否满足拼团条件
* 如果不满足会抛出异常
*
* @param activityId 活动编号
* @param userId 用户编号
* @param skuId sku 编号
* @param count 数量
*/
void validateCombination(Long activityId, Long userId, Long skuId, Integer count);
/**
* 获取正在进行的活动分页数据
*

View File

@ -17,12 +17,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.product
import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationActivityMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationProductMapper;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@ -33,7 +29,8 @@ 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.*;
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.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.*;
@ -53,16 +50,10 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
@Resource
private CombinationProductMapper combinationProductMapper;
@Resource
@Lazy // TODO @puhui999我感觉 validateCombination 可以挪到 CombinationRecordServiceImpl 因为它更偏向能不能创建拼团记录
private CombinationRecordService combinationRecordService;
@Resource
private ProductSpuApi productSpuApi;
@Resource
private ProductSkuApi productSkuApi;
@Resource
private TradeOrderApi tradeOrderApi;
@Override
@Transactional(rollbackFor = Exception.class)
@ -215,36 +206,6 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
return combinationProductMapper.selectListByActivityIds(activityIds);
}
@Override
public void validateCombination(Long activityId, Long userId, Long skuId, Integer count) {
// 1.1 校验拼团活动是否存在
CombinationActivityDO activity = validateCombinationActivityExists(activityId);
// 1.2 校验活动是否开启
if (ObjectUtil.equal(activity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
throw exception(COMBINATION_ACTIVITY_STATUS_DISABLE);
}
// 1.3 校验是否超出单次限购数量
if (count > activity.getSingleLimitCount()) {
throw exception(COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED);
}
// 2. 校验是否超出总限购数量
List<CombinationRecordDO> recordList = combinationRecordService.getRecordListByUserIdAndActivityId(userId, activityId);
if (CollUtil.isEmpty(recordList)) {
return;
}
// 过滤出拼团成功的
// TODO @puhui999count 要不存一个在 record
List<Long> skuIds = convertList(recordList, CombinationRecordDO::getSkuId,
item -> ObjectUtil.equals(item.getStatus(), CombinationRecordStatusEnum.SUCCESS.getStatus()));
Integer countSum = tradeOrderApi.getOrderItemCountSumByOrderIdAndSkuId(convertList(recordList,
CombinationRecordDO::getOrderId,
item -> ObjectUtil.equals(item.getStatus(), CombinationRecordStatusEnum.SUCCESS.getStatus())), skuIds);
if (activity.getTotalLimitCount() < countSum) {
throw exception(COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED);
}
}
@Override
public List<CombinationActivityDO> getCombinationActivityListByCount(Integer count) {
return combinationActivityMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus(), count);

View File

@ -22,6 +22,17 @@ public interface CombinationRecordService {
*/
void updateCombinationRecordStatusByUserIdAndOrderId(Integer status, Long userId, Long orderId);
/**
* 校验是否满足拼团条件
* 如果不满足会抛出异常
*
* @param activityId 活动编号
* @param userId 用户编号
* @param skuId sku 编号
* @param count 数量
*/
void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count);
/**
* 创建拼团记录
*

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.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
@ -14,6 +15,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationA
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -24,6 +26,7 @@ import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
// TODO 芋艿等拼团记录做完完整 review
@ -51,6 +54,8 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
@Resource
@Lazy
private ProductSkuApi productSkuApi;
@Resource
private TradeOrderApi tradeOrderApi;
@Override
@Transactional(rollbackFor = Exception.class)
@ -97,6 +102,35 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
}
// TODO @puhui999有一个应该在创建那要做下就是当前 activityId 已经有未支付的订单不允许在发起新的要么支付要么去掉先
@Override
public void validateCombinationRecord(Long activityId, Long userId, Long skuId, Integer count) {
// 1.1 校验拼团活动是否存在
CombinationActivityDO activity = combinationActivityService.validateCombinationActivityExists(activityId);
// 1.2 校验活动是否开启
if (ObjectUtil.equal(activity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
throw exception(COMBINATION_ACTIVITY_STATUS_DISABLE);
}
// 1.3 校验是否超出单次限购数量
if (count > activity.getSingleLimitCount()) {
throw exception(COMBINATION_RECORD_FAILED_SINGLE_LIMIT_COUNT_EXCEED);
}
// 2. 校验是否超出总限购数量
List<CombinationRecordDO> recordList = getRecordListByUserIdAndActivityId(userId, activityId);
if (CollUtil.isEmpty(recordList)) {
return;
}
// 过滤出拼团成功的
// TODO @puhui999count 要不存一个在 record
List<Long> skuIds = convertList(recordList, CombinationRecordDO::getSkuId,
item -> ObjectUtil.equals(item.getStatus(), CombinationRecordStatusEnum.SUCCESS.getStatus()));
Integer countSum = tradeOrderApi.getOrderItemCountSumByOrderIdAndSkuId(convertList(recordList,
CombinationRecordDO::getOrderId,
item -> ObjectUtil.equals(item.getStatus(), CombinationRecordStatusEnum.SUCCESS.getStatus())), skuIds);
if (activity.getTotalLimitCount() < countSum) {
throw exception(COMBINATION_RECORD_FAILED_TOTAL_LIMIT_COUNT_EXCEED);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
@ -105,21 +139,26 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
CombinationActivityDO activity = combinationActivityService.validateCombinationActivityExists(reqDTO.getActivityId());
// 1.2 需要校验下他当前是不是已经参加了该拼团
// TODO @puhui999拼团应该可以重复参加应该去校验总共的上限哈就是 activity.totalLimitCount
CombinationRecordDO recordDO = recordMapper.selectByUserIdAndOrderId(reqDTO.getUserId(), reqDTO.getOrderId());
if (recordDO != null) {
throw exception(COMBINATION_RECORD_EXISTS);
List<CombinationRecordDO> records = recordMapper.selectListByUserId(reqDTO.getUserId());
List<Long> orderIds = convertList(records, CombinationRecordDO::getOrderId);
// 1.2.1 如果存在订单才去校验
if (CollUtil.isNotEmpty(orderIds)) {
}
// 1.3 校验用户是否参加了其它拼团
List<CombinationRecordDO> recordDOList = recordMapper.selectListByUserIdAndStatus(reqDTO.getUserId(), CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
if (CollUtil.isNotEmpty(recordDOList)) {
throw exception(COMBINATION_RECORD_FAILED_HAVE_JOINED);
}
// TODO @puhui999有个开始时间未校验
// 1.4 校验当前活动是否过期
// 14 校验活动是否开启
if (LocalDateTime.now().isAfter(activity.getStartTime())) {
throw exception(COMBINATION_RECORD_FAILED_TIME_NOT_START);
}
// 1.5 校验当前活动是否过期
if (LocalDateTime.now().isAfter(activity.getEndTime())) {
throw exception(COMBINATION_RECORD_FAILED_TIME_END);
}
// 1.5 父拼团是否存在,是否已经满了
// 1.6 父拼团是否存在,是否已经满了
if (reqDTO.getHeadId() != null) {
// 查询进行中的父拼团
CombinationRecordDO record = recordMapper.selectOneByHeadId(reqDTO.getHeadId(), CombinationRecordStatusEnum.IN_PROGRESS.getStatus());

View File

@ -56,6 +56,7 @@ import cn.iocoder.yudao.module.trade.service.cart.CartService;
import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
import cn.iocoder.yudao.module.trade.service.message.TradeMessageService;
import cn.iocoder.yudao.module.trade.service.message.bo.TradeOrderMessageWhenDeliveryOrderReqBO;
import cn.iocoder.yudao.module.trade.service.order.bo.TradeAfterPayOrderReqBO;
import cn.iocoder.yudao.module.trade.service.order.bo.TradeBeforeOrderCreateReqBO;
import cn.iocoder.yudao.module.trade.service.order.handler.TradeOrderHandler;
import cn.iocoder.yudao.module.trade.service.price.TradePriceService;
@ -333,13 +334,9 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
if (updateCount == 0) {
throw exception(ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
}
// 校验活动
// 1拼团活动
// TODO @puhui999这块也抽象到 handler
if (Objects.equals(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
// 更新拼团状态 TODO puhui999订单支付失败或订单支付过期删除这条拼团记录
combinationRecordApi.updateRecordStatusToInProgress(order.getUserId(), order.getId(), LocalDateTime.now());
}
// 订单支付成功后
tradeOrderHandlers.forEach(tradeOrderHandler -> tradeOrderHandler.afterPayOrder(new TradeAfterPayOrderReqBO()
.setOrderId(order.getId()).setOrderType(order.getType()).setUserId(order.getUserId()).setPayTime(LocalDateTime.now())));
// TODO 芋艿发送订单变化的消息
// TODO 芋艿发送站内信
@ -742,7 +739,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
}
// 3. TODO 活动相关库存回滚需要活动 id活动 id 怎么获取app 端能否传过来回复从订单里拿呀
tradeOrderHandlers.forEach(handler -> handler.rollback());
tradeOrderHandlers.forEach(handler -> handler.cancelOrder());
// 4. 回滚库存
List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(id);

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.trade.service.order.bo;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 订单支付后 Request BO
*
* @author HUIHUI
*/
@Data
public class TradeAfterPayOrderReqBO {
/**
* 订单编号
*/
@Schema(description = "订单编号", example = "6")
private Long orderId;
/**
* 订单类型
*
* 枚举 {@link TradeOrderTypeEnum}
*/
@Schema(description = "订单类型", example = "3")
private Integer orderType;
/**
* 用户编号
*/
@Schema(description = "用户编号", example = "11")
private Long userId;
/**
* 订单支付时间
*/
@Schema(description = "订单支付时间", example = "2023-08-15 10:00:00")
private LocalDateTime payTime;
}

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi;
import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillActivityProductRespDTO;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.springframework.core.annotation.Order;
@ -31,7 +29,7 @@ public class TradeSeckillActivityPriceCalculator implements TradePriceCalculator
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 1判断订单类型和是否具有秒杀活动编号
if (ObjectUtil.notEqual(param.getType(), TradeOrderTypeEnum.SECKILL.getType()) && param.getSeckillActivityId() == null) {
if (param.getSeckillActivityId() == null) {
return;
}
// 2获取秒杀活动商品信息