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

Merge pull request !657 from puhui999/feature/mall_product
This commit is contained in:
芋道源码 2023-10-10 12:39:26 +00:00 committed by Gitee
commit 7181ccbcef
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
32 changed files with 609 additions and 168 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

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.api.combination;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
@ -29,9 +30,9 @@ public interface CombinationRecordApi {
* 创建开团记录
*
* @param reqDTO 请求 DTO
* @return 开团记录编号
* @return key 开团记录编号 value 团长编号
*/
Long createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
KeyValue<Long, Long> createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
/**
* 查询拼团记录是否成功

View File

@ -10,13 +10,22 @@ import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinResp
public interface SeckillActivityApi {
/**
* 更新秒杀库存
* 更新秒杀库存减少
*
* @param id 活动编号
* @param skuId sku 编号
* @param count 数量
* @param id 活动编号
* @param skuId sku 编号
* @param count 数量(正数)
*/
void updateSeckillStock(Long id, Long skuId, Integer count);
void updateSeckillStockDecr(Long id, Long skuId, Integer count);
/**
* 更新秒杀库存增加
*
* @param id 活动编号
* @param skuId sku 编号
* @param count 数量(正数)
*/
void updateSeckillStockIncr(Long id, Long skuId, Integer count);
/**
* 下单前校验是否参与秒杀活动
@ -24,8 +33,8 @@ public interface SeckillActivityApi {
* 如果校验失败则抛出业务异常
*
* @param activityId 活动编号
* @param skuId SKU 编号
* @param count 数量
* @param skuId SKU 编号
* @param count 数量
* @return 秒杀信息
*/
SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.api.combination;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
@ -29,7 +30,7 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
}
@Override
public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
public KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
return recordService.createCombinationRecord(reqDTO);
}

View File

