Commit 8d40f1d71a5e3793a79b6f6a05459d6807e88efb

Authored by owen
2 parents 2c10dde1 708af92a

Merge remote-tracking branch 'origin/feature/mall_product' into feature/mall_product

Showing 41 changed files with 1076 additions and 386 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
... ... @@ -72,6 +72,13 @@ public class CollectionUtils {
72 72 return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
73 73 }
74 74  
  75 + public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
  76 + return map.values()
  77 + .stream()
  78 + .flatMap(List::stream)
  79 + .collect(Collectors.toList());
  80 + }
  81 +
75 82 public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
76 83 if (CollUtil.isEmpty(from)) {
77 84 return new HashSet<>();
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApi.java
1 1 package cn.iocoder.yudao.module.promotion.api.combination;
2 2  
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
3 4 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
4 5 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
5 6  
... ... @@ -29,9 +30,9 @@ public interface CombinationRecordApi {
29 30 * 创建开团记录
30 31 *
31 32 * @param reqDTO 请求 DTO
32   - * @return 开团记录编号
  33 + * @return key 开团记录编号 value 团长编号
33 34 */
34   - Long createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
  35 + KeyValue<Long, Long> createCombinationRecord(@Valid CombinationRecordCreateReqDTO reqDTO);
35 36  
36 37 /**
37 38 * 查询拼团记录是否成功
... ...
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/api/seckill/SeckillActivityApi.java
... ... @@ -10,13 +10,22 @@ import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinResp
10 10 public interface SeckillActivityApi {
11 11  
12 12 /**
13   - * 更新秒杀库存
  13 + * 更新秒杀库存(减少)
14 14 *
15   - * @param id 活动编号
16   - * @param skuId sku 编号
17   - * @param count 数量
  15 + * @param id 活动编号
  16 + * @param skuId sku 编号
  17 + * @param count 数量(正数)
  18 + */
  19 + void updateSeckillStockDecr(Long id, Long skuId, Integer count);
  20 +
  21 + /**
  22 + * 更新秒杀库存(增加)
  23 + *
  24 + * @param id 活动编号
  25 + * @param skuId sku 编号
  26 + * @param count 数量(正数)
18 27 */
19   - void updateSeckillStock(Long id, Long skuId, Integer count);
  28 + void updateSeckillStockIncr(Long id, Long skuId, Integer count);
20 29  
21 30 /**
22 31 * 【下单前】校验是否参与秒杀活动
... ... @@ -24,8 +33,8 @@ public interface SeckillActivityApi {
24 33 * 如果校验失败,则抛出业务异常
25 34 *
26 35 * @param activityId 活动编号
27   - * @param skuId SKU 编号
28   - * @param count 数量
  36 + * @param skuId SKU 编号
  37 + * @param count 数量
29 38 * @return 秒杀信息
30 39 */
31 40 SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/combination/CombinationRecordApiImpl.java
1 1 package cn.iocoder.yudao.module.promotion.api.combination;
2 2  
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
3 4 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
4 5 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
5 6 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
... ... @@ -29,7 +30,7 @@ public class CombinationRecordApiImpl implements CombinationRecordApi {
29 30 }
30 31  
31 32 @Override
32   - public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
  33 + public KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
33 34 return recordService.createCombinationRecord(reqDTO);
34 35 }
35 36  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/api/seckill/SeckillActivityApiImpl.java
... ... @@ -18,8 +18,13 @@ public class SeckillActivityApiImpl implements SeckillActivityApi {
18 18 private SeckillActivityService activityService;
19 19  
20 20 @Override
21   - public void updateSeckillStock(Long id, Long skuId, Integer count) {
22   - activityService.updateSeckillStock(id, skuId, count);
  21 + public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
  22 + activityService.updateSeckillStockDecr(id, skuId, count);
  23 + }
  24 +
  25 + @Override
  26 + public void updateSeckillStockIncr(Long id, Long skuId, Integer count) {
  27 + activityService.updateSeckillStockIncr(id, skuId, count);
23 28 }
24 29  
25 30 @Override
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/CombinationRecordController.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.combination;
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordPageItemRespVO;
  6 +import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
6 7 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
7 8 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordSummaryVO;
8 9 import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
... ... @@ -49,15 +50,27 @@ public class CombinationRecordController {
49 50 return success(CombinationActivityConvert.INSTANCE.convert(recordPage, activities));
50 51 }
51 52  
  53 + @GetMapping("/page-by-headId")
  54 + @Operation(summary = "获得拼团记录分页")
  55 + @PreAuthorize("@ss.hasPermission('promotion:combination-record:query')")
  56 + public CommonResult<PageResult<CombinationRecordPageItemRespVO>> getBargainRecordPage(@Valid CombinationRecordReqPage2VO pageVO) {
  57 + // 包含团长和团员的分页记录
  58 + PageResult<CombinationRecordDO> recordPage = combinationRecordService.getCombinationRecordPage2(pageVO);
  59 + List<CombinationActivityDO> activities = combinationActivityService.getCombinationActivityListByIds(
  60 + convertSet(recordPage.getList(), CombinationRecordDO::getActivityId));
  61 + return success(CombinationActivityConvert.INSTANCE.convert(recordPage, activities));
  62 + }
  63 +
52 64 @GetMapping("/get-summary")
53 65 @Operation(summary = "获得拼团记录的概要信息", description = "用于拼团记录页面展示")
54 66 @PreAuthorize("@ss.hasPermission('promotion:combination-record:query')")
55 67 public CommonResult<CombinationRecordSummaryVO> getCombinationRecordSummary() {
56 68 CombinationRecordSummaryVO summaryVO = new CombinationRecordSummaryVO();
57   - summaryVO.setUserCount(combinationRecordService.getCombinationRecordCount(null, null)); // 获取所有拼团记录
  69 + summaryVO.setUserCount(combinationRecordService.getCombinationRecordCount(null, null, null)); // 获取拼团用户参与数量
58 70 summaryVO.setSuccessCount(combinationRecordService.getCombinationRecordCount( // 获取成团记录
59   - CombinationRecordStatusEnum.SUCCESS.getStatus(), null));
60   - summaryVO.setVirtualGroupCount(combinationRecordService.getCombinationRecordCount(null, Boolean.TRUE));// 获取虚拟成团记录
  71 + CombinationRecordStatusEnum.SUCCESS.getStatus(), null, CombinationRecordDO.HEAD_ID_GROUP));
  72 + summaryVO.setVirtualGroupCount(combinationRecordService.getCombinationRecordCount(// 获取虚拟成团记录
  73 + null, Boolean.TRUE, CombinationRecordDO.HEAD_ID_GROUP));
61 74 return success(summaryVO);
62 75 }
63 76  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/combination/vo/recrod/CombinationRecordReqPage2VO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +import javax.validation.constraints.NotNull;
  10 +
  11 +@Schema(description = "管理后台 - 拼团记录分页 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class CombinationRecordReqPage2VO extends PageParam {
  16 +
  17 + @Schema(description = "团长编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @NotNull(message = "团长编号不能为空")
  19 + private Long headId;
  20 +
  21 +}
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.http 0 → 100644
  1 +### /promotion/activity/list-by-spu-ids 获得多个商品,近期参与的每个活动
  2 +GET {{appApi}}/promotion/activity/list-by-spu-ids?spuIds=222&spuIds=633
  3 +Authorization: Bearer {{appToken}}
  4 +Content-Type: application/json
  5 +tenant-id: {{appTenentId}}
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainRecordController.java
... ... @@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.Operation;
27 27 import io.swagger.v3.oas.annotations.Parameter;
28 28 import io.swagger.v3.oas.annotations.Parameters;
29 29 import io.swagger.v3.oas.annotations.tags.Tag;
  30 +import org.springframework.context.annotation.Lazy;
30 31 import org.springframework.validation.annotation.Validated;
31 32 import org.springframework.web.bind.annotation.*;
32 33  
... ... @@ -46,18 +47,23 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
46 47 public class AppBargainRecordController {
47 48  
48 49 @Resource
  50 + private BargainHelpService bargainHelpService;
  51 + @Resource
  52 + @Lazy
49 53 private BargainRecordService bargainRecordService;
50 54 @Resource
  55 + @Lazy
51 56 private BargainActivityService bargainActivityService;
52   - @Resource
53   - private BargainHelpService bargainHelpService;
  57 +
54 58  
55 59 @Resource
  60 + private TradeOrderApi tradeOrderApi;
  61 + @Resource
  62 + @Lazy
56 63 private MemberUserApi memberUserApi;
57 64 @Resource
  65 + @Lazy
58 66 private ProductSpuApi productSpuApi;
59   - @Resource
60   - private TradeOrderApi tradeOrderApi;
61 67  
62 68 @GetMapping("/get-summary")
63 69 @Operation(summary = "获得砍价记录的概要信息", description = "用于小程序首页")
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java
... ... @@ -46,8 +46,8 @@ public class AppCombinationRecordController {
46 46 @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
47 47 public CommonResult<AppCombinationRecordSummaryRespVO> getCombinationRecordSummary() {
48 48 AppCombinationRecordSummaryRespVO summary = new AppCombinationRecordSummaryRespVO();
49   - // 1. 获得拼团记录数量
50   - Long count = combinationRecordService.getCombinationRecordCount(null, null);
  49 + // 1. 获得拼团参与用户数量
  50 + Long count = combinationRecordService.getCombinationRecordCount(null, null, null);
51 51 if (count == 0) {
52 52 summary.setAvatars(Collections.emptyList());
53 53 summary.setUserCount(count);
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/convert/combination/CombinationActivityConvert.java
... ... @@ -28,7 +28,7 @@ import org.mapstruct.Mapping;
28 28 import org.mapstruct.Mappings;
29 29 import org.mapstruct.factory.Mappers;
30 30  
31   -import java.time.LocalDateTime;
  31 +import java.util.ArrayList;
32 32 import java.util.List;
33 33 import java.util.Map;
34 34  
... ... @@ -114,9 +114,6 @@ public interface CombinationActivityConvert {
114 114 ProductSpuRespDTO spu, ProductSkuRespDTO sku) {
115 115 return convert(reqDTO).setVirtualGroup(false)
116 116 .setStatus(CombinationRecordStatusEnum.IN_PROGRESS.getStatus()) // 创建后默认状态为进行中
117   - .setStartTime(LocalDateTime.now()) // TODO @puhui999:想了下,这个 startTime 应该是团长的;
118   - // TODO @puhui999:有团长的情况下,expireTime 应该是团长的;
119   - .setExpireTime(activity.getStartTime().plusHours(activity.getLimitDuration()))
120 117 .setUserSize(activity.getUserSize()).setUserCount(1) // 默认就是 1 插入后会接着更新一次所有的拼团记录
121 118 // 用户信息
122 119 .setNickname(user.getNickname()).setAvatar(user.getAvatar())
... ... @@ -200,4 +197,35 @@ public interface CombinationActivityConvert {
200 197 return respVO;
201 198 }
202 199  
  200 + /**
  201 + * 转换生成虚拟成团虚拟记录
  202 + *
  203 + * @param virtualGroupHeadRecords 虚拟成团团长记录列表
  204 + * @return 虚拟记录列表
  205 + */
  206 + default List<CombinationRecordDO> convertVirtualGroupList(List<CombinationRecordDO> virtualGroupHeadRecords) {
  207 + List<CombinationRecordDO> createRecords = new ArrayList<>();
  208 + virtualGroupHeadRecords.forEach(headRecord -> {
  209 + // 计算需要创建的虚拟成团记录数量
  210 + int count = headRecord.getUserSize() - headRecord.getUserCount();
  211 + for (int i = 0; i < count; i++) {
  212 + // 基础信息和团长保持一致
  213 + CombinationRecordDO newRecord = new CombinationRecordDO().setActivityId(headRecord.getActivityId())
  214 + .setCombinationPrice(headRecord.getCombinationPrice()).setSpuId(headRecord.getSpuId()).setSpuName(headRecord.getSpuName())
  215 + .setPicUrl(headRecord.getPicUrl()).setSkuId(headRecord.getSkuId()).setHeadId(headRecord.getId())
  216 + .setStatus(headRecord.getStatus()) // 状态保持和创建时一致,创建完成后会接着处理
  217 + .setVirtualGroup(headRecord.getVirtualGroup()).setExpireTime(headRecord.getExpireTime())
  218 + .setStartTime(headRecord.getStartTime()).setUserSize(headRecord.getUserSize()).setUserCount(headRecord.getUserCount());
  219 + // 虚拟信息
  220 + newRecord.setCount(0);
  221 + newRecord.setUserId(0L);
  222 + newRecord.setNickname("");
  223 + newRecord.setAvatar("");
  224 + newRecord.setOrderId(0L);
  225 + createRecords.add(newRecord);
  226 + }
  227 + });
  228 + return createRecords;
  229 + }
  230 +
203 231 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java
... ... @@ -6,13 +6,15 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 7 import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.BargainActivityPageReqVO;
8 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
  9 +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
9 10 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
10 11 import org.apache.ibatis.annotations.Mapper;
11   -import org.apache.ibatis.annotations.Select;
  12 +import org.apache.ibatis.annotations.Param;
12 13  
13 14 import java.time.LocalDateTime;
14 15 import java.util.Collection;
15 16 import java.util.List;
  17 +import java.util.Map;
16 18  
17 19 /**
18 20 * 砍价活动 Mapper
... ... @@ -85,25 +87,25 @@ public interface BargainActivityMapper extends BaseMapperX&lt;BargainActivityDO&gt; {
85 87 .last("LIMIT " + count));
86 88 }
87 89  
88   - // TODO @puhui999:一个商品,在统一时间,不会参与多个活动;so 是不是不用 inner join 哈?
89   - // PS:如果可以参与多个,其实可以这样写 select * from promotion_bargain_activity group by spu_id ORDER BY create_time DESC;通过 group 来过滤
90 90 /**
91   - * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
  91 + * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
92 92 *
93 93 * @param spuIds spu 编号
94 94 * @param status 状态
95   - * @return 砍价活动列表
  95 + * @return 包含 spuId 和 activityId 的 map 对象列表
96 96 */
97   - @Select("SELECT p1.* " +
98   - "FROM promotion_bargain_activity p1 " +
99   - "INNER JOIN ( " +
100   - " SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
101   - " FROM promotion_bargain_activity " +
102   - " WHERE spu_id IN #{spuIds} " +
103   - " GROUP BY spu_id " +
104   - ") p2 " +
105   - "ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
106   - "ORDER BY p1.create_time DESC;")
107   - List<BargainActivityDO> selectListBySpuIds(Collection<Long> spuIds, Integer status);
  97 + default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
  98 + return selectMaps(new QueryWrapper<BargainActivityDO>()
  99 + .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
  100 + .in("spu_id", spuIds)
  101 + .eq("status", status)
  102 + .groupBy("spu_id"));
  103 + }
  104 +
  105 + default List<BargainActivityDO> selectListByIds(Collection<Long> ids) {
  106 + return selectList(new LambdaQueryWrapperX<BargainActivityDO>()
  107 + .in(BargainActivityDO::getId, ids)
  108 + .orderByDesc(BargainActivityDO::getCreateTime));
  109 + }
108 110  
109 111 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationActivityMapper.java
... ... @@ -6,12 +6,13 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 7 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO;
8 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
  9 +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
9 10 import org.apache.ibatis.annotations.Mapper;
10 11 import org.apache.ibatis.annotations.Param;
11   -import org.apache.ibatis.annotations.Select;
12 12  
13 13 import java.util.Collection;
14 14 import java.util.List;
  15 +import java.util.Map;
15 16  
16 17 /**
17 18 * 拼团活动 Mapper
... ... @@ -43,24 +44,24 @@ public interface CombinationActivityMapper extends BaseMapperX&lt;CombinationActivi
43 44 .last("LIMIT " + count));
44 45 }
45 46  
46   - // TODO @puhui999:类似 BargainActivityMapper
47 47 /**
48   - * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
49   - *
  48 + * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
50 49 * @param spuIds spu 编号
51 50 * @param status 状态
52   - * @return 拼团活动列表
  51 + * @return 包含 spuId 和 activityId 的 map 对象列表
53 52 */
54   - @Select("SELECT p1.* " +
55   - "FROM promotion_combination_activity p1 " +
56   - "INNER JOIN ( " +
57   - " SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
58   - " FROM promotion_combination_activity " +
59   - " WHERE spu_id IN #{spuIds} " +
60   - " GROUP BY spu_id " +
61   - ") p2 " +
62   - "ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
63   - "ORDER BY p1.create_time DESC;")
64   - List<CombinationActivityDO> selectListBySpuIds(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status);
  53 + default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
  54 + return selectMaps(new QueryWrapper<CombinationActivityDO>()
  55 + .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
  56 + .in("spu_id", spuIds)
  57 + .eq("status", status)
  58 + .groupBy("spu_id"));
  59 + }
  60 +
  61 + default List<CombinationActivityDO> selectListByIds(Collection<Long> ids) {
  62 + return selectList(new LambdaQueryWrapperX<CombinationActivityDO>()
  63 + .in(CombinationActivityDO::getId, ids)
  64 + .orderByDesc(CombinationActivityDO::getCreateTime));
  65 + }
65 66  
66 67 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java
... ... @@ -6,11 +6,13 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
7 7 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
8 8 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  9 +import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
9 10 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
10 11 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
11 12 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
12 13 import org.apache.ibatis.annotations.Mapper;
13 14  
  15 +import java.time.LocalDateTime;
14 16 import java.util.Collection;
15 17 import java.util.Collections;
16 18 import java.util.List;
... ... @@ -100,13 +102,40 @@ public interface CombinationRecordMapper extends BaseMapperX&lt;CombinationRecordDO
100 102 .betweenIfPresent(CombinationRecordDO::getCreateTime, pageVO.getCreateTime()));
101 103 }
102 104  
103   - // TODO @puhui999:这个最好把 headId 也作为一个参数;因为有个要求 userCount,它要 DISTINCT 下;整体可以参考 selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId
104   - default Long selectCountByHeadAndStatusAndVirtualGroup(Integer status, Boolean virtualGroup) {
105   - return selectCount(new LambdaQueryWrapperX<CombinationRecordDO>()
106   - .eq(status != null || virtualGroup != null,
107   - CombinationRecordDO::getHeadId, CombinationRecordDO.HEAD_ID_GROUP) // 统计团信息则指定团长
108   - .eqIfPresent(CombinationRecordDO::getStatus, status)
109   - .eqIfPresent(CombinationRecordDO::getVirtualGroup, virtualGroup));
  105 + default PageResult<CombinationRecordDO> selectPage(CombinationRecordReqPage2VO pageVO) {
  106 + return selectPage(pageVO, new LambdaQueryWrapperX<CombinationRecordDO>()
  107 + .eq(CombinationRecordDO::getId, pageVO.getHeadId())
  108 + .or()
  109 + .eq(CombinationRecordDO::getHeadId, pageVO.getHeadId()));
  110 + }
  111 +
  112 + /**
  113 + * 查询指定条件的记录数
  114 + * 如果参数都为 null 时则查询用户拼团记录(DISTINCT 去重),也就是说查询会员表中的用户有多少人参与过拼团活动每个人只统计一次
  115 + *
  116 + * @param status 状态,可为 null
  117 + * @param virtualGroup 是否虚拟成团,可为 null
  118 + * @param headId 团长编号,可为 null
  119 + * @return 记录数
  120 + */
  121 + default Long selectCountByHeadAndStatusAndVirtualGroup(Integer status, Boolean virtualGroup, Long headId) {
  122 + return selectCount(new QueryWrapper<CombinationRecordDO>()
  123 + .select(status == null && virtualGroup == null && headId == null, "DISTINCT (user_id)")
  124 + .eq(status != null, "status", status)
  125 + .eq(virtualGroup != null, "virtual_group", virtualGroup)
  126 + .eq(headId != null, "head_id", headId)
  127 + .groupBy("user_id"));
  128 + }
  129 +
  130 + default List<CombinationRecordDO> selectListByHeadIdAndStatusAndExpireTimeLt(Long headId, Integer status, LocalDateTime dateTime) {
  131 + return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()
  132 + .eq(CombinationRecordDO::getHeadId, headId)
  133 + .eq(CombinationRecordDO::getStatus, status)
  134 + .lt(CombinationRecordDO::getExpireTime, dateTime));
  135 + }
  136 +
  137 + default List<CombinationRecordDO> selectListByHeadIds(Collection<Long> headIds) {
  138 + return selectList(new LambdaQueryWrapperX<CombinationRecordDO>().in(CombinationRecordDO::getHeadId, headIds));
110 139 }
111 140  
112 141 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
1 1 package cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.hutool.core.util.ObjectUtil;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
... ... @@ -7,12 +8,14 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 8 import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO;
8 9 import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
9 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
  11 +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
10 12 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
11 13 import org.apache.ibatis.annotations.Mapper;
12   -import org.apache.ibatis.annotations.Select;
  14 +import org.apache.ibatis.annotations.Param;
13 15  
14 16 import java.util.Collection;
15 17 import java.util.List;
  18 +import java.util.Map;
16 19  
17 20 /**
18 21 * 秒杀活动 Mapper
... ... @@ -37,18 +40,32 @@ public interface SeckillActivityMapper extends BaseMapperX&lt;SeckillActivityDO&gt; {
37 40 }
38 41  
39 42 /**
40   - * 更新活动库存
  43 + * 更新活动库存(减少)
41 44 *
42 45 * @param id 活动编号
43   - * @param count 扣减的库存数量
  46 + * @param count 扣减的库存数量(正数)
44 47 * @return 影响的行数
45 48 */
46   - default int updateStock(Long id, int count) {
  49 + default int updateStockDecr(Long id, int count) {
  50 + Assert.isTrue(count > 0);
47 51 return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
48 52 .eq(SeckillActivityDO::getId, id)
49   - .gt(SeckillActivityDO::getTotalStock, 0)
50   - .setSql("stock = stock + " + count)
51   - .setSql("total_stock = total_stock - " + count));
  53 + .gt(SeckillActivityDO::getStock, count)
  54 + .setSql("stock = stock - " + count));
  55 + }
  56 +
  57 + /**
  58 + * 更新活动库存(增加)
  59 + *
  60 + * @param id 活动编号
  61 + * @param count 增加的库存数量(正数)
  62 + * @return 影响的行数
  63 + */
  64 + default int updateStockIncr(Long id, int count) {
  65 + Assert.isTrue(count > 0);
  66 + return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
  67 + .eq(SeckillActivityDO::getId, id)
  68 + .setSql("stock = stock + " + count));
52 69 }
53 70  
54 71 default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
... ... @@ -58,24 +75,25 @@ public interface SeckillActivityMapper extends BaseMapperX&lt;SeckillActivityDO&gt; {
58 75 .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
59 76 }
60 77  
61   - // TODO @puhui999:类似 BargainActivityMapper
62 78 /**
63   - * 获取指定 spu 编号最近参加的活动,每个 spuId 只返回一条记录
  79 + * 查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
64 80 *
65 81 * @param spuIds spu 编号
66 82 * @param status 状态
67   - * @return 秒杀活动列表
  83 + * @return 包含 spuId 和 activityId 的 map 对象列表
68 84 */
69   - @Select("SELECT p1.* " +
70   - "FROM promotion_seckill_activity p1 " +
71   - "INNER JOIN ( " +
72   - " SELECT spu_id, MAX(DISTINCT(create_time)) AS max_create_time " +
73   - " FROM promotion_seckill_activity " +
74   - " WHERE spu_id IN #{spuIds} " +
75   - " GROUP BY spu_id " +
76   - ") p2 " +
77   - "ON p1.spu_id = p2.spu_id AND p1.create_time = p2.max_create_time AND p1.status = #{status} " +
78   - "ORDER BY p1.create_time DESC;")
79   - List<SeckillActivityDO> selectListBySpuIds(Collection<Long> spuIds, Integer status);
  85 + default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
  86 + return selectMaps(new QueryWrapper<SeckillActivityDO>()
  87 + .select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
  88 + .in("spu_id", spuIds)
  89 + .eq("status", status)
  90 + .groupBy("spu_id"));
  91 + }
  92 +
  93 + default List<SeckillActivityDO> selectListByIds(Collection<Long> ids) {
  94 + return selectList(new LambdaQueryWrapperX<SeckillActivityDO>()
  95 + .in(SeckillActivityDO::getId, ids)
  96 + .orderByDesc(SeckillActivityDO::getCreateTime));
  97 + }
