feat(pricing): 增加商品和打包配置的优惠券及券码使用限制

- 在 PriceBundleConfig 和 PriceProductConfig 中添加是否可使用优惠券和券码的字段
- 修改 CouponDiscountProvider 和 VoucherDiscountProvider,增加对商品和打包配置的检查
- 更新 PriceCalculationServiceImpl 中的优惠计算逻辑,将一口价改为打包购买
- 调整 DiscountDetail 中的描述和排序顺序,以适应新的优惠方式
This commit is contained in:
2025-09-05 11:09:28 +08:00
parent bd077b9252
commit 5210b50adb
11 changed files with 231 additions and 24 deletions

View File

@@ -75,15 +75,16 @@ public class DiscountDetail {
} }
/** /**
* 创建一口价折扣明细 * 创建打包购买折扣明细
*/ */
public static DiscountDetail createBundleDiscount(BigDecimal discountAmount) { public static DiscountDetail createBundleDiscount(BigDecimal discountAmount) {
DiscountDetail detail = new DiscountDetail(); DiscountDetail detail = new DiscountDetail();
detail.setDiscountType("BUNDLE"); detail.setDiscountType("BUNDLE");
detail.setDiscountName("一口价优惠"); detail.setDiscountName("打包购买优惠");
detail.setDiscountAmount(discountAmount); detail.setDiscountAmount(discountAmount);
detail.setDescription("一口价购买更优惠"); detail.setDescription("多商品打包购买更优惠");
detail.setSortOrder(4); // 一口价排在最后 detail.setSortOrder(5); // 打包购买排在后面
return detail; return detail;
} }
} }

View File

@@ -79,4 +79,9 @@ public class DiscountInfo {
* 优惠券ID(如果是coupon类型) * 优惠券ID(如果是coupon类型)
*/ */
private Long couponId; private Long couponId;
/**
* 一口价信息(如果是一口价优惠)
*/
private OnePriceInfo onePriceInfo;
} }

View File

