mall: 发送优惠券功能优化

This commit is contained in:
owen 2023-09-01 23:45:50 +08:00
parent 76f510f247
commit 3fbce643ee
11 changed files with 55 additions and 109 deletions

View File

@ -15,11 +15,9 @@ import java.util.Arrays;
@Getter @Getter
public enum CouponTakeTypeEnum implements IntArrayValuable { public enum CouponTakeTypeEnum implements IntArrayValuable {
COMMON(0, "通用"), // TODO @疯狂要不去掉通用"和“兑换”,保持和 crmeb 一致;就手动领取、指定发送、新人券
BY_USER(1, "直接领取"), // 用户可在首页每日领劵直接领取 BY_USER(1, "直接领取"), // 用户可在首页每日领劵直接领取
BY_ADMIN(2, "指定发放"), // 后台指定会员赠送优惠劵 BY_ADMIN(2, "指定发放"), // 后台指定会员赠送优惠劵
BY_REGISTER(3, "新人券"), // 注册时自动领取 BY_REGISTER(3, "新人券"), // 注册时自动领取
BY_EXCHANGE(4, "兑换"), // 一般渠道券通过兑换领取
; ;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTakeTypeEnum::getValue).toArray(); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTakeTypeEnum::getValue).toArray();

View File

@ -76,8 +76,8 @@ public class CouponController {
@Operation(summary = "发送优惠劵") @Operation(summary = "发送优惠劵")
@PreAuthorize("@ss.hasPermission('promotion:coupon:send')") @PreAuthorize("@ss.hasPermission('promotion:coupon:send')")
public CommonResult<Boolean> sendCoupon(@Valid @RequestBody CouponSendReqVO reqVO) { public CommonResult<Boolean> sendCoupon(@Valid @RequestBody CouponSendReqVO reqVO) {
Boolean result = couponService.sendCoupon(reqVO.getTemplateId(), reqVO.getUserIds()); couponService.takeCouponByAdmin(reqVO.getTemplateId(), reqVO.getUserIds());
return success(result); return success(true);
} }
} }

View File

@ -1,12 +1,10 @@
package cn.iocoder.yudao.module.promotion.controller.admin.coupon; package cn.iocoder.yudao.module.promotion.controller.admin.coupon;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.*; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.*;
import cn.iocoder.yudao.module.promotion.convert.coupon.CouponTemplateConvert; import cn.iocoder.yudao.module.promotion.convert.coupon.CouponTemplateConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
import cn.iocoder.yudao.module.promotion.service.coupon.CouponTemplateService; import cn.iocoder.yudao.module.promotion.service.coupon.CouponTemplateService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -17,7 +15,6 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -78,15 +75,4 @@ public class CouponTemplateController {
PageResult<CouponTemplateDO> pageResult = couponTemplateService.getCouponTemplatePage(pageVO); PageResult<CouponTemplateDO> pageResult = couponTemplateService.getCouponTemplatePage(pageVO);
return success(CouponTemplateConvert.INSTANCE.convertPage(pageResult)); return success(CouponTemplateConvert.INSTANCE.convertPage(pageResult));
} }
// TODO @疯狂是不是可以合并到 getCouponTemplatePage 接口作为一个参数选择
@GetMapping("/can-take-page")
@Operation(summary = "获得可用于领取的优惠劵模板分页")
@PreAuthorize("@ss.hasPermission('promotion:coupon-template:query')")
public CommonResult<PageResult<CouponTemplateRespVO>> getCanTakeCouponTemplatePage(@Valid CouponTemplatePageReqVO pageVO) {
List<Integer> canTakeTypes = ListUtil.of(CouponTakeTypeEnum.COMMON.getValue(), CouponTakeTypeEnum.BY_ADMIN.getValue());
PageResult<CouponTemplateDO> pageResult = couponTemplateService.getCanTakeCouponTemplatePage(pageVO, canTakeTypes);
return success(CouponTemplateConvert.INSTANCE.convertPage(pageResult));
}
} }

View File

@ -8,6 +8,7 @@ import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ -30,4 +31,6 @@ public class CouponTemplatePageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime; private LocalDateTime[] createTime;
@Schema(description = "可以领取的类型", example = "[1,2]")
private List<Integer> canTakeTypes;
} }