80 98  
81 99 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillProductMapper.java
1 1 package cn.iocoder.yudao.module.promotion.dal.mysql.seckill.seckillactivity;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 5 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
5 6 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
... ... @@ -30,17 +31,32 @@ public interface SeckillProductMapper extends BaseMapperX&lt;SeckillProductDO&gt; {
30 31 }
31 32  
32 33 /**
33   - * 更新活动库存
  34 + * 更新活动库存(减少)
34 35 *
35 36 * @param id 活动编号
36   - * @param count 扣减的库存数量
  37 + * @param count 扣减的库存数量(减少库存)
37 38 * @return 影响的行数
38 39 */
39   - default int updateStock(Long id, int count) {
  40 + default int updateStockDecr(Long id, int count) {
  41 + Assert.isTrue(count > 0);
40 42 return update(null, new LambdaUpdateWrapper<SeckillProductDO>()
41 43 .eq(SeckillProductDO::getId, id)
42   - .gt(SeckillProductDO::getStock, count)
  44 + .ge(SeckillProductDO::getStock, count)
43 45 .setSql("stock = stock - " + count));
44 46 }
45 47  
  48 + /**
  49 + * 更新活动库存(增加)
  50 + *
  51 + * @param id 活动编号
  52 + * @param count 需要增加的库存(增加库存)
  53 + * @return 影响的行数
  54 + */
  55 + default int updateStockIncr(Long id, int count) {
  56 + Assert.isTrue(count > 0);
  57 + return update(null, new LambdaUpdateWrapper<SeckillProductDO>()
  58 + .eq(SeckillProductDO::getId, id)
  59 + .setSql("stock = stock + " + count));
  60 + }
  61 +
46 62 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/combination/CombinationRecordExpireJob.java 0 → 100644
  1 +package cn.iocoder.yudao.module.promotion.job.combination;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  5 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
  6 +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
  7 +import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import javax.annotation.Resource;
  11 +
  12 +/**
  13 + * 拼团过期 Job
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +@Component
  18 +public class CombinationRecordExpireJob implements JobHandler {
  19 +
  20 + @Resource
  21 + private CombinationRecordService combinationRecordService;
  22 +
  23 + @Override
  24 + @TenantJob
  25 + public String execute(String param) throws Exception {
  26 + KeyValue<Integer, Integer> keyValue = combinationRecordService.expireCombinationRecord();
  27 + return StrUtil.format("过期拼团 {} 个, 虚拟成团 {} 个", keyValue.getKey(), keyValue.getValue());
  28 + }
  29 +
  30 +}
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/bargain/BargainActivityServiceImpl.java
1 1 package cn.iocoder.yudao.module.promotion.service.bargain;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
3 5 import cn.hutool.core.util.ObjUtil;
4 6 import cn.hutool.core.util.ObjectUtil;
5 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
... ... @@ -20,12 +22,11 @@ import org.springframework.validation.annotation.Validated;
20 22  
21 23 import javax.annotation.Resource;
22 24 import java.time.LocalDateTime;
23   -import java.util.Collection;
24   -import java.util.List;
25   -import java.util.Set;
  25 +import java.util.*;
26 26  
27 27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
28 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
  29 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
29 30 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
30 31 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
31 32  
... ... @@ -84,11 +85,16 @@ public class BargainActivityServiceImpl implements BargainActivityService {
84 85  
85 86 @Override
86 87 public void updateBargainActivityStock(Long id, Integer count) {
87   - // 更新库存。如果更新失败,则抛出异常
88   - int updateCount = bargainActivityMapper.updateStock(id, count);
89   - if (updateCount == 0) {
90   - throw exception(BARGAIN_ACTIVITY_STOCK_NOT_ENOUGH);
  88 + if (count < 0) {
  89 + // 更新库存。如果更新失败,则抛出异常
  90 + int updateCount = bargainActivityMapper.updateStock(id, count);
  91 + if (updateCount == 0) {
  92 + throw exception(BARGAIN_ACTIVITY_STOCK_NOT_ENOUGH);
  93 + }
  94 + } else if (count > 0) {
  95 + bargainActivityMapper.updateStock(id, count);
91 96 }
  97 +
92 98 }
93 99  
94 100 private void validateBargainConflict(Long spuId, Long activityId) {
... ... @@ -139,7 +145,7 @@ public class BargainActivityServiceImpl implements BargainActivityService {
139 145  
140 146 @Override
141 147 public List<BargainActivityDO> getBargainActivityList(Set<Long> ids) {
142   - return bargainActivityMapper.selectBatchIds(ids);
  148 + return bargainActivityMapper.selectBatchIds(ids);
143 149 }
144 150  
145 151 @Override
... ... @@ -178,7 +184,13 @@ public class BargainActivityServiceImpl implements BargainActivityService {
178 184  
179 185 @Override
180 186 public List<BargainActivityDO> getBargainActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
181   - return bargainActivityMapper.selectListBySpuIds(spuIds, status);
  187 + // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
  188 + List<Map<String, Object>> spuIdAndActivityIdMaps = bargainActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
  189 + if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
  190 + return Collections.emptyList();
  191 + }
  192 + // 2.查询活动详情
  193 + return bargainActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
182 194 }
183 195  
184 196 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java
1 1 package cn.iocoder.yudao.module.promotion.service.combination;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
4 5 import cn.hutool.core.util.ObjectUtil;
5 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 7 import cn.iocoder.yudao.framework.common.pojo.PageParam;
... ... @@ -25,12 +26,12 @@ import org.springframework.validation.annotation.Validated;
25 26  
26 27 import javax.annotation.Resource;
27 28 import java.util.Collection;
  29 +import java.util.Collections;
28 30 import java.util.List;
29 31 import java.util.Map;
30 32  
31 33 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
32   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
33   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
  34 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
34 35 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
35 36 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS;
36 37 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
... ... @@ -228,7 +229,13 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
228 229  
229 230 @Override
230 231 public List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
231   - return combinationActivityMapper.selectListBySpuIds(spuIds, status);
  232 + // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
  233 + List<Map<String, Object>> spuIdAndActivityIdMaps = combinationActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
  234 + if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
  235 + return Collections.emptyList();
  236 + }
  237 + // 2.查询活动详情
  238 + return combinationActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
232 239 }
233 240  
234 241 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
6 6 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
  7 +import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
7 8 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
8 9 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
9 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
... ... @@ -49,9 +50,9 @@ public interface CombinationRecordService {
49 50 * 创建拼团记录
50 51 *
51 52 * @param reqDTO 创建信息
52   - * @return 开团记录编号
  53 + * @return key 开团记录编号 value 团长编号
53 54 */
54   - Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
  55 + KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO);
