mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-08-14 18:21:53 +08:00
【功能优化】商城:价格计算时,返回可用 + 不可用的优惠劵
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user