View File

@ -55,8 +55,7 @@ public interface CouponMapper extends BaseMapperX<CouponDO> {
.eq(CouponDO::getStatus, status)); .eq(CouponDO::getStatus, status));
} }
// TODO @疯狂 selectList default List<CouponDO> selectListByTemplateIdAndUserId(Long templateId, Collection<Long> userIds) {
default List<CouponDO> selectByTemplateIdAndUserId(Long templateId, Collection<Long> userIds) {
return selectList(new LambdaQueryWrapperX<CouponDO>() return selectList(new LambdaQueryWrapperX<CouponDO>()
.eq(CouponDO::getTemplateId, templateId) .eq(CouponDO::getTemplateId, templateId)
.in(CouponDO::getUserId, userIds) .in(CouponDO::getUserId, userIds)

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.coupon; package cn.iocoder.yudao.module.promotion.dal.mysql.coupon;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
@ -11,7 +12,6 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@ -23,27 +23,23 @@ import java.util.function.Consumer;
public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> { public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
default PageResult<CouponTemplateDO> selectPage(CouponTemplatePageReqVO reqVO) { default PageResult<CouponTemplateDO> selectPage(CouponTemplatePageReqVO reqVO) {
Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = null;
if (CollUtil.isNotEmpty(reqVO.getCanTakeTypes())) {
// 构建可领取的查询条件, 好啰嗦 ( -_-)
canTakeConsumer = w ->
w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的
.in(CouponTemplateDO::getTakeType, reqVO.getCanTakeTypes()) // 2. 领取方式一致
.and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime) // 3. 未过期
.or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()))
.apply(" take_count < total_count "); // 4. 剩余数量大于 0
}
return selectPage(reqVO, new LambdaQueryWrapperX<CouponTemplateDO>() return selectPage(reqVO, new LambdaQueryWrapperX<CouponTemplateDO>()
.likeIfPresent(CouponTemplateDO::getName, reqVO.getName()) .likeIfPresent(CouponTemplateDO::getName, reqVO.getName())
.eqIfPresent(CouponTemplateDO::getStatus, reqVO.getStatus()) .eqIfPresent(CouponTemplateDO::getStatus, reqVO.getStatus())
.eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType()) .eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType())
.betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime()) .betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(CouponTemplateDO::getId)); .and(canTakeConsumer != null, canTakeConsumer)
}
default PageResult<CouponTemplateDO> selectCanTakePage(CouponTemplatePageReqVO reqVO, Collection<Integer> takeTypes) {
// 构建可领取的查询条件, 好啰嗦 ( -_-)
Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = w ->
w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的
.in(CouponTemplateDO::getTakeType, takeTypes) // 2. 领取方式一致
.and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime) // 3. 未过期
.or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()))
.apply(" take_count < total_count "); // 4. 剩余数量大于 0
return selectPage(reqVO, new LambdaQueryWrapperX<CouponTemplateDO>()
.likeIfPresent(CouponTemplateDO::getName, reqVO.getName())
.eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType())
.betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime())
.and(canTakeConsumer)
.orderByDesc(CouponTemplateDO::getId)); .orderByDesc(CouponTemplateDO::getId));
} }

View File