55 56  
56 57 /**
57 58 * 获得拼团记录
... ... @@ -90,9 +91,10 @@ public interface CombinationRecordService {
90 91 *
91 92 * @param status 状态-允许为空
92 93 * @param virtualGroup 是否虚拟成团-允许为空
  94 + * @param headId 团长编号,允许空。目的 headId 设置为 {@link CombinationRecordDO#HEAD_ID_GROUP} 时,可以设置
93 95 * @return 记录数
94 96 */
95   - Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup);
  97 + Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup, @Nullable Long headId);
96 98  
97 99 /**
98 100 * 获取最近的 count 条拼团记录
... ... @@ -137,6 +139,15 @@ public interface CombinationRecordService {
137 139 PageResult<CombinationRecordDO> getCombinationRecordPage(CombinationRecordReqPageVO pageVO);
138 140  
139 141 /**
  142 + * 获取拼团记录分页数据(通过团长查询)
  143 + *
  144 + * @param pageVO 分页请求
  145 + * @return 拼团记录分页数据(包括团长的)
  146 + */
  147 + PageResult<CombinationRecordDO> getCombinationRecordPage2(CombinationRecordReqPage2VO pageVO);
  148 +
  149 +
  150 + /**
140 151 * 【拼团活动】获得拼团记录数量 Map
141 152 *
142 153 * @param activityIds 活动记录编号数组
... ... @@ -167,5 +178,11 @@ public interface CombinationRecordService {
167 178 */
168 179 void cancelCombinationRecord(Long userId, Long id, Long headId);
169 180  
  181 + /**
  182 + * 处理过期拼团
  183 + *
  184 + * @return key 过期拼团数量, value 虚拟成团数量
  185 + */
  186 + KeyValue<Integer, Integer> expireCombinationRecord();
170 187  
171 188 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.promotion.service.combination;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.extra.spring.SpringUtil;
5 6 import cn.iocoder.yudao.framework.common.core.KeyValue;
6 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
... ... @@ -13,6 +14,7 @@ import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
13 14 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
14 15 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
15 16 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO;
  17 +import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPage2VO;
16 18 import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.recrod.CombinationRecordReqPageVO;
17 19 import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
18 20 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
... ... @@ -22,17 +24,18 @@ import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecord
22 24 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
23 25 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
24 26 import org.springframework.context.annotation.Lazy;
  27 +import org.springframework.scheduling.annotation.Async;
25 28 import org.springframework.stereotype.Service;
26 29 import org.springframework.transaction.annotation.Transactional;
27 30 import org.springframework.validation.annotation.Validated;
28 31  
29 32 import javax.annotation.Nullable;
30 33 import javax.annotation.Resource;
  34 +import java.time.LocalDateTime;
31 35 import java.util.*;
32 36  
33 37 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
34   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.findFirst;
35   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
  38 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
36 39 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.afterNow;
37 40 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeNow;
38 41 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
... ... @@ -52,7 +55,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
52 55 @Lazy
53 56 private CombinationActivityService combinationActivityService;
54 57 @Resource
55   - private CombinationRecordMapper recordMapper;
  58 + private CombinationRecordMapper combinationRecordMapper;
56 59  
57 60 @Resource
58 61 private MemberUserApi memberUserApi;
... ... @@ -62,6 +65,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
62 65 @Resource
63 66 @Lazy
64 67 private ProductSkuApi productSkuApi;
  68 +
65 69 @Resource
66 70 private TradeOrderApi tradeOrderApi;
67 71  
... ... @@ -74,12 +78,12 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
74 78  
75 79 // 更新状态
76 80 record.setStatus(status);
77   - recordMapper.updateById(record);
  81 + combinationRecordMapper.updateById(record);
