From e9102e8e58657a8d0efe72d74160911b844969a5 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 18 Sep 2025 10:47:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(pricing):=20=E6=96=B0=E5=A2=9E=E6=89=93?= =?UTF-8?q?=E5=8C=85=E8=B4=AD=E4=B9=B0=E4=BC=98=E6=83=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加打包购买优惠信息类 BundleDiscountInfo - 实现打包购买优惠提供者 BundleDiscountProvider - 添加打包购买优惠服务接口 IBundleDiscountService 及其实现类 BundleDiscountServiceImpl - 在 DiscountInfo 中添加 bundleDiscountInfo 字段以支持打包优惠 - 更新 CLAUDE.md 文档,详细说明打包购买优惠系统的设计和实现 --- .../java/com/ycwl/basic/pricing/CLAUDE.md | 112 ++++++- .../basic/pricing/dto/BundleDiscountInfo.java | 79 +++++ .../ycwl/basic/pricing/dto/DiscountInfo.java | 5 + .../service/IBundleDiscountService.java | 68 ++++ .../service/impl/BundleDiscountProvider.java | 204 ++++++++++++ .../impl/BundleDiscountServiceImpl.java | 303 ++++++++++++++++++ 6 files changed, 761 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/pricing/dto/BundleDiscountInfo.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/IBundleDiscountService.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountProvider.java create mode 100644 src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountServiceImpl.java diff --git a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md index 1774c367..396d59e2 100644 --- a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md @@ -25,6 +25,7 @@ com.ycwl.basic.pricing/ │ │ DiscountResult.java, DiscountCombinationResult.java │ ├── BundleProductItem.java, MobilePriceCalculationRequest.java │ ├── OnePriceConfigRequest.java, OnePriceInfo.java +│ ├── BundleDiscountInfo.java # 打包购买优惠信息 │ ├── req/ # 券码管理请求DTO │ │ ├── VoucherBatchCreateReq(.java|V2) │ │ ├── VoucherBatchQueryReq.java, VoucherCodeQueryReq.java, VoucherClaimReq.java @@ -58,6 +59,7 @@ com.ycwl.basic.pricing/ │ └── PriceOnePriceConfigMapper.java, VoucherPrintRecordMapper.java └── service/ # 业务层接口与实现 ├── IPriceCalculationService.java, IDiscountDetectionService.java, IDiscountProvider.java + ├── IBundleDiscountService.java # 打包购买优惠服务接口 ├── IProductConfigService.java, IPricingManagementService.java, IPriceBundleService.java ├── ICouponService.java, ICouponManagementService.java ├── IOnePricePurchaseService.java, IVoucherService.java, IVoucherUsageService.java @@ -69,7 +71,8 @@ com.ycwl.basic.pricing/ ├── VoucherServiceImpl.java, VoucherDiscountProvider.java, ├── VoucherBatchServiceImpl.java, VoucherCodeServiceImpl.java, VoucherPrintServiceImpl.java, ├── VoucherUsageServiceImpl.java, - └── OnePricePurchaseServiceImpl.java, OnePricePurchaseDiscountProvider.java + ├── OnePricePurchaseServiceImpl.java, OnePricePurchaseDiscountProvider.java + └── BundleDiscountServiceImpl.java, BundleDiscountProvider.java # 打包购买优惠实现 ``` ## 核心功能 @@ -333,30 +336,37 @@ public interface IDiscountDetectionService { ### 2. 优惠提供者实现(当前实现与优先级) -#### VoucherDiscountProvider (优先级: 100) +#### OnePricePurchaseDiscountProvider (优先级: 120) +- 处理一口价优惠逻辑(景区级统一价格) +- **最高优先级**,优先于所有其他优惠类型 +- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定 + +#### BundleDiscountProvider (优先级: 100) +- 处理打包购买优惠逻辑(多商品组合优惠) +- 支持多种优惠类型:固定减免、百分比折扣、固定价格 +- 可配置叠加规则(与优惠券、券码、一口价的组合限制) +- 自动检测购物车中符合条件的商品组合 + +#### VoucherDiscountProvider (优先级: 80) - 处理券码优惠逻辑 - 支持用户主动输入券码或自动选择最优券码 - 全场免费券码不可与其他优惠叠加 -#### CouponDiscountProvider (优先级: 80) +#### CouponDiscountProvider (优先级: 60) - 处理优惠券优惠逻辑 +- **最低优先级**,在所有其他优惠之后应用 - 自动选择最优优惠券 -- 可与券码叠加使用(除全场免费券码外) - -#### OnePricePurchaseDiscountProvider (优先级: 60) -- 处理一口价优惠逻辑(景区级统一价格) -- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定 ### 3. 优惠应用策略 #### 优先级规则 ``` -券码 (100) → 优惠券 (80) → 一口价 (60) +一口价 (120) → 打包购买 (100) → 券码 (80) → 优惠券 (60) ``` #### 叠加逻辑 ```java -原价 → 券码 → 优惠券 → 一口价 → 最终价格 +原价 → 一口价 → 打包购买 → 券码 → 优惠券 → 最终价格 特殊情况: - 全场免费券码:直接最终价=0,停止后续优惠 @@ -385,6 +395,88 @@ public class FlashSaleDiscountProvider implements IDiscountProvider { // 按优先级排序并注册到 DiscountDetectionService 中 ``` +## 打包购买优惠系统 (Bundle Discount System) + +### 1. 核心特性 + +打包购买优惠系统是新增的优惠类型,支持多商品组合优惠策略,具有第二高优先级(仅次于一口价)。 + +#### 优惠类型支持 +- **FIXED_DISCOUNT**: 固定减免金额(如满2件减50元) +- **PERCENTAGE_DISCOUNT**: 百分比折扣(如多商品组合9折) +- **FIXED_PRICE**: 固定套餐价格(如照片+视频套餐199元) + +#### 触发条件 +- **商品数量要求**: 最低购买数量限制 +- **商品金额要求**: 最低购买金额限制 +- **商品类型组合**: 特定商品类型的组合(如照片+视频) + +### 2. 业务规则 + +#### 自动检测规则 +```java +// 多商品类型组合优惠 +- 条件:购买不同类型商品 >= 2种 +- 优惠:9折优惠 +- 可叠加:可与优惠券、券码叠加,不可与一口价叠加 + +// 大批量购买优惠 +- 条件:总数量 >= 10件 且 总金额 >= 500元 +- 优惠:减免50元 +- 可叠加:可与优惠券、券码叠加,不可与一口价叠加 + +// 特定组合套餐 +- 条件:同时购买照片集和Vlog视频 +- 优惠:套餐价199元 +- 可叠加:不可与其他优惠叠加 +``` + +#### 叠加规则配置 +每个打包优惠规则都可以独立配置与其他优惠的叠加关系: +- `canUseWithCoupon`: 是否可与优惠券叠加 +- `canUseWithVoucher`: 是否可与券码叠加 +- `canUseWithOnePrice`: 是否可与一口价叠加 + +### 3. 核心接口 + +#### IBundleDiscountService +```java +// 检测可用的打包优惠 +List detectAvailableBundleDiscounts(DiscountDetectionContext context); + +// 计算打包优惠金额 +BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List products); + +// 获取最优的打包优惠组合 +BundleDiscountInfo getBestBundleDiscount(List products, Long scenicId); +``` + +#### BundleDiscountProvider +- 实现 `IDiscountProvider` 接口 +- 优先级:100(第二高,仅次于一口价的120) +- 自动集成到统一优惠检测系统 + +### 4. 扩展开发 + +#### 添加新的打包规则 +```java +// 在 BundleDiscountServiceImpl 中添加新规则 +private BundleDiscountInfo createNewBundleRule() { + BundleDiscountInfo bundle = new BundleDiscountInfo(); + bundle.setBundleConfigId(4L); + bundle.setBundleName("新打包规则"); + bundle.setDiscountType("PERCENTAGE_DISCOUNT"); + bundle.setDiscountValue(new BigDecimal("0.85")); // 8.5折 + bundle.setMinQuantity(5); + bundle.setMinAmount(new BigDecimal("300")); + // 配置叠加规则... + return bundle; +} +``` + +#### 数据库配置支持 +后续可以扩展为从数据库加载打包规则配置,替换当前的硬编码规则。 + ## API 接口扩展 ### 1. 价格计算接口扩展 diff --git a/src/main/java/com/ycwl/basic/pricing/dto/BundleDiscountInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/BundleDiscountInfo.java new file mode 100644 index 00000000..95ff5787 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/BundleDiscountInfo.java @@ -0,0 +1,79 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 打包购买优惠信息 + */ +@Data +public class BundleDiscountInfo { + + /** + * 打包配置ID + */ + private Long bundleConfigId; + + /** + * 打包名称 + */ + private String bundleName; + + /** + * 打包描述 + */ + private String bundleDescription; + + /** + * 打包优惠类型 + * FIXED_DISCOUNT: 固定减免金额 + * PERCENTAGE_DISCOUNT: 百分比折扣 + * FIXED_PRICE: 固定价格 + */ + private String discountType; + + /** + * 优惠值(根据类型不同含义不同) + * FIXED_DISCOUNT: 减免金额 + * PERCENTAGE_DISCOUNT: 折扣百分比(如0.8表示8折) + * FIXED_PRICE: 固定价格 + */ + private BigDecimal discountValue; + + /** + * 满足条件的商品列表 + */ + private List eligibleProducts; + + /** + * 最低购买数量要求 + */ + private Integer minQuantity; + + /** + * 最低购买金额要求 + */ + private BigDecimal minAmount; + + /** + * 实际优惠金额 + */ + private BigDecimal actualDiscountAmount; + + /** + * 是否可与其他优惠叠加 + */ + private Boolean canUseWithCoupon = true; + + /** + * 是否可与券码叠加 + */ + private Boolean canUseWithVoucher = true; + + /** + * 是否可与一口价叠加 + */ + private Boolean canUseWithOnePrice = true; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java index 2df8aab9..ed9fc7e4 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountInfo.java @@ -84,4 +84,9 @@ public class DiscountInfo { * 一口价信息(如果是一口价优惠) */ private OnePriceInfo onePriceInfo; + + /** + * 打包优惠信息(如果是打包优惠) + */ + private BundleDiscountInfo bundleDiscountInfo; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IBundleDiscountService.java b/src/main/java/com/ycwl/basic/pricing/service/IBundleDiscountService.java new file mode 100644 index 00000000..cd865639 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/IBundleDiscountService.java @@ -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 detectAvailableBundleDiscounts(DiscountDetectionContext context); + + /** + * 计算打包优惠金额 + * + * @param bundleDiscount 打包优惠信息 + * @param products 商品列表 + * @return 优惠金额 + */ + BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List products); + + /** + * 检查是否符合打包条件 + * + * @param products 商品列表 + * @param minQuantity 最少数量要求 + * @param minAmount 最少金额要求 + * @return 是否符合条件 + */ + boolean isEligibleForBundle(List products, Integer minQuantity, BigDecimal minAmount); + + /** + * 根据商品类型和数量获取打包优惠规则 + * + * @param products 商品列表 + * @param scenicId 景区ID(可选) + * @return 匹配的打包优惠规则 + */ + List getBundleDiscountRules(List products, Long scenicId); + + /** + * 验证打包优惠是否仍然有效 + * + * @param bundleDiscount 打包优惠信息 + * @param context 优惠检测上下文 + * @return 是否有效 + */ + boolean isBundleDiscountValid(BundleDiscountInfo bundleDiscount, DiscountDetectionContext context); + + /** + * 获取最优的打包优惠组合 + * + * @param products 商品列表 + * @param scenicId 景区ID(可选) + * @return 最优打包优惠 + */ + BundleDiscountInfo getBestBundleDiscount(List products, Long scenicId); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountProvider.java new file mode 100644 index 00000000..be2131ac --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountProvider.java @@ -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 detectAvailableDiscounts(DiscountDetectionContext context) { + List discounts = new ArrayList<>(); + + try { + if (context.getProducts() == null || context.getProducts().isEmpty()) { + log.debug("打包优惠检测失败: 商品列表为空"); + return discounts; + } + + // 检测所有可用的打包优惠 + List 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountServiceImpl.java new file mode 100644 index 00000000..121c6645 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/BundleDiscountServiceImpl.java @@ -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 detectAvailableBundleDiscounts(DiscountDetectionContext context) { + List bundleDiscounts = new ArrayList<>(); + + try { + if (context.getProducts() == null || context.getProducts().isEmpty()) { + log.debug("商品列表为空,无法检测打包优惠"); + return bundleDiscounts; + } + + // 获取所有可能的打包优惠规则 + List 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 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 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 getBundleDiscountRules(List products, Long scenicId) { + List 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 products, Long scenicId) { + try { + List 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 products) { + Set productTypes = products.stream() + .map(ProductItem::getProductType) + .collect(Collectors.toSet()); + return productTypes.size() >= 2; + } + + /** + * 检查是否有大批量商品 + */ + private boolean hasLargeQuantity(List products) { + int totalQuantity = products.stream() + .mapToInt(ProductItem::getQuantity) + .sum(); + return totalQuantity >= 10; // 示例:10件以上 + } + + /** + * 检查是否有特定商品组合 + */ + private boolean hasSpecificCombination(List products) { + Set 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; + } +} \ No newline at end of file