@ -78,27 +78,23 @@ public interface CouponService {
*/ */
Long getUnusedCouponCount(Long userId); Long getUnusedCouponCount(Long userId);
// TODO @疯狂可以返回 void因为都是 true = =
/** /**
* 领取优惠券 * 领取优惠券
* *
* @param templateId 优惠券模板编号 * @param templateId 优惠券模板编号
* @param userIds 用户编号列表 * @param userIds 用户编号列表
* @param takeType 领取方式 * @param takeType 领取方式
* @return 领取结果
*/ */
Boolean takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType); void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType);
// TODO @疯狂感觉 3 个方法的命名改成 takeCouponByAdmintakeCouponByUsertakeCouponByRegister 会更容易理解哈现在两个都叫 sendCoupon 感觉不太好懂
/** /**
* 管理员给用户发送优惠券 * 管理员给用户发送优惠券
* *
* @param templateId 优惠券模板编号 * @param templateId 优惠券模板编号
* @param userIds 用户编号列表 * @param userIds 用户编号列表
* @return 发送结果
*/ */
default Boolean sendCoupon(Long templateId, Set<Long> userIds) { default void takeCouponByAdmin(Long templateId, Set<Long> userIds) {
return takeCoupon(templateId, userIds, CouponTakeTypeEnum.BY_ADMIN); takeCoupon(templateId, userIds, CouponTakeTypeEnum.BY_ADMIN);
} }
/** /**
@ -106,10 +102,9 @@ public interface CouponService {
* *
* @param templateId 优惠券模板编号 * @param templateId 优惠券模板编号
* @param userId 用户编号 * @param userId 用户编号
* @return 发送结果
*/ */
default Boolean receiveCoupon(Long templateId, Long userId) { default void takeCouponByUser(Long templateId, Long userId) {
return takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_USER); takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_USER);
} }
/** /**
@ -117,9 +112,8 @@ public interface CouponService {
* *
* @param templateId 优惠券模板编号 * @param templateId 优惠券模板编号
* @param userId 用户编号列表 * @param userId 用户编号列表
* @return 发送结果
*/ */
default Boolean sendCoupon(Long templateId, Long userId) { default void takeCouponByRegister(Long templateId, Long userId) {
return takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_REGISTER); takeCoupon(templateId, CollUtil.newHashSet(userId), CouponTakeTypeEnum.BY_REGISTER);
} }
} }

View File

@ -30,6 +30,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -134,74 +135,63 @@ public class CouponServiceImpl implements CouponService {
} }
@Override @Override
public Boolean takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) { public void takeCoupon(Long templateId, Set<Long> userIds, CouponTakeTypeEnum takeType) {
// 1. 校验并过滤用户
CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
userIds = validateAndFilterTakeUserId(template, userIds, takeType); // 1. 过滤掉达到领取限制的用户
removeTakeLimitUserId(template, userIds);
// 2. 校验用户
validateCanTake(template, userIds, takeType);
// 2. 批量保存优惠劵 // 3. 批量保存优惠劵
// TODO @疯狂这里可以使用 CollectionUtils.convertList 更简洁stream 可以简化很多代码常用的 stream 操作使用 util 可以进一步简洁同时提升可读性 couponMapper.insertBatch(convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)));
List<CouponDO> couponList = userIds.stream()
.map(userId -> CouponConvert.INSTANCE.convert(template, userId))
.collect(Collectors.toList());
couponMapper.insertBatch(couponList);
// 3. 增加优惠劵模板的领取数量 // 3. 增加优惠劵模板的领取数量
couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size());
return true;
} }
/** /**
* 校验优惠券模板, 并过滤不可以领取的用户 * 校验优惠券是否可以领取
* *
* @param couponTemplate 优惠券模板 * @param couponTemplate 优惠券模板
* @param userIds 领取人列表 * @param userIds 领取人列表
* @param takeType 领取方式 * @param takeType 领取方式
* @return 可领取此券的用户列表
*/ */
// TODO @疯狂我建议哈校验模版和过滤用户分成两个方法混在一起有点小重后续单测可能也比较难写哈 private void validateCanTake(CouponTemplateDO couponTemplate, Set<Long> userIds, CouponTakeTypeEnum takeType) {
private Set<Long> validateAndFilterTakeUserId(CouponTemplateDO couponTemplate, Set<Long> userIds, CouponTakeTypeEnum takeType) { // 如果所有用户都领取过则抛出异常
// 1.1 校验模板 if (userIds.isEmpty()) {
throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
}
// 校验模板
if (couponTemplate == null) { if (couponTemplate == null) {
throw exception(COUPON_TEMPLATE_NOT_EXISTS); throw exception(COUPON_TEMPLATE_NOT_EXISTS);
} }
// 1.2 校验剩余数量 // 校验剩余数量
if (couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { if (couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
throw exception(COUPON_TEMPLATE_NOT_ENOUGH); throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
} }
// 1.3 校验"固定日期"的有效期类型是否过期 // 校验"固定日期"的有效期类型是否过期
if (CouponTemplateValidityTypeEnum.DATE.getType().equals(couponTemplate.getValidityType())) { if (CouponTemplateValidityTypeEnum.DATE.getType().equals(couponTemplate.getValidityType())) {
if (LocalDateTimeUtils.beforeNow(couponTemplate.getValidEndTime())) { if (LocalDateTimeUtils.beforeNow(couponTemplate.getValidEndTime())) {
throw exception(COUPON_TEMPLATE_EXPIRED); throw exception(COUPON_TEMPLATE_EXPIRED);
} }
} }
// 1.4 校验领取方式
// TODO @疯狂如果要做这样的判断使用 !ObjectUtils.equalsAny() 会更简洁 // 校验领取方式
if (!CouponTakeTypeEnum.COMMON.getValue().equals(couponTemplate.getTakeType())) {
if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) { if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) {
throw exception(COUPON_TEMPLATE_CANNOT_TAKE); throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
} }
} }
// 2.1 过滤掉已经领取到上限的用户 private void removeTakeLimitUserId(CouponTemplateDO couponTemplate, Set<Long> userIds) {
List<CouponDO> alreadyTakeCoupons = couponMapper.selectByTemplateIdAndUserId(couponTemplate.getId(), userIds); // 查询已领过券的用户
// 校验新人券 List<CouponDO> alreadyTakeCoupons = couponMapper.selectListByTemplateIdAndUserId(couponTemplate.getId(), userIds);
// TODO @疯狂我在想这个判断是不是和下面的 couponTemplate.getTakeLimitCount() > 0 冗余了可以先都过滤然后最终去判断 userIds 是不是空了
if (CouponTakeTypeEnum.BY_REGISTER.equals(takeType)) { // 移除达到领取限制的用户
if (!alreadyTakeCoupons.isEmpty()) {
throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
}
}
// 校验领取数量限制
if (couponTemplate.getTakeLimitCount() > 0) { if (couponTemplate.getTakeLimitCount() > 0) {
Map<Long, Integer> userTakeCountMap = CollStreamUtil.groupBy(alreadyTakeCoupons, CouponDO::getUserId, Collectors.summingInt(c -> 1)); Map<Long, Integer> userTakeCountMap = CollStreamUtil.groupBy(alreadyTakeCoupons, CouponDO::getUserId, Collectors.summingInt(c -> 1));
userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount()); userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
// 2.2 如果所有用户都领取过则抛出异常
if (userIds.isEmpty()) {
throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
} }
} }
return userIds;
}
} }