78 82 }
79 83  
80 84 private CombinationRecordDO validateCombinationRecord(Long userId, Long orderId) {
81 85 // 校验拼团是否存在
82   - CombinationRecordDO recordDO = recordMapper.selectByUserIdAndOrderId(userId, orderId);
  86 + CombinationRecordDO recordDO = combinationRecordMapper.selectByUserIdAndOrderId(userId, orderId);
83 87 if (recordDO == null) {
84 88 throw exception(COMBINATION_RECORD_NOT_EXISTS);
85 89 }
... ... @@ -108,7 +112,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
108 112 // 2. 父拼团是否存在,是否已经满了
109 113 if (headId != null) {
110 114 // 2.1. 查询进行中的父拼团
111   - CombinationRecordDO record = recordMapper.selectByHeadId(headId, CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
  115 + CombinationRecordDO record = combinationRecordMapper.selectByHeadId(headId, CombinationRecordStatusEnum.IN_PROGRESS.getStatus());
112 116 if (record == null) {
113 117 throw exception(COMBINATION_RECORD_HEAD_NOT_EXISTS);
114 118 }
... ... @@ -143,7 +147,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
143 147 }
144 148  
145 149 // 6.1 校验是否有拼团记录
146   - List<CombinationRecordDO> recordList = recordMapper.selectListByUserIdAndActivityId(userId, activityId);
  150 + List<CombinationRecordDO> recordList = combinationRecordMapper.selectListByUserIdAndActivityId(userId, activityId);
147 151 recordList.removeIf(record -> CombinationRecordStatusEnum.isFailed(record.getStatus())); // 取消的订单,不算数
148 152 if (CollUtil.isEmpty(recordList)) { // 如果为空,说明可以参与,直接返回
149 153 return new KeyValue<>(activity, product);
... ... @@ -164,32 +168,36 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
164 168  
165 169 @Override
166 170 @Transactional(rollbackFor = Exception.class)
167   - public Long createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
168   - // 1. 校验拼团活动
  171 + public KeyValue<Long, Long> createCombinationRecord(CombinationRecordCreateReqDTO reqDTO) {
  172 + // 1.校验拼团活动
169 173 KeyValue<CombinationActivityDO, CombinationProductDO> keyValue = validateCombinationRecord(reqDTO.getUserId(),
170 174 reqDTO.getActivityId(), reqDTO.getHeadId(), reqDTO.getSkuId(), reqDTO.getCount());
171 175  
172   - // 2.1 组合数据创建拼团记录
  176 + // 2.组合数据创建拼团记录
173 177 MemberUserRespDTO user = memberUserApi.getUser(reqDTO.getUserId());
174 178 ProductSpuRespDTO spu = productSpuApi.getSpu(reqDTO.getSpuId());
175 179 ProductSkuRespDTO sku = productSkuApi.getSku(reqDTO.getSkuId());
176 180 CombinationRecordDO record = CombinationActivityConvert.INSTANCE.convert(reqDTO, keyValue.getKey(), user, spu, sku);
177   - // 2.2 如果是团长需要设置 headId 为 CombinationRecordDO#HEAD_ID_GROUP
  181 + // 2.1.如果是团长需要设置 headId 为 CombinationRecordDO#HEAD_ID_GROUP
178 182 if (record.getHeadId() == null) {
179   - record.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
  183 + record.setStartTime(LocalDateTime.now())
  184 + .setExpireTime(keyValue.getKey().getStartTime().plusHours(keyValue.getKey().getLimitDuration()))
  185 + .setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
  186 + } else {
  187 + // 2.2.有团长的情况下需要设置开始时间和过期时间为团长的
  188 + CombinationRecordDO headRecord = combinationRecordMapper.selectByHeadId(record.getHeadId(),
  189 + CombinationRecordStatusEnum.IN_PROGRESS.getStatus()); // 查询进行中的父拼团
  190 + record.setStartTime(headRecord.getStartTime()).setExpireTime(headRecord.getExpireTime());
180 191 }
181   - recordMapper.insert(record);
  192 + combinationRecordMapper.insert(record);
182 193  
183 194 if (ObjUtil.equal(CombinationRecordDO.HEAD_ID_GROUP, record.getHeadId())) {
184   - return record.getId();
  195 + return new KeyValue<>(record.getId(), record.getHeadId());
185 196 }
186 197  
187   - // TODO @puhui:是不是这里的更新,放到 order 模块那;支付完成后;
188   - // 4、更新拼团相关信息到订单
189   - tradeOrderApi.updateOrderCombinationInfo(record.getOrderId(), record.getActivityId(), record.getId(), record.getHeadId());
190   - // 4、更新拼团记录
  198 + // 3、更新拼团记录
191 199 updateCombinationRecordWhenCreate(reqDTO.getHeadId(), keyValue.getKey());
192   - return record.getId();
  200 + return new KeyValue<>(record.getId(), record.getHeadId());
193 201 }
194 202  
195 203 /**
... ... @@ -204,31 +212,33 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
204 212 if (CollUtil.isEmpty(records)) {
205 213 return;
206 214 }
207   - CombinationRecordDO headRecord = recordMapper.selectById(headId);
  215 + CombinationRecordDO headRecord = combinationRecordMapper.selectById(headId);
208 216  
209 217 // 2. 批量更新记录
210 218 List<CombinationRecordDO> updateRecords = new ArrayList<>();
211 219 records.add(headRecord); // 加入团长,团长也需要更新
212 220 boolean isFull = records.size() >= activity.getUserSize();
  221 + LocalDateTime now = LocalDateTime.now();
213 222 records.forEach(item -> {
214 223 CombinationRecordDO updateRecord = new CombinationRecordDO();
215 224 updateRecord.setId(item.getId()).setUserCount(records.size());
216 225 if (isFull) {
217 226 updateRecord.setStatus(CombinationRecordStatusEnum.SUCCESS.getStatus());
  227 + updateRecord.setEndTime(now);
218 228 }
219 229 updateRecords.add(updateRecord);
220 230 });
221   - recordMapper.updateBatch(updateRecords);
  231 + combinationRecordMapper.updateBatch(updateRecords);
222 232 }
223 233  
224 234 @Override
225 235 public CombinationRecordDO getCombinationRecord(Long userId, Long orderId) {
226   - return recordMapper.selectByUserIdAndOrderId(userId, orderId);
  236 + return combinationRecordMapper.selectByUserIdAndOrderId(userId, orderId);
227 237 }
228 238  
229 239 @Override
230 240 public List<CombinationRecordDO> getCombinationRecordListByUserIdAndActivityId(Long userId, Long activityId) {
231   - return recordMapper.selectListByUserIdAndActivityId(userId, activityId);
  241 + return combinationRecordMapper.selectListByUserIdAndActivityId(userId, activityId);
232 242 }
233 243  
234 244 @Override
... ... @@ -241,52 +251,57 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
241 251 }
242 252  
243 253 @Override
244   - public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup) {
245   - return recordMapper.selectCountByHeadAndStatusAndVirtualGroup(status, virtualGroup);
  254 + public Long getCombinationRecordCount(@Nullable Integer status, @Nullable Boolean virtualGroup, @Nullable Long headId) {
  255 + return combinationRecordMapper.selectCountByHeadAndStatusAndVirtualGroup(status, virtualGroup, headId);
246 256 }
247 257  
248 258 @Override
249 259 public List<CombinationRecordDO> getLatestCombinationRecordList(int count) {
250   - return recordMapper.selectLatestList(count);
  260 + return combinationRecordMapper.selectLatestList(count);
251 261 }
252 262  
253 263 @Override
254 264 public List<CombinationRecordDO> getHeadCombinationRecordList(Long activityId, Integer status, Integer count) {
255   - return recordMapper.selectListByActivityIdAndStatusAndHeadId(activityId, status,
  265 + return combinationRecordMapper.selectListByActivityIdAndStatusAndHeadId(activityId, status,
256 266 CombinationRecordDO.HEAD_ID_GROUP, count);
257 267 }
258 268  
259 269 @Override
260 270 public CombinationRecordDO getCombinationRecordById(Long id) {
261   - return recordMapper.selectById(id);
  271 + return combinationRecordMapper.selectById(id);
262 272 }
263 273  
264 274 @Override
265 275 public List<CombinationRecordDO> getCombinationRecordListByHeadId(Long headId) {
266   - return recordMapper.selectList(CombinationRecordDO::getHeadId, headId);
  276 + return combinationRecordMapper.selectList(CombinationRecordDO::getHeadId, headId);
267 277 }
268 278  
269 279 @Override
270 280 public PageResult<CombinationRecordDO> getCombinationRecordPage(CombinationRecordReqPageVO pageVO) {
271   - return recordMapper.selectPage(pageVO);
  281 + return combinationRecordMapper.selectPage(pageVO);
  282 + }
  283 +
  284 + @Override
  285 + public PageResult<CombinationRecordDO> getCombinationRecordPage2(CombinationRecordReqPage2VO pageVO) {
  286 + return combinationRecordMapper.selectPage(pageVO);
272 287 }
273 288  
274 289 @Override
275 290 public Map<Long, Integer> getCombinationRecordCountMapByActivity(Collection<Long> activityIds,
276 291 @Nullable Integer status, @Nullable Long headId) {
277   - return recordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId);
  292 + return combinationRecordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId);
278 293 }
279 294  
280 295 @Override
281 296 public CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id) {
282   - return recordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
  297 + return combinationRecordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
283 298 }
284 299  
285 300 @Override
286 301 @Transactional(rollbackFor = Exception.class)
287 302 public void cancelCombinationRecord(Long userId, Long id, Long headId) {
288 303 // 删除记录
289   - recordMapper.deleteById(id);
  304 + combinationRecordMapper.deleteById(id);
290 305  
291 306 // 需要更新的记录
292 307 List<CombinationRecordDO> updateRecords = new ArrayList<>();
... ... @@ -313,7 +328,7 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
313 328 });
314 329 } else { // 情况二:团员
315 330 // 团长
316   - CombinationRecordDO recordHead = recordMapper.selectById(headId);
  331 + CombinationRecordDO recordHead = combinationRecordMapper.selectById(headId);
317 332 // 团员
318 333 List<CombinationRecordDO> records = getCombinationRecordListByHeadId(headId);
319 334 if (CollUtil.isEmpty(records)) {
... ... @@ -329,7 +344,112 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
329 344 }
330 345  
331 346 // 更新拼团记录
332   - recordMapper.updateBatch(updateRecords);
  347 + combinationRecordMapper.updateBatch(updateRecords);
  348 + }
  349 +
  350 + @Override
  351 + public KeyValue<Integer, Integer> expireCombinationRecord() {
  352 + // 1。获取所有正在进行中的过期的父拼团
  353 + List<CombinationRecordDO> headExpireRecords = combinationRecordMapper.selectListByHeadIdAndStatusAndExpireTimeLt(
  354 + CombinationRecordDO.HEAD_ID_GROUP, CombinationRecordStatusEnum.IN_PROGRESS.getStatus(), LocalDateTime.now());
  355 + if (CollUtil.isEmpty(headExpireRecords)) {
  356 + return new KeyValue<>(0, 0);
  357 + }
  358 +
  359 + // 2.获取拼团活动
  360 + List<CombinationActivityDO> combinationActivities = combinationActivityService.getCombinationActivityListByIds(
  361 + convertSet(headExpireRecords, CombinationRecordDO::getActivityId));
  362 + Map<Long, CombinationActivityDO> activityMap = convertMap(combinationActivities, CombinationActivityDO::getId);
  363 +
  364 + // 3.校验是否虚拟成团
  365 + List<CombinationRecordDO> virtualGroupHeadRecords = new ArrayList<>(); // 虚拟成团
  366 + for (Iterator<CombinationRecordDO> iterator = headExpireRecords.iterator(); iterator.hasNext(); ) {
  367 + CombinationRecordDO record = iterator.next();
  368 + // 3.1 不匹配,则直接跳过
  369 + CombinationActivityDO activityDO = activityMap.get(record.getActivityId());
  370 + if (activityDO == null || !activityDO.getVirtualGroup()) { // 取不到活动的或者不是虚拟拼团的
  371 + continue;
  372 + }
  373 + // 3.2 匹配,则移除,添加到虚拟成团中,并结束寻找
  374 + virtualGroupHeadRecords.add(record);
  375 + iterator.remove();
  376 + break;
  377 + }
  378 +
  379 + // 4.处理过期的拼团
  380 + getSelf().handleExpireRecord(headExpireRecords);
  381 + // 5.虚拟成团
  382 + getSelf().handleVirtualGroupRecord(virtualGroupHeadRecords);
  383 +
  384 + return new KeyValue<>(headExpireRecords.size(), virtualGroupHeadRecords.size());
  385 + }
  386 +
  387 + @Async
  388 + protected void handleExpireRecord(List<CombinationRecordDO> headExpireRecords) {
  389 + if (CollUtil.isEmpty(headExpireRecords)) {
  390 + return;
  391 + }
  392 +
  393 + // 1.更新拼团记录
  394 + List<CombinationRecordDO> headsAndRecords = updateBatchCombinationRecords(headExpireRecords,
  395 + CombinationRecordStatusEnum.FAILED);
  396 + if (headsAndRecords == null) {
  397 + return;
  398 + }
  399 +
  400 + // 2.订单取消 TODO 以现在的取消回滚逻辑好像只能循环了
  401 + headsAndRecords.forEach(item -> {
  402 + tradeOrderApi.cancelPaidOrder(item.getUserId(), item.getOrderId());
  403 + });
  404 +
  405 + }
  406 +
  407 + @Async
  408 + protected void handleVirtualGroupRecord(List<CombinationRecordDO> virtualGroupHeadRecords) {
  409 + if (CollUtil.isEmpty(virtualGroupHeadRecords)) {
  410 + return;
  411 + }
  412 +
  413 + // 1.团员补齐
  414 + combinationRecordMapper.insertBatch(CombinationActivityConvert.INSTANCE.convertVirtualGroupList(virtualGroupHeadRecords));
  415 + // 2.更新拼团记录
  416 + updateBatchCombinationRecords(virtualGroupHeadRecords, CombinationRecordStatusEnum.SUCCESS);
  417 + }
  418 +
  419 + private List<CombinationRecordDO> updateBatchCombinationRecords(List<CombinationRecordDO> headRecords, CombinationRecordStatusEnum status) {
  420 + // 1. 查询团成员
  421 + List<CombinationRecordDO> records = combinationRecordMapper.selectListByHeadIds(
  422 + convertSet(headRecords, CombinationRecordDO::getId));
  423 + if (CollUtil.isEmpty(records)) {
  424 + return null;
  425 + }
  426 + Map<Long, List<CombinationRecordDO>> recordsMap = convertMultiMap(records, CombinationRecordDO::getHeadId);
  427 + headRecords.forEach(item -> {
  428 + recordsMap.get(item.getId()).add(item); // 把团长加进团里
  429 + });
  430 + // 2.批量更新拼团记录 status 和 失败/成团时间
  431 + List<CombinationRecordDO> headsAndRecords = mergeValuesFromMap(recordsMap);
  432 + List<CombinationRecordDO> updateRecords = new ArrayList<>(headsAndRecords.size());
  433 + LocalDateTime now = LocalDateTime.now();
  434 + headsAndRecords.forEach(item -> {
  435 + CombinationRecordDO record = new CombinationRecordDO().setId(item.getId())
  436 + .setStatus(status.getStatus()).setEndTime(now);
  437 + if (CombinationRecordStatusEnum.isSuccess(status.getStatus())) { // 虚拟成团完事更改状态成功后还需要把参与人数修改为成团需要人数
  438 + record.setUserCount(record.getUserSize());
  439 + }
  440 + updateRecords.add(record);
  441 + });
  442 + combinationRecordMapper.updateBatch(updateRecords);
  443 + return headsAndRecords;
  444 + }
  445 +
  446 + /**
  447 + * 获得自身的代理对象,解决 AOP 生效问题
  448 + *
  449 + * @return 自己
  450 + */
  451 + private CombinationRecordServiceImpl getSelf() {
  452 + return SpringUtil.getBean(getClass());
333 453 }
334 454  
335 455 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityService.java
... ... @@ -36,13 +36,22 @@ public interface SeckillActivityService {
36 36 void updateSeckillActivity(@Valid SeckillActivityUpdateReqVO updateReqVO);
37 37  
38 38 /**
39   - * 更新秒杀库存
  39 + * 更新秒杀库存(减少)
40 40 *
41 41 * @param id 活动编号
42 42 * @param skuId sku 编号
43   - * @param count 数量
  43 + * @param count 数量(正数)
44 44 */
45   - void updateSeckillStock(Long id, Long skuId, Integer count);
  45 + void updateSeckillStockDecr(Long id, Long skuId, Integer count);
  46 +
  47 + /**
  48 + * 更新秒杀库存(增加)
  49 + *
  50 + * @param id 活动编号
  51 + * @param skuId sku 编号
  52 + * @param count 数量(正数)
  53 + */
  54 + void updateSeckillStockIncr(Long id, Long skuId, Integer count);
46 55  
47 56 /**
48 57 * 关闭秒杀活动
... ... @@ -113,8 +122,8 @@ public interface SeckillActivityService {
113 122 * 如果校验失败,则抛出业务异常
114 123 *
115 124 * @param activityId 活动编号
116   - * @param skuId SKU 编号
117   - * @param count 数量
  125 + * @param skuId SKU 编号
  126 + * @param count 数量
118 127 * @return 秒杀信息
119 128 */
120 129 SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java
1 1 package cn.iocoder.yudao.module.promotion.service.seckill;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.hutool.core.collection.CollectionUtil;
  5 +import cn.hutool.core.map.MapUtil;
4 6 import cn.hutool.core.util.ObjectUtil;
5 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
... ... @@ -27,6 +29,7 @@ import org.springframework.validation.annotation.Validated;
27 29  
28 30 import javax.annotation.Resource;
29 31 import java.util.Collection;
  32 +import java.util.Collections;
30 33 import java.util.List;
31 34 import java.util.Map;
32 35  
... ... @@ -154,7 +157,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
154 157  
155 158 @Override
156 159 @Transactional(rollbackFor = Exception.class)
157   - public void updateSeckillStock(Long id, Long skuId, Integer count) {
  160 + public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
158 161 // 1.1 校验活动库存是否充足
159 162 SeckillActivityDO seckillActivity = validateSeckillActivityExists(id);
160 163 if (count > seckillActivity.getTotalStock()) {
... ... @@ -167,18 +170,28 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
167 170 }
168 171  
169 172 // 2.1 更新活动商品库存
170   - int updateCount = seckillProductMapper.updateStock(product.getId(), count);
  173 + int updateCount = seckillProductMapper.updateStockDecr(product.getId(), count);
171 174 if (updateCount == 0) {
172 175 throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
173 176 }
174 177  
175 178 // 2.2 更新活动库存
176   - updateCount = seckillActivityMapper.updateStock(seckillActivity.getId(), count);
  179 + updateCount = seckillActivityMapper.updateStockDecr(seckillActivity.getId(), count);
177 180 if (updateCount == 0) {
178 181 throw exception(SECKILL_ACTIVITY_UPDATE_STOCK_FAIL);
179 182 }
180 183 }
181 184  
  185 + @Override
  186 + @Transactional(rollbackFor = Exception.class)
  187 + public void updateSeckillStockIncr(Long id, Long skuId, Integer count) {
  188 + SeckillProductDO product = seckillProductMapper.selectByActivityIdAndSkuId(id, skuId);
  189 + // 更新活动商品库存
  190 + seckillProductMapper.updateStockIncr(product.getId(), count);
  191 + // 更新活动库存
  192 + seckillActivityMapper.updateStockIncr(id, count);
  193 + }
  194 +
182 195 /**
183 196 * 更新秒杀商品
184 197 *
... ... @@ -312,7 +325,13 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
312 325  
313 326 @Override
314 327 public List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
315   - return seckillActivityMapper.selectListBySpuIds(spuIds, status);
  328 + // 1.查询出指定 spuId 的 spu 参加的活动最接近现在的一条记录。多个的话,一个 spuId 对应一个最近的活动编号
  329 + List<Map<String, Object>> spuIdAndActivityIdMaps = seckillActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
  330 + if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
  331 + return Collections.emptyList();
  332 + }
  333 + // 2.查询活动详情
  334 + return seckillActivityMapper.selectListByIds(convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")));
316 335 }
317 336  
318 337 }
... ...
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java
... ... @@ -33,7 +33,8 @@ public interface ErrorCodeConstants {
33 33 ErrorCode ORDER_UPDATE_PRICE_FAIL_PRICE_ERROR = new ErrorCode(1_011_000_028, "支付订单调价失败,原因:调整后支付价格不能小于 0.01 元");
34 34 ErrorCode ORDER_DELETE_FAIL_STATUS_NOT_CANCEL = new ErrorCode(1_011_000_029, "交易订单删除失败,订单不是【已取消】状态");
35 35 ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
36   - ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单已发货");
  36 + ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
  37 + ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
37 38  
38 39 // ========== After Sale 模块 1-011-000-100 ==========
39 40 ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/api/order/TradeOrderApiImpl.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
5 5 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
6 6 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
7 7 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
  8 +import org.springframework.context.annotation.Lazy;
8 9 import org.springframework.stereotype.Service;
9 10 import org.springframework.validation.annotation.Validated;
10 11  
... ... @@ -25,9 +26,10 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_NOT_F
25 26 public class TradeOrderApiImpl implements TradeOrderApi {
26 27  
27 28 @Resource
28   - private TradeOrderQueryService tradeOrderQueryService;
29   - @Resource
30 29 private TradeOrderUpdateService tradeOrderUpdateService;
  30 + @Resource
  31 + @Lazy
  32 + private TradeOrderQueryService tradeOrderQueryService;
31 33  
32 34 @Override
33 35 public List<TradeOrderRespDTO> getOrderList(Collection<Long> ids) {
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/order/TradeOrderMapper.java
... ... @@ -93,4 +93,16 @@ public interface TradeOrderMapper extends BaseMapperX&lt;TradeOrderDO&gt; {
93 93 return selectOne(TradeOrderDO::getPickUpVerifyCode, pickUpVerifyCode);
94 94 }
95 95  
  96 + default TradeOrderDO selectByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status) {
  97 + return selectOne(new LambdaQueryWrapperX<TradeOrderDO>()
  98 + .and(q -> q.eq(TradeOrderDO::getUserId, userId)
  99 + .eq(TradeOrderDO::getStatus, status))
  100 + .and(q -> q.eq(TradeOrderDO::getCombinationActivityId, activityId)
  101 + .or()
  102 + .eq(TradeOrderDO::getSeckillActivityId, activityId)
  103 + .or()
  104 + .eq(TradeOrderDO::getBargainActivityId, activityId))
  105 + );
  106 + }
  107 +
96 108 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryService.java
... ... @@ -39,6 +39,16 @@ public interface TradeOrderQueryService {
39 39 TradeOrderDO getOrder(Long userId, Long id);
40 40  
41 41 /**
  42 + * 获得指定用户,指定活动,指定状态的交易订单
  43 + *
  44 + * @param userId 用户编号
  45 + * @param activityId 活动编号
  46 + * @param status 订单状态
  47 + * @return 交易订单
  48 + */
  49 + TradeOrderDO getActivityOrderByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status);
  50 +
  51 + /**
42 52 * 获得订单列表
43 53 *
44 54 * @param ids 订单编号数组
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java
... ... @@ -71,6 +71,11 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
71 71 }
72 72  
73 73 @Override
  74 + public TradeOrderDO getActivityOrderByUserIdAndActivityIdAndStatus(Long userId, Long activityId, Integer status) {
  75 + return tradeOrderMapper.selectByUserIdAndActivityIdAndStatus(userId, activityId, status);
  76 + }
  77 +
  78 + @Override
74 79 public List<TradeOrderDO> getOrderList(Collection<Long> ids) {
75 80 if (CollUtil.isEmpty(ids)) {
76 81 return Collections.emptyList();
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
... ... @@ -14,23 +14,12 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
14 14 import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
15 15 import cn.iocoder.yudao.module.member.api.address.AddressApi;
16 16 import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
17   -import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
18   -import cn.iocoder.yudao.module.member.api.point.MemberPointApi;
19   -import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
20   -import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
21   -import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
22   -import cn.iocoder.yudao.module.member.enums.point.MemberPointBizTypeEnum;
23 17 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
24 18 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
25 19 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
26 20 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
27 21 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
28 22 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
29   -import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
30   -import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
31   -import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
32   -import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
33   -import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
34 23 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
35 24 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO;
36 25 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO;
... ... @@ -40,7 +29,6 @@ import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettle
40 29 import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO;
41 30 import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
42 31 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
43   -import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
44 32 import cn.iocoder.yudao.module.trade.dal.dataobject.cart.CartDO;
45 33 import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
46 34 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
... ... @@ -48,15 +36,11 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
48 36 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
49 37 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
50 38 import cn.iocoder.yudao.module.trade.dal.redis.no.TradeNoRedisDAO;
51   -import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
52 39 import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
53 40 import cn.iocoder.yudao.module.trade.enums.order.*;
54 41 import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
55 42 import cn.iocoder.yudao.module.trade.framework.order.core.annotations.TradeOrderLog;
56 43 import cn.iocoder.yudao.module.trade.framework.order.core.utils.TradeOrderLogUtils;
57   -import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageRecordService;
58   -import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageUserService;
59   -import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageAddReqBO;
60 44 import cn.iocoder.yudao.module.trade.service.cart.CartService;
61 45 import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
62 46 import cn.iocoder.yudao.module.trade.service.message.TradeMessageService;
... ... @@ -68,13 +52,15 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
68 52 import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper;
69 53 import lombok.extern.slf4j.Slf4j;
70 54 import org.jetbrains.annotations.NotNull;
71   -import org.springframework.scheduling.annotation.Async;
72 55 import org.springframework.stereotype.Service;
73 56 import org.springframework.transaction.annotation.Transactional;
74 57  
75 58 import javax.annotation.Resource;
76 59 import java.time.LocalDateTime;
77   -import java.util.*;
  60 +import java.util.ArrayList;
  61 +import java.util.List;
  62 +import java.util.Objects;
  63 +import java.util.Set;
78 64  
79 65 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
80 66 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
... ... @@ -109,30 +95,12 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
109 95 private DeliveryExpressService deliveryExpressService;
110 96 @Resource
111 97 private TradeMessageService tradeMessageService;
112   - @Resource
113   - private BrokerageUserService brokerageUserService;
114   - @Resource
115   - private BrokerageRecordService brokerageRecordService;
116 98  
117 99 @Resource
118   - private ProductSpuApi productSpuApi;
119   - @Resource
120   - private ProductSkuApi productSkuApi;
121   - @Resource
122 100 private PayOrderApi payOrderApi;
123 101 @Resource
124 102 private AddressApi addressApi;
125 103 @Resource
126   - private CouponApi couponApi;
127   - @Resource
128   - private CombinationRecordApi combinationRecordApi;
129   - @Resource
130   - private MemberUserApi memberUserApi;
131   - @Resource
132   - private MemberLevelApi memberLevelApi;
133   - @Resource
134   - private MemberPointApi memberPointApi;
135   - @Resource
136 104 private ProductCommentApi productCommentApi;
137 105  
138 106 @Resource
... ... @@ -199,7 +167,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
199 167 List<TradeOrderItemDO> orderItems = buildTradeOrderItems(order, calculateRespBO);
200 168  
201 169 // 2. 订单创建前的逻辑
202   - beforeCreateTradeOrder(order, orderItems);
  170 + tradeOrderHandlers.forEach(handler -> handler.beforeOrderCreate(order, orderItems));
203 171  
204 172 // 3. 保存订单
205 173 tradeOrderMapper.insert(order);
... ... @@ -234,11 +202,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
234 202 order.setReceiverName(createReqVO.getReceiverName()).setReceiverMobile(createReqVO.getReceiverMobile());
235 203 order.setPickUpVerifyCode(RandomUtil.randomNumbers(8)); // 随机一个核销码,长度为 8 位
236 204 }
237   - // 设置订单推广人
238   - BrokerageUserDO brokerageUser = brokerageUserService.getBrokerageUser(order.getUserId());
239   - if (brokerageUser != null && brokerageUser.getBindUserId() != null) {
240   - order.setBrokerageUserId(brokerageUser.getBindUserId());
241   - }
242 205 return order;
243 206 }
244 207  
... ... @@ -248,21 +211,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
248 211 }
249 212  
250 213 /**
251   - * 订单创建前,执行前置逻辑
252   - *
253   - * @param order 订单
254   - * @param orderItems 订单项
255   - */
256   - private void beforeCreateTradeOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
257   - // 1. 执行订单创建前置处理器
258   - // TODO @puhui999:这里有个纠结点;handler 的定义是只处理指定类型的订单的拓展逻辑;还是通用的 handler,类似可以处理优惠劵等等
259   - tradeOrderHandlers.forEach(handler -> handler.beforeOrderCreate(order, orderItems));
260   -
261   - // 2. 下单时扣减商品库存
262   - productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convertNegative(orderItems));
263   - }
264   -
265   - /**
266 214 * 订单创建后,执行后置逻辑
267 215 * <p>
268 216 * 例如说:优惠劵的扣减、积分的扣减、支付单的创建等等
... ... @@ -276,27 +224,16 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
276 224 // 1. 执行订单创建后置处理器
277 225 tradeOrderHandlers.forEach(handler -> handler.afterOrderCreate(order, orderItems));
278 226  
279   - // 2. 有使用优惠券时更新
280   - // 不在前置扣减的原因,是因为优惠劵要记录使用的订单号
281   - if (order.getCouponId() != null) {
282   - couponApi.useCoupon(new CouponUseReqDTO().setId(order.getCouponId()).setUserId(order.getUserId())
283   - .setOrderId(order.getId()));
284   - }
285   -
286   - // 3. 扣减积分(抵扣)
287   - // 不在前置扣减的原因,是因为积分扣减时,需要记录关联业务
288   - reduceUserPoint(order.getUserId(), order.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE, order.getId());
289   -
290   - // 4. 删除购物车商品
  227 + // 2. 删除购物车商品
291 228 Set<Long> cartIds = convertSet(createReqVO.getItems(), AppTradeOrderSettlementReqVO.Item::getCartId);
292 229 if (CollUtil.isNotEmpty(cartIds)) {
293 230 cartService.deleteCart(order.getUserId(), cartIds);
294 231 }
295 232  
296   - // 5. 生成预支付
  233 + // 3. 生成预支付
297 234 createPayOrder(order, orderItems);
298 235  
299   - // 6. 插入订单日志
  236 + // 4. 插入订单日志
300 237 TradeOrderLogUtils.setOrderInfo(order.getId(), null, order.getStatus());
301 238  
302 239 // TODO @LeeYan9: 是可以思考下, 订单的营销优惠记录, 应该记录在哪里, 微信讨论起来!
... ... @@ -330,18 +267,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
330 267 throw exception(ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
331 268 }
332 269  
333   - // 3、订单支付成功后
  270 + // 3. 执行 TradeOrderHandler 的后置处理
334 271 List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(id);
335 272 tradeOrderHandlers.forEach(handler -> handler.afterPayOrder(order, orderItems));
336 273  
337   - // 4.1 增加用户积分(赠送)
338   - addUserPoint(order.getUserId(), order.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE, order.getId());
339   - // 4.2 增加用户经验
340   - getSelf().addUserExperienceAsync(order.getUserId(), order.getPayPrice(), order.getId());
341   - // 4.3 增加用户佣金
342   - getSelf().addBrokerageAsync(order.getUserId(), order.getId());
343   -
344   - // 5. 记录订单日志
  274 + // 4. 记录订单日志
345 275 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.UNDELIVERED.getStatus());
346 276 TradeOrderLogUtils.setUserInfo(order.getUserId(), UserTypeEnum.MEMBER.getValue());
347 277 }
... ... @@ -434,8 +364,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
434 364 .put("logisticsNo", express != null ? deliveryReqVO.getLogisticsNo() : "").build());
435 365  
436 366 // 4. 发送站内信
437   - tradeMessageService.sendMessageWhenDeliveryOrder(new TradeOrderMessageWhenDeliveryOrderReqBO().setOrderId(order.getId())
438   - .setUserId(order.getUserId()).setMessage(null));
  367 + tradeMessageService.sendMessageWhenDeliveryOrder(new TradeOrderMessageWhenDeliveryOrderReqBO()
  368 + .setOrderId(order.getId()).setUserId(order.getUserId()).setMessage(null));
439 369 }
440 370  
441 371 /**
... ... @@ -448,18 +378,13 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
448 378 */
449 379 private TradeOrderDO validateOrderDeliverable(Long id) {
450 380 TradeOrderDO order = validateOrderExists(id);
451   - // 校验订单是否退款
  381 + // 1. 校验订单是否未发货
452 382 if (ObjectUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) {
453 383 throw exception(ORDER_DELIVERY_FAIL_REFUND_STATUS_NOT_NONE);
454 384 }
455   - // 订单类型:拼团
456   - if (Objects.equals(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) {
457   - // 校验订单拼团是否成功
458   - if (!combinationRecordApi.isCombinationRecordSuccess(order.getUserId(), order.getId())) {
459   - throw exception(ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS);
460   - }
461   - }
462 385  
  386 + // 2. 执行 TradeOrderHandler 前置处理
  387 + tradeOrderHandlers.forEach(handler -> handler.beforeDeliveryOrder(order));
463 388 return order;
464 389 }
465 390  
... ... @@ -616,30 +541,19 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
616 541 * @param cancelType 取消类型
617 542 */
618 543 private void cancelOrder0(TradeOrderDO order, TradeOrderCancelTypeEnum cancelType) {
619   - Long id = order.getId();
620 544 // 1. 更新 TradeOrderDO 状态为已取消
621   - int updateCount = tradeOrderMapper.updateByIdAndStatus(id, order.getStatus(),
  545 + int updateCount = tradeOrderMapper.updateByIdAndStatus(order.getId(), order.getStatus(),
622 546 new TradeOrderDO().setStatus(TradeOrderStatusEnum.CANCELED.getStatus())
623 547 .setCancelType(cancelType.getType()).setCancelTime(LocalDateTime.now()));
624 548 if (updateCount == 0) {
625 549 throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID);
626 550 }
627 551  
628   - List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(id);
629   - // 3. 回滚库存
630   - productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(orderItems));
631   - // 3.1、 活动相关的回滚
632   - tradeOrderHandlers.forEach(handler -> handler.cancelOrder(order, orderItems));
633   -
634   - // 4. 回滚优惠券
635   - if (order.getCouponId() != null && order.getCouponId() > 0) {
636   - couponApi.returnUsedCoupon(order.getCouponId());
637   - }
638   -
639   - // 5. 回滚积分(抵扣的)
640   - addUserPoint(order.getUserId(), order.getUsePoint(), MemberPointBizTypeEnum.ORDER_CANCEL, order.getId());
  552 + // 2. 执行 TradeOrderHandler 的后置处理
  553 + List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(order.getId());
  554 + tradeOrderHandlers.forEach(handler -> handler.afterCancelOrder(order, orderItems));
641 555  
642   - // 6. 增加订单日志
  556 + // 3. 增加订单日志
643 557 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.CANCELED.getStatus());
644 558 }
645 559  
... ... @@ -660,8 +574,9 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
660 574 .setStatus(TradeOrderStatusEnum.CANCELED.getStatus())
661 575 .setCancelType(TradeOrderCancelTypeEnum.AFTER_SALE_CLOSE.getType()).setCancelTime(LocalDateTime.now()));
662 576  
663   - // 2. 退还优惠券
664   - couponApi.returnUsedCoupon(order.getCouponId());
  577 + // 2. 执行 TradeOrderHandler 的后置处理
  578 + List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(order.getId());
  579 + tradeOrderHandlers.forEach(handler -> handler.afterCancelOrder(order, orderItems));
