You've already forked FrameTour-BE
refactor(order): 重构重复购买检查和定价逻辑
- 引入商品类型能力配置,替代硬编码的商品类型判断 - 实现策略模式处理不同商品类型的重复购买检查 - 抽象定价模式,支持固定价格和数量计价等不同方式 - 新增策略工厂自动注册各类检查器实现 - 添加缓存机制提升商品类型配置查询性能 - 解耦订单服务与具体商品类型的紧耦合关系 - 提高代码可维护性和扩展性,便于新增商品类型
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user