View File

@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.Cou
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.List;
/** /**
* 优惠劵模板 Service 接口 * 优惠劵模板 Service 接口
@ -62,17 +61,6 @@ public interface CouponTemplateService {
*/ */
PageResult<CouponTemplateDO> getCouponTemplatePage(CouponTemplatePageReqVO pageReqVO); PageResult<CouponTemplateDO> getCouponTemplatePage(CouponTemplatePageReqVO pageReqVO);
/**
* 获得优惠劵模板分页
*
* @param pageReqVO 分页查询
* @param canTakeTypes 可领取的方式
* @return 优惠劵模板分页
*/
PageResult<CouponTemplateDO> getCanTakeCouponTemplatePage(CouponTemplatePageReqVO pageReqVO,
List<Integer> canTakeTypes);
/** /**
* 更新优惠劵模板的领取数量 * 更新优惠劵模板的领取数量
* *

View File

@ -12,7 +12,6 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS;
@ -88,11 +87,6 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
return couponTemplateMapper.selectPage(pageReqVO); return couponTemplateMapper.selectPage(pageReqVO);
} }
@Override
public PageResult<CouponTemplateDO> getCanTakeCouponTemplatePage(CouponTemplatePageReqVO pageReqVO, List<Integer> canTakeTypes) {
return couponTemplateMapper.selectCanTakePage(pageReqVO, canTakeTypes);
}
@Override @Override
public void updateCouponTemplateTakeCount(Long id, int incrCount) { public void updateCouponTemplateTakeCount(Long id, int incrCount) {
couponTemplateMapper.updateTakeCount(id, incrCount); couponTemplateMapper.updateTakeCount(id, incrCount);

View File

@ -217,8 +217,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
// TODO @puhui999订单超时自动取消 // TODO @puhui999订单超时自动取消
// TODO @疯狂用户手动取消订单
/** /**
* 校验收件地址是否存在 * 校验收件地址是否存在
* *