refactor(order): 重构重复购买检查和定价逻辑

- 引入商品类型能力配置,替代硬编码的商品类型判断
- 实现策略模式处理不同商品类型的重复购买检查
- 抽象定价模式,支持固定价格和数量计价等不同方式
- 新增策略工厂自动注册各类检查器实现
- 添加缓存机制提升商品类型配置查询性能
- 解耦订单服务与具体商品类型的紧耦合关系
- 提高代码可维护性和扩展性,便于新增商品类型
This commit is contained in:
2025-11-27 09:34:10 +08:00
parent 1945639f90
commit 3ce3972875
15 changed files with 1062 additions and 139 deletions

View File

@@ -0,0 +1,63 @@
package com.ycwl.basic.product.capability;
/**
* 重复购买检查策略枚举
* 定义不同商品类型的重复购买检查规则
*/
public enum DuplicateCheckStrategy {
/**
* 不检查(如打印类商品)
* 允许重复购买
*/
NO_CHECK("NO_CHECK", "不检查"),
/**
* 按视频ID检查(VLOG_VIDEO)
* 同一个视频不允许重复购买
*/
CHECK_BY_VIDEO_ID("CHECK_BY_VIDEO_ID", "按视频ID检查"),
/**
* 按套餐ID检查(RECORDING_SET, PHOTO_SET)
* 同一个套餐不允许重复购买
*/
CHECK_BY_SET_ID("CHECK_BY_SET_ID", "按套餐ID检查"),
/**
* 自定义策略(通过扩展实现)
* 预留扩展点,用于未来更复杂的检查逻辑
*/
CUSTOM("CUSTOM", "自定义策略");
private final String code;
private final String description;
DuplicateCheckStrategy(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码获取枚举值
*/
public static DuplicateCheckStrategy fromCode(String code) {
if (code == null) {
return null;
}
for (DuplicateCheckStrategy strategy : DuplicateCheckStrategy.values()) {
if (strategy.code.equals(code)) {
return strategy;
}
}
throw new IllegalArgumentException("未知的重复检查策略: " + code);
}
}

View File

@@ -0,0 +1,57 @@
package com.ycwl.basic.product.capability;
/**
* 定价模式枚举
* 定义商品的价格计算方式
*/
public enum PricingMode {
/**
* 固定价格(不受数量影响)
* 适用于:视频类、照片集类商品
*/
FIXED("FIXED", "固定价格"),
/**
* 基于数量(价格 = 单价 × 数量)
* 适用于:打印类商品
*/
QUANTITY_BASED("QUANTITY_BASED", "基于数量"),
/**
* 分层定价(根据数量区间)
* 适用于:支持阶梯定价的商品
*/
TIERED("TIERED", "分层定价");
private final String code;
private final String description;
PricingMode(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码获取枚举值
*/
public static PricingMode fromCode(String code) {
if (code == null) {
return null;
}
for (PricingMode mode : PricingMode.values()) {
if (mode.code.equals(code)) {
return mode;
}
}
throw new IllegalArgumentException("未知的定价模式: " + code);
}
}

View File

@@ -0,0 +1,156 @@
package com.ycwl.basic.product.capability;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 商品类型能力配置实体
* 统一管理商品类型的各项能力和行为特征
*
* 设计目标:
* 1. 消除代码中的硬编码,将商品类型相关的业务规则配置化
* 2. 提供统一的商品类型能力查询接口
* 3. 支持通过配置快速扩展新商品类型
*/
@Data
@TableName(value = "product_type_capability", autoResultMap = true)
public class ProductTypeCapability {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 商品类型代码(唯一)
* 如:VLOG_VIDEO, PHOTO_PRINT 等
*/
private String productType;
/**
* 显示名称
* 用于前端展示和支付页面
*/
private String displayName;
/**
* 商品分类
* 如:VIDEO(视频类), PRINT(打印类), PHOTO_SET(照片集类)
*/
private String category;
// ========== 定价相关 ==========
/**
* 定价模式
* FIXED: 固定价格
* QUANTITY_BASED: 基于数量(价格 = 单价 × 数量)
* TIERED: 分层定价
*/
private String pricingMode;
/**
* 是否支持阶梯定价
*/
private Boolean supportsTierPricing;
// ========== 购买限制 ==========
/**
* 是否允许重复购买
*/
private Boolean allowDuplicatePurchase;
/**
* 重复购买检查策略
* NO_CHECK: 不检查
* CHECK_BY_VIDEO_ID: 按视频ID检查
* CHECK_BY_SET_ID: 按套餐ID检查
* CUSTOM: 自定义策略
*/
private String duplicateCheckStrategy;
// ========== 优惠能力 ==========
/**
* 是否可使用优惠券
*/
private Boolean canUseCoupon;
/**
* 是否可使用券码
*/
private Boolean canUseVoucher;
/**
* 是否可使用一口价优惠
*/
private Boolean canUseOnePrice;
/**
* 是否可参与打包优惠
*/
private Boolean canUseBundle;
// ========== 扩展属性 ==========
/**
* 扩展属性(JSON 格式)
* 用于存储特定商品类型的额外配置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, Object> metadata;
/**
* 是否启用
*/
private Boolean isActive;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 更新时间
*/
private LocalDateTime updatedAt;
// ========== 便捷方法 ==========
/**
* 获取定价模式枚举
*/
public PricingMode getPricingModeEnum() {
return PricingMode.fromCode(this.pricingMode);
}
/**
* 获取重复检查策略枚举
*/
public DuplicateCheckStrategy getDuplicateCheckStrategyEnum() {
return DuplicateCheckStrategy.fromCode(this.duplicateCheckStrategy);
}
/**
* 设置定价模式枚举
*/
public void setPricingModeEnum(PricingMode mode) {
this.pricingMode = mode != null ? mode.getCode() : null;
}
/**
* 设置重复检查策略枚举
*/
public void setDuplicateCheckStrategyEnum(DuplicateCheckStrategy strategy) {
this.duplicateCheckStrategy = strategy != null ? strategy.getCode() : null;
}
}

View File

@@ -0,0 +1,21 @@
package com.ycwl.basic.product.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycwl.basic.product.capability.ProductTypeCapability;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 商品类型能力配置 Mapper
*/
@Mapper
public interface ProductTypeCapabilityMapper extends BaseMapper<ProductTypeCapability> {
/**
* 根据商品类型代码查询能力配置
*
* @param productType 商品类型代码
* @return 能力配置,不存在时返回 null
*/
ProductTypeCapability selectByProductType(@Param("productType") String productType);
}

View File

@@ -0,0 +1,70 @@
package com.ycwl.basic.product.service;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import com.ycwl.basic.product.capability.PricingMode;
import com.ycwl.basic.product.capability.ProductTypeCapability;
/**
* 商品类型能力服务接口
* 提供商品类型能力配置的查询和管理功能
*
* 设计原则:
* 1. 缓存优先:所有查询都支持缓存,提高性能
* 2. 降级兜底:查询失败时返回安全的默认配置
* 3. 接口简洁:提供便捷方法,简化调用方代码
*/
public interface IProductTypeCapabilityService {
/**
* 获取商品类型能力配置(支持缓存)
*
* @param productType 商品类型代码(如 VLOG_VIDEO)
* @return 商品类型能力配置,查询失败时返回默认配置
*/
ProductTypeCapability getCapability(String productType);
/**
* 获取商品显示名称
*
* @param productType 商品类型代码
* @return 显示名称,查询失败时返回"景区商品"
*/
String getDisplayName(String productType);
/**
* 判断是否允许重复购买
*
* @param productType 商品类型代码
* @return true-允许重复购买, false-不允许
*/
boolean allowDuplicatePurchase(String productType);
/**
* 获取定价模式
*
* @param productType 商品类型代码
* @return 定价模式枚举
*/
PricingMode getPricingMode(String productType);
/**
* 获取重复检查策略
*
* @param productType 商品类型代码
* @return 重复检查策略枚举
*/
DuplicateCheckStrategy getDuplicateCheckStrategy(String productType);
/**
* 刷新缓存
* 用于配置更新后手动触发缓存刷新
*/
void refreshCache();
/**
* 刷新指定商品类型的缓存
*
* @param productType 商品类型代码
*/
void refreshCache(String productType);
}

View File

@@ -0,0 +1,162 @@
package com.ycwl.basic.product.service.impl;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import com.ycwl.basic.product.capability.PricingMode;
import com.ycwl.basic.product.capability.ProductTypeCapability;
import com.ycwl.basic.product.mapper.ProductTypeCapabilityMapper;
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* 商品类型能力服务实现
*
* 缓存策略:
* - 使用 Spring Cache 进行缓存
* - 缓存名称:productTypeCapability
* - 缓存key:商品类型代码
* - 缓存失效:手动调用 refreshCache 方法
*/
@Slf4j
@Service
@CacheConfig(cacheNames = "productTypeCapability")
public class ProductTypeCapabilityServiceImpl implements IProductTypeCapabilityService {
@Autowired
private ProductTypeCapabilityMapper mapper;
@Cacheable(key = "#productType")
@Override
public ProductTypeCapability getCapability(String productType) {
if (productType == null || productType.trim().isEmpty()) {
log.warn("商品类型代码为空,返回默认配置");
return getDefaultCapability(null);
}
try {
ProductTypeCapability capability = mapper.selectByProductType(productType);
if (capability == null) {
log.warn("未找到商品类型能力配置: {}, 使用默认配置", productType);
return getDefaultCapability(productType);
}
// 验证配置完整性
if (!isValidCapability(capability)) {
log.warn("商品类型能力配置不完整: {}, 使用默认配置", productType);
return getDefaultCapability(productType);
}
return capability;
} catch (Exception e) {
log.error("查询商品类型能力配置失败: {}, 降级到默认配置", productType, e);
return getDefaultCapability(productType);
}
}
@Override
public String getDisplayName(String productType) {
ProductTypeCapability capability = getCapability(productType);
return capability.getDisplayName();
}
@Override
public boolean allowDuplicatePurchase(String productType) {
ProductTypeCapability capability = getCapability(productType);
return Boolean.TRUE.equals(capability.getAllowDuplicatePurchase());
}
@Override
public PricingMode getPricingMode(String productType) {
ProductTypeCapability capability = getCapability(productType);
return capability.getPricingModeEnum();
}
@Override
public DuplicateCheckStrategy getDuplicateCheckStrategy(String productType) {
ProductTypeCapability capability = getCapability(productType);
return capability.getDuplicateCheckStrategyEnum();
}
@CacheEvict(allEntries = true)
@Override
public void refreshCache() {
log.info("刷新所有商品类型能力缓存");
}
@CacheEvict(key = "#productType")
@Override
public void refreshCache(String productType) {
log.info("刷新商品类型能力缓存: {}", productType);
}
/**
* 获取默认能力配置(兜底方案)
* 设计原则:安全第一,宁可限制也不放开
*
* @param productType 商品类型代码(可为null)
* @return 安全的默认配置
*/
private ProductTypeCapability getDefaultCapability(String productType) {
ProductTypeCapability defaultCap = new ProductTypeCapability();
defaultCap.setProductType(productType);
defaultCap.setDisplayName("景区商品");
defaultCap.setCategory("GENERAL");
// 定价相关:默认固定价格
defaultCap.setPricingMode(PricingMode.FIXED.getCode());
defaultCap.setSupportsTierPricing(false);
// 购买限制:默认不允许重复购买(安全策略)
defaultCap.setAllowDuplicatePurchase(false);
defaultCap.setDuplicateCheckStrategy(DuplicateCheckStrategy.NO_CHECK.getCode());
// 优惠能力:默认全部允许
defaultCap.setCanUseCoupon(true);
defaultCap.setCanUseVoucher(true);
defaultCap.setCanUseOnePrice(true);
defaultCap.setCanUseBundle(true);
defaultCap.setIsActive(true);
return defaultCap;
}
/**
* 验证配置完整性
*/
private boolean isValidCapability(ProductTypeCapability capability) {
if (capability == null) {
return false;
}
// 必填字段检查
if (capability.getProductType() == null || capability.getProductType().trim().isEmpty()) {
return false;
}
if (capability.getDisplayName() == null || capability.getDisplayName().trim().isEmpty()) {
return false;
}
if (capability.getPricingMode() == null) {
return false;
}
// 验证枚举值有效性
try {
PricingMode.fromCode(capability.getPricingMode());
if (capability.getDuplicateCheckStrategy() != null) {
DuplicateCheckStrategy.fromCode(capability.getDuplicateCheckStrategy());
}
} catch (IllegalArgumentException e) {
log.error("商品类型能力配置包含无效的枚举值: {}", capability.getProductType(), e);
return false;
}
return true;
}
}