【功能优化】商城:价格计算时,返回可用 + 不可用的优惠劵

This commit is contained in:
YunaiV 2024-09-07 12:05:42 +08:00
parent 30fd7996f6
commit 840cfad84a
21 changed files with 240 additions and 263 deletions

View File

@ -15,12 +15,12 @@
<!-- 各种 module 拓展 -->
<module>yudao-module-system</module>
<module>yudao-module-infra</module>
<module>yudao-module-member</module>
<!-- <module>yudao-module-member</module>-->
<!-- <module>yudao-module-bpm</module>-->
<!-- <module>yudao-module-report</module>-->
<!-- <module>yudao-module-mp</module>-->
<module>yudao-module-pay</module>
<module>yudao-module-mall</module>
<!-- <module>yudao-module-pay</module>-->
<!-- <module>yudao-module-mall</module>-->
<!-- <module>yudao-module-crm</module>-->
<!-- <module>yudao-module-erp</module>-->
<!-- <module>yudao-module-ai</module>-->

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.promotion.api.coupon;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import jakarta.validation.Valid;
import java.util.List;
@ -15,6 +14,15 @@ import java.util.Map;
*/
public interface CouponApi {
/**
* 获得用户的优惠劵列表
*
* @param userId 用户编号
* @param status 优惠劵状态
* @return 优惠劵列表
*/
List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status);
/**
* 使用优惠劵
*
@ -29,14 +37,6 @@ public interface CouponApi {
*/
void returnUsedCoupon(Long id);
/**
* 校验优惠劵
*
* @param validReqDTO 校验请求
* @return 优惠劵
*/
CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO);
/**
* 管理员给指定用户批量发送优惠券
*

View File

@ -1,27 +0,0 @@
package cn.iocoder.yudao.module.promotion.api.coupon.dto;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
/**
* 优惠劵使用 Request DTO
*
* @author 芋道源码
*/
@Data
public class CouponValidReqDTO {
/**
* 优惠劵编号
*/
@NotNull(message = "优惠劵编号不能为空")
private Long id;
/**
* 用户编号
*/
@NotNull(message = "用户编号不能为空")
private Long userId;
}

View File

@ -20,8 +20,6 @@ public interface ErrorCodeConstants {
ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1_013_002_000, "Banner 不存在");
// ========== Coupon 相关 1-013-003-000 ============
ErrorCode COUPON_NO_MATCH_SPU = new ErrorCode(1_013_003_000, "优惠劵没有可使用的商品!");
ErrorCode COUPON_NO_MATCH_MIN_PRICE = new ErrorCode(1_013_003_001, "所结算的商品中未满足使用的金额");
// ========== 优惠劵模板 1-013-004-000 ==========
ErrorCode COUPON_TEMPLATE_NOT_EXISTS = new ErrorCode(1_013_004_000, "优惠劵模板不存在");

View File