665 580 }
666 581  
667 582 @Override
... ... @@ -742,9 +657,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
742 657 public void updateOrderAddress(TradeOrderUpdateAddressReqVO reqVO) {
743 658 // 校验交易订单
744 659 TradeOrderDO order = validateOrderExists(reqVO.getId());
745   - // 发货后,不允许修改;
746   - // TODO @puhui999:只有待发货,可以执行 update
747   - if (TradeOrderStatusEnum.isDelivered(order.getStatus())) {
  660 + // 只有待发货状态才可以修改订单收货地址;
  661 + if (!TradeOrderStatusEnum.isUndelivered(order.getStatus())) {
748 662 throw exception(ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED);
749 663 }
750 664  
... ... @@ -790,13 +704,15 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
790 704 @Override
791 705 @Transactional(rollbackFor = Exception.class)
792 706 public void updateOrderItemWhenAfterSaleSuccess(Long id, Integer refundPrice) {
793   - // 1. 更新订单项
  707 + // 1.1 更新订单项
794 708 updateOrderItemAfterSaleStatus(id, TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(),
795 709 TradeOrderItemAfterSaleStatusEnum.SUCCESS.getStatus(), null);
796   -
797   - // 2.1 更新订单的退款金额、积分
  710 + // 1.2 执行 TradeOrderHandler 的后置处理
798 711 TradeOrderItemDO orderItem = tradeOrderItemMapper.selectById(id);
799 712 TradeOrderDO order = tradeOrderMapper.selectById(orderItem.getOrderId());
  713 + tradeOrderHandlers.forEach(handler -> handler.afterCancelOrderItem(order, orderItem));
  714 +
  715 + // 2.1 更新订单的退款金额、积分
800 716 Integer orderRefundPrice = order.getRefundPrice() + refundPrice;
801 717 Integer orderRefundPoint = order.getRefundPoint() + orderItem.getUsePoint();
802 718 Integer refundStatus = isAllOrderItemAfterSaleSuccess(order.getId()) ?
... ... @@ -807,23 +723,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
807 723 .setRefundPrice(orderRefundPrice).setRefundPoint(orderRefundPoint));
808 724 // 2.2 如果全部退款,则进行取消订单
809 725 getSelf().cancelOrderByAfterSale(order, orderRefundPrice);
810   -
811   -
812   - // 3. 回滚库存
813   - productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(Collections.singletonList(orderItem)));
814   - // 3.1、 活动相关的回滚
815   - tradeOrderHandlers.forEach(handler -> handler.cancelOrder(order, Collections.singletonList(orderItem)));
816   -
817   - // 4.1 回滚积分:扣减用户积分(赠送的)
818   - reduceUserPoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.AFTER_SALE_DEDUCT_GIVE, orderItem.getAfterSaleId());
819   - // 4.2 回滚积分:增加用户积分(返还抵扣)
820   - addUserPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.AFTER_SALE_REFUND_USED, orderItem.getAfterSaleId());
821   -
822   - // 5. 回滚经验:扣减用户经验
823   - getSelf().reduceUserExperienceAsync(order.getUserId(), refundPrice, orderItem.getAfterSaleId());
824   -
825   - // 6. 回滚佣金:更新分佣记录为已失效
826   - getSelf().cancelBrokerageAsync(order.getUserId(), id);
827 726 }
828 727  
829 728 @Override
... ... @@ -840,6 +739,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
840 739 if (updateCount <= 0) {
841 740 throw exception(ORDER_ITEM_UPDATE_AFTER_SALE_STATUS_FAIL);
842 741 }
  742 +
