feat(pricing): 新增打包购买优惠功能

- 添加打包购买优惠信息类 BundleDiscountInfo
- 实现打包购买优惠提供者 BundleDiscountProvider
- 添加打包购买优惠服务接口 IBundleDiscountService 及其实现类 BundleDiscountServiceImpl
- 在 DiscountInfo 中添加 bundleDiscountInfo 字段以支持打包优惠
- 更新 CLAUDE.md 文档,详细说明打包购买优惠系统的设计和实现
This commit is contained in:
2025-09-18 10:47:40 +08:00
parent e86dc85afe
commit e9102e8e58
6 changed files with 761 additions and 10 deletions

View File

@@ -0,0 +1,68 @@
package com.ycwl.basic.pricing.service;
import com.ycwl.basic.pricing.dto.BundleDiscountInfo;
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
import com.ycwl.basic.pricing.dto.ProductItem;
import java.math.BigDecimal;
import java.util.List;
/**
* 打包购买优惠服务接口
*/
public interface IBundleDiscountService {
/**
* 检测可用的打包优惠
*
* @param context 优惠检测上下文
* @return 可用的打包优惠列表
*/
List<BundleDiscountInfo> detectAvailableBundleDiscounts(DiscountDetectionContext context);
/**
* 计算打包优惠金额
*
* @param bundleDiscount 打包优惠信息
* @param products 商品列表
* @return 优惠金额
*/
BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List<ProductItem> products);
/**
* 检查是否符合打包条件
*
* @param products 商品列表
* @param minQuantity 最少数量要求
* @param minAmount 最少金额要求
* @return 是否符合条件
*/
boolean isEligibleForBundle(List<ProductItem> products, Integer minQuantity, BigDecimal minAmount);
/**
* 根据商品类型和数量获取打包优惠规则
*
* @param products 商品列表
* @param scenicId 景区ID(可选)
* @return 匹配的打包优惠规则
*/
List<BundleDiscountInfo> getBundleDiscountRules(List<ProductItem> products, Long scenicId);
/**
* 验证打包优惠是否仍然有效
*
* @param bundleDiscount 打包优惠信息
* @param context 优惠检测上下文
* @return 是否有效
*/
boolean isBundleDiscountValid(BundleDiscountInfo bundleDiscount, DiscountDetectionContext context);
/**
* 获取最优的打包优惠组合
*
* @param products 商品列表
* @param scenicId 景区ID(可选)
* @return 最优打包优惠
*/
BundleDiscountInfo getBestBundleDiscount(List<ProductItem> products, Long scenicId);
}

View File