@@ -60,6 +60,16 @@ public class PriceBundleConfig {
*/ */
private Boolean isActive; private Boolean isActive;
/**
* 是否可使用优惠券
*/
private Boolean canUseCoupon;
/**
* 是否可使用券码
*/
private Boolean canUseVoucher;
@TableField("create_time") @TableField("create_time")
private Date createTime; private Date createTime;

View File

@@ -59,6 +59,16 @@ public class PriceProductConfig {
*/ */
private Boolean isActive; private Boolean isActive;
/**
* 是否可使用优惠券
*/
private Boolean canUseCoupon;
/**
* 是否可使用券码
*/
private Boolean canUseVoucher;
@TableField("create_time") @TableField("create_time")
private Date createTime; private Date createTime;

View File

@@ -17,7 +17,7 @@ public interface PriceBundleConfigMapper extends BaseMapper<PriceBundleConfig> {
*/ */
@Select("SELECT id, bundle_name, scenic_id, bundle_price, " + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " +
"included_products, excluded_products, " + "included_products, excluded_products, " +
"description, is_active, create_time, update_time " + "description, is_active, can_use_coupon, can_use_voucher, create_time, update_time " +
"FROM price_bundle_config WHERE is_active = 1") "FROM price_bundle_config WHERE is_active = 1")
@Results({ @Results({
@Result(column = "included_products", property = "includedProducts", @Result(column = "included_products", property = "includedProducts",
@@ -32,7 +32,7 @@ public interface PriceBundleConfigMapper extends BaseMapper<PriceBundleConfig> {
*/ */
@Select("SELECT id, bundle_name, scenic_id, bundle_price, " + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " +
"included_products, excluded_products, " + "included_products, excluded_products, " +
"description, is_active, create_time, update_time " + "description, is_active, can_use_coupon, can_use_voucher, create_time, update_time " +
"FROM price_bundle_config WHERE id = #{id} AND is_active = 1") "FROM price_bundle_config WHERE id = #{id} AND is_active = 1")
@Results({ @Results({
@Result(column = "included_products", property = "includedProducts", @Result(column = "included_products", property = "includedProducts",
@@ -49,7 +49,7 @@ public interface PriceBundleConfigMapper extends BaseMapper<PriceBundleConfig> {
*/ */
@Select("SELECT id, bundle_name, scenic_id, bundle_price, " + @Select("SELECT id, bundle_name, scenic_id, bundle_price, " +
"included_products, excluded_products, " + "included_products, excluded_products, " +
"description, is_active, create_time, update_time " + "description, is_active, can_use_coupon, can_use_voucher, create_time, update_time " +
"FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC") "FROM price_bundle_config ORDER BY is_active DESC, bundle_name ASC")
@Results({ @Results({
@Result(column = "included_products", property = "includedProducts", @Result(column = "included_products", property = "includedProducts",
@@ -63,9 +63,9 @@ public interface PriceBundleConfigMapper extends BaseMapper<PriceBundleConfig> {
* 插入一口价配置 * 插入一口价配置
*/ */
@Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " + @Insert("INSERT INTO price_bundle_config (bundle_name, scenic_id, bundle_price, included_products, excluded_products, " +
"description, is_active, create_time, update_time) VALUES " + "description, is_active, can_use_coupon, can_use_voucher, create_time, update_time) VALUES " +
"(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + "(#{bundleName}, #{scenicId}, #{bundlePrice}, #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " +
"#{description}, #{isActive}, NOW(), NOW())") "#{description}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, NOW(), NOW())")
int insertBundleConfig(PriceBundleConfig config); int insertBundleConfig(PriceBundleConfig config);
/** /**
@@ -73,7 +73,7 @@ public interface PriceBundleConfigMapper extends BaseMapper<PriceBundleConfig> {
*/ */
@Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " + @Update("UPDATE price_bundle_config SET bundle_name = #{bundleName}, scenic_id = #{scenicId}, bundle_price = #{bundlePrice}, " +
"included_products = #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " + "included_products = #{includedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, excluded_products = #{excludedProducts,typeHandler=com.ycwl.basic.pricing.handler.BundleProductListTypeHandler}, " +
"description = #{description}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") "description = #{description}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, update_time = NOW() WHERE id = #{id}")
int updateBundleConfig(PriceBundleConfig config); int updateBundleConfig(PriceBundleConfig config);
/** /**

View File

@@ -57,15 +57,15 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
/** /**
* 插入商品价格配置 * 插入商品价格配置
*/ */
@Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, create_time, update_time) " + @Insert("INSERT INTO price_product_config (product_type, product_id, scenic_id, product_name, base_price, original_price, unit, is_active, can_use_coupon, can_use_voucher, create_time, update_time) " +
"VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, NOW(), NOW())") "VALUES (#{productType}, #{productId}, #{scenicId}, #{productName}, #{basePrice}, #{originalPrice}, #{unit}, #{isActive}, #{canUseCoupon}, #{canUseVoucher}, NOW(), NOW())")
int insertProductConfig(PriceProductConfig config); int insertProductConfig(PriceProductConfig config);
/** /**
* 更新商品价格配置 * 更新商品价格配置
*/ */
@Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " + @Update("UPDATE price_product_config SET product_id = #{productId}, scenic_id = #{scenicId}, product_name = #{productName}, base_price = #{basePrice}, " +
"original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, update_time = NOW() WHERE id = #{id}") "original_price = #{originalPrice}, unit = #{unit}, is_active = #{isActive}, can_use_coupon = #{canUseCoupon}, can_use_voucher = #{canUseVoucher}, update_time = NOW() WHERE id = #{id}")
int updateProductConfig(PriceProductConfig config); int updateProductConfig(PriceProductConfig config);
/** /**

View File

@@ -29,6 +29,14 @@ public interface IPriceBundleService {
*/ */
BigDecimal getBundlePrice(List<ProductItem> products); BigDecimal getBundlePrice(List<ProductItem> products);
/**
* 获取适用的一口价配置
*
* @param products 商品列表
* @return 适用的一口价配置,如果不适用则返回null
*/
PriceBundleConfig getBundleConfig(List<ProductItem> products);
/** /**
* 获取所有启用的一口价配置 * 获取所有启用的一口价配置
* *

View File

@@ -1,8 +1,12 @@
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.service.ICouponService; import com.ycwl.basic.pricing.service.ICouponService;
import com.ycwl.basic.pricing.service.IDiscountProvider; import com.ycwl.basic.pricing.service.IDiscountProvider;
import com.ycwl.basic.pricing.service.IPriceBundleService;
import com.ycwl.basic.pricing.service.IProductConfigService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -20,6 +24,8 @@ import java.util.List;
public class CouponDiscountProvider implements IDiscountProvider { public class CouponDiscountProvider implements IDiscountProvider {
private final ICouponService couponService; private final ICouponService couponService;
private final IProductConfigService productConfigService;
private final IPriceBundleService bundleService;
@Override @Override
public String getProviderType() { public String getProviderType() {
@@ -39,6 +45,12 @@ public class CouponDiscountProvider implements IDiscountProvider {
return discounts; return discounts;
} }
// 检查商品配置和打包配置是否允许使用优惠券
if (!checkCanUseCoupon(context)) {
log.debug("商品配置不允许使用优惠券,跳过优惠券检测");
return discounts;
}
try { try {
CouponInfo bestCoupon = couponService.selectBestCoupon( CouponInfo bestCoupon = couponService.selectBestCoupon(
context.getUserId(), context.getUserId(),
@@ -101,6 +113,70 @@ public class CouponDiscountProvider implements IDiscountProvider {
public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) { public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) {
return "COUPON".equals(discountInfo.getDiscountType()) && return "COUPON".equals(discountInfo.getDiscountType()) &&
Boolean.TRUE.equals(context.getAutoUseCoupon()) && Boolean.TRUE.equals(context.getAutoUseCoupon()) &&
context.getUserId() != null; context.getUserId() != null &&
checkCanUseCoupon(context);
}
/**
* 检查商品配置和打包配置是否允许使用优惠券
*/
private boolean checkCanUseCoupon(DiscountDetectionContext context) {
if (context.getProducts() == null || context.getProducts().isEmpty()) {
return true; // 如果没有商品信息,默认允许
}
try {
// 检查是否使用了打包购买
BigDecimal bundlePrice = bundleService.getBundlePrice(context.getProducts());
if (bundlePrice != null) {
// 如果使用了打包购买,检查打包配置的优惠券使用开关
PriceBundleConfig bundleConfig = bundleService.getBundleConfig(context.getProducts());
if (bundleConfig != null) {
boolean canUseCoupon = Boolean.TRUE.equals(bundleConfig.getCanUseCoupon());
log.debug("打包配置优惠券开关检查: bundleId={}, canUseCoupon={}", bundleConfig.getId(), canUseCoupon);
return canUseCoupon;
}
}
// 检查单个商品的优惠券使用开关
for (ProductItem product : context.getProducts()) {
String productId = product.getProductId() != null ? product.getProductId() : "default";
try {
PriceProductConfig productConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), productId);
if (productConfig != null) {
if (!Boolean.TRUE.equals(productConfig.getCanUseCoupon())) {
log.debug("商品配置不允许使用优惠券: productType={}, productId={}",
product.getProductType().getCode(), productId);
return false;
}
}
} catch (Exception e) {
// 如果获取具体商品配置失败,尝试获取default配置
try {
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), "default");
if (defaultConfig != null) {
if (!Boolean.TRUE.equals(defaultConfig.getCanUseCoupon())) {
log.debug("商品默认配置不允许使用优惠券: productType={}",
product.getProductType().getCode());
return false;
}
}
} catch (Exception ex) {
log.warn("获取商品配置失败,默认允许使用优惠券: productType={}, productId={}",
product.getProductType().getCode(), productId);
}
}
}
} catch (Exception e) {
log.error("检查优惠券使用开关时发生异常,默认允许使用", e);
}
return true; // 默认允许使用优惠券
} }
} }

View File

@@ -75,6 +75,27 @@ public class PriceBundleServiceImpl implements IPriceBundleService {
return null; return null;
} }
@Override
public PriceBundleConfig getBundleConfig(List<ProductItem> products) {
if (!isBundleApplicable(products)) {
return null;
}
List<PriceBundleConfig> bundles = getActiveBundles();
Set<String> productTypes = new HashSet<>();
for (ProductItem product : products) {
productTypes.add(product.getProductType().getCode());
}
for (PriceBundleConfig bundle : bundles) {
if (isProductsMatchBundle(productTypes, bundle)) {
return bundle;
}
}
return null;
}
@Override @Override
// @Cacheable(value = "active-bundles") // @Cacheable(value = "active-bundles")
public List<PriceBundleConfig> getActiveBundles() { public List<PriceBundleConfig> getActiveBundles() {

View File

@@ -49,13 +49,13 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
discountDetails.add(DiscountDetail.createLimitedTimeDiscount(limitedTimeDiscount)); discountDetails.add(DiscountDetail.createLimitedTimeDiscount(limitedTimeDiscount));
} }
// 检查一口价优惠 // 检查打包购买优惠
BigDecimal bundlePrice = bundleService.getBundlePrice(request.getProducts()); BigDecimal packagePrice = bundleService.getBundlePrice(request.getProducts());
if (bundlePrice != null && bundlePrice.compareTo(totalAmount) < 0) { if (packagePrice != null && packagePrice.compareTo(totalAmount) < 0) {
BigDecimal bundleDiscount = totalAmount.subtract(bundlePrice); BigDecimal packageDiscount = totalAmount.subtract(packagePrice);
discountDetails.add(DiscountDetail.createBundleDiscount(bundleDiscount)); discountDetails.add(DiscountDetail.createBundleDiscount(packageDiscount));
totalAmount = bundlePrice; totalAmount = packagePrice;
log.info("使用一口价: {}, 优惠: {}", bundlePrice, bundleDiscount); log.info("使用打包购买: {}, 优惠: {}", packagePrice, packageDiscount);
} }
// 构建价格计算结果 // 构建价格计算结果
@@ -77,7 +77,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
// 重新排序 // 重新排序
allDiscountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder)); allDiscountDetails.sort(Comparator.comparing(DiscountDetail::getSortOrder));
// 计算总优惠金额(包括限时立减、一口价和其他优惠) // 计算总优惠金额(包括限时立减、打包购买和其他优惠)
BigDecimal totalDiscountAmount = allDiscountDetails.stream() BigDecimal totalDiscountAmount = allDiscountDetails.stream()
.map(DiscountDetail::getDiscountAmount) .map(DiscountDetail::getDiscountAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -98,7 +98,7 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
} else { } else {
log.warn("优惠计算失败: {}", discountResult.getErrorMessage()); log.warn("优惠计算失败: {}", discountResult.getErrorMessage());
// 降级处理:仅使用基础优惠(限时立减、一口价 // 降级处理:仅使用基础优惠(限时立减、打包购买
BigDecimal totalDiscountAmount = discountDetails.stream() BigDecimal totalDiscountAmount = discountDetails.stream()
.map(DiscountDetail::getDiscountAmount) .map(DiscountDetail::getDiscountAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);

View File

@@ -1,8 +1,12 @@
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.enums.VoucherDiscountType; import com.ycwl.basic.pricing.enums.VoucherDiscountType;
import com.ycwl.basic.pricing.service.IDiscountProvider; import com.ycwl.basic.pricing.service.IDiscountProvider;
import com.ycwl.basic.pricing.service.IPriceBundleService;
import com.ycwl.basic.pricing.service.IProductConfigService;
import com.ycwl.basic.pricing.service.IVoucherService; import com.ycwl.basic.pricing.service.IVoucherService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -22,6 +26,8 @@ import java.util.List;
public class VoucherDiscountProvider implements IDiscountProvider { public class VoucherDiscountProvider implements IDiscountProvider {
private final IVoucherService voucherService; private final IVoucherService voucherService;
private final IProductConfigService productConfigService;
private final IPriceBundleService bundleService;
@Override @Override
public String getProviderType() { public String getProviderType() {
@@ -41,6 +47,12 @@ public class VoucherDiscountProvider implements IDiscountProvider {
return discounts; return discounts;
} }
// 检查商品配置和打包配置是否允许使用券码
if (!checkCanUseVoucher(context)) {
log.debug("商品配置不允许使用券码,跳过券码检测");
return discounts;
}
try { try {
VoucherInfo voucherInfo = null; VoucherInfo voucherInfo = null;
@@ -149,7 +161,8 @@ public class VoucherDiscountProvider implements IDiscountProvider {
return "VOUCHER".equals(discountInfo.getDiscountType()) && return "VOUCHER".equals(discountInfo.getDiscountType()) &&
context.getFaceId() != null && context.getFaceId() != null &&
context.getScenicId() != null && context.getScenicId() != null &&
StringUtils.hasText(discountInfo.getVoucherCode()); StringUtils.hasText(discountInfo.getVoucherCode()) &&
checkCanUseVoucher(context);
} }
/** /**
@@ -172,4 +185,67 @@ public class VoucherDiscountProvider implements IDiscountProvider {
// 全场免费券码不可与其他优惠叠加 // 全场免费券码不可与其他优惠叠加
return voucherInfo.getDiscountType() != VoucherDiscountType.FREE_ALL; return voucherInfo.getDiscountType() != VoucherDiscountType.FREE_ALL;
} }
/**
* 检查商品配置和打包配置是否允许使用券码
*/
private boolean checkCanUseVoucher(DiscountDetectionContext context) {
if (context.getProducts() == null || context.getProducts().isEmpty()) {
return true; // 如果没有商品信息,默认允许
}
try {
// 检查是否使用了打包购买
BigDecimal bundlePrice = bundleService.getBundlePrice(context.getProducts());
if (bundlePrice != null) {
// 如果使用了打包购买,检查打包配置的券码使用开关
PriceBundleConfig bundleConfig = bundleService.getBundleConfig(context.getProducts());
if (bundleConfig != null) {
boolean canUseVoucher = Boolean.TRUE.equals(bundleConfig.getCanUseVoucher());
log.debug("打包配置券码开关检查: bundleId={}, canUseVoucher={}", bundleConfig.getId(), canUseVoucher);
return canUseVoucher;
}
}
// 检查单个商品的券码使用开关
for (ProductItem product : context.getProducts()) {
String productId = product.getProductId() != null ? product.getProductId() : "default";
try {
PriceProductConfig productConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), productId);
if (productConfig != null) {
if (!Boolean.TRUE.equals(productConfig.getCanUseVoucher())) {
log.debug("商品配置不允许使用券码: productType={}, productId={}",
product.getProductType().getCode(), productId);
return false;
}
}
} catch (Exception e) {
// 如果获取具体商品配置失败,尝试获取default配置
try {
PriceProductConfig defaultConfig = productConfigService.getProductConfig(
product.getProductType().getCode(), "default");
if (defaultConfig != null) {
if (!Boolean.TRUE.equals(defaultConfig.getCanUseVoucher())) {
log.debug("商品默认配置不允许使用券码: productType={}",
product.getProductType().getCode());
return false;
}
}
} catch (Exception ex) {
log.warn("获取商品配置失败,默认允许使用券码: productType={}, productId={}",
product.getProductType().getCode(), productId);
}
}
}
} catch (Exception e) {
log.error("检查券码使用开关时发生异常,默认允许使用", e);
}
return true; // 默认允许使用券码
}
} }