package cn.iocoder.yudao.module.promotion.service.seckill; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; 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.seckill.dto.SeckillValidateJoinRespDTO; import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityUpdateReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.product.SeckillProductBaseVO; import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO; import cn.iocoder.yudao.module.promotion.convert.seckill.SeckillActivityConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillConfigDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO; import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillActivityMapper; import cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity.SeckillProductMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import java.util.Map; import static cn.hutool.core.collection.CollUtil.isNotEmpty; 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.date.LocalDateTimeUtils.isBetween; 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.*; import static java.util.Collections.singletonList; /** * 秒杀活动 Service 实现类 * * @author halfninety */ @Service @Validated public class SeckillActivityServiceImpl implements SeckillActivityService { @Resource private SeckillActivityMapper seckillActivityMapper; @Resource private SeckillProductMapper seckillProductMapper; @Resource private SeckillConfigService seckillConfigService; @Resource private ProductSpuApi productSpuApi; @Resource private ProductSkuApi productSkuApi; @Override @Transactional(rollbackFor = Exception.class) public Long createSeckillActivity(SeckillActivityCreateReqVO createReqVO) { // 1.1 校验商品秒杀时段是否冲突 validateProductConflict(createReqVO.getConfigIds(), createReqVO.getSpuId(), null); // 1.2 校验商品是否存在 validateProductExists(createReqVO.getSpuId(), createReqVO.getProducts()); // 2.1 插入秒杀活动 SeckillActivityDO activity = SeckillActivityConvert.INSTANCE.convert(createReqVO) .setStatus(CommonStatusEnum.ENABLE.getStatus()) .setStock(getSumValue(createReqVO.getProducts(), SeckillProductBaseVO::getStock, Integer::sum)); activity.setTotalStock(activity.getStock()); seckillActivityMapper.insert(activity); // 2.2 插入商品 List products = SeckillActivityConvert.INSTANCE.convertList(createReqVO.getProducts(), activity); seckillProductMapper.insertBatch(products); return activity.getId(); } /** * 校验秒杀商品参与的活动是否存在冲突 * * 1. 校验秒杀时段是否存在 * 2. 秒杀商品是否参加其它活动 * * @param configIds 秒杀时段数组 * @param spuId 商品 SPU 编号 * @param activityId 秒杀活动编号 */ private void validateProductConflict(List configIds, Long spuId, Long activityId) { // 1. 校验秒杀时段是否存在 seckillConfigService.validateSeckillConfigExists(configIds); // 2.1 查询所有开启的秒杀活动 List activityList = seckillActivityMapper.selectListBySpuIdAndStatus(spuId, CommonStatusEnum.ENABLE.getStatus()); if (activityId != null) { // 排除自己 activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId)); } // 2.2 过滤出所有 configIds 有交集的活动,判断是否存在重叠 List conflictActivityList = filterList(activityList, s -> containsAny(s.getConfigIds(), configIds)); if (isNotEmpty(conflictActivityList)) { throw exception(SECKILL_ACTIVITY_SPU_CONFLICTS); } } /** * 校验秒杀商品是否都存在 * * @param spuId 商品 SPU 编号 * @param products 秒杀商品 */ private void validateProductExists(Long spuId, List products) { // 1. 校验商品 spu 是否存在 ProductSpuRespDTO spu = productSpuApi.getSpu(spuId); if (spu == null) { throw exception(SPU_NOT_EXISTS); } // 2. 校验商品 sku 都存在 List skus = productSkuApi.getSkuListBySpuId(singletonList(spuId)); Map skuMap = convertMap(skus, ProductSkuRespDTO::getId); products.forEach(product -> { if (!skuMap.containsKey(product.getSkuId())) { throw exception(SKU_NOT_EXISTS); } }); } @Override @Transactional(rollbackFor = Exception.class) public void updateSeckillActivity(SeckillActivityUpdateReqVO updateReqVO) { // 1.1 校验存在 SeckillActivityDO activity = validateSeckillActivityExists(updateReqVO.getId()); if (CommonStatusEnum.DISABLE.getStatus().equals(activity.getStatus())) { throw exception(SECKILL_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); } // 1.2 校验商品是否冲突 validateProductConflict(updateReqVO.getConfigIds(), updateReqVO.getSpuId(), updateReqVO.getId()); // 1.3 校验商品是否存在 validateProductExists(updateReqVO.getSpuId(), updateReqVO.getProducts()); // 2.1 更新活动 SeckillActivityDO updateObj = SeckillActivityConvert.INSTANCE.convert(updateReqVO) .setStock(getSumValue(updateReqVO.getProducts(), SeckillProductBaseVO::getStock, Integer::sum)); if (updateObj.getStock() > activity.getTotalStock()) { // 如果更新的库存大于原来的库存,则更新总库存 updateObj.setTotalStock(updateObj.getStock()); } seckillActivityMapper.updateById(updateObj); // 2.2 更新商品 updateSeckillProduct(updateObj, updateReqVO.getProducts()); } @Override @Transactional(rollbackFor = Exception.class) public void updateSeckillStockDecr(Long id, Long skuId, Integer count) { // 1.1 校验活动库存是否充足 SeckillActivityDO seckillActivity = validateSeckillActivityExists(id); if (count > seckillActivity.getStock()) { throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL); } // 1.2 校验商品库存是否充足 SeckillProductDO product = seckillProductMapper.selectByActivityIdAndSkuId(id, skuId); if (product == null || count > product.getStock()) { throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL); } // 2.1 更新活动商品库存 int updateCount = seckillProductMapper.updateStockDecr(product.getId(), count); if (updateCount == 0) { throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL); } // 2.2 更新活动库存 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); } /** * 更新秒杀商品 * * @param activity 秒杀活动 * @param products 该活动的最新商品配置 */ private void updateSeckillProduct(SeckillActivityDO activity, List products) { // 第一步,对比新老数据,获得添加、修改、删除的列表 List newList = SeckillActivityConvert.INSTANCE.convertList(products, activity); List oldList = seckillProductMapper.selectListByActivityId(activity.getId()); List> diffList = diffList(oldList, newList, (oldVal, newVal) -> { boolean same = ObjectUtil.equal(oldVal.getSkuId(), newVal.getSkuId()); if (same) { newVal.setId(oldVal.getId()); } return same; }); // 第二步,批量添加、修改、删除 if (isNotEmpty(diffList.get(0))) { seckillProductMapper.insertBatch(diffList.get(0)); } if (isNotEmpty(diffList.get(1))) { seckillProductMapper.updateBatch(diffList.get(1)); } if (isNotEmpty(diffList.get(2))) { seckillProductMapper.deleteByIds(convertList(diffList.get(2), SeckillProductDO::getId)); } } @Override public void closeSeckillActivity(Long id) { // 校验存在 SeckillActivityDO activity = validateSeckillActivityExists(id); if (CommonStatusEnum.DISABLE.getStatus().equals(activity.getStatus())) { throw exception(SECKILL_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); } // 更新 SeckillActivityDO updateObj = new SeckillActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus()); seckillActivityMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void deleteSeckillActivity(Long id) { // 校验存在 SeckillActivityDO seckillActivity = this.validateSeckillActivityExists(id); if (CommonStatusEnum.ENABLE.getStatus().equals(seckillActivity.getStatus())) { throw exception(SECKILL_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END); } // 删除活动 seckillActivityMapper.deleteById(id); // 删除活动商品 List products = seckillProductMapper.selectListByActivityId(id); seckillProductMapper.deleteByIds(convertSet(products, SeckillProductDO::getId)); } private SeckillActivityDO validateSeckillActivityExists(Long id) { SeckillActivityDO seckillActivity = seckillActivityMapper.selectById(id); if (seckillActivity == null) { throw exception(SECKILL_ACTIVITY_NOT_EXISTS); } return seckillActivity; } @Override public SeckillActivityDO getSeckillActivity(Long id) { return seckillActivityMapper.selectById(id); } @Override public PageResult getSeckillActivityPage(SeckillActivityPageReqVO pageReqVO) { return seckillActivityMapper.selectPage(pageReqVO); } @Override public List getSeckillProductListByActivityId(Long activityId) { return seckillProductMapper.selectListByActivityId(activityId); } @Override public List getSeckillProductListByActivityIds(Collection activityIds) { return seckillProductMapper.selectListByActivityId(activityIds); } @Override public List getSeckillActivityListByConfigIdAndStatus(Long configId, Integer status) { return filterList(seckillActivityMapper.selectList(SeckillActivityDO::getStatus, status), item -> anyMatch(item.getConfigIds(), id -> ObjectUtil.equal(id, configId)) // 校验时段 && isBetween(item.getStartTime(), item.getEndTime())); // 追加当前日期是否处在活动日期之间的校验条件 } @Override public PageResult getSeckillActivityAppPageByConfigId(AppSeckillActivityPageReqVO pageReqVO) { return seckillActivityMapper.selectPage(pageReqVO, CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now()); } @Override public SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count) { // 1.1 校验秒杀活动是否存在 SeckillActivityDO activity = validateSeckillActivityExists(activityId); if (CommonStatusEnum.isDisable(activity.getStatus())) { throw exception(SECKILL_JOIN_ACTIVITY_STATUS_CLOSED); } // 1.2 是否在活动时间范围内 if (!LocalDateTimeUtils.isBetween(activity.getStartTime(), activity.getEndTime())) { throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR); } SeckillConfigDO config = seckillConfigService.getCurrentSeckillConfig(); if (config == null || !CollectionUtil.contains(activity.getConfigIds(), config.getId()) || !LocalDateTimeUtils.isBetween(config.getStartTime(), config.getEndTime())) { throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR); } // 1.3 超过单次购买限制 if (count > activity.getSingleLimitCount()) { throw exception(SECKILL_JOIN_ACTIVITY_SINGLE_LIMIT_COUNT_EXCEED); } // 2.1 校验秒杀商品是否存在 SeckillProductDO product = seckillProductMapper.selectByActivityIdAndSkuId(activityId, skuId); if (product == null) { throw exception(SECKILL_JOIN_ACTIVITY_PRODUCT_NOT_EXISTS); } // 2.2 校验库存是否充足 if (count > product.getStock()) { throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL); } return SeckillActivityConvert.INSTANCE.convert02(activity, product); } @Override public SeckillActivityDO getMatchSeckillActivityBySpuId(Long spuId) { return seckillActivityMapper.selectBySpuIdAndStatusAndNow(spuId, CommonStatusEnum.ENABLE.getStatus()); } @Override public List getSeckillActivityListByIds(Collection ids) { return seckillActivityMapper.selectList(SeckillActivityDO::getId, ids); } }