@@ -0,0 +1,204 @@
package com.ycwl.basic.pricing.service.impl;
import com.ycwl.basic.pricing.dto.*;
import com.ycwl.basic.pricing.service.IBundleDiscountService;
import com.ycwl.basic.pricing.service.IDiscountProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 打包购买优惠提供者
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class BundleDiscountProvider implements IDiscountProvider {
private final IBundleDiscountService bundleDiscountService;
@Override
public String getProviderType() {
return "BUNDLE_PURCHASE";
}
@Override
public int getPriority() {
return 100; // 第二高优先级,仅次于一口价
}
@Override
public List<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
List<DiscountInfo> discounts = new ArrayList<>();
try {
if (context.getProducts() == null || context.getProducts().isEmpty()) {
log.debug("打包优惠检测失败: 商品列表为空");
return discounts;
}
// 检测所有可用的打包优惠
List<BundleDiscountInfo> bundleDiscounts = bundleDiscountService.detectAvailableBundleDiscounts(context);
for (BundleDiscountInfo bundleDiscount : bundleDiscounts) {
if (bundleDiscount.getActualDiscountAmount() != null &&
bundleDiscount.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
// 创建优惠信息
DiscountInfo discountInfo = new DiscountInfo();
discountInfo.setProviderType(getProviderType());
discountInfo.setDiscountName(bundleDiscount.getBundleName());
discountInfo.setDiscountAmount(bundleDiscount.getActualDiscountAmount());
discountInfo.setDiscountDescription(bundleDiscount.getBundleDescription());
discountInfo.setBundleDiscountInfo(bundleDiscount);
discountInfo.setPriority(getPriority());
discountInfo.setStackable(true); // 默认可叠加,具体规则由配置控制
discounts.add(discountInfo);
log.info("检测到打包优惠: 名称={}, 优惠金额={}",
bundleDiscount.getBundleName(), bundleDiscount.getActualDiscountAmount());
}
}
} catch (Exception e) {
log.error("打包优惠检测失败", e);
}
return discounts;
}
@Override
public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) {
DiscountResult result = new DiscountResult();
result.setDiscountInfo(discountInfo);
result.setSuccess(false);
try {
if (!getProviderType().equals(discountInfo.getProviderType())) {
result.setFailureReason("优惠类型不匹配");
return result;
}
BundleDiscountInfo bundleDiscount = discountInfo.getBundleDiscountInfo();
if (bundleDiscount == null) {
result.setFailureReason("打包优惠信息为空");
return result;
}
// 检查优惠的叠加限制
boolean canUseWithOtherDiscounts = checkDiscountCombinationRules(bundleDiscount, context);
if (!canUseWithOtherDiscounts) {
result.setFailureReason("打包优惠不可与其他优惠叠加使用");
return result;
}
// 重新验证打包优惠有效性
if (!bundleDiscountService.isBundleDiscountValid(bundleDiscount, context)) {
result.setFailureReason("打包优惠已失效");
return result;
}
// 计算实际优惠金额
BigDecimal actualDiscount = bundleDiscountService.calculateBundleDiscount(bundleDiscount, context.getProducts());
if (actualDiscount.compareTo(BigDecimal.ZERO) <= 0) {
result.setFailureReason("打包优惠金额为零");
return result;
}
// 应用打包优惠
BigDecimal finalAmount = context.getCurrentAmount().subtract(actualDiscount);
if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
finalAmount = BigDecimal.ZERO;
actualDiscount = context.getCurrentAmount();
}
result.setSuccess(true);
result.setActualDiscountAmount(actualDiscount);
result.setFinalAmount(finalAmount);
result.setFailureReason("打包购买优惠已应用");
log.info("打包优惠应用成功: 优惠金额={}, 最终金额={}", actualDiscount, finalAmount);
} catch (Exception e) {
log.error("打包优惠应用失败", e);
result.setFailureReason("打包优惠应用失败: " + e.getMessage());
}
return result;
}
@Override
public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) {
try {
if (!getProviderType().equals(discountInfo.getProviderType())) {
return false;
}
BundleDiscountInfo bundleDiscount = discountInfo.getBundleDiscountInfo();
if (bundleDiscount == null) {
return false;
}
// 检查打包优惠是否仍然有效
return bundleDiscountService.isBundleDiscountValid(bundleDiscount, context);
} catch (Exception e) {
log.error("检查打包优惠可用性失败", e);
return false;
}
}
@Override
public BigDecimal getMaxPossibleDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) {
try {
BundleDiscountInfo bundleDiscount = discountInfo.getBundleDiscountInfo();
if (bundleDiscount != null && bundleDiscount.getActualDiscountAmount() != null) {
return bundleDiscount.getActualDiscountAmount();
}
// 如果没有预计算的优惠金额,重新计算
if (bundleDiscount != null && context.getProducts() != null) {
return bundleDiscountService.calculateBundleDiscount(bundleDiscount, context.getProducts());
}
} catch (Exception e) {
log.error("获取打包优惠最大优惠金额失败", e);
}
return BigDecimal.ZERO;
}
/**
* 检查优惠叠加规则
*/
private boolean checkDiscountCombinationRules(BundleDiscountInfo bundleDiscount, DiscountDetectionContext context) {
// 检查是否可以与优惠券叠加
if (Boolean.FALSE.equals(bundleDiscount.getCanUseWithCoupon()) &&
Boolean.TRUE.equals(context.getAutoUseCoupon())) {
log.debug("打包优惠配置不允许与优惠券叠加使用");
return false;
}
// 检查是否可以与券码叠加
if (Boolean.FALSE.equals(bundleDiscount.getCanUseWithVoucher()) &&
(Boolean.TRUE.equals(context.getAutoUseVoucher()) ||
context.getVoucherCode() != null)) {
log.debug("打包优惠配置不允许与券码叠加使用");
return false;
}
// 检查是否可以与一口价叠加
// 注意:由于一口价优先级更高,这个检查主要用于记录和调试
if (Boolean.FALSE.equals(bundleDiscount.getCanUseWithOnePrice())) {
log.debug("打包优惠配置不允许与一口价叠加使用");
// 这里不返回false,因为一口价会优先应用,打包优惠不会被触发
}
return true;
}
}

View File