@ -18,8 +18,13 @@ public class SeckillActivityApiImpl implements SeckillActivityApi {
private SeckillActivityService activityService;
@Override
public void updateSeckillStock(Long id, Long skuId, Integer count) {
activityService.updateSeckillStock(id, skuId, count);
public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
activityService.updateSeckillStockDecr(id, skuId, count);
}
@Override
public void updateSeckillStockIncr(Long id, Long skuId, Integer count) {
activityService.updateSeckillStockIncr(id, skuId, count);
}
@Override

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.combination;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordPageItemRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordSummaryVO;
import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
@ -49,15 +50,27 @@ public class CombinationRecordController {
return success(CombinationActivityConvert.INSTANCE.convert(recordPage, activities));
}
@GetMapping("/page-by-headId")
@Operation(summary = "获得拼团记录分页")
@PreAuthorize("@ss.hasPermission('promotion:combination-record:query')")
public CommonResult<PageResult<CombinationRecordPageItemRespVO>> getBargainRecordPage(@Valid CombinationRecordReqPage2VO pageVO) {
// 包含团长和团员的分页记录
PageResult<CombinationRecordDO> recordPage = combinationRecordService.getCombinationRecordPage2(pageVO);
List<CombinationActivityDO> activities = combinationActivityService.getCombinationActivityListByIds(
convertSet(recordPage.getList(), CombinationRecordDO::getActivityId));
return success(CombinationActivityConvert.INSTANCE.convert(recordPage, activities));
}
@GetMapping("/get-summary")
@Operation(summary = "获得拼团记录的概要信息", description = "用于拼团记录页面展示")
@PreAuthorize("@ss.hasPermission('promotion:combination-record:query')")
public CommonResult<CombinationRecordSummaryVO> getCombinationRecordSummary() {
CombinationRecordSummaryVO summaryVO = new CombinationRecordSummaryVO();
summaryVO.setUserCount(combinationRecordService.getCombinationRecordCount(null, null)); // 获取所有拼团记录
summaryVO.setUserCount(combinationRecordService.getCombinationRecordCount(null, null, null)); // 获取拼团用户参与数量
summaryVO.setSuccessCount(combinationRecordService.getCombinationRecordCount( // 获取成团记录
CombinationRecordStatusEnum.SUCCESS.getStatus(), null));
summaryVO.setVirtualGroupCount(combinationRecordService.getCombinationRecordCount(null, Boolean.TRUE));// 获取虚拟成团记录
CombinationRecordStatusEnum.SUCCESS.getStatus(), null, CombinationRecordDO.HEAD_ID_GROUP));
summaryVO.setVirtualGroupCount(combinationRecordService.getCombinationRecordCount(// 获取虚拟成团记录
null, Boolean.TRUE, CombinationRecordDO.HEAD_ID_GROUP));
return success(summaryVO);
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 拼团记录分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class CombinationRecordReqPage2VO extends PageParam {
@Schema(description = "团长编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "团长编号不能为空")
private Long headId;
}

View File

@ -0,0 +1,5 @@
### /promotion/activity/list-by-spu-ids 获得多个商品,近期参与的每个活动
GET {{appApi}}/promotion/activity/list-by-spu-ids?spuIds=222&spuIds=633
Authorization: Bearer {{appToken}}
Content-Type: application/json
tenant-id: {{appTenentId}}

View File

@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.context.annotation.Lazy;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -46,18 +47,23 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
public class AppBargainRecordController {
@Resource
private BargainHelpService bargainHelpService;
@Resource
@Lazy
private BargainRecordService bargainRecordService;
@Resource
@Lazy
private BargainActivityService bargainActivityService;
@Resource
private BargainHelpService bargainHelpService;
@Resource
private TradeOrderApi tradeOrderApi;
@Resource
@Lazy
private MemberUserApi memberUserApi;
@Resource
@Lazy
private ProductSpuApi productSpuApi;
@Resource
private TradeOrderApi tradeOrderApi;
@GetMapping("/get-summary")
@Operation(summary = "获得砍价记录的概要信息", description = "用于小程序首页")

View File

@ -46,8 +46,8 @@ public class AppCombinationRecordController {
@Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
public CommonResult<AppCombinationRecordSummaryRespVO> getCombinationRecordSummary() {
AppCombinationRecordSummaryRespVO summary = new AppCombinationRecordSummaryRespVO();
// 1. 获得拼团记录数量
Long count = combinationRecordService.getCombinationRecordCount(null, null);
// 1. 获得拼团参与用户数量
Long count = combinationRecordService.getCombinationRecordCount(null, null, null);
if (count == 0) {
summary.setAvatars(Collections.emptyList());
summary.setUserCount(count);

View File

@ -28,7 +28,7 @@ import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -114,9 +114,6 @@ public interface CombinationActivityConvert {
ProductSpuRespDTO spu, ProductSkuRespDTO sku) {
return convert(reqDTO).setVirtualGroup(false)
.setStatus(CombinationRecordStatusEnum.IN_PROGRESS.getStatus()) // 创建后默认状态为进行中
.setStartTime(LocalDateTime.now()) // TODO @puhui999想了下这个 startTime 应该是团长的
// TODO @puhui999有团长的情况下expireTime 应该是团长的
.setExpireTime(activity.getStartTime().plusHours(activity.getLimitDuration()))
.setUserSize(activity.getUserSize()).setUserCount(1) // 默认就是 1 插入后会接着更新一次所有的拼团记录
// 用户信息
.setNickname(user.getNickname()).setAvatar(user.getAvatar())
@ -200,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

@ -6,13 +6,15 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.BargainActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 砍价活动 Mapper
@ -85,25 +87,25 @@ public interface BargainActivityMapper extends BaseMapperX<BargainActivityDO> {
.last("LIMIT " + count));
}
// TODO @puhui999一个商品在统一时间不会参与多个活动so 是不是不用 inner join
// PS如果可以参与多个其实可以这样写 select * from promotion_bargain_activity group by spu_id ORDER BY create_time DESC通过 group 来过滤
/**
* 获取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
* 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
*
* @param spuIds spu 编号
* @param status 状态
* @return 砍价活动列表
* @return 包含 spuId activityId map 对象列表
*/
@Select("SELECT p1.* " +
"FROM promotion_bargain_activity p1 " +
"INNER JOIN ( " +
" SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
" FROM promotion_bargain_activity " +
" WHERE spu_id IN #{spuIds} " +
" GROUP BY spu_id " +
") p2 " +
"ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
"ORDER BY p1.create_time DESC;")
List<BargainActivityDO> selectListBySpuIds(Collection<Long> spuIds, Integer status);
default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
return selectMaps(new QueryWrapper<BargainActivityDO>()
.select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
.in("spu_id", spuIds)
.eq("status", status)
.groupBy("spu_id"));
}
default List<BargainActivityDO> selectListByIds(Collection<Long> ids) {
return selectList(new LambdaQueryWrapperX<BargainActivityDO>()
.in(BargainActivityDO::getId, ids)
.orderByDesc(BargainActivityDO::getCreateTime));
}
}

View File

@ -6,12 +6,13 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 拼团活动 Mapper
@ -43,24 +44,24 @@ public interface CombinationActivityMapper extends BaseMapperX<CombinationActivi
.last("LIMIT " + count));
}
// TODO @puhui999类似 BargainActivityMapper
/**
* 获取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
*
* 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
* @param spuIds spu 编号
* @param status 状态
* @return 拼团活动列表
* @return 包含 spuId activityId map 对象列表
*/
@Select("SELECT p1.* " +
"FROM promotion_combination_activity p1 " +
"INNER JOIN ( " +
" SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
" FROM promotion_combination_activity " +
" WHERE spu_id IN #{spuIds} " +
" GROUP BY spu_id " +
") p2 " +
"ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
"ORDER BY p1.create_time DESC;")
List<CombinationActivityDO> selectListBySpuIds(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status);
default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
return selectMaps(new QueryWrapper<CombinationActivityDO>()
.select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
.in("spu_id", spuIds)
.eq("status", status)
.groupBy("spu_id"));
}
default List<CombinationActivityDO> selectListByIds(Collection<Long> ids) {
return selectList(new LambdaQueryWrapperX<CombinationActivityDO>()
.in(CombinationActivityDO::getId, ids)
.orderByDesc(CombinationActivityDO::getCreateTime));
}
}

