Merge branch 'print-price'

This commit is contained in:
2025-09-19 15:08:55 +08:00
10 changed files with 842 additions and 31 deletions

View File

@@ -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. 价格计算接口扩展

View File

@@ -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;
}

View File

@@ -84,4 +84,9 @@ public class DiscountInfo {
* 一口价信息(如果是一口价优惠)
*/
private OnePriceInfo onePriceInfo;
/**
* 打包优惠信息(如果是打包优惠)
*/
private BundleDiscountInfo bundleDiscountInfo;
}

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;
}
}

View File

@@ -34,7 +34,7 @@ public class CouponDiscountProvider implements IDiscountProvider {
@Override
public int getPriority() {
return 80; // 优惠券优先级为80,低于券码的100
return 60; // 优惠券优先级最低
}
@Override

View File

@@ -31,7 +31,7 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
@Override
public int getPriority() {
return 60; // 中等优先级,在券码和优惠券之间
return 120; // 最高优先级,一口价优先应用
}
@Override

View File

@@ -36,7 +36,7 @@ public class VoucherDiscountProvider implements IDiscountProvider {
@Override
public int getPriority() {
return 100; // 券码优先级最高
return 80; // 券码优先级第三,仅次于一口价和打包购买
}
@Override

View File

@@ -14,6 +14,11 @@ import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.service.IPriceCalculationService;
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
@@ -52,8 +57,6 @@ public class PrinterServiceImpl implements PrinterService {
@Autowired
private PrinterMapper printerMapper;
@Autowired
private PriceRepository priceRepository;
@Autowired
private SourceMapper sourceMapper;
@Autowired
private MemberMapper memberMapper;
@@ -66,6 +69,8 @@ public class PrinterServiceImpl implements PrinterService {
private WxPayService wxPayService;
@Autowired
private PrintTaskMapper printTaskMapper;
@Autowired
private IPriceCalculationService priceCalculationService;
@Override
public List<PrinterResp> listByScenicId(Long scenicId) {
@@ -205,15 +210,49 @@ public class PrinterServiceImpl implements PrinterService {
@Override
public PriceObj queryPrice(Long memberId, Long scenicId) {
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId);
// 判断几张
PriceConfigEntity priceConfig = priceRepository.getPriceConfigByScenicTypeGoods(scenicId, 3, null);
// 计算照片总数量
long count = userPhotoList.stream()
.filter(item -> Objects.nonNull(item.getQuantity()))
.mapToInt(MemberPrintResp::getQuantity)
.sum();
PriceObj obj = new PriceObj();
long count = userPhotoList.stream().filter(item -> Objects.nonNull(item.getQuantity())).mapToInt(MemberPrintResp::getQuantity).sum();
obj.setPrice(priceConfig.getPrice().multiply(BigDecimal.valueOf(count)));
obj.setSlashPrice(priceConfig.getSlashPrice().multiply(BigDecimal.valueOf(count)));
if (count == 0) {
// 如果没有照片,返回零价格
obj.setPrice(BigDecimal.ZERO);
obj.setSlashPrice(BigDecimal.ZERO);
obj.setGoodsType(3);
obj.setFree(false);
obj.setScenicId(scenicId);
return obj;
}
// 构建价格计算请求
PriceCalculationRequest request = new PriceCalculationRequest();
request.setUserId(memberId);
request.setScenicId(scenicId);
// 创建照片打印商品项
ProductItem photoItem = new ProductItem();
photoItem.setProductType(ProductType.PHOTO_PRINT);
photoItem.setProductId(scenicId.toString());
photoItem.setQuantity(Long.valueOf(count).intValue());
photoItem.setPurchaseCount(1);
photoItem.setScenicId(scenicId.toString());
request.setProducts(Collections.singletonList(photoItem));
// 使用统一价格计算服务
PriceCalculationResult result = priceCalculationService.calculatePrice(request);
// 转换为原有的 PriceObj 格式
obj.setPrice(result.getFinalAmount());
obj.setSlashPrice(result.getOriginalAmount());
obj.setGoodsType(3);
obj.setFree(false);
obj.setFree(result.getFinalAmount().compareTo(BigDecimal.ZERO) == 0);
obj.setScenicId(scenicId);
return obj;
}
@@ -250,11 +289,13 @@ public class PrinterServiceImpl implements PrinterService {
throw new BaseException("打印机不属于该景区");
}
}
PriceConfigEntity priceConfig = priceRepository.getPriceConfigByScenicTypeGoods(scenicId, 3, null);
if (priceConfig == null) {
throw new BaseException("该套餐暂未开放购买");
// 验证照片数量
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId);
long count = userPhotoList.stream().filter(item -> Objects.nonNull(item.getQuantity())).mapToInt(MemberPrintResp::getQuantity).sum();
if (count == 0) {
throw new BaseException("没有可打印的照片");
}
log.info("创建打印订单,价格配置:{}", priceConfig);
OrderEntity order = new OrderEntity();
Long orderId = SnowFlakeUtil.getLongId();
order.setId(orderId);
@@ -262,9 +303,10 @@ public class PrinterServiceImpl implements PrinterService {
MemberRespVO member = memberMapper.getById(memberId);
order.setOpenId(member.getOpenId());
order.setScenicId(scenicId);
order.setType(priceConfig.getType());
order.setType(3); // 照片打印类型
batchSetUserPhotoListToPrinter(memberId, scenicId, printerId);
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId);
// 重新获取照片列表(包含打印机信息)
userPhotoList = getUserPhotoList(memberId, scenicId);
List<OrderItemEntity> orderItems = userPhotoList.stream().map(goods -> {
OrderItemEntity orderItem = new OrderItemEntity();
orderItem.setOrderId(orderId);
@@ -273,10 +315,28 @@ public class PrinterServiceImpl implements PrinterService {
orderItem.setGoodsType(3);
return orderItem;
}).collect(Collectors.toList());
long count = userPhotoList.stream().filter(item -> Objects.nonNull(item.getQuantity())).mapToInt(MemberPrintResp::getQuantity).sum();
order.setPrice(priceConfig.getPrice().multiply(BigDecimal.valueOf(count)));
order.setSlashPrice(priceConfig.getSlashPrice().multiply(BigDecimal.valueOf(count)));
order.setPayPrice(priceConfig.getPrice().multiply(BigDecimal.valueOf(count)));
// 使用统一价格计算服务计算最终价格
PriceCalculationRequest request = new PriceCalculationRequest();
request.setUserId(memberId);
request.setScenicId(scenicId);
// 创建照片打印商品项
ProductItem photoItem = new ProductItem();
photoItem.setProductType(ProductType.PHOTO_PRINT);
photoItem.setProductId(scenicId.toString());
photoItem.setQuantity(Long.valueOf(count).intValue());
photoItem.setPurchaseCount(1);
photoItem.setScenicId(scenicId.toString());
request.setProducts(Collections.singletonList(photoItem));
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(request);
order.setPrice(priceResult.getFinalAmount());
order.setSlashPrice(priceResult.getOriginalAmount());
order.setPayPrice(priceResult.getFinalAmount());
// order.setFaceId();
if (order.getPayPrice().equals(BigDecimal.ZERO)) {
order.setStatus(OrderStateEnum.PAID.getState());