843 743 }
844 744  
845 745 /**
... ... @@ -940,7 +840,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
940 840 @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_COMMENT)
941 841 public void createOrderItemCommentBySystemBySystem(TradeOrderDO order) {
942 842 // 1. 查询未评论的订单项
943   - List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderIdAndCommentStatus(order.getId(), Boolean.FALSE);
  843 + List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderIdAndCommentStatus(
  844 + order.getId(), Boolean.FALSE);
944 845 if (CollUtil.isEmpty(orderItems)) {
945 846 return;
946 847 }
... ... @@ -983,73 +884,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
983 884  
984 885 // =================== 营销相关的操作 ===================
985 886  
986   - @Async
987   - protected void addUserExperienceAsync(Long userId, Integer payPrice, Long orderId) {
988   - int bizType = MemberExperienceBizTypeEnum.ORDER.getType();
989   - memberLevelApi.addExperience(userId, payPrice, bizType, String.valueOf(orderId));
990   - }
991   -
992   - @Async
993   - protected void reduceUserExperienceAsync(Long userId, Integer refundPrice, Long afterSaleId) {
994   - int bizType = MemberExperienceBizTypeEnum.REFUND.getType();
995   - memberLevelApi.addExperience(userId, -refundPrice, bizType, String.valueOf(afterSaleId));
996   - }
997   -
998   - /**
999   - * 添加用户积分
1000   - * <p>
1001   - * 目前是支付成功后,就会创建积分记录。
1002   - * <p>
1003   - * 业内还有两种做法,可以根据自己的业务调整:
1004   - * 1. 确认收货后,才创建积分记录
1005   - * 2. 支付 or 下单成功时,创建积分记录(冻结),确认收货解冻或者 n 天后解冻
1006   - *
1007   - * @param userId 用户编号
1008   - * @param point 增加积分数量
1009   - * @param bizType 业务编号
1010   - * @param bizId 业务编号
1011   - */
1012   - protected void addUserPoint(Long userId, Integer point, MemberPointBizTypeEnum bizType, Long bizId) {
1013   - if (point != null && point > 0) {
1014   - memberPointApi.addPoint(userId, point, bizType.getType(), String.valueOf(bizId));
1015   - }
1016   - }
1017   -
1018   - protected void reduceUserPoint(Long userId, Integer point, MemberPointBizTypeEnum bizType, Long bizId) {
1019   - if (point != null && point > 0) {
1020   - memberPointApi.reducePoint(userId, point, bizType.getType(), String.valueOf(bizId));
1021   - }
1022   - }
1023   -
1024   - /**
1025   - * 创建分销记录
1026   - * <p>
1027   - * 目前是支付成功后,就会创建分销记录。
1028   - * <p>
1029   - * 业内还有两种做法,可以根据自己的业务调整:
1030   - * 1. 确认收货后,才创建分销记录
1031   - * 2. 支付 or 下单成功时,创建分销记录(冻结),确认收货解冻或者 n 天后解冻
1032   - *
1033   - * @param userId 用户编号
1034   - * @param orderId 订单编号
1035   - */
1036   - @Async
1037   - protected void addBrokerageAsync(Long userId, Long orderId) {
1038   - MemberUserRespDTO user = memberUserApi.getUser(userId);
1039   - Assert.notNull(user);
1040   - // 每一个订单项,都会去生成分销记录
1041   - List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(orderId);
1042   - List<BrokerageAddReqBO> addList = convertList(orderItems,
1043   - item -> TradeOrderConvert.INSTANCE.convert(user, item,
1044   - productSpuApi.getSpu(item.getSpuId()), productSkuApi.getSku(item.getSkuId())));
1045   - brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList);
1046   - }
1047   -
1048   - @Async
1049   - protected void cancelBrokerageAsync(Long userId, Long orderItemId) {
1050   - brokerageRecordService.cancelBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, String.valueOf(orderItemId));
1051   - }
1052   -
1053 887 /**
1054 888 * 获得自身的代理对象,解决 AOP 生效问题
1055 889 *
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBargainHandler.java renamed to yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBargainOrderHandler.java
1 1 package cn.iocoder.yudao.module.trade.service.order.handler;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
3 5 import cn.iocoder.yudao.module.promotion.api.bargain.BargainActivityApi;
4 6 import cn.iocoder.yudao.module.promotion.api.bargain.BargainRecordApi;
5 7 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
... ... @@ -11,12 +13,12 @@ import javax.annotation.Resource;
11 13 import java.util.List;
12 14  
13 15 /**
14   - * 砍价订单 handler 实现类
  16 + * 砍价订单的 {@link TradeOrderHandler} 实现类
15 17 *
16 18 * @author HUIHUI
17 19 */
18 20 @Component
19   -public class TradeBargainHandler implements TradeOrderHandler {
  21 +public class TradeBargainOrderHandler implements TradeOrderHandler {
20 22  
21 23 @Resource
22 24 private BargainActivityApi bargainActivityApi;
... ... @@ -25,9 +27,11 @@ public class TradeBargainHandler implements TradeOrderHandler {
25 27  
26 28 @Override
27 29 public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
28   - if (TradeOrderTypeEnum.isBargain(order.getType())) {
  30 + if (!TradeOrderTypeEnum.isBargain(order.getType())) {
29 31 return;
30 32 }
  33 + // 明确校验一下
  34 + Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
31 35  
32 36 // 扣减砍价活动的库存
33 37 bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(),
... ... @@ -36,20 +40,39 @@ public class TradeBargainHandler implements TradeOrderHandler {
36 40  
37 41 @Override
38 42 public void afterOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
39   - if (TradeOrderTypeEnum.isBargain(order.getType())) {
  43 + if (!TradeOrderTypeEnum.isBargain(order.getType())) {
40 44 return;
41 45 }
  46 + // 明确校验一下
  47 + Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
42 48  
43 49 // 记录砍价记录对应的订单编号
44 50 bargainRecordApi.updateBargainRecordOrderId(order.getBargainRecordId(), order.getId());
45 51 }
46 52  
47 53 @Override
48   - public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
49   - if (TradeOrderTypeEnum.isBargain(order.getType())) {
  54 + public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  55 + if (!TradeOrderTypeEnum.isBargain(order.getType())) {
50 56 return;
51 57 }
52   - // TODO 芋艿:取消订单时,需要增加库存
  58 + // 明确校验一下
  59 + Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品");
  60 +
  61 + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
  62 + orderItems = filterOrderItemListByNoneAfterSale(orderItems);
  63 + if (CollUtil.isEmpty(orderItems)) {
  64 + return;
  65 + }
  66 + afterCancelOrderItem(order, orderItems.get(0));
  67 + }
  68 +
  69 + @Override
  70 + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
  71 + if (!TradeOrderTypeEnum.isBargain(order.getType())) {
  72 + return;
  73 + }
  74 + // 恢复(增加)砍价活动的库存
  75 + bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(), orderItem.getCount());
53 76 }
54 77  
55 78 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeBrokerageOrderHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.trade.service.order.handler;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
  6 +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
  7 +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
  8 +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
  9 +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
  10 +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
  11 +import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
  12 +import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
  13 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
  14 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  15 +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
  16 +import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageRecordService;
  17 +import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageUserService;
  18 +import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageAddReqBO;
  19 +import org.springframework.stereotype.Component;
  20 +
  21 +import javax.annotation.Resource;
  22 +import java.util.List;
  23 +
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  25 +
  26 +/**
  27 + * 订单分销的 {@link TradeOrderHandler} 实现类
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@Component
  32 +public class TradeBrokerageOrderHandler implements TradeOrderHandler {
  33 +
  34 + @Resource
  35 + private MemberUserApi memberUserApi;
  36 + @Resource
  37 + private ProductSpuApi productSpuApi;
  38 + @Resource
  39 + private ProductSkuApi productSkuApi;
  40 +
  41 + @Resource
  42 + private BrokerageRecordService brokerageRecordService;
  43 + @Resource
  44 + private BrokerageUserService brokerageUserService;
  45 +
  46 + @Override
  47 + public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  48 + // 设置订单推广人
  49 + BrokerageUserDO brokerageUser = brokerageUserService.getBrokerageUser(order.getUserId());
  50 + if (brokerageUser != null && brokerageUser.getBindUserId() != null) {
  51 + order.setBrokerageUserId(brokerageUser.getBindUserId());
  52 + }
  53 + }
  54 +
  55 + @Override
  56 + public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  57 + if (order.getBrokerageUserId() == null) {
  58 + return;
  59 + }
  60 + addBrokerage(order.getUserId(), orderItems);
  61 + }
  62 +
  63 + @Override
  64 + public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  65 + // 如果是未支付的订单,不会产生分销结果,所以直接 return
  66 + if (!order.getPayStatus()) {
  67 + return;
  68 + }
  69 + if (order.getBrokerageUserId() == null) {
  70 + return;
  71 + }
  72 +
  73 + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
  74 + orderItems = filterOrderItemListByNoneAfterSale(orderItems);
  75 + if (CollUtil.isEmpty(orderItems)) {
  76 + return;
  77 + }
  78 + orderItems.forEach(orderItem -> afterCancelOrderItem(order, orderItem));
  79 + }
  80 +
  81 + @Override
  82 + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
  83 + if (order.getBrokerageUserId() == null) {
  84 + return;
  85 + }
  86 + cancelBrokerage(order.getId(), orderItem.getOrderId());
  87 + }
  88 +
  89 + /**
  90 + * 创建分销记录
  91 + * <p>
  92 + * 目前是支付成功后,就会创建分销记录。
  93 + * <p>
  94 + * 业内还有两种做法,可以根据自己的业务调整:
  95 + * 1. 确认收货后,才创建分销记录
  96 + * 2. 支付 or 下单成功时,创建分销记录(冻结),确认收货解冻或者 n 天后解冻
  97 + *
  98 + * @param userId 用户编号
  99 + * @param orderItems 订单项
  100 + */
  101 + protected void addBrokerage(Long userId, List<TradeOrderItemDO> orderItems) {
  102 + MemberUserRespDTO user = memberUserApi.getUser(userId);
  103 + Assert.notNull(user);
  104 + ProductSpuRespDTO spu = productSpuApi.getSpu(orderItems.get(0).getSpuId());
  105 + Assert.notNull(spu);
  106 + ProductSkuRespDTO sku = productSkuApi.getSku(orderItems.get(0).getSkuId());
  107 +
  108 + // 每一个订单项,都会去生成分销记录
  109 + List<BrokerageAddReqBO> addList = convertList(orderItems,
  110 + item -> TradeOrderConvert.INSTANCE.convert(user, item, spu, sku));
  111 + brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList);
  112 + }
  113 +
  114 + protected void cancelBrokerage(Long userId, Long orderItemId) {
  115 + brokerageRecordService.cancelBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, String.valueOf(orderItemId));
  116 + }
  117 +
  118 +}
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationHandler.java renamed to yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java
1 1 package cn.iocoder.yudao.module.trade.service.order.handler;
2 2  
3 3 import cn.hutool.core.lang.Assert;
  4 +import cn.iocoder.yudao.framework.common.core.KeyValue;
