Commit 3fbce643ee6f4e1cdcc871dd3b2608d518e4c0c2

Authored by owen
1 parent 76f510f2

mall: 发送优惠券功能优化

Showing 11 changed files with 55 additions and 109 deletions
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/coupon/CouponTakeTypeEnum.java
... ... @@ -15,11 +15,9 @@ import java.util.Arrays;
15 15 @Getter
16 16 public enum CouponTakeTypeEnum implements IntArrayValuable {
17 17  
18   - COMMON(0, "通用"), // TODO @疯狂:要不去掉“通用"和“兑换”,保持和 crmeb 一致;就手动领取、指定发送、新人券
19 18 BY_USER(1, "直接领取"), // 用户可在首页、每日领劵直接领取
20 19 BY_ADMIN(2, "指定发放"), // 后台指定会员赠送优惠劵
21 20 BY_REGISTER(3, "新人券"), // 注册时自动领取
22   - BY_EXCHANGE(4, "兑换"), // 一般渠道券通过兑换领取
23 21 ;
24 22  
25 23 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTakeTypeEnum::getValue).toArray();
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/CouponController.java
... ... @@ -76,8 +76,8 @@ public class CouponController {
76 76 @Operation(summary = "发送优惠劵")
77 77 @PreAuthorize("@ss.hasPermission('promotion:coupon:send')")
78 78 public CommonResult<Boolean> sendCoupon(@Valid @RequestBody CouponSendReqVO reqVO) {
79   - Boolean result = couponService.sendCoupon(reqVO.getTemplateId(), reqVO.getUserIds());
80   - return success(result);
  79 + couponService.takeCouponByAdmin(reqVO.getTemplateId(), reqVO.getUserIds());
  80 + return success(true);
81 81 }
82 82  
83 83 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/CouponTemplateController.java
1 1 package cn.iocoder.yudao.module.promotion.controller.admin.coupon;
2 2  
3   -import cn.hutool.core.collection.ListUtil;
4 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 5 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.*;
7 6 import cn.iocoder.yudao.module.promotion.convert.coupon.CouponTemplateConvert;
8 7 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
9   -import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
10 8 import cn.iocoder.yudao.module.promotion.service.coupon.CouponTemplateService;
11 9 import io.swagger.v3.oas.annotations.Operation;
12 10 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -17,7 +15,6 @@ import org.springframework.web.bind.annotation.*;
17 15  
18 16 import javax.annotation.Resource;
19 17 import javax.validation.Valid;
20   -import java.util.List;
21 18  
22 19 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
23 20  
... ... @@ -78,15 +75,4 @@ public class CouponTemplateController {
78 75 PageResult<CouponTemplateDO> pageResult = couponTemplateService.getCouponTemplatePage(pageVO);
79 76 return success(CouponTemplateConvert.INSTANCE.convertPage(pageResult));
80 77 }
81   -
82   - // TODO @疯狂:是不是可以合并到 getCouponTemplatePage 接口,作为一个参数选择
83   - @GetMapping("/can-take-page")
84   - @Operation(summary = "获得可用于领取的优惠劵模板分页")
85   - @PreAuthorize("@ss.hasPermission('promotion:coupon-template:query')")
86   - public CommonResult<PageResult<CouponTemplateRespVO>> getCanTakeCouponTemplatePage(@Valid CouponTemplatePageReqVO pageVO) {
87   - List<Integer> canTakeTypes = ListUtil.of(CouponTakeTypeEnum.COMMON.getValue(), CouponTakeTypeEnum.BY_ADMIN.getValue());
88   - PageResult<CouponTemplateDO> pageResult = couponTemplateService.getCanTakeCouponTemplatePage(pageVO, canTakeTypes);
89   - return success(CouponTemplateConvert.INSTANCE.convertPage(pageResult));
90   - }
91   -
92 78 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/coupon/vo/template/CouponTemplatePageReqVO.java
... ... @@ -8,6 +8,7 @@ import lombok.ToString;
8 8 import org.springframework.format.annotation.DateTimeFormat;
9 9  
10 10 import java.time.LocalDateTime;
  11 +import java.util.List;
11 12  
12 13 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
13 14  
... ... @@ -30,4 +31,6 @@ public class CouponTemplatePageReqVO extends PageParam {
30 31 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
31 32 private LocalDateTime[] createTime;
32 33  
  34 + @Schema(description = "可以领取的类型", example = "[1,2]")
  35 + private List<Integer> canTakeTypes;
33 36 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponMapper.java
... ... @@ -55,8 +55,7 @@ public interface CouponMapper extends BaseMapperX&lt;CouponDO&gt; {
55 55 .eq(CouponDO::getStatus, status));
56 56 }
57 57  
58   - // TODO @疯狂:要 selectList 哈;
59   - default List<CouponDO> selectByTemplateIdAndUserId(Long templateId, Collection<Long> userIds) {
  58 + default List<CouponDO> selectListByTemplateIdAndUserId(Long templateId, Collection<Long> userIds) {
60 59 return selectList(new LambdaQueryWrapperX<CouponDO>()
61 60 .eq(CouponDO::getTemplateId, templateId)
62 61 .in(CouponDO::getUserId, userIds)
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
1 1 package cn.iocoder.yudao.module.promotion.dal.mysql.coupon;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
... ... @@ -11,7 +12,6 @@ import org.apache.ibatis.annotations.Mapper;
11 12 import org.apache.ibatis.annotations.Param;
12 13  
13 14 import java.time.LocalDateTime;
14   -import java.util.Collection;
15 15 import java.util.function.Consumer;
16 16  
17 17 /**
... ... @@ -23,27 +23,23 @@ import java.util.function.Consumer;
23 23 public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
24 24  
25 25 default PageResult<CouponTemplateDO> selectPage(CouponTemplatePageReqVO reqVO) {
26   - return selectPage(reqVO, new LambdaQueryWrapperX<CouponTemplateDO>()
27   - .likeIfPresent(CouponTemplateDO::getName, reqVO.getName())
28   - .eqIfPresent(CouponTemplateDO::getStatus, reqVO.getStatus())
29   - .eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType())
30   - .betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime())
31   - .orderByDesc(CouponTemplateDO::getId));
32   - }
  26 + Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = null;
  27 + if (CollUtil.isNotEmpty(reqVO.getCanTakeTypes())) {
  28 + // 构建可领取的查询条件, 好啰嗦 ( ╯-_-)╯┴—┴
  29 + canTakeConsumer = w ->
  30 + w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的
  31 + .in(CouponTemplateDO::getTakeType, reqVO.getCanTakeTypes()) // 2. 领取方式一致
  32 + .and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime) // 3. 未过期
  33 + .or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()))
  34 + .apply(" take_count < total_count "); // 4. 剩余数量大于 0
  35 + }
33 36  
34   - default PageResult<CouponTemplateDO> selectCanTakePage(CouponTemplatePageReqVO reqVO, Collection<Integer> takeTypes) {
35   - // 构建可领取的查询条件, 好啰嗦 ( ╯-_-)╯┴—┴
36   - Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = w ->
37   - w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的
38   - .in(CouponTemplateDO::getTakeType, takeTypes) // 2. 领取方式一致
39   - .and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime) // 3. 未过期
40   - .or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()))
41   - .apply(" take_count < total_count "); // 4. 剩余数量大于 0
42 37 return selectPage(reqVO, new LambdaQueryWrapperX<CouponTemplateDO>()
43 38 .likeIfPresent(CouponTemplateDO::getName, reqVO.getName())
  39 + .eqIfPresent(CouponTemplateDO::getStatus, reqVO.getStatus())
44 40 .eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType())
45 41 .betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime())
46   - .and(canTakeConsumer)
  42 + .and(canTakeConsumer != null, canTakeConsumer)
47 43 .orderByDesc(CouponTemplateDO::getId));
48 44 }
49 45  
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponService.java
... ... @@ -78,27 +78,23 @@ public interface CouponService {
78 78 */
79 79 Long getUnusedCouponCount(Long userId);
80 80  
81   - // TODO @疯狂:可以返回 void;因为都是 true = =
82 81 /**
83 82 * 领取优惠券
84 83 *
85 84 * @param templateId 优惠券模板编号
86 85 * @param userIds 用户编号列表
87   - * @param takeType 领取方式
88   - * @return 领取结果
  86 + * @param takeType 领取方式
89 87 */
90   - Boolean takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
  88 + void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
91 89  
92   - // TODO @疯狂:感觉 3 个方法的命名,改成 takeCouponByAdmin;takeCouponByUser;takeCouponByRegister 会更容易理解哈;现在两个都叫 sendCoupon ,感觉不太好懂
93 90 /**
94 91 * 【管理员】给用户发送优惠券
95 92 *
96 93 * @param templateId 优惠券模板编号
97 94 * @param userIds 用户编号列表
98   - * @return 发送结果
99 95 */
100   - default Boolean sendCoupon(Long templateId, Set<Long> userIds) {
101   - return takeCoupon(templateId, userIds, CouponTakeTypeEnum.BY_ADMIN);
  96 + default void takeCouponByAdmin(Long templateId, Set<Long> userIds) {
  97 + takeCoupon(templateId, userIds, CouponTakeTypeEnum.BY_ADMIN);
102 98 }
103 99  
104 100 /**
... ... @@ -106,10 +102,9 @@ public interface CouponService {
106 102 *
107 103 * @param templateId 优惠券模板编号
108 104 * @param userId 用户编号
109   - * @return 发送结果
110 105 */
111   - default Boolean receiveCoupon(Long templateId, Long userId) {
112   - return takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_USER);
  106 + default void takeCouponByUser(Long templateId, Long userId) {
  107 + takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_USER);
113 108 }
114 109  
115 110 /**
... ... @@ -117,9 +112,8 @@ public interface CouponService {
117 112 *
118 113 * @param templateId 优惠券模板编号
119 114 * @param userId 用户编号列表
120   - * @return 发送结果
121 115 */
122   - default Boolean sendCoupon(Long templateId, Long userId) {
123   - return takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_REGISTER);
  116 + default void takeCouponByRegister(Long templateId, Long userId) {
  117 + takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_REGISTER);
124 118 }
125 119 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
... ... @@ -30,6 +30,7 @@ import java.util.Set;
30 30 import java.util.stream.Collectors;
31 31  
32 32 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
33 34 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
34 35 import static java.util.Arrays.asList;
35 36  
... ... @@ -134,74 +135,63 @@ public class CouponServiceImpl implements CouponService {
134 135 }
135 136  
136 137 @Override
137   - public Boolean takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
138   - // 1. 校验并过滤用户
  138 + public void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
139 139 CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
140   - userIds = validateAndFilterTakeUserId(template, userIds, takeType);
  140 + // 1. 过滤掉达到领取限制的用户
  141 + removeTakeLimitUserId(template, userIds);
  142 + // 2. 校验用户
  143 + validateCanTake(template, userIds, takeType);
141 144  
142   - // 2. 批量保存优惠劵
143   - // TODO @疯狂:这里可以使用 CollectionUtils.convertList 更简洁;stream 可以简化很多代码,常用的 stream 操作,使用 util 可以进一步简洁,同时提升可读性
144   - List<CouponDO> couponList = userIds.stream()
145   - .map(userId -> CouponConvert.INSTANCE.convert(template, userId))
146   - .collect(Collectors.toList());
147   - couponMapper.insertBatch(couponList);
  145 + // 3. 批量保存优惠劵
  146 + couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)));
