You've already forked FrameTour-BE
feat(pricing): 新增打包购买优惠功能
- 添加打包购买优惠信息类 BundleDiscountInfo - 实现打包购买优惠提供者 BundleDiscountProvider - 添加打包购买优惠服务接口 IBundleDiscountService 及其实现类 BundleDiscountServiceImpl - 在 DiscountInfo 中添加 bundleDiscountInfo 字段以支持打包优惠 - 更新 CLAUDE.md 文档,详细说明打包购买优惠系统的设计和实现
This commit is contained in:
@@ -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<BundleDiscountInfo> detectAvailableBundleDiscounts(DiscountDetectionContext context);
|
||||
|
||||
// 计算打包优惠金额
|
||||
BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List<ProductItem> products);
|
||||
|
||||
// 获取最优的打包优惠组合
|
||||
BundleDiscountInfo getBestBundleDiscount(List<ProductItem> 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. 价格计算接口扩展
|
||||
|
@@ -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<ProductItem> eligibleProducts;
|
||||
|
||||
/**
|
||||
* 最低购买数量要求
|
||||
*/
|
||||
private Integer minQuantity;
|
||||
|
||||
/**
|
||||
* 最低购买金额要求
|
||||
*/
|
||||
private BigDecimal minAmount;
|
||||
|
||||
/**
|
||||
* 实际优惠金额
|
||||
*/
|
||||
private BigDecimal actualDiscountAmount;
|
||||
|
||||
/**
|
||||
* 是否可与其他优惠叠加
|
||||
*/
|
||||
private Boolean canUseWithCoupon = true;
|
||||
|
||||
/**
|
||||
* 是否可与券码叠加
|
||||
*/
|
||||
private Boolean canUseWithVoucher = true;
|
||||
|
||||
/**
|
||||
* 是否可与一口价叠加
|
||||
*/
|
||||
private Boolean canUseWithOnePrice = true;
|
||||
}
|
@@ -84,4 +84,9 @@ public class DiscountInfo {
|
||||
* 一口价信息(如果是一口价优惠)
|
||||
*/
|
||||
private OnePriceInfo onePriceInfo;
|
||||
|
||||
/**
|
||||
* 打包优惠信息(如果是打包优惠)
|
||||
*/
|
||||
private BundleDiscountInfo bundleDiscountInfo;
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user