View File

@ -6,11 +6,13 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
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;
@ -100,13 +102,40 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO
.betweenIfPresent(CombinationRecordDO::getCreateTime, pageVO.getCreateTime()));
}
// TODO @puhui999这个最好把 headId 也作为一个参数因为有个要求 userCount它要 DISTINCT 整体可以参考 selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId
default Long selectCountByHeadAndStatusAndVirtualGroup(Integer status, Boolean virtualGroup) {
return selectCount(new LambdaQueryWrapperX<CombinationRecordDO>()
.eq(status != null || virtualGroup != null,
CombinationRecordDO::getHeadId, CombinationRecordDO.HEAD_ID_GROUP) // 统计团信息则指定团长
.eqIfPresent(CombinationRecordDO::getStatus, status)
.eqIfPresent(CombinationRecordDO::getVirtualGroup, virtualGroup));
default PageResult<CombinationRecordDO> selectPage(CombinationRecordReqPage2VO pageVO) {
return selectPage(pageVO, new LambdaQueryWrapperX<CombinationRecordDO>()
.eq(CombinationRecordDO::getId, pageVO.getHeadId())
.or()
.eq(CombinationRecordDO::getHeadId, pageVO.getHeadId()));
}
/**
* 查询指定条件的记录数
* 如果参数都为 null 时则查询用户拼团记录DISTINCT 去重也就是说查询会员表中的用户有多少人参与过拼团活动每个人只统计一次
*
* @param status 状态可为 null
* @param virtualGroup 是否虚拟成团可为 null
* @param headId 团长编号可为 null
* @return 记录数
*/
default Long selectCountByHeadAndStatusAndVirtualGroup(Integer status, Boolean virtualGroup, Long headId) {
return selectCount(new QueryWrapper<CombinationRecordDO>()
.select(status == null && virtualGroup == null && headId == null, "DISTINCT (user_id)")
.eq(status != null, "status", status)
.eq(virtualGroup != null, "virtual_group", virtualGroup)
.eq(headId != null, "head_id", headId)
.groupBy("user_id"));
}
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

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
@ -7,12 +8,14 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 秒杀活动 Mapper
@ -37,18 +40,32 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
}
/**
* 更新活动库存
* 更新活动库存(减少)
*
* @param id 活动编号
* @param count 扣减的库存数量
* @param count 扣减的库存数量(正数)
* @return 影响的行数
*/
default int updateStock(Long id, int count) {
default int updateStockDecr(Long id, int count) {
Assert.isTrue(count > 0);
return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
.eq(SeckillActivityDO::getId, id)
.gt(SeckillActivityDO::getTotalStock, 0)
.setSql("stock = stock + " + count)
.setSql("total_stock = total_stock - " + count));
.gt(SeckillActivityDO::getStock, count)
.setSql("stock = stock - " + count));
}
/**
* 更新活动库存增加
*
* @param id 活动编号
* @param count 增加的库存数量(正数)
* @return 影响的行数
*/
default int updateStockIncr(Long id, int count) {
Assert.isTrue(count > 0);
return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
.eq(SeckillActivityDO::getId, id)
.setSql("stock = stock + " + count));
}
default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
@ -58,24 +75,25 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
.apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
}
// TODO @puhui999类似 BargainActivityMapper
/**
* 获取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
* 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
*
* @param spuIds spu 编号
* @param status 状态
* @return 秒杀活动列表
* @return 包含 spuId activityId map 对象列表
*/
@Select("SELECT p1.* " +
"FROM promotion_seckill_activity p1 " +
"INNER JOIN ( " +
" SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
" FROM promotion_seckill_activity " +
" WHERE spu_id IN #{spuIds} " +
" GROUP BY spu_id " +
") p2 " +
"ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
"ORDER BY p1.create_time DESC;")
List<SeckillActivityDO> selectListBySpuIds(Collection<Long> spuIds, Integer status);
default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
return selectMaps(new QueryWrapper<SeckillActivityDO>()
.select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
.in("spu_id", spuIds)
.eq("status", status)
.groupBy("spu_id"));
}
default List<SeckillActivityDO> selectListByIds(Collection<Long> ids) {
return selectList(new LambdaQueryWrapperX<SeckillActivityDO>()
.in(SeckillActivityDO::getId, ids)
.orderByDesc(SeckillActivityDO::getCreateTime));
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@ -30,17 +31,32 @@ public interface SeckillProductMapper extends BaseMapperX<SeckillProductDO> {
}
/**
* 更新活动库存
* 更新活动库存减少
*
* @param id 活动编号
* @param count 扣减的库存数量
* @param count 扣减的库存数量(减少库存)
* @return 影响的行数
*/
default int updateStock(Long id, int count) {
default int updateStockDecr(Long id, int count) {
Assert.isTrue(count > 0);
return update(null, new LambdaUpdateWrapper<SeckillProductDO>()
.eq(SeckillProductDO::getId, id)
.gt(SeckillProductDO::getStock, count)
.setSql("stock = stock - " + count));
}
/**
* 更新活动库存增加
*
* @param id 活动编号
* @param count 需要增加的库存增加库存
* @return 影响的行数
*/
default int updateStockIncr(Long id, int count) {
Assert.isTrue(count > 0);
return update(null, new LambdaUpdateWrapper<SeckillProductDO>()
.eq(SeckillProductDO::getId, id)
.setSql("stock = stock + " + count));
}
}

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

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.promotion.service.bargain;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@ -20,12 +22,11 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
@ -84,11 +85,16 @@ public class BargainActivityServiceImpl implements BargainActivityService {
@Override
public void updateBargainActivityStock(Long id, Integer count) {
// 更新库存如果更新失败则抛出异常
int updateCount = bargainActivityMapper.updateStock(id, count);
if (updateCount == 0) {
throw exception(BARGAIN_ACTIVITY_STOCK_NOT_ENOUGH);
if (count < 0) {
// 更新库存如果更新失败则抛出异常
int updateCount = bargainActivityMapper.updateStock(id, count);
if (updateCount == 0) {
throw exception(BARGAIN_ACTIVITY_STOCK_NOT_ENOUGH);
}
} else if (count > 0) {
bargainActivityMapper.updateStock(id, count);
}
}
private void validateBargainConflict(Long spuId, Long activityId) {
@ -139,7 +145,7 @@ public class BargainActivityServiceImpl implements BargainActivityService {
@Override
public List<BargainActivityDO> getBargainActivityList(Set<Long> ids) {
return bargainActivityMapper.selectBatchIds(ids);
return bargainActivityMapper.selectBatchIds(ids);
}
@Override
@ -178,7 +184,13 @@ public class BargainActivityServiceImpl implements BargainActivityService {
@Override
public List<BargainActivityDO> getBargainActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
return bargainActivityMapper.selectListBySpuIds(spuIds, status);
// 1.查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
List<Map<String, Object>> spuIdAndActivityIdMaps = bargainActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
return Collections.emptyList();
}
// 2.查询活动详情
return bargainActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
}
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.promotion.service.combination;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@ -25,12 +26,12 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Collections;
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.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
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.*;
@ -228,7 +229,13 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
@Override
public List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
return combinationActivityMapper.selectListBySpuIds(spuIds, status);
// 1.查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
List<Map<String, Object>> spuIdAndActivityIdMaps = combinationActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
return Collections.emptyList();
}
// 2.查询活动详情
return combinationActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
}
}

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
@ -49,9 +50,9 @@ public interface CombinationRecordService {
* 创建拼团记录
*
* @param reqDTO 创建信息
* @return 开团记录编号
* @return key 开团记录编号 value 团长编号
*/
Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
/**
* 获得拼团记录
@ -90,9 +91,10 @@ public interface CombinationRecordService {
*
* @param status 状态-允许为空
* @param virtualGroup 是否虚拟成团-允许为空
* @param headId 团长编号允许空目的 headId 设置为 {@link CombinationRecordDO#HEAD_ID_GROUP} 可以设置
* @return 记录数
*/
Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup);
Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup, @Nullable Long headId);
/**
* 获取最近的 count 条拼团记录
@ -136,6 +138,15 @@ public interface CombinationRecordService {
*/
PageResult<CombinationRecordDO> getCombinationRecordPage(CombinationRecordReqPageVO pageVO);
/**
* 获取拼团记录分页数据通过团长查询
*
* @param pageVO 分页请求
* @return 拼团记录分页数据包括团长的
*/
PageResult<CombinationRecordDO> getCombinationRecordPage2(CombinationRecordReqPage2VO pageVO);
/**
* 拼团活动获得拼团记录数量 Map
*
@ -167,5 +178,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;
@ -13,6 +14,7 @@ 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.combination.dto.CombinationRecordCreateReqDTO;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
@ -22,17 +24,18 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecord
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;
import javax.annotation.Nullable;
import javax.annotation.Resource;
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 +55,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
@Lazy
private CombinationActivityService combinationActivityService;
@Resource
private CombinationRecordMapper recordMapper;
private CombinationRecordMapper combinationRecordMapper;
@Resource
private MemberUserApi memberUserApi;
@ -62,6 +65,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
@Resource
@Lazy
private ProductSkuApi productSkuApi;
@Resource
private TradeOrderApi tradeOrderApi;
@ -74,12 +78,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);
}
@ -108,7 +112,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);
}
@ -143,7 +147,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);
@ -164,32 +168,36 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
// 1. 校验拼团活动
public KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
// 1.校验拼团活动
KeyValue<CombinationActivityDO, CombinationProductDO> keyValue = validateCombinationRecord(reqDTO.getUserId(),
reqDTO.getActivityId(), reqDTO.getHeadId(), reqDTO.getSkuId(), reqDTO.getCount());
// 2.1 组合数据创建拼团记录
// 2.组合数据创建拼团记录
MemberUserRespDTO user = memberUserApi.getUser(reqDTO.getUserId());
ProductSpuRespDTO spu = productSpuApi.getSpu(reqDTO.getSpuId());
ProductSkuRespDTO sku = productSkuApi.getSku(reqDTO.getSkuId());
CombinationRecordDO record = CombinationActivityConvert.INSTANCE.convert(reqDTO, keyValue.getKey(), user, spu, sku);
// 2.2 如果是团长需要设置 headId CombinationRecordDO#HEAD_ID_GROUP
// 2.1.如果是团长需要设置 headId CombinationRecordDO#HEAD_ID_GROUP
if (record.getHeadId() == null) {
record.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
record.setStartTime(LocalDateTime.now())
.setExpireTime(keyValue.getKey().getStartTime().plusHours(keyValue.getKey().getLimitDuration()))
.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
} else {
// 2.2.有团长的情况下需要设置开始时间和过期时间为团长的
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 record.getId();
return new KeyValue<>(record.getId(), record.getHeadId());
}
// TODO @puhui是不是这里的更新放到 order 模块那支付完成后
// 4更新拼团相关信息到订单
tradeOrderApi.updateOrderCombinationInfo(record.getOrderId(), record.getActivityId(), record.getId(), record.getHeadId());
// 4更新拼团记录
// 3更新拼团记录
updateCombinationRecordWhenCreate(reqDTO.getHeadId(), keyValue.getKey());
return record.getId();
return new KeyValue<>(record.getId(), record.getHeadId());
}
/**
@ -204,31 +212,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
@ -241,52 +251,57 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
}
@Override
public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup) {
return recordMapper.selectCountByHeadAndStatusAndVirtualGroup(status, virtualGroup);
public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup, @Nullable Long 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 PageResult<CombinationRecordDO> getCombinationRecordPage2(CombinationRecordReqPage2VO 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<>();
@ -313,7 +328,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)) {
@ -329,7 +344,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());
}
}

View File

@ -36,13 +36,22 @@ public interface SeckillActivityService {
void updateSeckillActivity(@Valid SeckillActivityUpdateReqVO updateReqVO);
/**
* 更新秒杀库存
* 更新秒杀库存减少
*
* @param id 活动编号
* @param skuId sku 编号
* @param count 数量
* @param count 数量正数
*/
void updateSeckillStock(Long id, Long skuId, Integer count);
void updateSeckillStockDecr(Long id, Long skuId, Integer count);
/**
* 更新秒杀库存增加
*
* @param id 活动编号
* @param skuId sku 编号
* @param count 数量正数
*/
void updateSeckillStockIncr(Long id, Long skuId, Integer count);
/**
* 关闭秒杀活动
@ -113,8 +122,8 @@ public interface SeckillActivityService {
* 如果校验失败则抛出业务异常
*
* @param activityId 活动编号
* @param skuId SKU 编号
* @param count 数量
* @param skuId SKU 编号
* @param count 数量
* @return 秒杀信息
*/
SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);