148 147  
149 148 // 3. 增加优惠劵模板的领取数量
150 149 couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
151   - return true;
152 150 }
153 151  
154 152 /**
155   - * 校验优惠券模板, 并过滤不可以领取的用户
  153 + * 校验优惠券是否可以领取
156 154 *
157 155 * @param couponTemplate 优惠券模板
158 156 * @param userIds 领取人列表
159 157 * @param takeType 领取方式
160   - * @return 可领取此券的用户列表
161 158 */
162   - // TODO @疯狂:我建议哈,校验模版,和过滤用户分成两个方法;混在一起,有点小重,后续单测可能也比较难写哈;
163   - private Set<Long> validateAndFilterTakeUserId(CouponTemplateDO couponTemplate, Set<Long> userIds, CouponTakeTypeEnum takeType) {
164   - // 1.1 校验模板
  159 + private void validateCanTake(CouponTemplateDO couponTemplate, Set<Long> userIds, CouponTakeTypeEnum takeType) {
  160 + // 如果所有用户都领取过,则抛出异常
  161 + if (userIds.isEmpty()) {
  162 + throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
  163 + }
  164 +
  165 + // 校验模板
165 166 if (couponTemplate == null) {
166 167 throw exception(COUPON_TEMPLATE_NOT_EXISTS);
167 168 }
168   - // 1.2 校验剩余数量
  169 + // 校验剩余数量
169 170 if (couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
170 171 throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
171 172 }
172   - // 1.3 校验"固定日期"的有效期类型是否过期
  173 + // 校验"固定日期"的有效期类型是否过期
173 174 if (CouponTemplateValidityTypeEnum.DATE.getType().equals(couponTemplate.getValidityType())) {
174 175 if (LocalDateTimeUtils.beforeNow(couponTemplate.getValidEndTime())) {
175 176 throw exception(COUPON_TEMPLATE_EXPIRED);
176 177 }
177 178 }
178   - // 1.4 校验领取方式
179   - // TODO @疯狂:如果要做这样的判断,使用 !ObjectUtils.equalsAny() 会更简洁
180   - if (!CouponTakeTypeEnum.COMMON.getValue().equals(couponTemplate.getTakeType())) {
181   - if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) {
182   - throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
183   - }
184   - }
185 179  
186   - // 2.1 过滤掉,已经领取到上限的用户
187   - List<CouponDO> alreadyTakeCoupons = couponMapper.selectByTemplateIdAndUserId(couponTemplate.getId(), userIds);
188   - // 校验新人券
189   - // TODO @疯狂:我在想,这个判断,是不是和下面的 couponTemplate.getTakeLimitCount() > 0 冗余了;可以先都过滤,然后最终去判断 userIds 是不是空了;
190   - if (CouponTakeTypeEnum.BY_REGISTER.equals(takeType)) {
191   - if (!alreadyTakeCoupons.isEmpty()) {
192   - throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
193   - }
  180 + // 校验领取方式
  181 + if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) {
  182 + throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
194 183 }
195   - // 校验领取数量限制
  184 + }
  185 +
  186 + private void removeTakeLimitUserId(CouponTemplateDO couponTemplate, Set<Long> userIds) {
  187 + // 查询已领过券的用户
  188 + List<CouponDO> alreadyTakeCoupons = couponMapper.selectListByTemplateIdAndUserId(couponTemplate.getId(), userIds);
  189 +
  190 + // 移除达到领取限制的用户
196 191 if (couponTemplate.getTakeLimitCount() > 0) {
197 192 Map<Long, Integer> userTakeCountMap = CollStreamUtil.groupBy(alreadyTakeCoupons, CouponDO::getUserId, Collectors.summingInt(c -> 1));
198 193 userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
199   - // 2.2 如果所有用户都领取过,则抛出异常
200   - if (userIds.isEmpty()) {
201   - throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
202   - }
203 194 }
204   - return userIds;
205 195 }
206 196  
207 197 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateService.java
... ... @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.Cou
7 7 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
8 8  
9 9 import javax.validation.Valid;
10   -import java.util.List;
11 10  
12 11 /**
13 12 * 优惠劵模板 Service 接口
... ... @@ -62,17 +61,6 @@ public interface CouponTemplateService {
62 61 */
63 62 PageResult<CouponTemplateDO> getCouponTemplatePage(CouponTemplatePageReqVO pageReqVO);
64 63  
65   -
66   - /**
67   - * 获得优惠劵模板分页
68   - *
69   - * @param pageReqVO 分页查询
70   - * @param canTakeTypes 可领取的方式
71   - * @return 优惠劵模板分页
72   - */
73   - PageResult<CouponTemplateDO> getCanTakeCouponTemplatePage(CouponTemplatePageReqVO pageReqVO,
74   - List<Integer> canTakeTypes);
75   -
76 64 /**
77 65 * 更新优惠劵模板的领取数量
78 66 *
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
... ... @@ -12,7 +12,6 @@ import org.springframework.stereotype.Service;
12 12 import org.springframework.validation.annotation.Validated;
13 13  
14 14 import javax.annotation.Resource;
15   -import java.util.List;
16 15  
17 16 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
18 17 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS;
... ... @@ -89,11 +88,6 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
89 88 }
90 89  
91 90 @Override
92   - public PageResult<CouponTemplateDO> getCanTakeCouponTemplatePage(CouponTemplatePageReqVO pageReqVO, List<Integer> canTakeTypes) {
93   - return couponTemplateMapper.selectCanTakePage(pageReqVO, canTakeTypes);
94   - }
95   -
96   - @Override
97 91 public void updateCouponTemplateTakeCount(Long id, int incrCount) {
98 92 couponTemplateMapper.updateTakeCount(id, incrCount);
99 93 }
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
... ... @@ -217,8 +217,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
217 217  
218 218 // TODO @puhui999:订单超时,自动取消;
219 219  
220   - // TODO @疯狂:用户手动取消订单;
221   -
222 220 /**
223 221 * 校验收件地址是否存在
224 222 *
... ...