feat(pricing): 新增升单检测功能

- 添加升单检测接口和相关 DTO 类
- 实现升单检测逻辑,包括价格汇总、一口价评估和打包优惠评估
- 优化商品列表复制和规范化处理
- 新增 IBundleDiscountService 依赖
This commit is contained in:
2025-09-18 19:51:13 +08:00
parent 1220348bae
commit a5c815b6ed
8 changed files with 432 additions and 0 deletions

View File

@@ -59,6 +59,20 @@ public class PriceCalculationController {
return null;
}
/**
* 升单检测:判断是否命中一口价或打包优惠
*/
@PostMapping("/upgrade-check")
public ApiResponse<UpgradeCheckResult> upgradeCheck(@RequestBody UpgradeCheckRequest request) {
log.info("升单检测请求: scenicId={}, purchased={}, intending={}",
request.getScenicId(),
request.getPurchasedProducts() != null ? request.getPurchasedProducts().size() : 0,
request.getIntendingProducts() != null ? request.getIntendingProducts().size() : 0);
UpgradeCheckResult result = priceCalculationService.checkUpgrade(request);
return ApiResponse.success(result);
}
/**
* 查询用户可用优惠券
*/

View File

@@ -0,0 +1,62 @@
package com.ycwl.basic.pricing.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 升单检测打包优惠结果
*/
@Data
public class UpgradeBundleDiscountResult {
/**
* 是否命中打包优惠
*/
private boolean applicable;
/**
* 打包配置ID
*/
private Long bundleConfigId;
/**
* 打包优惠名称
*/
private String bundleName;
/**
* 打包优惠描述
*/
private String bundleDescription;
/**
* 优惠类型
*/
private String discountType;
/**
* 优惠值
*/
private BigDecimal discountValue;
/**
* 实际优惠金额
*/
private BigDecimal discountAmount;
/**
* 满足条件的最少数量
*/
private Integer minQuantity;
/**
* 满足条件的最少金额
*/
private BigDecimal minAmount;
/**
* 使用优惠后的预计应付金额
*/
private BigDecimal estimatedFinalAmount;
}

View File

@@ -0,0 +1,32 @@
package com.ycwl.basic.pricing.dto;
import lombok.Data;
import java.util.List;
/**
* 升单检测请求
*/
@Data
public class UpgradeCheckRequest {
/**
* 景区ID
*/
private Long scenicId;
/**
* 用户faceId
*/
private Long faceId;
/**
* 已购买商品列表
*/
private List<ProductItem> purchasedProducts;
/**
* 准备购买的商品列表
*/
private List<ProductItem> intendingProducts;
}

View File

@@ -0,0 +1,47 @@
package com.ycwl.basic.pricing.dto;
import lombok.Data;
import java.util.List;
/**
* 升单检测结果
*/
@Data
public class UpgradeCheckResult {
/**
* 景区ID
*/
private Long scenicId;
/**
* 用户faceId
*/
private Long faceId;
/**
* 价格汇总信息
*/
private UpgradePriceSummary priceSummary;
/**
* 一口价检测结果
*/
private UpgradeOnePriceResult onePriceResult;
/**
* 打包优惠检测结果
*/
private UpgradeBundleDiscountResult bundleDiscountResult;
/**
* 已购买商品明细(带计算价)
*/
private List<ProductItem> purchasedProducts;
/**
* 计划购买商品明细(带计算价)
*/
private List<ProductItem> intendingProducts;
}

View File

@@ -0,0 +1,52 @@
package com.ycwl.basic.pricing.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 升单检测一口价结果
*/
@Data
public class UpgradeOnePriceResult {
/**
* 是否命中一口价规则
*/
private boolean applicable;
/**
* 一口价配置ID
*/
private Long bundleConfigId;
/**
* 一口价名称
*/
private String bundleName;
/**
* 一口价描述
*/
private String description;
/**
* 适用景区ID
*/
private String scenicId;
/**
* 一口价金额
*/
private BigDecimal bundlePrice;
/**
* 优惠金额(合并小计 - 一口价金额)
*/
private BigDecimal discountAmount;
/**
* 使用一口价后的预计应付金额
*/
private BigDecimal estimatedFinalAmount;
}

