fix(pricing): 修复优惠券计算逻辑,防止优惠金额溢出

- 重构优惠券折扣计算方法,确保固定金额优惠券不超过适用商品总价
- 修改百分比优惠券计算逻辑,基于适用商品总价而非购物车总价
- 新增适用商品总价计算方法,支持按商品类型过滤
- 添加防止优惠金额超出适用商品总价的保护逻辑
- 完善无商品类型限制时的全商品适用逻辑
- 增加多种边界情况和多SKU场景的单元测试
- 修复百分比优惠券最大折扣限制的计算顺序问题
This commit is contained in:
2025-11-17 17:38:46 +08:00
parent e2b450682b
commit ebf05ab189
2 changed files with 369 additions and 4 deletions

View File

@@ -70,17 +70,27 @@ public class CouponServiceImpl implements ICouponService {
if (!isCouponApplicable(coupon, products, totalAmount)) {
return BigDecimal.ZERO;
}
// 计算适用商品的总价
BigDecimal applicableProductsTotal = calculateApplicableProductsTotal(coupon, products);
BigDecimal discount;
if (coupon.getCouponType() == CouponType.PERCENTAGE) {
discount = totalAmount.multiply(coupon.getDiscountValue().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
// 百分比优惠券基于适用商品总价计算,而非购物车总价
discount = applicableProductsTotal.multiply(coupon.getDiscountValue().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
if (coupon.getMaxDiscount() != null && discount.compareTo(coupon.getMaxDiscount()) > 0) {
discount = coupon.getMaxDiscount();
}
} else {
// 固定金额优惠券
discount = coupon.getDiscountValue();
}
// 限制优惠金额不超过适用商品总价,防止优惠溢出到其他SKU
if (discount.compareTo(applicableProductsTotal) > 0) {
discount = applicableProductsTotal;
}
return discount.setScale(2, RoundingMode.HALF_UP);
}
@@ -128,7 +138,37 @@ public class CouponServiceImpl implements ICouponService {
return false;
}
}
/**
* 计算适用商品的总价
* @param coupon 优惠券配置
* @param products 商品列表
* @return 适用商品的总价
*/
private BigDecimal calculateApplicableProductsTotal(PriceCouponConfig coupon, List<ProductItem> products) {
// 如果优惠券没有商品类型限制,返回所有商品总价
if (coupon.getApplicableProducts() == null || coupon.getApplicableProducts().isEmpty()) {
return products.stream()
.map(ProductItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
try {
// 解析适用商品类型列表
List<String> applicableProductTypes = objectMapper.readValue(
coupon.getApplicableProducts(), new TypeReference<List<String>>() {});
// 计算适用商品的总价
return products.stream()
.filter(product -> applicableProductTypes.contains(product.getProductType().getCode()))
.map(ProductItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
} catch (Exception e) {
log.error("解析适用商品类型失败", e);
return BigDecimal.ZERO;
}
}
@Override
@Transactional
public CouponUseResult useCoupon(CouponUseRequest request) {