feat(pricing): 优化升单价格计算逻辑支持补差价功能

- 修改 UpgradeBundleDiscountResult 和 UpgradeOnePriceResult 中 estimatedFinalAmount 字段含义为补差价金额
- 在 UpgradeCheckRequest 中新增 paidAmount 字段用于传递已支付金额
- 在 UpgradeCheckResult 中新增 bestUpgradeType 和 bestPayableAmount 字段提供最优升单建议
- 在 UpgradePriceSummary 中新增 paidAmount 字段记录已支付金额
- 更新价格计算服务实现,加入已支付金额处理逻辑
- 新增 normalizeAmount、calculateSupplementAmount 等工具方法确保金额计算精度
- 修复测试代码中的数据类型不匹配问题
This commit is contained in:
2026-01-19 20:25:44 +08:00
parent b1cfef278d
commit 91626626f4
9 changed files with 160 additions and 40 deletions

View File

@@ -616,20 +616,21 @@ UpgradeCheckResult checkUpgrade(UpgradeCheckRequest request);
- `scenicId`: 景区ID - `scenicId`: 景区ID
- `purchasedProducts`: 已购商品列表 - `purchasedProducts`: 已购商品列表
- `intendingProducts`: 待购商品列表 - `intendingProducts`: 待购商品列表
- `paidAmount`: 已支付金额(内部代码传入,前端不必传)
#### 检测流程 #### 检测流程
1. **商品规范化**: 对已购和待购商品进行规范化处理 1. **商品规范化**: 对已购和待购商品进行规范化处理
2. **价格汇总**: 分别计算已购和待购商品的总价格 2. **价格汇总**: 分别计算已购和待购商品的总价格,并合并已支付金额
3. **一口价评估**: 判断合并商品是否满足一口价条件 3. **一口价评估**: 判断合并商品是否满足一口价条件
4. **打包优惠评估**: 检测是否满足打包购买优惠条件 4. **打包优惠评估**: 检测是否满足打包购买优惠条件
5. **结果汇总**: 生成升单检测结果和建议 5. **结果汇总**: 生成升单检测结果和建议
#### 响应结果 (UpgradeCheckResult) #### 响应结果 (UpgradeCheckResult)
- `summary`: 价格汇总信息(原价、优惠价、最终价 - `summary`: 价格汇总信息(包含已支付金额
- `onePriceResult`: 一口价检测结果(如适用 - `onePriceResult`: 一口价检测结果(含补差价金额
- `bundleResult`: 打包优惠检测结果(如适用 - `bundleDiscountResult`: 打包优惠检测结果(含补差价金额
- `upgradeAvailable`: 是否可升单(布尔值 - `bestUpgradeType`: 最优升单类型(ONE_PRICE / BUNDLE_DISCOUNT
- `savingsAmount`: 升单可节省金额 - `bestPayableAmount`: 最低补差价金额
### 4. 业务价值 ### 4. 业务价值

View File

@@ -56,7 +56,7 @@ public class UpgradeBundleDiscountResult {
private BigDecimal minAmount; private BigDecimal minAmount;
/** /**
* 使用优惠后的预计应付金额 * 使用优惠后的补差价金额(已支付金额已扣减)
*/ */
private BigDecimal estimatedFinalAmount; private BigDecimal estimatedFinalAmount;
} }

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.dto;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
/** /**
@@ -29,4 +30,9 @@ public class UpgradeCheckRequest {
* 准备购买的商品列表 * 准备购买的商品列表
*/ */
private List<ProductItem> intendingProducts; private List<ProductItem> intendingProducts;
/**
* 已支付金额(内部代码传入,前端不必传)
*/
private BigDecimal paidAmount;
} }

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.dto;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
/** /**
@@ -35,6 +36,16 @@ public class UpgradeCheckResult {
*/ */
private UpgradeBundleDiscountResult bundleDiscountResult; private UpgradeBundleDiscountResult bundleDiscountResult;
/**
* 最优升单类型(ONE_PRICE / BUNDLE_DISCOUNT)
*/
private String bestUpgradeType;
/**
* 最低补差价金额
*/
private BigDecimal bestPayableAmount;
/** /**
* 已购买商品明细(带计算价) * 已购买商品明细(带计算价)
*/ */

View File

@@ -46,7 +46,7 @@ public class UpgradeOnePriceResult {
private BigDecimal discountAmount; private BigDecimal discountAmount;
/** /**
* 使用一口价后的预计应付金额 * 使用一口价后的补差价金额(已支付金额已扣减)
*/ */
private BigDecimal estimatedFinalAmount; private BigDecimal estimatedFinalAmount;
} }

View File

@@ -20,6 +20,11 @@ public class UpgradePriceSummary {
*/ */
private BigDecimal purchasedSubtotalAmount = BigDecimal.ZERO; private BigDecimal purchasedSubtotalAmount = BigDecimal.ZERO;
/**
* 已支付金额(用于升单补差)
*/
private BigDecimal paidAmount = BigDecimal.ZERO;
/** /**
* 计划购买原价合计 * 计划购买原价合计
*/ */

View File

@@ -33,6 +33,9 @@ import java.util.Set;
public class PriceCalculationServiceImpl implements IPriceCalculationService { public class PriceCalculationServiceImpl implements IPriceCalculationService {
private static final String CAPABILITY_METADATA_ATTRIBUTE_KEYS = "pricingAttributeKeys"; private static final String CAPABILITY_METADATA_ATTRIBUTE_KEYS = "pricingAttributeKeys";
private static final int AMOUNT_SCALE = 2;
private static final String UPGRADE_TYPE_ONE_PRICE = "ONE_PRICE";
private static final String UPGRADE_TYPE_BUNDLE_DISCOUNT = "BUNDLE_DISCOUNT";
private final IProductConfigService productConfigService; private final IProductConfigService productConfigService;
private final ICouponService couponService; private final ICouponService couponService;
@@ -166,7 +169,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
if (request == null) { if (request == null) {
throw new PriceCalculationException("升单检测请求不能为空"); throw new PriceCalculationException("升单检测请求不能为空");
} }
List<ProductItem> purchasedProducts = cloneProducts(request.getPurchasedProducts()); List<ProductItem> purchasedProducts = cloneProducts(request.getPurchasedProducts());
List<ProductItem> intendingProducts = cloneProducts(request.getIntendingProducts()); List<ProductItem> intendingProducts = cloneProducts(request.getIntendingProducts());
@@ -176,29 +179,34 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
normalizeProducts(purchasedProducts); normalizeProducts(purchasedProducts);
normalizeProducts(intendingProducts); normalizeProducts(intendingProducts);
Long scenicId = request.getScenicId();
PriceDetails purchasedDetails = purchasedProducts.isEmpty() PriceDetails purchasedDetails = purchasedProducts.isEmpty()
? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO) ? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO)
: calculateProductsPriceWithOriginal(purchasedProducts); : calculateProductsPriceWithOriginal(purchasedProducts, scenicId);
PriceDetails intendingDetails = intendingProducts.isEmpty() PriceDetails intendingDetails = intendingProducts.isEmpty()
? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO) ? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO)
: calculateProductsPriceWithOriginal(intendingProducts); : calculateProductsPriceWithOriginal(intendingProducts, scenicId);
List<ProductItem> combinedProducts = new ArrayList<>(); List<ProductItem> combinedProducts = new ArrayList<>();
combinedProducts.addAll(purchasedProducts); combinedProducts.addAll(purchasedProducts);
combinedProducts.addAll(intendingProducts); combinedProducts.addAll(intendingProducts);
PriceDetails combinedDetails = calculateProductsPriceWithOriginal(combinedProducts); PriceDetails combinedDetails = calculateProductsPriceWithOriginal(combinedProducts, scenicId);
UpgradePriceSummary priceSummary = buildPriceSummary(purchasedDetails, intendingDetails, combinedDetails); BigDecimal paidAmount = resolvePaidAmount(request, purchasedDetails);
UpgradeOnePriceResult onePriceResult = evaluateOnePrice(request.getScenicId(), combinedProducts, combinedDetails); BigDecimal currentTotalAmount = calculateCurrentTotalAmount(paidAmount, intendingDetails);
UpgradeBundleDiscountResult bundleDiscountResult = evaluateBundleDiscount(request.getScenicId(), combinedProducts, combinedDetails);
UpgradePriceSummary priceSummary = buildPriceSummary(purchasedDetails, intendingDetails, combinedDetails, paidAmount);
UpgradeOnePriceResult onePriceResult = evaluateOnePrice(scenicId, combinedProducts, combinedDetails, paidAmount, currentTotalAmount);
UpgradeBundleDiscountResult bundleDiscountResult = evaluateBundleDiscount(scenicId, combinedProducts, combinedDetails, paidAmount, currentTotalAmount);
UpgradeCheckResult result = new UpgradeCheckResult(); UpgradeCheckResult result = new UpgradeCheckResult();
result.setScenicId(request.getScenicId()); result.setScenicId(scenicId);
result.setFaceId(request.getFaceId()); result.setFaceId(request.getFaceId());
result.setPriceSummary(priceSummary); result.setPriceSummary(priceSummary);
result.setOnePriceResult(onePriceResult); result.setOnePriceResult(onePriceResult);
result.setBundleDiscountResult(bundleDiscountResult); result.setBundleDiscountResult(bundleDiscountResult);
fillBestUpgrade(result, onePriceResult, bundleDiscountResult);
result.setPurchasedProducts(purchasedProducts); result.setPurchasedProducts(purchasedProducts);
result.setIntendingProducts(intendingProducts); result.setIntendingProducts(intendingProducts);
return result; return result;
@@ -435,7 +443,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
return new ProductPriceInfo(actualPrice, originalPrice); return new ProductPriceInfo(actualPrice, originalPrice);
} }
private UpgradePriceSummary buildPriceSummary(PriceDetails purchased, PriceDetails intending, PriceDetails combined) { private UpgradePriceSummary buildPriceSummary(PriceDetails purchased, PriceDetails intending, PriceDetails combined, BigDecimal paidAmount) {
UpgradePriceSummary summary = new UpgradePriceSummary(); UpgradePriceSummary summary = new UpgradePriceSummary();
summary.setPurchasedOriginalAmount(purchased.getOriginalTotalAmount()); summary.setPurchasedOriginalAmount(purchased.getOriginalTotalAmount());
summary.setPurchasedSubtotalAmount(purchased.getTotalAmount()); summary.setPurchasedSubtotalAmount(purchased.getTotalAmount());
@@ -443,10 +451,15 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
summary.setIntendingSubtotalAmount(intending.getTotalAmount()); summary.setIntendingSubtotalAmount(intending.getTotalAmount());
summary.setCombinedOriginalAmount(combined.getOriginalTotalAmount()); summary.setCombinedOriginalAmount(combined.getOriginalTotalAmount());
summary.setCombinedSubtotalAmount(combined.getTotalAmount()); summary.setCombinedSubtotalAmount(combined.getTotalAmount());
summary.setPaidAmount(paidAmount);
return summary; return summary;
} }
private UpgradeOnePriceResult evaluateOnePrice(Long scenicId, List<ProductItem> combinedProducts, PriceDetails combinedDetails) { private UpgradeOnePriceResult evaluateOnePrice(Long scenicId,
List<ProductItem> combinedProducts,
PriceDetails combinedDetails,
BigDecimal paidAmount,
BigDecimal currentTotalAmount) {
UpgradeOnePriceResult result = new UpgradeOnePriceResult(); UpgradeOnePriceResult result = new UpgradeOnePriceResult();
result.setApplicable(false); result.setApplicable(false);
@@ -458,23 +471,32 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
BigDecimal bundlePrice = bundleConfig.getBundlePrice() != null BigDecimal bundlePrice = bundleConfig.getBundlePrice() != null
? bundleConfig.getBundlePrice() ? bundleConfig.getBundlePrice()
: combinedDetails.getTotalAmount(); : combinedDetails.getTotalAmount();
BigDecimal discountAmount = combinedDetails.getTotalAmount().subtract(bundlePrice); BigDecimal normalizedBundlePrice = normalizeAmount(bundlePrice);
if (discountAmount.compareTo(BigDecimal.ZERO) < 0) { BigDecimal normalizedCurrentTotal = normalizeAmount(currentTotalAmount);
discountAmount = BigDecimal.ZERO; BigDecimal normalizedPaidAmount = normalizeAmount(paidAmount);
if (!isUpgradeBeneficial(normalizedCurrentTotal, normalizedBundlePrice)) {
return result;
} }
BigDecimal discountAmount = normalizedCurrentTotal.subtract(normalizedBundlePrice);
result.setApplicable(true); result.setApplicable(true);
result.setBundleConfigId(bundleConfig.getId()); result.setBundleConfigId(bundleConfig.getId());
result.setBundleName(bundleConfig.getBundleName()); result.setBundleName(bundleConfig.getBundleName());
result.setDescription(bundleConfig.getDescription()); result.setDescription(bundleConfig.getDescription());
result.setScenicId(bundleConfig.getScenicId()); result.setScenicId(bundleConfig.getScenicId());
result.setBundlePrice(bundlePrice); result.setBundlePrice(normalizedBundlePrice);
result.setDiscountAmount(discountAmount); result.setDiscountAmount(discountAmount);
result.setEstimatedFinalAmount(bundlePrice); result.setEstimatedFinalAmount(calculateSupplementAmount(normalizedBundlePrice, normalizedPaidAmount));
return result; return result;
} }
private UpgradeBundleDiscountResult evaluateBundleDiscount(Long scenicId, List<ProductItem> combinedProducts, PriceDetails combinedDetails) { private UpgradeBundleDiscountResult evaluateBundleDiscount(Long scenicId,
List<ProductItem> combinedProducts,
PriceDetails combinedDetails,
BigDecimal paidAmount,
BigDecimal currentTotalAmount) {
UpgradeBundleDiscountResult result = new UpgradeBundleDiscountResult(); UpgradeBundleDiscountResult result = new UpgradeBundleDiscountResult();
result.setApplicable(false); result.setApplicable(false);
@@ -491,21 +513,28 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
return result; return result;
} }
BigDecimal normalizedPaidAmount = normalizeAmount(paidAmount);
BigDecimal normalizedCurrentTotal = normalizeAmount(currentTotalAmount);
BigDecimal normalizedDiscount = normalizeAmount(discountAmount);
BigDecimal targetTotal = combinedDetails.getTotalAmount().subtract(normalizedDiscount);
if (targetTotal.compareTo(BigDecimal.ZERO) < 0) {
targetTotal = BigDecimal.ZERO;
}
if (!isUpgradeBeneficial(normalizedCurrentTotal, targetTotal)) {
return result;
}
result.setApplicable(true); result.setApplicable(true);
result.setBundleConfigId(bestDiscount.getBundleConfigId()); result.setBundleConfigId(bestDiscount.getBundleConfigId());
result.setBundleName(bestDiscount.getBundleName()); result.setBundleName(bestDiscount.getBundleName());
result.setBundleDescription(bestDiscount.getBundleDescription()); result.setBundleDescription(bestDiscount.getBundleDescription());
result.setDiscountType(bestDiscount.getDiscountType()); result.setDiscountType(bestDiscount.getDiscountType());
result.setDiscountValue(bestDiscount.getDiscountValue()); result.setDiscountValue(bestDiscount.getDiscountValue());
result.setDiscountAmount(discountAmount); result.setDiscountAmount(normalizedDiscount);
result.setMinQuantity(bestDiscount.getMinQuantity()); result.setMinQuantity(bestDiscount.getMinQuantity());
result.setMinAmount(bestDiscount.getMinAmount()); result.setMinAmount(bestDiscount.getMinAmount());
result.setEstimatedFinalAmount(calculateSupplementAmount(targetTotal, normalizedPaidAmount));
BigDecimal finalAmount = combinedDetails.getTotalAmount().subtract(discountAmount);
if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
finalAmount = BigDecimal.ZERO;
}
result.setEstimatedFinalAmount(finalAmount);
return result; return result;
} }
@@ -562,6 +591,71 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
} }
return configScenicId.equals(String.valueOf(scenicId)); return configScenicId.equals(String.valueOf(scenicId));
} }
private BigDecimal resolvePaidAmount(UpgradeCheckRequest request, PriceDetails purchasedDetails) {
BigDecimal paidAmount = request != null ? request.getPaidAmount() : null;
if (paidAmount == null && purchasedDetails != null) {
paidAmount = purchasedDetails.getTotalAmount();
}
return normalizeAmount(paidAmount);
}
private BigDecimal calculateCurrentTotalAmount(BigDecimal paidAmount, PriceDetails intendingDetails) {
BigDecimal intendingAmount = intendingDetails != null ? intendingDetails.getTotalAmount() : BigDecimal.ZERO;
BigDecimal normalizedPaidAmount = normalizeAmount(paidAmount);
BigDecimal normalizedIntendingAmount = normalizeAmount(intendingAmount);
return normalizedPaidAmount.add(normalizedIntendingAmount).setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
}
private BigDecimal calculateSupplementAmount(BigDecimal targetTotalAmount, BigDecimal paidAmount) {
BigDecimal normalizedTarget = normalizeAmount(targetTotalAmount);
BigDecimal normalizedPaid = normalizeAmount(paidAmount);
BigDecimal supplementAmount = normalizedTarget.subtract(normalizedPaid);
if (supplementAmount.compareTo(BigDecimal.ZERO) < 0) {
supplementAmount = BigDecimal.ZERO;
}
return supplementAmount.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
}
private boolean isUpgradeBeneficial(BigDecimal currentTotalAmount, BigDecimal targetTotalAmount) {
BigDecimal normalizedCurrent = normalizeAmount(currentTotalAmount);
BigDecimal normalizedTarget = normalizeAmount(targetTotalAmount);
return normalizedCurrent.compareTo(normalizedTarget) > 0;
}
private BigDecimal normalizeAmount(BigDecimal amount) {
if (amount == null) {
return BigDecimal.ZERO.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
}
BigDecimal normalized = amount.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : amount;
return normalized.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
}
private void fillBestUpgrade(UpgradeCheckResult result,
UpgradeOnePriceResult onePriceResult,
UpgradeBundleDiscountResult bundleDiscountResult) {
if (result == null) {
return;
}
BigDecimal bestPayableAmount = null;
String bestUpgradeType = null;
if (onePriceResult != null && onePriceResult.isApplicable()) {
bestPayableAmount = onePriceResult.getEstimatedFinalAmount();
bestUpgradeType = UPGRADE_TYPE_ONE_PRICE;
}
if (bundleDiscountResult != null && bundleDiscountResult.isApplicable()) {
BigDecimal bundlePayableAmount = bundleDiscountResult.getEstimatedFinalAmount();
if (bestPayableAmount == null || (bundlePayableAmount != null
&& bundlePayableAmount.compareTo(bestPayableAmount) < 0)) {
bestPayableAmount = bundlePayableAmount;
bestUpgradeType = UPGRADE_TYPE_BUNDLE_DISCOUNT;
}
}
result.setBestPayableAmount(bestPayableAmount);
result.setBestUpgradeType(bestUpgradeType);
}
/** /**
* 计算优惠(券码 + 优惠券) * 计算优惠(券码 + 优惠券)

View File

@@ -46,7 +46,7 @@ class CouponSwitchFieldsMappingTest {
// 测试打包配置的优惠券开关字段 // 测试打包配置的优惠券开关字段
PriceBundleConfig config = new PriceBundleConfig(); PriceBundleConfig config = new PriceBundleConfig();
config.setBundleName("测试套餐"); config.setBundleName("测试套餐");
config.setScenicId(1L); config.setScenicId("1");
config.setBundlePrice(new BigDecimal("99.00")); config.setBundlePrice(new BigDecimal("99.00"));
config.setDescription("测试描述"); config.setDescription("测试描述");
config.setIsActive(true); config.setIsActive(true);
@@ -91,4 +91,4 @@ class CouponSwitchFieldsMappingTest {
System.out.println("布尔值判断逻辑测试通过"); System.out.println("布尔值判断逻辑测试通过");
} }
} }

View File

@@ -16,7 +16,8 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.Duration;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -109,8 +110,10 @@ class CouponServiceImplTest {
coupon.setId(1L); coupon.setId(1L);
coupon.setCouponName("新客券"); coupon.setCouponName("新客券");
coupon.setIsActive(true); coupon.setIsActive(true);
coupon.setValidFrom(LocalDateTime.now().minusDays(1)); Date now = new Date();
coupon.setValidUntil(LocalDateTime.now().plusDays(1)); long oneDayMillis = Duration.ofDays(1).toMillis();
coupon.setValidFrom(new Date(now.getTime() - oneDayMillis));
coupon.setValidUntil(new Date(now.getTime() + oneDayMillis));
coupon.setTotalQuantity(100); coupon.setTotalQuantity(100);
coupon.setClaimedQuantity(0); coupon.setClaimedQuantity(0);
return coupon; return coupon;