@ -1,11 +1,9 @@
package cn.iocoder.yudao.module.promotion.api.coupon;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@ -26,6 +24,11 @@ public class CouponApiImpl implements CouponApi {
@Resource
private CouponService couponService;
@Override
public List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status) {
return BeanUtils.toBean(couponService.getCouponList(userId, status), CouponRespDTO.class);
}
@Override
public void useCoupon(CouponUseReqDTO useReqDTO) {
couponService.useCoupon(useReqDTO.getId(), useReqDTO.getUserId(),
@ -37,12 +40,6 @@ public class CouponApiImpl implements CouponApi {
couponService.returnUsedCoupon(id);
}
@Override
public CouponRespDTO validateCoupon(CouponValidReqDTO validReqDTO) {
CouponDO coupon = couponService.validCoupon(validReqDTO.getId(), validReqDTO.getUserId());
return CouponConvert.INSTANCE.convert(coupon);
}
@Override
public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
return couponService.takeCouponsByAdmin(giveCoupons, userId);

View File

@ -5,7 +5,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.*;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponTakeReqVO;
import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
@ -15,13 +17,12 @@ import cn.iocoder.yudao.module.promotion.service.coupon.CouponTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@ -56,14 +57,6 @@ public class AppCouponController {
return success(canTakeAgain);
}
@GetMapping("/match-list")
@Operation(summary = "获得匹配指定商品的优惠劵列表", description = "用于下单页,展示优惠劵列表")
public CommonResult<List<AppCouponMatchRespVO>> getMatchCouponList(AppCouponMatchReqVO matchReqVO) {
// todo: 优化优惠金额倒序
List<CouponDO> list = couponService.getMatchCouponList(getLoginUserId(), matchReqVO);
return success(BeanUtils.toBean(list, AppCouponMatchRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "我的优惠劵列表")
@PreAuthenticated

View File

@ -1,30 +0,0 @@
package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
@Schema(description = "用户 App - 优惠劵的匹配 Request VO")
@Data
public class AppCouponMatchReqVO {
@Schema(description = "商品金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "商品金额不能为空")
private Integer price;
@Schema(description = "商品 SPU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
@NotEmpty(message = "商品 SPU 编号不能为空")
private List<Long> spuIds;
@Schema(description = "商品 SKU 编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
@NotEmpty(message = "商品 SKU 编号不能为空")
private List<Long> skuIds;
@Schema(description = "分类编号的数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[10, 20]")
@NotEmpty(message = "分类编号不能为空")
private List<Long> categoryIds;
}

View File

@ -1,16 +0,0 @@
package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 App - 优惠劵 Response VO")
@Data
public class AppCouponMatchRespVO extends AppCouponRespVO {
@Schema(description = "是否匹配", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean match;
@Schema(description = "匹配条件的提示", example = "所结算商品没有符合条件的商品")
private String description;
}

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.Min;
import java.time.LocalDateTime;
import java.util.List;
@ -42,7 +41,6 @@ public class AppCouponRespVO {
private Integer discountPercent;
@Schema(description = "优惠金额", example = "10")
@Min(value = 0, message = "优惠金额需要大于等于 0")
private Integer discountPrice;
@Schema(description = "折扣上限", example = "100") // 单位仅在 discountType PERCENT 使用

View File

@ -4,9 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchRespVO;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
@ -16,7 +14,6 @@ import org.mapstruct.factory.Mappers;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
/**
* 优惠劵 Convert

View File

@ -1,13 +1,11 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.coupon;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.github.yulichang.toolkit.MPJWrappers;
import org.apache.ibatis.annotations.Mapper;
@ -16,8 +14,6 @@ import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@ -84,22 +80,6 @@ public interface CouponMapper extends BaseMapperX<CouponDO> {
return convertMap(list, map -> MapUtil.getLong(map, templateIdAlias), map -> MapUtil.getInt(map, countAlias));
}
default List<CouponDO> selectListByUserIdAndStatusAndUsePriceLeAndProductScope(
Long userId, Integer status, Integer usePrice, List<Long> spuIds, List<Long> categoryIds) {
Function<List<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
.map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
.collect(Collectors.joining(" OR "));
return selectList(new LambdaQueryWrapperX<CouponDO>()
.eq(CouponDO::getUserId, userId)
.eq(CouponDO::getStatus, status)
.le(CouponDO::getUsePrice, usePrice) // 价格小于等于满足价格使用条件
.and(w -> w.eq(CouponDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()) // 商品范围一全部
.or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.SPU.getScope()) // 商品范围二满足指定商品
.apply(productScopeValuesFindInSetFunc.apply(spuIds)))
.or(ww -> ww.eq(CouponDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope()) // 商品范围三满足指定分类
.apply(productScopeValuesFindInSetFunc.apply(categoryIds)))));
}
default List<CouponDO> selectListByStatusAndValidEndTimeLe(Integer status, LocalDateTime validEndTime) {
return selectList(new LambdaQueryWrapperX<CouponDO>()
.eq(CouponDO::getStatus, status)

View File

@ -30,15 +30,9 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
.orderByDesc(RewardActivityDO::getId));
}
default List<RewardActivityDO> selectListByProductScopeAndStatus(Integer productScope, Integer status) {
return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
.eq(RewardActivityDO::getProductScope, productScope)
.eq(RewardActivityDO::getStatus, status));
}
default List<RewardActivityDO> selectListBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
.map(id -> StrUtil.format("FIND_IN_SET({}, product_spu_ids) ", id))
.map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
.collect(Collectors.joining(" OR "));
return selectList(new QueryWrapper<RewardActivityDO>()
.eq("status", status)

View File

@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
@ -18,26 +17,6 @@ import java.util.*;
*/
public interface CouponService {
/**
* 校验优惠劵包括状态有限期
* <p>
* 1. 如果校验通过则返回优惠劵信息
* 2. 如果校验不通过则直接抛出业务异常
*
* @param id 优惠劵编号
* @param userId 用户编号
* @return 优惠劵信息
*/
CouponDO validCoupon(Long id, Long userId);
/**
* 校验优惠劵包括状态有限期
*
* @param coupon 优惠劵
* @see #validCoupon(Long, Long) 逻辑相同只是入参不同
*/
void validCoupon(CouponDO coupon);
/**
* 使用优惠劵
*
@ -171,15 +150,6 @@ public interface CouponService {
return MapUtil.getInt(map, templateId, 0);
}
/**
* 获取用户匹配的优惠券列表
*
* @param userId 用户编号
* @param matchReqVO 匹配参数
* @return 优惠券列表
*/
List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO);
/**
* 获取用户是否可以领取优惠券
*

View File

@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO;
import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
@ -56,18 +55,9 @@ public class CouponServiceImpl implements CouponService {
private MemberUserApi memberUserApi;
@Override
public CouponDO validCoupon(Long id, Long userId) {
CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId);
if (coupon == null) {
throw exception(COUPON_NOT_EXISTS);
}
validCoupon(coupon);
return coupon;
}
@Override
public void validCoupon(CouponDO coupon) {
public void useCoupon(Long id, Long userId, Long orderId) {
// 校验状态
CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId);
if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) {
throw exception(COUPON_STATUS_NOT_UNUSED);
}
@ -75,12 +65,6 @@ public class CouponServiceImpl implements CouponService {
if (!LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) {
throw exception(COUPON_VALID_TIME_NOT_NOW);
}
}
@Override
public void useCoupon(Long id, Long userId, Long orderId) {
// 校验优惠劵
validCoupon(id, userId);
// 更新状态
int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
@ -354,16 +338,6 @@ public class CouponServiceImpl implements CouponService {
return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
}
@Override
public List<CouponDO> getMatchCouponList(Long userId, AppCouponMatchReqVO matchReqVO) {
List<CouponDO> list = couponMapper.selectListByUserIdAndStatusAndUsePriceLeAndProductScope(userId,
CouponStatusEnum.UNUSED.getStatus(),
matchReqVO.getPrice(), matchReqVO.getSpuIds(), matchReqVO.getCategoryIds());
// 兜底逻辑如果 CouponExpireJob 未执行status 未变成 EXPIRE 但是 validEndTime 已经过期了需要进行过滤
list.removeIf(coupon -> !LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime()));
return list;
}
@Override
public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
// 1. 未登录时都显示可以领取

View File

@ -61,7 +61,7 @@ public interface ErrorCodeConstants {
ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_006, "计算快递运费异常,配送方式不匹配");
ErrorCode PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:优惠金额超过订单金额");
ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_007, "该优惠劵无法使用,原因:{}」");
// ========== 物流 Express 模块 1-011-004-000 ==========
ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");

View File

@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.trade.controller.app.order.vo;
import cn.iocoder.yudao.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "用户 App - 交易订单结算信息 Response VO")
@ -19,6 +19,9 @@ public class AppTradeOrderSettlementRespVO {
@Schema(description = "购物项数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Item> items;
@Schema(description = "优惠劵数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Coupon> coupons; // 可用 + 不可用
@Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED)
private Price price;
@ -109,7 +112,6 @@ public class AppTradeOrderSettlementRespVO {
private String mobile;
@Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "地区编号不能为空")
private Long areaId;
@Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区")
private String areaName;
@ -122,4 +124,43 @@ public class AppTradeOrderSettlementRespVO {
}
@Schema(description = "优惠劵信息")
@Data
public static class Coupon {
@Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送")
private String name;
@Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位0 - 不限制
private Integer usePrice;
@Schema(description = "固定日期 - 生效开始时间")
private LocalDateTime validStartTime;
@Schema(description = "固定日期 - 生效结束时间")
private LocalDateTime validEndTime;
@Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer discountType;
@Schema(description = "折扣百分比", example = "80") // 例如说80% 80
private Integer discountPercent;
@Schema(description = "优惠金额", example = "10")
private Integer discountPrice;
@Schema(description = "折扣上限", example = "100") // 单位仅在 discountType PERCENT 使用
private Integer discountLimitPrice;
@Schema(description = "是否可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean match;
@Schema(description = "不可用原因", example = "优惠劵已过期")
private String mismatchReason;
}
}

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@ -45,9 +46,13 @@ public class TradePriceCalculateRespBO {
private List<Promotion> promotions;
/**
* 优惠劵编号
* 使用的优惠劵编号
*/
private Long couponId;
/**
* 用户的优惠劵列表可用 + 不可用
*/
private List<Coupon> coupons;
/**
* 会员剩余积分
@ -339,4 +344,62 @@ public class TradePriceCalculateRespBO {
}
/**
* 优惠劵信息
*/
@Data
public static class Coupon {
/**
* 优惠劵编号
*/
private Long id;
/**
* 优惠劵名
*/
private String name;
/**
* 是否设置满多少金额可用单位
*/
private Integer usePrice;
/**
* 生效开始时间
*/
private LocalDateTime validStartTime;
/**
* 生效结束时间
*/
private LocalDateTime validEndTime;
/**
* 优惠类型
*/
private Integer discountType;
/**
* 折扣百分比
*/
private Integer discountPercent;
/**
* 优惠金额单位
*/
private Integer discountPrice;
/**
* 折扣上限单位
*/
private Integer discountLimitPrice;
/**
* 是否匹配
*/
private Boolean match;
/**
* 不匹配的原因
*/
private String mismatchReason;
}
}

View File

@ -1,31 +1,31 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import jakarta.annotation.Resource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.function.Predicate;
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.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_CAN_NOT_USE;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH;
/**
* 优惠劵的 {@link TradePriceCalculator} 实现类
@ -41,34 +41,37 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 1.1 校验优惠劵
// 只有普通订单才允许使用优惠劵
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
}
return;
}
// 1.1 加载用户的优惠劵列表
List<CouponRespDTO> coupons = couponApi.getCouponListByUserId(param.getUserId(), CouponStatusEnum.UNUSED.getStatus());
coupons.removeIf(coupon -> LocalDateTimeUtils.beforeNow(coupon.getValidEndTime()));
// 1.2 计算优惠劵的使用条件
result.setCoupons(calculateCoupons(coupons, result));
// 2. 校验优惠劵是否可用
if (param.getCouponId() == null) {
return;
}
CouponRespDTO coupon = couponApi.validateCoupon(new CouponValidReqDTO()
.setId(param.getCouponId()).setUserId(param.getUserId()));
Assert.notNull(coupon, "校验通过的优惠劵({}),不能为空", param.getCouponId());
// 1.2 只有普通订单才允许使用优惠劵
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
TradePriceCalculateRespBO.Coupon couponBO = CollUtil.findOne(result.getCoupons(), item -> item.getId().equals(param.getCouponId()));
CouponRespDTO coupon = CollUtil.findOne(coupons, item -> item.getId().equals(param.getCouponId()));
if (couponBO == null || coupon == null) {
throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, "优惠劵不存在");
}
// 2.1 获得匹配的商品 SKU 数组
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
if (CollUtil.isEmpty(orderItems)) {
throw exception(COUPON_NO_MATCH_SPU);
}
// 2.2 计算是否满足优惠劵的使用金额
Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
if (totalPayPrice < coupon.getUsePrice()) {
throw exception(COUPON_NO_MATCH_MIN_PRICE);
if (Boolean.FALSE.equals(couponBO.getMatch())) {
throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, couponBO.getMismatchReason());
}
// 3.1 计算可以优惠的金额
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
if (couponPrice <= totalPayPrice) {
throw exception(PRICE_CALCULATE_COUPON_PRICE_TOO_MUCH);
}
// 3.2 计算分摊的优惠金额
List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);
@ -76,7 +79,7 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
result.setCouponId(param.getCouponId());
// 4.2 记录优惠明细
TradePriceCalculatorHelper.addPromotion(result, orderItems,
param.getCouponId(), coupon.getName(), PromotionTypeEnum.COUPON.getType(),
param.getCouponId(), couponBO.getName(), PromotionTypeEnum.COUPON.getType(),
StrUtil.format("优惠劵:省 {} 元", TradePriceCalculatorHelper.formatPrice(couponPrice)),
divideCouponPrices);
// 4.3 更新 SKU 优惠金额
@ -88,6 +91,43 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
TradePriceCalculatorHelper.recountAllPrice(result);
}
/**
* 计算用户的优惠劵列表可用 + 不可用
*
* @param coupons 优惠劵
* @param result 计算结果
* @return 优惠劵列表
*/
private List<TradePriceCalculateRespBO.Coupon> calculateCoupons(List<CouponRespDTO> coupons,
TradePriceCalculateRespBO result) {
return convertList(coupons, coupon -> {
TradePriceCalculateRespBO.Coupon matchCoupon = BeanUtils.toBean(coupon, TradePriceCalculateRespBO.Coupon.class);
// 1.1 优惠劵未到使用时间
if (LocalDateTimeUtils.afterNow(coupon.getValidStartTime())) {
return matchCoupon.setMatch(false).setMismatchReason("优惠劵未到使用时间");
}
// 1.2 优惠劵没有匹配的商品
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
if (CollUtil.isEmpty(orderItems)) {
return matchCoupon.setMatch(false).setMismatchReason("优惠劵没有匹配的商品");
}
// 1.3 %1$,.2f 元可用优惠劵
Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
if (totalPayPrice < coupon.getUsePrice()) {
return matchCoupon.setMatch(false)
.setMismatchReason(String.format("差 %1$,.2f 元可用优惠劵", (coupon.getUsePrice() - totalPayPrice) / 100D));
}
// 1.4 优惠金额超过订单金额
Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
if (couponPrice >= totalPayPrice) {
return matchCoupon.setMatch(false).setMismatchReason("优惠金额超过订单金额");
}
// 2. 满足条件
return matchCoupon.setMatch(true);
});
}
private Integer getCouponPrice(CouponRespDTO coupon, Integer totalPayPrice) {
if (PromotionDiscountTypeEnum.PRICE.getType().equals(coupon.getDiscountType())) { // 减价
return coupon.getDiscountPrice();

View File

@ -1,12 +1,13 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
@ -14,8 +15,10 @@ import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.time.Duration;
import java.util.ArrayList;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -69,8 +72,10 @@ public class TradeCouponPriceCalculatorTest extends BaseMockitoUnitTest {
CouponRespDTO coupon = randomPojo(CouponRespDTO.class, o -> o.setId(1024L).setName("程序员节")
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
.setUsePrice(350).setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType())
.setDiscountPercent(50).setDiscountLimitPrice(70));
when(couponApi.validateCoupon(eq(new CouponValidReqDTO().setId(1024L).setUserId(233L)))).thenReturn(coupon);
.setDiscountPercent(50).setDiscountLimitPrice(70))
.setValidStartTime(addTime(Duration.ofDays(1))).setValidEndTime(addTime(Duration.ofDays(2)));
when(couponApi.getCouponListByUserId(eq(233L), eq(CouponStatusEnum.UNUSED.getStatus())))
.thenReturn(ListUtil.toList(coupon));
// 调用
tradeCouponPriceCalculator.calculate(param, result);

View File

@ -144,7 +144,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
// 调用并断言
assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId),
new ErrorCode(401, "刷新令牌已过期"));
assertEquals(0, oauth2RefreshTokenMapper.selectCount());
assertEquals(0, oauth2AccessTokenMapper.selectCount());
}
@Test

View File

@ -33,11 +33,11 @@
</dependency>
<!-- 会员中心。默认注释,保证编译速度 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-member-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-member-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 数据报表。默认注释,保证编译速度 -->
<!-- <dependency>-->
@ -52,11 +52,11 @@
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 支付服务。默认注释,保证编译速度 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-pay-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-pay-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 微信公众号模块。默认注释,保证编译速度 -->
<!-- <dependency>-->
@ -66,26 +66,26 @@
<!-- </dependency>-->
<!-- 商城相关模块。默认注释,保证编译速度-->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-promotion-biz</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-product-biz</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-trade-biz</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-statistics-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-promotion-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-product-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-trade-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-statistics-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- CRM 相关模块。默认注释,保证编译速度 -->
<!-- <dependency>-->