View File

@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.promotion.service.seckill;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -27,6 +29,7 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -154,7 +157,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
@Override
@Transactional(rollbackFor = Exception.class)
public void updateSeckillStock(Long id, Long skuId, Integer count) {
public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
// 1.1 校验活动库存是否充足
SeckillActivityDO seckillActivity = validateSeckillActivityExists(id);
if (count > seckillActivity.getTotalStock()) {
@ -167,18 +170,28 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
}
// 2.1 更新活动商品库存
int updateCount = seckillProductMapper.updateStock(product.getId(), count);
int updateCount = seckillProductMapper.updateStockDecr(product.getId(), count);
if (updateCount == 0) {
throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
}
// 2.2 更新活动库存
updateCount = seckillActivityMapper.updateStock(seckillActivity.getId(), count);
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);
}
/**
* 更新秒杀商品
*
@ -312,7 +325,13 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
@Override
public List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
return seckillActivityMapper.selectListBySpuIds(spuIds, status);
// 1.查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
List<Map<String, Object>> spuIdAndActivityIdMaps = seckillActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
return Collections.emptyList();
}
// 2.查询活动详情
return seckillActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
}
}

View File

@ -33,7 +33,8 @@ public interface ErrorCodeConstants {
ErrorCode ORDER_UPDATE_PRICE_FAIL_PRICE_ERROR = new ErrorCode(1_011_000_028, "支付订单调价失败,原因:调整后支付价格不能小于 0.01 元");
ErrorCode ORDER_DELETE_FAIL_STATUS_NOT_CANCEL = new ErrorCode(1_011_000_029, "交易订单删除失败,订单不是【已取消】状态");
ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单已发货");
ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
// ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderSummaryRespDTO;
import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@ -22,10 +23,11 @@ import java.util.List;
@Validated
public class TradeOrderApiImpl implements TradeOrderApi {
@Resource
private TradeOrderQueryService tradeOrderQueryService;
@Resource
private TradeOrderUpdateService tradeOrderUpdateService;
@Resource
@Lazy
private TradeOrderQueryService tradeOrderQueryService;
@Override
public List<TradeOrderRespDTO> getOrderList(Collection<Long> ids) {

View File

@ -110,4 +110,16 @@ public interface TradeOrderMapper extends BaseMapperX<TradeOrderDO> {
return selectOne(TradeOrderDO::getPickUpVerifyCode, pickUpVerifyCode);
}
default TradeOrderDO selectByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status) {
return selectOne(new LambdaQueryWrapperX<TradeOrderDO>()
.and(q -> q.eq(TradeOrderDO::getUserId, userId)
.eq(TradeOrderDO::getStatus, status))
.and(q -> q.eq(TradeOrderDO::getCombinationActivityId, activityId)
.or()
.eq(TradeOrderDO::getSeckillActivityId, activityId)
.or()
.eq(TradeOrderDO::getBargainActivityId, activityId))
);
}
}

View File

@ -40,6 +40,16 @@ public interface TradeOrderQueryService {
*/
TradeOrderDO getOrder(Long userId, Long id);
/**
* 获得指定用户指定活动指定状态的交易订单
*
* @param userId 用户编号
* @param activityId 活动编号
* @param status 订单状态
* @return 交易订单
*/
TradeOrderDO getActivityOrderByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status);
/**
* 获得订单列表
*
@ -95,7 +105,7 @@ public interface TradeOrderQueryService {
/**
* 会员在指定秒杀活动下用户购买的商品数量
*
* @param userId 用户编号
* @param userId 用户编号
* @param activityId 活动编号
* @return 秒杀商品数量
*/