4 5 import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
5 6 import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
6 7 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
7 8 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  9 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
8 10 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
  11 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
  12 +import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
  13 +import org.springframework.context.annotation.Lazy;
9 14 import org.springframework.stereotype.Component;
10 15  
11 16 import javax.annotation.Resource;
12 17 import java.util.List;
13 18  
  19 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  20 +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_CREATE_FAIL_EXIST_UNPAID;
  21 +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS;
  22 +
14 23 /**
15   - * 拼团订单 handler 接口实现类
  24 + * 拼团订单的 {@link TradeOrderHandler} 实现类
16 25 *
17 26 * @author HUIHUI
18 27 */
19 28 @Component
20   -public class TradeCombinationHandler implements TradeOrderHandler {
  29 +public class TradeCombinationOrderHandler implements TradeOrderHandler {
21 30  
22 31 @Resource
  32 + @Lazy
  33 + private TradeOrderUpdateService orderUpdateService;
  34 + @Resource
  35 + @Lazy
  36 + private TradeOrderQueryService orderQueryService;
  37 +
  38 + @Resource
  39 + @Lazy
23 40 private CombinationRecordApi combinationRecordApi;
24 41  
25 42 @Override
26 43 public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
27 44 // 如果不是拼团订单则结束
28   - if (TradeOrderTypeEnum.isCombination(order.getType())) {
  45 + if (!TradeOrderTypeEnum.isCombination(order.getType())) {
29 46 return;
30 47 }
31 48 Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品");
32 49  
33   - // 校验是否满足拼团活动相关限制
  50 + // 1. 校验是否满足拼团活动相关限制
34 51 TradeOrderItemDO item = orderItems.get(0);
35 52 combinationRecordApi.validateCombinationRecord(order.getUserId(), order.getCombinationActivityId(),
36 53 order.getCombinationHeadId(), item.getSkuId(), item.getCount());
37   - // TODO @puhui999:这里还要限制下,是不是已经 createOrder;就是还没支付的时候,重复下单了;需要校验下;不然的话,一个拼团可以下多个单子了;
  54 + // 2. 校验该用户是否存在未支付的拼团活动订单;就是还没支付的时候,重复下单了;需要校验下;不然的话,一个拼团可以下多个单子了;
  55 + TradeOrderDO activityOrder = orderQueryService.getActivityOrderByUserIdAndActivityIdAndStatus(
  56 + order.getUserId(), order.getCombinationActivityId(), TradeOrderStatusEnum.UNPAID.getStatus());
  57 + if (activityOrder != null) {
  58 + throw exception(ORDER_CREATE_FAIL_EXIST_UNPAID);
  59 + }
38 60 }
39 61  
40 62 @Override
41 63 public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
42   - // 如果不是拼团订单则结束
43   - if (TradeOrderTypeEnum.isCombination(order.getType())) {
  64 + // 1.如果不是拼团订单则结束
  65 + if (!TradeOrderTypeEnum.isCombination(order.getType())) {
44 66 return;
45 67 }
46 68 Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品");
47 69  
48   - // 获取商品信息
  70 + // 2. 创建拼团记录
49 71 TradeOrderItemDO item = orderItems.get(0);
50   - // 创建拼团记录
51   - combinationRecordApi.createCombinationRecord(TradeOrderConvert.INSTANCE.convert(order, item));
  72 + KeyValue<Long, Long> recordIdAndHeadId = combinationRecordApi.createCombinationRecord(
  73 + TradeOrderConvert.INSTANCE.convert(order, item));
  74 +
  75 + // 3. 更新拼团相关信息到订单
  76 + // TODO 芋艿,只需要更新 record
  77 + orderUpdateService.updateOrderCombinationInfo(order.getId(), order.getCombinationActivityId(),
  78 + recordIdAndHeadId.getKey(), recordIdAndHeadId.getValue());
52 79 }
53 80  
54 81 @Override
55   - public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
56   - if (TradeOrderTypeEnum.isCombination(order.getType())) {
  82 + public void beforeDeliveryOrder(TradeOrderDO order) {
  83 + if (!TradeOrderTypeEnum.isCombination(order.getType())) {
57 84 return;
58 85 }
  86 + // 校验订单拼团是否成功
  87 + if (!combinationRecordApi.isCombinationRecordSuccess(order.getUserId(), order.getId())) {
  88 + throw exception(ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS);
  89 + }
59 90 }
60 91  
61 92 }
  93 +
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCouponOrderHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.trade.service.order.handler;
  2 +
  3 +import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
  4 +import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
  5 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
  6 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * 优惠劵的 {@link TradeOrderHandler} 实现类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Component
  18 +public class TradeCouponOrderHandler implements TradeOrderHandler {
  19 +
  20 + @Resource
  21 + private CouponApi couponApi;
  22 +
  23 + @Override
  24 + public void afterOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  25 + if (order.getCouponId() == null || order.getCouponId() <= 0) {
  26 + return;
  27 + }
  28 + // 不在前置扣减的原因,是因为优惠劵要记录使用的订单号
  29 + couponApi.useCoupon(new CouponUseReqDTO().setId(order.getCouponId()).setUserId(order.getUserId())
  30 + .setOrderId(order.getId()));
  31 + }
  32 +
  33 + @Override
  34 + public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  35 + if (order.getCouponId() == null || order.getCouponId() <= 0) {
  36 + return;
  37 + }
  38 + // 退回优惠劵
  39 + couponApi.returnUsedCoupon(order.getCouponId());
  40 + }
  41 +
  42 +}
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeMemberPointOrderHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.trade.service.order.handler;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
  5 +import cn.iocoder.yudao.module.member.api.point.MemberPointApi;
  6 +import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
  7 +import cn.iocoder.yudao.module.member.enums.point.MemberPointBizTypeEnum;
  8 +import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
  9 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
  10 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  11 +import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
  12 +import org.springframework.stereotype.Component;
  13 +
  14 +import javax.annotation.Resource;
  15 +import java.util.List;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
  18 +
  19 +/**
  20 + * 会员积分、等级的 {@link TradeOrderHandler} 实现类
  21 + *
  22 + * @author owen
  23 + */
  24 +@Component
  25 +public class TradeMemberPointOrderHandler implements TradeOrderHandler {
  26 +
  27 + @Resource
  28 + private MemberPointApi memberPointApi;
  29 + @Resource
  30 + private MemberLevelApi memberLevelApi;
  31 +
  32 + @Resource
  33 + private AfterSaleService afterSaleService;
  34 +
  35 + @Override
  36 + public void afterOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  37 + // 扣减用户积分(订单抵扣)。不在前置扣减的原因,是因为积分扣减时,需要记录关联业务
  38 + reducePoint(order.getUserId(), order.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE, order.getId());
  39 + }
  40 +
  41 + @Override
  42 + public void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  43 + // 增加用户积分(订单赠送)
  44 + addPoint(order.getUserId(), order.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE,
  45 + order.getId());
  46 +
  47 + // 增加用户经验
  48 + memberLevelApi.addExperience(order.getUserId(), order.getPayPrice(),
  49 + MemberExperienceBizTypeEnum.ORDER_GIVE.getType(), String.valueOf(order.getId()));
  50 + }
  51 +
  52 + @Override
  53 + public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  54 + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
  55 + orderItems = filterOrderItemListByNoneAfterSale(orderItems);
  56 + if (CollUtil.isEmpty(orderItems)) {
  57 + return;
  58 + }
  59 +
  60 + // 增加(回滚)用户积分(订单抵扣)
  61 + Integer usePoint = getSumValue(orderItems, TradeOrderItemDO::getUsePoint, Integer::sum);
  62 + addPoint(order.getUserId(), usePoint, MemberPointBizTypeEnum.ORDER_USE_CANCEL,
  63 + order.getId());
  64 +
  65 + // 如下的返还,需要经过支持,也就是经历 afterPayOrder 流程
  66 + if (!order.getPayStatus()) {
  67 + return;
  68 + }
  69 + // 扣减(回滚)积分(订单赠送)
  70 + Integer givePoint = getSumValue(orderItems, TradeOrderItemDO::getGivePoint, Integer::sum);
  71 + reducePoint(order.getUserId(), givePoint, MemberPointBizTypeEnum.ORDER_GIVE_CANCEL,
  72 + order.getId());
  73 + // 扣减(回滚)用户经验
  74 + int payPrice = order.getPayPrice() - order.getRefundPrice();
  75 + memberLevelApi.addExperience(order.getUserId(), payPrice,
  76 + MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL.getType(), String.valueOf(order.getId()));
  77 + }
  78 +
  79 + @Override
  80 + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
  81 + // 扣减(回滚)积分(订单赠送)
  82 + reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM,
  83 + orderItem.getId());
  84 + // 增加(回滚)积分(订单抵扣)
  85 + addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM,
  86 + orderItem.getId());
  87 +
  88 + // 扣减(回滚)用户经验
  89 + AfterSaleDO afterSale = afterSaleService.getAfterSale(orderItem.getAfterSaleId());
  90 + memberLevelApi.reduceExperience(order.getUserId(), afterSale.getRefundPrice(),
  91 + MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL_ITEM.getType(), String.valueOf(orderItem.getId()));
  92 + }
  93 +
  94 + /**
  95 + * 添加用户积分
  96 + * <p>
  97 + * 目前是支付成功后,就会创建积分记录。
  98 + * <p>
  99 + * 业内还有两种做法,可以根据自己的业务调整:
  100 + * 1. 确认收货后,才创建积分记录
  101 + * 2. 支付 or 下单成功时,创建积分记录(冻结),确认收货解冻或者 n 天后解冻
  102 + *
  103 + * @param userId 用户编号
  104 + * @param point 增加积分数量
  105 + * @param bizType 业务编号
  106 + * @param bizId 业务编号
  107 + */
  108 + protected void addPoint(Long userId, Integer point, MemberPointBizTypeEnum bizType, Long bizId) {
  109 + if (point != null && point > 0) {
  110 + memberPointApi.addPoint(userId, point, bizType.getType(), String.valueOf(bizId));
  111 + }
  112 + }
  113 +
  114 + protected void reducePoint(Long userId, Integer point, MemberPointBizTypeEnum bizType, Long bizId) {
  115 + if (point != null && point > 0) {
  116 + memberPointApi.reducePoint(userId, point, bizType.getType(), String.valueOf(bizId));
  117 + }
  118 + }
  119 +
  120 +}
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeOrderHandler.java
1 1 package cn.iocoder.yudao.module.trade.service.order.handler;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
3 4 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
4 5 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  6 +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum;
5 7  
6 8 import java.util.List;
7 9  
... ... @@ -35,16 +37,42 @@ public interface TradeOrderHandler {
35 37 * @param order 订单
36 38 * @param orderItems 订单项
37 39 */
38   - default void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
39   - }
  40 + default void afterPayOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {}
