You've already forked FrameTour-BE
feat(pricing): 新增升单检测功能
- 添加升单检测接口和相关 DTO 类 - 实现升单检测逻辑,包括价格汇总、一口价评估和打包优惠评估 - 优化商品列表复制和规范化处理 - 新增 IBundleDiscountService 依赖
This commit is contained in:
@@ -59,6 +59,20 @@ public class PriceCalculationController {
|
|||||||
return null;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询用户可用优惠券
|
* 查询用户可用优惠券
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.ycwl.basic.pricing.service;
|
|||||||
|
|
||||||
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
||||||
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
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 价格计算结果
|
* @return 价格计算结果
|
||||||
*/
|
*/
|
||||||
PriceCalculationResult calculatePrice(PriceCalculationRequest request);
|
PriceCalculationResult calculatePrice(PriceCalculationRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 升单检测:综合已购与待购商品,判断是否命中一口价或打包优惠
|
||||||
|
*
|
||||||
|
* @param request 升单检测请求
|
||||||
|
* @return 检测结果
|
||||||
|
*/
|
||||||
|
UpgradeCheckResult checkUpgrade(UpgradeCheckRequest request);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ycwl.basic.pricing.service.impl;
|
package com.ycwl.basic.pricing.service.impl;
|
||||||
|
|
||||||
import com.ycwl.basic.pricing.dto.*;
|
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.PriceProductConfig;
|
||||||
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
import com.ycwl.basic.pricing.entity.PriceTierConfig;
|
||||||
import com.ycwl.basic.pricing.enums.ProductType;
|
import com.ycwl.basic.pricing.enums.ProductType;
|
||||||
@@ -27,6 +28,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
private final IProductConfigService productConfigService;
|
private final IProductConfigService productConfigService;
|
||||||
private final ICouponService couponService;
|
private final ICouponService couponService;
|
||||||
private final IPriceBundleService bundleService;
|
private final IPriceBundleService bundleService;
|
||||||
|
private final IBundleDiscountService bundleDiscountService;
|
||||||
private final IDiscountDetectionService discountDetectionService;
|
private final IDiscountDetectionService discountDetectionService;
|
||||||
private final IVoucherService voucherService;
|
private final IVoucherService voucherService;
|
||||||
|
|
||||||
@@ -130,6 +132,49 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
return result;
|
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) {
|
private BigDecimal calculateProductsPrice(List<ProductItem> products) {
|
||||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
@@ -308,6 +353,134 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
|
|||||||
return new ProductPriceInfo(actualPrice, originalPrice);
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算优惠(券码 + 优惠券)
|
* 计算优惠(券码 + 优惠券)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user