View File

@ -72,6 +72,11 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
return order;
}
@Override
public TradeOrderDO getActivityOrderByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status) {
return tradeOrderMapper.selectByUserIdAndActivityIdAndStatus(userId, activityId, status);
}
@Override
public List<TradeOrderDO> getOrderList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {

View File

@ -742,9 +742,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
public void updateOrderAddress(TradeOrderUpdateAddressReqVO reqVO) {
// 校验交易订单
TradeOrderDO order = validateOrderExists(reqVO.getId());
// 发货后不允许修改
// TODO @puhui999只有待发货可以执行 update
if (TradeOrderStatusEnum.isDelivered(order.getStatus())) {
// 只有待发货状态才可以修改订单收货地址
if (!TradeOrderStatusEnum.isUndelivered(order.getStatus())) {
throw exception(ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED);
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.trade.service.order.handler;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.promotion.api.bargain.BargainActivityApi;
import cn.iocoder.yudao.module.promotion.api.bargain.BargainRecordApi;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
@ -28,6 +29,8 @@ public class TradeBargainHandler implements TradeOrderHandler {
if (TradeOrderTypeEnum.isBargain(order.getType())) {
return;
}
// 明确校验一下
Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
// 扣减砍价活动的库存
bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(),
@ -39,6 +42,8 @@ public class TradeBargainHandler implements TradeOrderHandler {
if (TradeOrderTypeEnum.isBargain(order.getType())) {
return;
}
// 明确校验一下
Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
// 记录砍价记录对应的订单编号
bargainRecordApi.updateBargainRecordOrderId(order.getBargainRecordId(), order.getId());
@ -49,7 +54,12 @@ public class TradeBargainHandler implements TradeOrderHandler {
if (TradeOrderTypeEnum.isBargain(order.getType())) {
return;
}
// TODO 芋艿取消订单时需要增加库存
// 明确校验一下
Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
// 恢复砍价活动的库存
bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(),
orderItems.get(0).getCount());
}
}

View File

@ -1,16 +1,24 @@
package cn.iocoder.yudao.module.trade.service.order.handler;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_CREATE_FAIL_EXIST_UNPAID;
/**
* 拼团订单 handler 接口实现类
*
@ -20,6 +28,14 @@ import java.util.List;
public class TradeCombinationHandler implements TradeOrderHandler {
@Resource
@Lazy
private TradeOrderUpdateService orderUpdateService;
@Resource
@Lazy
private TradeOrderQueryService orderQueryService;
@Resource
@Lazy
private CombinationRecordApi combinationRecordApi;
@Override
@ -34,28 +50,30 @@ public class TradeCombinationHandler implements TradeOrderHandler {
TradeOrderItemDO item = orderItems.get(0);
combinationRecordApi.validateCombinationRecord(order.getUserId(), order.getCombinationActivityId(),
order.getCombinationHeadId(), item.getSkuId(), item.getCount());
// TODO @puhui999这里还要限制下是不是已经 createOrder就是还没支付的时候重复下单了需要校验下不然的话一个拼团可以下多个单子了
// 校验该用户是否存在未支付的拼团活动订单就是还没支付的时候重复下单了需要校验下不然的话一个拼团可以下多个单子了
TradeOrderDO activityOrder = orderQueryService.getActivityOrderByUserIdAndActivityIdAndStatus(
order.getUserId(), order.getCombinationActivityId(), TradeOrderStatusEnum.UNPAID.getStatus());
if (activityOrder != null) {
throw exception(ORDER_CREATE_FAIL_EXIST_UNPAID);
}
}
@Override
public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
// 如果不是拼团订单则结束
// 1.如果不是拼团订单则结束
if (TradeOrderTypeEnum.isCombination(order.getType())) {
return;
}
Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品");
// 获取商品信息
// 2.获取商品信息
TradeOrderItemDO item = orderItems.get(0);
// 创建拼团记录
combinationRecordApi.createCombinationRecord(TradeOrderConvert.INSTANCE.convert(order, item));
}
@Override
public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
if (TradeOrderTypeEnum.isCombination(order.getType())) {
return;
}
// 2.1.创建拼团记录
KeyValue<Long, Long> recordIdAndHeadId = combinationRecordApi.createCombinationRecord(
TradeOrderConvert.INSTANCE.convert(order, item));
// 3.更新拼团相关信息到订单
orderUpdateService.updateOrderCombinationInfo(order.getId(), order.getCombinationActivityId(),
recordIdAndHeadId.getKey(), recordIdAndHeadId.getValue());
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.trade.service.order.handler;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
@ -25,9 +26,11 @@ public class TradeSeckillHandler implements TradeOrderHandler {
if (TradeOrderTypeEnum.isSeckill(order.getType())) {
return;
}
// 明确校验一下
Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品");
// 扣减秒杀活动的库存
seckillActivityApi.updateSeckillStock(order.getSeckillActivityId(),
seckillActivityApi.updateSeckillStockDecr(order.getSeckillActivityId(),
orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
}
@ -36,7 +39,12 @@ public class TradeSeckillHandler implements TradeOrderHandler {
if (TradeOrderTypeEnum.isSeckill(order.getType())) {
return;
}
// 明确校验一下
Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品");
// 恢复秒杀活动的库存
seckillActivityApi.updateSeckillStockIncr(order.getSeckillActivityId(),
orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
}
}