40 41  
41 42 /**
42   - * 订单取消
  43 + * 订单取消后
43 44 *
44 45 * @param order 订单
45 46 * @param orderItems 订单项
46 47 */
47   - default void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  48 + default void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {}
  49 +
  50 + /**
  51 + * 订单项取消后
  52 + *
  53 + * @param order 订单
  54 + * @param orderItem 订单项
  55 + */
  56 + default void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {}
  57 +
  58 + /**
  59 + * 订单发货前
  60 + *
  61 + * @param order 订单
  62 + */
  63 + default void beforeDeliveryOrder(TradeOrderDO order) {}
  64 +
  65 + // ========== 公用方法 ==========
  66 +
  67 + /**
  68 + * 过滤“未售后”的订单项列表
  69 + *
  70 + * @param orderItems 订单项列表
  71 + * @return 过滤后的订单项列表
  72 + */
  73 + default List<TradeOrderItemDO> filterOrderItemListByNoneAfterSale(List<TradeOrderItemDO> orderItems) {
  74 + return CollectionUtils.filterList(orderItems,
  75 + item -> TradeOrderItemAfterSaleStatusEnum.isNone(item.getAfterSaleStatus()));
48 76 }
49 77  
50 78 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeProductSkuOrderHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.trade.service.order.handler;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
  5 +import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
  6 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
  7 +import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import javax.annotation.Resource;
  11 +import java.util.List;
  12 +
  13 +import static java.util.Collections.singletonList;
  14 +
  15 +/**
  16 + * 商品 SKU 库存的 {@link TradeOrderHandler} 实现类
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Component
  21 +public class TradeProductSkuOrderHandler implements TradeOrderHandler {
  22 +
  23 + @Resource
  24 + private ProductSkuApi productSkuApi;
  25 +
  26 + @Override
  27 + public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  28 + productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convertNegative(orderItems));
  29 + }
  30 +
  31 + @Override
  32 + public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  33 + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
  34 + orderItems = filterOrderItemListByNoneAfterSale(orderItems);
  35 + if (CollUtil.isEmpty(orderItems)) {
  36 + return;
  37 + }
  38 + productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(orderItems));
  39 + }
  40 +
  41 + @Override
  42 + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
  43 + productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(singletonList(orderItem)));
  44 + }
  45 +
  46 +}
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeSeckillHandler.java renamed to yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeSeckillOrderHandler.java
1 1 package cn.iocoder.yudao.module.trade.service.order.handler;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
3 5 import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi;
4 6 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
5 7 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
... ... @@ -10,33 +12,53 @@ import javax.annotation.Resource;
10 12 import java.util.List;
11 13  
12 14 /**
13   - * 秒杀订单 handler 实现类
  15 + * 秒杀订单的 {@link TradeOrderHandler} 实现类
14 16 *
15 17 * @author HUIHUI
16 18 */
17 19 @Component
18   -public class TradeSeckillHandler implements TradeOrderHandler {
  20 +public class TradeSeckillOrderHandler implements TradeOrderHandler {
19 21  
20 22 @Resource
21 23 private SeckillActivityApi seckillActivityApi;
22 24  
23 25 @Override
24 26 public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
25   - if (TradeOrderTypeEnum.isSeckill(order.getType())) {
  27 + if (!TradeOrderTypeEnum.isSeckill(order.getType())) {
26 28 return;
27 29 }
  30 + // 明确校验一下
  31 + Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品");
28 32  
29 33 // 扣减秒杀活动的库存
30   - seckillActivityApi.updateSeckillStock(order.getSeckillActivityId(),
  34 + seckillActivityApi.updateSeckillStockDecr(order.getSeckillActivityId(),
31 35 orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
32 36 }
33 37  
34 38 @Override
35   - public void cancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
36   - if (TradeOrderTypeEnum.isSeckill(order.getType())) {
  39 + public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
  40 + if (!TradeOrderTypeEnum.isSeckill(order.getType())) {
37 41 return;
38 42 }
  43 + // 明确校验一下
  44 + Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品");
39 45  
  46 + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚
  47 + orderItems = filterOrderItemListByNoneAfterSale(orderItems);
  48 + if (CollUtil.isEmpty(orderItems)) {
  49 + return;
  50 + }
  51 + afterCancelOrderItem(order, orderItems.get(0));
  52 + }
  53 +
  54 + @Override
  55 + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
  56 + if (!TradeOrderTypeEnum.isSeckill(order.getType())) {
  57 + return;
  58 + }
  59 + // 恢复秒杀活动的库存
  60 + seckillActivityApi.updateSeckillStockIncr(order.getSeckillActivityId(),
  61 + orderItem.getSkuId(), orderItem.getCount());
40 62 }
41 63  
42 64 }
... ...
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/level/MemberLevelApi.java
... ... @@ -28,4 +28,14 @@ public interface MemberLevelApi {
28 28 */
29 29 void addExperience(Long userId, Integer experience, Integer bizType, String bizId);
30 30  
  31 + /**
  32 + * 扣减会员经验
  33 + *
  34 + * @param userId 会员ID
  35 + * @param experience 经验
  36 + * @param bizType 业务类型 {@link MemberExperienceBizTypeEnum}
  37 + * @param bizId 业务编号
  38 + */
  39 + void reduceExperience(Long userId, Integer experience, Integer bizType, String bizId);
  40 +
31 41 }
... ...
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberExperienceBizTypeEnum.java
... ... @@ -20,10 +20,11 @@ public enum MemberExperienceBizTypeEnum {
20 20 */
21 21 ADMIN(0, "管理员调整", "管理员调整获得 {} 经验", true),
22 22 INVITE_REGISTER(1, "邀新奖励", "邀请好友获得 {} 经验", true),
23   - ORDER(2, "下单奖励", "下单获得 {} 经验", true),
24   - REFUND(3, "退单扣除", "退单获得 {} 经验", false),
25 23 SIGN_IN(4, "签到奖励", "签到获得 {} 经验", true),
26 24 LOTTERY(5, "抽奖奖励", "抽奖获得 {} 经验", true),
  25 + ORDER_GIVE(11, "下单奖励", "下单获得 {} 经验", true),
  26 + ORDER_GIVE_CANCEL(12, "下单奖励(整单取消)", "取消订单获得 {} 经验", false), // ORDER_GIVE 的取消
  27 + ORDER_GIVE_CANCEL_ITEM(13, "下单奖励(单个退款)", "退款订单获得 {} 经验", false), // ORDER_GIVE 的取消
27 28 ;
28 29  
29 30 /**
... ...
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/point/MemberPointBizTypeEnum.java
... ... @@ -18,11 +18,14 @@ public enum MemberPointBizTypeEnum implements IntArrayValuable {
18 18  
19 19 SIGN(1, "签到", "签到获得 {} 积分", true),
20 20 ADMIN(2, "管理员修改", "管理员修改 {} 积分", true),
21   - ORDER_GIVE(10, "订单奖励", "下单获得 {} 积分", true), // 支付订单时,赠送积分
22   - ORDER_CANCEL(11, "订单取消", "订单取消,退还 {} 积分", true), // 取消订单时,退回积分
23   - ORDER_USE(12, "订单使用", "下单使用 {} 积分", false), // 下单时,扣减积分
24   - AFTER_SALE_REFUND_USED(13, "订单退款", "订单退款,退还 {} 积分", true), // 售后订单成功时,退回积分(对应 ORDER_USE 操作)
25   - AFTER_SALE_DEDUCT_GIVE(14, "订单退款", "订单退款,扣除赠送的 {} 积分", false), // 售后订单成功时,扣减积分(对应 ORDER_GIVE 操作)
  21 +
  22 + ORDER_USE(11, "订单积分抵扣", "下单使用 {} 积分", false), // 下单时,扣减积分
  23 + ORDER_USE_CANCEL(12, "订单积分抵扣(整单取消)", "订单取消,退还 {} 积分", true), // ORDER_USE 的取消
  24 + ORDER_USE_CANCEL_ITEM(13, "订单积分抵扣(单个退款)", "订单退款,退还 {} 积分", true), // ORDER_USE 的取消
  25 +
  26 + ORDER_GIVE(21, "订单积分奖励", "下单获得 {} 积分", true), // 支付订单时,赠送积分
  27 + ORDER_GIVE_CANCEL(22, "订单积分奖励(整单取消)", "订单取消,退还 {} 积分", false), // ORDER_GIVE 的取消
  28 + ORDER_GIVE_CANCEL_ITEM(23, "订单积分奖励(单个退款)", "订单退款,扣除赠送的 {} 积分", false) // ORDER_GIVE 的取消
26 29 ;
27 30  
28 31 /**
... ...
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/api/level/MemberLevelApiImpl.java
... ... @@ -38,4 +38,9 @@ public class MemberLevelApiImpl implements MemberLevelApi {
38 38 memberLevelService.addExperience(userId, experience, bizTypeEnum, bizId);
39 39 }
40 40  
  41 + @Override
  42 + public void reduceExperience(Long userId, Integer experience, Integer bizType, String bizId) {
  43 + addExperience(userId, -experience, bizType, bizId);
  44 + }
  45 +
41 46 }
... ...