@@ -0,0 +1,303 @@
package com.ycwl.basic.pricing.service.impl;
import com.ycwl.basic.pricing.dto.BundleDiscountInfo;
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.service.IBundleDiscountService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
/**
* 打包购买优惠服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class BundleDiscountServiceImpl implements IBundleDiscountService {
@Override
public List<BundleDiscountInfo> detectAvailableBundleDiscounts(DiscountDetectionContext context) {
List<BundleDiscountInfo> bundleDiscounts = new ArrayList<>();
try {
if (context.getProducts() == null || context.getProducts().isEmpty()) {
log.debug("商品列表为空,无法检测打包优惠");
return bundleDiscounts;
}
// 获取所有可能的打包优惠规则
List<BundleDiscountInfo> allRules = getBundleDiscountRules(context.getProducts(), context.getScenicId());
for (BundleDiscountInfo rule : allRules) {
if (isBundleDiscountValid(rule, context)) {
// 计算实际优惠金额
BigDecimal discountAmount = calculateBundleDiscount(rule, context.getProducts());
if (discountAmount.compareTo(BigDecimal.ZERO) > 0) {
rule.setActualDiscountAmount(discountAmount);
bundleDiscounts.add(rule);
}
}
}
log.info("检测到 {} 个可用的打包优惠", bundleDiscounts.size());
} catch (Exception e) {
log.error("检测打包优惠失败", e);
}
return bundleDiscounts;
}
@Override
public BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List<ProductItem> products) {
try {
if (bundleDiscount == null || products == null || products.isEmpty()) {
return BigDecimal.ZERO;
}
// 计算符合条件的商品总金额
BigDecimal totalAmount = products.stream()
.map(ProductItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 计算符合条件的商品总数量
int totalQuantity = products.stream()
.mapToInt(ProductItem::getQuantity)
.sum();
// 检查是否满足最低条件
if (!isEligibleForBundle(products, bundleDiscount.getMinQuantity(), bundleDiscount.getMinAmount())) {
return BigDecimal.ZERO;
}
// 根据优惠类型计算折扣
return switch (bundleDiscount.getDiscountType()) {
case "FIXED_DISCOUNT" -> {
// 固定减免金额
BigDecimal discount = bundleDiscount.getDiscountValue();
yield discount.min(totalAmount); // 优惠不能超过总金额
}
case "PERCENTAGE_DISCOUNT" -> {
// 百分比折扣
BigDecimal discountRate = BigDecimal.ONE.subtract(bundleDiscount.getDiscountValue());
yield totalAmount.multiply(discountRate).setScale(2, RoundingMode.HALF_UP);
}
case "FIXED_PRICE" -> {
// 固定价格
BigDecimal fixedPrice = bundleDiscount.getDiscountValue();
BigDecimal discount = totalAmount.subtract(fixedPrice);
yield discount.max(BigDecimal.ZERO); // 固定价格不能高于原价
}
default -> {
log.warn("未知的打包优惠类型: {}", bundleDiscount.getDiscountType());
yield BigDecimal.ZERO;
}
};
} catch (Exception e) {
log.error("计算打包优惠金额失败", e);
return BigDecimal.ZERO;
}
}
@Override
public boolean isEligibleForBundle(List<ProductItem> products, Integer minQuantity, BigDecimal minAmount) {
if (products == null || products.isEmpty()) {
return false;
}
// 检查数量要求
if (minQuantity != null && minQuantity > 0) {
int totalQuantity = products.stream()
.mapToInt(ProductItem::getQuantity)
.sum();
if (totalQuantity < minQuantity) {
return false;
}
}
// 检查金额要求
if (minAmount != null && minAmount.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal totalAmount = products.stream()
.map(ProductItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (totalAmount.compareTo(minAmount) < 0) {
return false;
}
}
return true;
}
@Override
public List<BundleDiscountInfo> getBundleDiscountRules(List<ProductItem> products, Long scenicId) {
List<BundleDiscountInfo> rules = new ArrayList<>();
try {
// 这里实现获取打包优惠规则的逻辑
// 可以从数据库加载配置,或者使用硬编码的规则
// 示例规则1:多商品打包优惠
if (hasMultipleProductTypes(products)) {
BundleDiscountInfo multiProductBundle = createMultiProductBundleRule();
rules.add(multiProductBundle);
}
// 示例规则2:大批量优惠
if (hasLargeQuantity(products)) {
BundleDiscountInfo bulkBundle = createBulkDiscountRule();
rules.add(bulkBundle);
}
// 示例规则3:特定商品组合优惠
if (hasSpecificCombination(products)) {
BundleDiscountInfo combinationBundle = createCombinationDiscountRule();
rules.add(combinationBundle);
}
log.debug("为 {} 个商品获取到 {} 个打包优惠规则", products.size(), rules.size());
} catch (Exception e) {
log.error("获取打包优惠规则失败", e);
}
return rules;
}
@Override
public boolean isBundleDiscountValid(BundleDiscountInfo bundleDiscount, DiscountDetectionContext context) {
try {
if (bundleDiscount == null) {
return false;
}
// 检查是否满足基本条件
if (!isEligibleForBundle(context.getProducts(), bundleDiscount.getMinQuantity(), bundleDiscount.getMinAmount())) {
return false;
}
// 可以添加更多的验证逻辑,比如时间范围、用户类型等
// TODO: 根据实际业务需求实现更多验证逻辑
return true;
} catch (Exception e) {
log.error("验证打包优惠有效性失败", e);
return false;
}
}
@Override
public BundleDiscountInfo getBestBundleDiscount(List<ProductItem> products, Long scenicId) {
try {
List<BundleDiscountInfo> availableRules = getBundleDiscountRules(products, scenicId);
return availableRules.stream()
.filter(rule -> {
BigDecimal discount = calculateBundleDiscount(rule, products);
rule.setActualDiscountAmount(discount);
return discount.compareTo(BigDecimal.ZERO) > 0;
})
.max(Comparator.comparing(BundleDiscountInfo::getActualDiscountAmount))
.orElse(null);
} catch (Exception e) {
log.error("获取最优打包优惠失败", e);
return null;
}
}
/**
* 检查是否有多种商品类型
*/
private boolean hasMultipleProductTypes(List<ProductItem> products) {
Set<ProductType> productTypes = products.stream()
.map(ProductItem::getProductType)
.collect(Collectors.toSet());
return productTypes.size() >= 2;
}
/**
* 检查是否有大批量商品
*/
private boolean hasLargeQuantity(List<ProductItem> products) {
int totalQuantity = products.stream()
.mapToInt(ProductItem::getQuantity)
.sum();
return totalQuantity >= 10; // 示例:10件以上
}
/**
* 检查是否有特定商品组合
*/
private boolean hasSpecificCombination(List<ProductItem> products) {
Set<ProductType> productTypes = products.stream()
.map(ProductItem::getProductType)
.collect(Collectors.toSet());
// 示例:照片+视频组合
return productTypes.contains(ProductType.PHOTO_SET) &&
productTypes.contains(ProductType.VLOG_VIDEO);
}
/**
* 创建多商品打包规则
*/
private BundleDiscountInfo createMultiProductBundleRule() {
BundleDiscountInfo bundle = new BundleDiscountInfo();
bundle.setBundleConfigId(1L);
bundle.setBundleName("多商品组合优惠");
bundle.setBundleDescription("购买不同类型商品享受组合优惠");
bundle.setDiscountType("PERCENTAGE_DISCOUNT");
bundle.setDiscountValue(new BigDecimal("0.9")); // 9折
bundle.setMinQuantity(2);
bundle.setMinAmount(new BigDecimal("100"));
bundle.setCanUseWithCoupon(true);
bundle.setCanUseWithVoucher(true);
bundle.setCanUseWithOnePrice(false); // 不能与一口价叠加
return bundle;
}
/**
* 创建大批量优惠规则
*/
private BundleDiscountInfo createBulkDiscountRule() {
BundleDiscountInfo bundle = new BundleDiscountInfo();
bundle.setBundleConfigId(2L);
bundle.setBundleName("大批量购买优惠");
bundle.setBundleDescription("购买数量达到要求享受批量优惠");
bundle.setDiscountType("FIXED_DISCOUNT");
bundle.setDiscountValue(new BigDecimal("50")); // 减免50元
bundle.setMinQuantity(10);
bundle.setMinAmount(new BigDecimal("500"));
bundle.setCanUseWithCoupon(true);
bundle.setCanUseWithVoucher(true);
bundle.setCanUseWithOnePrice(false);
return bundle;
}
/**
* 创建特定组合优惠规则
*/
private BundleDiscountInfo createCombinationDiscountRule() {
BundleDiscountInfo bundle = new BundleDiscountInfo();
bundle.setBundleConfigId(3L);
bundle.setBundleName("照片+视频套餐");
bundle.setBundleDescription("同时购买照片和视频享受套餐优惠");
bundle.setDiscountType("FIXED_PRICE");
bundle.setDiscountValue(new BigDecimal("199")); // 套餐价199元
bundle.setMinQuantity(2);
bundle.setMinAmount(new BigDecimal("200"));
bundle.setCanUseWithCoupon(false);
bundle.setCanUseWithVoucher(false);
bundle.setCanUseWithOnePrice(false);
return bundle;
}
}