View File

@@ -0,0 +1,42 @@
package com.ycwl.basic.pricing.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 升单检测价格汇总
*/
@Data
public class UpgradePriceSummary {
/**
* 已购买原价合计
*/
private BigDecimal purchasedOriginalAmount = BigDecimal.ZERO;
/**
* 已购买小计金额
*/
private BigDecimal purchasedSubtotalAmount = BigDecimal.ZERO;
/**
* 计划购买原价合计
*/
private BigDecimal intendingOriginalAmount = BigDecimal.ZERO;
/**
* 计划购买小计金额
*/
private BigDecimal intendingSubtotalAmount = BigDecimal.ZERO;
/**
* 合并后的原价合计
*/
private BigDecimal combinedOriginalAmount = BigDecimal.ZERO;
/**
* 合并后的小计金额
*/
private BigDecimal combinedSubtotalAmount = BigDecimal.ZERO;
}

View File

@@ -2,6 +2,8 @@ package com.ycwl.basic.pricing.service;
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.UpgradeCheckRequest;
import com.ycwl.basic.pricing.dto.UpgradeCheckResult;
/**
* 价格计算服务接口
@@ -15,4 +17,12 @@ public interface IPriceCalculationService {
* @return 价格计算结果
*/
PriceCalculationResult calculatePrice(PriceCalculationRequest request);
/**
* 升单检测:综合已购与待购商品,判断是否命中一口价或打包优惠
*
* @param request 升单检测请求
* @return 检测结果
*/
UpgradeCheckResult checkUpgrade(UpgradeCheckRequest request);
}

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.pricing.service.impl;
import com.ycwl.basic.pricing.dto.*;
import com.ycwl.basic.pricing.entity.PriceBundleConfig;
import com.ycwl.basic.pricing.entity.PriceProductConfig;
import com.ycwl.basic.pricing.entity.PriceTierConfig;
import com.ycwl.basic.pricing.enums.ProductType;
@@ -27,6 +28,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
private final IProductConfigService productConfigService;
private final ICouponService couponService;
private final IPriceBundleService bundleService;
private final IBundleDiscountService bundleDiscountService;
private final IDiscountDetectionService discountDetectionService;
private final IVoucherService voucherService;
@@ -130,6 +132,49 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
return result;
}
@Override
public UpgradeCheckResult checkUpgrade(UpgradeCheckRequest request) {
if (request == null) {
throw new PriceCalculationException("升单检测请求不能为空");
}
List<ProductItem> purchasedProducts = cloneProducts(request.getPurchasedProducts());
List<ProductItem> intendingProducts = cloneProducts(request.getIntendingProducts());
if (purchasedProducts.isEmpty() && intendingProducts.isEmpty()) {
throw new PriceCalculationException("已购和待购商品列表不能同时为空");
}
normalizeProducts(purchasedProducts);
normalizeProducts(intendingProducts);
PriceDetails purchasedDetails = purchasedProducts.isEmpty()
? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO)
: calculateProductsPriceWithOriginal(purchasedProducts);
PriceDetails intendingDetails = intendingProducts.isEmpty()
? new PriceDetails(BigDecimal.ZERO, BigDecimal.ZERO)
: calculateProductsPriceWithOriginal(intendingProducts);
List<ProductItem> 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);
UpgradeCheckResult result = new UpgradeCheckResult();
result.setScenicId(request.getScenicId());
result.setFaceId(request.getFaceId());
result.setPriceSummary(priceSummary);
result.setOnePriceResult(onePriceResult);
result.setBundleDiscountResult(bundleDiscountResult);
result.setPurchasedProducts(purchasedProducts);
result.setIntendingProducts(intendingProducts);
return result;
}
private BigDecimal calculateProductsPrice(List<ProductItem> products) {
BigDecimal totalAmount = BigDecimal.ZERO;
@@ -308,6 +353,134 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
return new ProductPriceInfo(actualPrice, originalPrice);
}
private UpgradePriceSummary buildPriceSummary(PriceDetails purchased, PriceDetails intending, PriceDetails combined) {
UpgradePriceSummary summary = new UpgradePriceSummary();
summary.setPurchasedOriginalAmount(purchased.getOriginalTotalAmount());
summary.setPurchasedSubtotalAmount(purchased.getTotalAmount());
summary.setIntendingOriginalAmount(intending.getOriginalTotalAmount());
summary.setIntendingSubtotalAmount(intending.getTotalAmount());
summary.setCombinedOriginalAmount(combined.getOriginalTotalAmount());
summary.setCombinedSubtotalAmount(combined.getTotalAmount());
return summary;
}
private UpgradeOnePriceResult evaluateOnePrice(Long scenicId, List<ProductItem> combinedProducts, PriceDetails combinedDetails) {
UpgradeOnePriceResult result = new UpgradeOnePriceResult();
result.setApplicable(false);
PriceBundleConfig bundleConfig = bundleService.getBundleConfig(combinedProducts);
if (bundleConfig == null || !matchesScenic(bundleConfig.getScenicId(), scenicId)) {
return result;
}
BigDecimal bundlePrice = bundleConfig.getBundlePrice() != null
? bundleConfig.getBundlePrice()
: combinedDetails.getTotalAmount();
BigDecimal discountAmount = combinedDetails.getTotalAmount().subtract(bundlePrice);
if (discountAmount.compareTo(BigDecimal.ZERO) < 0) {
discountAmount = BigDecimal.ZERO;
}
result.setApplicable(true);
result.setBundleConfigId(bundleConfig.getId());
result.setBundleName(bundleConfig.getBundleName());
result.setDescription(bundleConfig.getDescription());
result.setScenicId(bundleConfig.getScenicId());
result.setBundlePrice(bundlePrice);
result.setDiscountAmount(discountAmount);
result.setEstimatedFinalAmount(bundlePrice);
return result;
}
private UpgradeBundleDiscountResult evaluateBundleDiscount(Long scenicId, List<ProductItem> combinedProducts, PriceDetails combinedDetails) {
UpgradeBundleDiscountResult result = new UpgradeBundleDiscountResult();
result.setApplicable(false);
BundleDiscountInfo bestDiscount = bundleDiscountService.getBestBundleDiscount(combinedProducts, scenicId);
if (bestDiscount == null) {
return result;
}
BigDecimal discountAmount = bestDiscount.getActualDiscountAmount();
if (discountAmount == null || discountAmount.compareTo(BigDecimal.ZERO) <= 0) {
discountAmount = bundleDiscountService.calculateBundleDiscount(bestDiscount, combinedProducts);
}
if (discountAmount == null || discountAmount.compareTo(BigDecimal.ZERO) <= 0) {
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.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);
return result;
}
private List<ProductItem> cloneProducts(List<ProductItem> source) {
List<ProductItem> copies = new ArrayList<>();
if (source == null) {
return copies;
}
for (ProductItem item : source) {
if (item == null) {
continue;
}
copies.add(cloneProductItem(item));
}
return copies;
}
private ProductItem cloneProductItem(ProductItem source) {
ProductItem copy = new ProductItem();
copy.setProductType(source.getProductType());
copy.setProductId(source.getProductId());
copy.setQuantity(source.getQuantity());
copy.setPurchaseCount(source.getPurchaseCount());
copy.setOriginalPrice(source.getOriginalPrice());
copy.setUnitPrice(source.getUnitPrice());
copy.setSubtotal(source.getSubtotal());
copy.setScenicId(source.getScenicId());
return copy;
}
private void normalizeProducts(List<ProductItem> products) {
for (ProductItem product : products) {
if (product.getProductType() == null) {
throw new PriceCalculationException("商品类型不能为空");
}
if (product.getProductId() == null) {
throw new PriceCalculationException("商品ID不能为空");
}
if (product.getPurchaseCount() == null) {
product.setPurchaseCount(1);
}
if (product.getQuantity() == null) {
product.setQuantity(1);
}
}
}
private boolean matchesScenic(String configScenicId, Long scenicId) {
if (scenicId == null) {
return true;
}
if (configScenicId == null || configScenicId.isEmpty()) {
return true;
}
return configScenicId.equals(String.valueOf(scenicId));
}
/**
* 计算优惠(券码 + 优惠券)
*/