diff --git a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md index 39483131..fe681fe7 100644 --- a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md @@ -616,20 +616,21 @@ UpgradeCheckResult checkUpgrade(UpgradeCheckRequest request); - `scenicId`: 景区ID - `purchasedProducts`: 已购商品列表 - `intendingProducts`: 待购商品列表 +- `paidAmount`: 已支付金额(内部代码传入,前端不必传) #### 检测流程 1. **商品规范化**: 对已购和待购商品进行规范化处理 -2. **价格汇总**: 分别计算已购和待购商品的总价格 +2. **价格汇总**: 分别计算已购和待购商品的总价格,并合并已支付金额 3. **一口价评估**: 判断合并商品是否满足一口价条件 4. **打包优惠评估**: 检测是否满足打包购买优惠条件 5. **结果汇总**: 生成升单检测结果和建议 #### 响应结果 (UpgradeCheckResult) -- `summary`: 价格汇总信息(原价、优惠价、最终价) -- `onePriceResult`: 一口价检测结果(如适用) -- `bundleResult`: 打包优惠检测结果(如适用) -- `upgradeAvailable`: 是否可升单(布尔值) -- `savingsAmount`: 升单可节省金额 +- `summary`: 价格汇总信息(包含已支付金额) +- `onePriceResult`: 一口价检测结果(含补差价金额) +- `bundleDiscountResult`: 打包优惠检测结果(含补差价金额) +- `bestUpgradeType`: 最优升单类型(ONE_PRICE / BUNDLE_DISCOUNT) +- `bestPayableAmount`: 最低补差价金额 ### 4. 业务价值 diff --git a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeBundleDiscountResult.java b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeBundleDiscountResult.java index b88b84ff..3c8b02fe 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeBundleDiscountResult.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeBundleDiscountResult.java @@ -56,7 +56,7 @@ public class UpgradeBundleDiscountResult { private BigDecimal minAmount; /** - * 使用优惠后的预计应付金额 + * 使用优惠后的补差价金额(已支付金额已扣减) */ private BigDecimal estimatedFinalAmount; } diff --git a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckRequest.java index 7b0206e5..28163b6c 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckRequest.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckRequest.java @@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.dto; import lombok.Data; +import java.math.BigDecimal; import java.util.List; /** @@ -29,4 +30,9 @@ public class UpgradeCheckRequest { * 准备购买的商品列表 */ private List intendingProducts; + + /** + * 已支付金额(内部代码传入,前端不必传) + */ + private BigDecimal paidAmount; } diff --git a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckResult.java b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckResult.java index 79421b69..d77a711d 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckResult.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeCheckResult.java @@ -2,6 +2,7 @@ package com.ycwl.basic.pricing.dto; import lombok.Data; +import java.math.BigDecimal; import java.util.List; /** @@ -35,6 +36,16 @@ public class UpgradeCheckResult { */ private UpgradeBundleDiscountResult bundleDiscountResult; + /** + * 最优升单类型(ONE_PRICE / BUNDLE_DISCOUNT) + */ + private String bestUpgradeType; + + /** + * 最低补差价金额 + */ + private BigDecimal bestPayableAmount; + /** * 已购买商品明细(带计算价) */ diff --git a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeOnePriceResult.java b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeOnePriceResult.java index 1e38740d..8677c471 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/UpgradeOnePriceResult.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/UpgradeOnePriceResult.java @@ -46,7 +46,7 @@ public class UpgradeOnePriceResult { private BigDecimal discountAmount; /** - * 使用一口价后的预计应付金额 + * 使用一口价后的补差价金额(已支付金额已扣减) */ private BigDecimal estimatedFinalAmount; } diff --git a/src/main/java/com/ycwl/basic/pricing/dto/UpgradePriceSummary.java b/src/main/java/com/ycwl/basic/pricing/dto/UpgradePriceSummary.java index 71a8edef..2adb88d5 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/UpgradePriceSummary.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/UpgradePriceSummary.java @@ -20,6 +20,11 @@ public class UpgradePriceSummary { */ private BigDecimal purchasedSubtotalAmount = BigDecimal.ZERO; + /** + * 已支付金额(用于升单补差) + */ + private BigDecimal paidAmount = BigDecimal.ZERO; + /** * 计划购买原价合计 */ diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java index e0783e2e..baa6c72a 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PriceCalculationServiceImpl.java @@ -33,6 +33,9 @@ import java.util.Set; public class PriceCalculationServiceImpl implements IPriceCalculationService { 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 ICouponService couponService; @@ -166,7 +169,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { if (request == null) { throw new PriceCalculationException("升单检测请求不能为空"); } - + List purchasedProducts = cloneProducts(request.getPurchasedProducts()); List intendingProducts = cloneProducts(request.getIntendingProducts()); @@ -176,29 +179,34 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { normalizeProducts(purchasedProducts); normalizeProducts(intendingProducts); - + + Long scenicId = request.getScenicId(); PriceDetails purchasedDetails = purchasedProducts.isEmpty() ? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO) - : calculateProductsPriceWithOriginal(purchasedProducts); + : calculateProductsPriceWithOriginal(purchasedProducts, scenicId); PriceDetails intendingDetails = intendingProducts.isEmpty() ? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO) - : calculateProductsPriceWithOriginal(intendingProducts); - + : calculateProductsPriceWithOriginal(intendingProducts, scenicId); + List combinedProducts = new ArrayList<>(); combinedProducts.addAll(purchasedProducts); combinedProducts.addAll(intendingProducts); - PriceDetails combinedDetails = calculateProductsPriceWithOriginal(combinedProducts); - - UpgradePriceSummary priceSummary = buildPriceSummary(purchasedDetails, intendingDetails, combinedDetails); - UpgradeOnePriceResult onePriceResult = evaluateOnePrice(request.getScenicId(), combinedProducts, combinedDetails); - UpgradeBundleDiscountResult bundleDiscountResult = evaluateBundleDiscount(request.getScenicId(), combinedProducts, combinedDetails); - + PriceDetails combinedDetails = calculateProductsPriceWithOriginal(combinedProducts, scenicId); + + BigDecimal paidAmount = resolvePaidAmount(request, purchasedDetails); + BigDecimal currentTotalAmount = calculateCurrentTotalAmount(paidAmount, intendingDetails); + + 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(); - result.setScenicId(request.getScenicId()); + result.setScenicId(scenicId); result.setFaceId(request.getFaceId()); result.setPriceSummary(priceSummary); result.setOnePriceResult(onePriceResult); result.setBundleDiscountResult(bundleDiscountResult); + fillBestUpgrade(result, onePriceResult, bundleDiscountResult); result.setPurchasedProducts(purchasedProducts); result.setIntendingProducts(intendingProducts); return result; @@ -435,7 +443,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { 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(); summary.setPurchasedOriginalAmount(purchased.getOriginalTotalAmount()); summary.setPurchasedSubtotalAmount(purchased.getTotalAmount()); @@ -443,10 +451,15 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { summary.setIntendingSubtotalAmount(intending.getTotalAmount()); summary.setCombinedOriginalAmount(combined.getOriginalTotalAmount()); summary.setCombinedSubtotalAmount(combined.getTotalAmount()); + summary.setPaidAmount(paidAmount); return summary; } - private UpgradeOnePriceResult evaluateOnePrice(Long scenicId, List combinedProducts, PriceDetails combinedDetails) { + private UpgradeOnePriceResult evaluateOnePrice(Long scenicId, + List combinedProducts, + PriceDetails combinedDetails, + BigDecimal paidAmount, + BigDecimal currentTotalAmount) { UpgradeOnePriceResult result = new UpgradeOnePriceResult(); result.setApplicable(false); @@ -458,23 +471,32 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { BigDecimal bundlePrice = bundleConfig.getBundlePrice() != null ? bundleConfig.getBundlePrice() : combinedDetails.getTotalAmount(); - BigDecimal discountAmount = combinedDetails.getTotalAmount().subtract(bundlePrice); - if (discountAmount.compareTo(BigDecimal.ZERO) < 0) { - discountAmount = BigDecimal.ZERO; + BigDecimal normalizedBundlePrice = normalizeAmount(bundlePrice); + BigDecimal normalizedCurrentTotal = normalizeAmount(currentTotalAmount); + BigDecimal normalizedPaidAmount = normalizeAmount(paidAmount); + + if (!isUpgradeBeneficial(normalizedCurrentTotal, normalizedBundlePrice)) { + return result; } + + BigDecimal discountAmount = normalizedCurrentTotal.subtract(normalizedBundlePrice); result.setApplicable(true); result.setBundleConfigId(bundleConfig.getId()); result.setBundleName(bundleConfig.getBundleName()); result.setDescription(bundleConfig.getDescription()); result.setScenicId(bundleConfig.getScenicId()); - result.setBundlePrice(bundlePrice); + result.setBundlePrice(normalizedBundlePrice); result.setDiscountAmount(discountAmount); - result.setEstimatedFinalAmount(bundlePrice); + result.setEstimatedFinalAmount(calculateSupplementAmount(normalizedBundlePrice, normalizedPaidAmount)); return result; } - private UpgradeBundleDiscountResult evaluateBundleDiscount(Long scenicId, List combinedProducts, PriceDetails combinedDetails) { + private UpgradeBundleDiscountResult evaluateBundleDiscount(Long scenicId, + List combinedProducts, + PriceDetails combinedDetails, + BigDecimal paidAmount, + BigDecimal currentTotalAmount) { UpgradeBundleDiscountResult result = new UpgradeBundleDiscountResult(); result.setApplicable(false); @@ -491,21 +513,28 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { 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.setBundleConfigId(bestDiscount.getBundleConfigId()); result.setBundleName(bestDiscount.getBundleName()); result.setBundleDescription(bestDiscount.getBundleDescription()); result.setDiscountType(bestDiscount.getDiscountType()); result.setDiscountValue(bestDiscount.getDiscountValue()); - result.setDiscountAmount(discountAmount); + result.setDiscountAmount(normalizedDiscount); result.setMinQuantity(bestDiscount.getMinQuantity()); result.setMinAmount(bestDiscount.getMinAmount()); - - BigDecimal finalAmount = combinedDetails.getTotalAmount().subtract(discountAmount); - if (finalAmount.compareTo(BigDecimal.ZERO) < 0) { - finalAmount = BigDecimal.ZERO; - } - result.setEstimatedFinalAmount(finalAmount); + result.setEstimatedFinalAmount(calculateSupplementAmount(targetTotal, normalizedPaidAmount)); return result; } @@ -562,6 +591,71 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService { } 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); + } /** * 计算优惠(券码 + 优惠券) diff --git a/src/test/java/com/ycwl/basic/pricing/mapper/CouponSwitchFieldsMappingTest.java b/src/test/java/com/ycwl/basic/pricing/mapper/CouponSwitchFieldsMappingTest.java index b83f7a07..45c1a455 100644 --- a/src/test/java/com/ycwl/basic/pricing/mapper/CouponSwitchFieldsMappingTest.java +++ b/src/test/java/com/ycwl/basic/pricing/mapper/CouponSwitchFieldsMappingTest.java @@ -46,7 +46,7 @@ class CouponSwitchFieldsMappingTest { // 测试打包配置的优惠券开关字段 PriceBundleConfig config = new PriceBundleConfig(); config.setBundleName("测试套餐"); - config.setScenicId(1L); + config.setScenicId("1"); config.setBundlePrice(new BigDecimal("99.00")); config.setDescription("测试描述"); config.setIsActive(true); @@ -91,4 +91,4 @@ class CouponSwitchFieldsMappingTest { System.out.println("布尔值判断逻辑测试通过"); } -} \ No newline at end of file +} diff --git a/src/test/java/com/ycwl/basic/pricing/service/CouponServiceImplTest.java b/src/test/java/com/ycwl/basic/pricing/service/CouponServiceImplTest.java index 2377faf9..592c211c 100644 --- a/src/test/java/com/ycwl/basic/pricing/service/CouponServiceImplTest.java +++ b/src/test/java/com/ycwl/basic/pricing/service/CouponServiceImplTest.java @@ -16,7 +16,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; 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.mockito.Mockito.*; @@ -109,8 +110,10 @@ class CouponServiceImplTest { coupon.setId(1L); coupon.setCouponName("新客券"); coupon.setIsActive(true); - coupon.setValidFrom(LocalDateTime.now().minusDays(1)); - coupon.setValidUntil(LocalDateTime.now().plusDays(1)); + Date now = new Date(); + long oneDayMillis = Duration.ofDays(1).toMillis(); + coupon.setValidFrom(new Date(now.getTime() - oneDayMillis)); + coupon.setValidUntil(new Date(now.getTime() + oneDayMillis)); coupon.setTotalQuantity(100); coupon.setClaimedQuantity(0); return coupon;