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,68 @@
package com.ycwl.basic.order.factory;
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 重复购买检查策略工厂
*
* 设计原则:
* 1. 自动注册:Spring 自动注入所有策略实现并注册
* 2. 类型安全:根据枚举类型查找策略
* 3. 失败快速:找不到策略时抛出异常
*/
@Slf4j
@Service
public class DuplicatePurchaseCheckerFactory {
private final Map<DuplicateCheckStrategy, IDuplicatePurchaseChecker> checkerMap = new HashMap<>();
/**
* 构造函数:自动注册所有策略实现
*/
@Autowired
public DuplicatePurchaseCheckerFactory(List<IDuplicatePurchaseChecker> checkers) {
for (IDuplicatePurchaseChecker checker : checkers) {
DuplicateCheckStrategy strategy = checker.getStrategyType();
checkerMap.put(strategy, checker);
log.info("注册重复购买检查策略: {} -> {}", strategy, checker.getClass().getSimpleName());
}
log.info("重复购买检查策略工厂初始化完成,共注册 {} 个策略", checkerMap.size());
}
/**
* 根据策略类型获取检查器
*
* @param strategy 策略类型
* @return 对应的检查器实现
* @throws IllegalArgumentException 如果找不到对应的策略实现
*/
public IDuplicatePurchaseChecker getChecker(DuplicateCheckStrategy strategy) {
IDuplicatePurchaseChecker checker = checkerMap.get(strategy);
if (checker == null) {
throw new IllegalArgumentException("未找到重复购买检查策略: " + strategy);
}
return checker;
}
/**
* 检查是否支持指定策略
*/
public boolean supportsStrategy(DuplicateCheckStrategy strategy) {
return checkerMap.containsKey(strategy);
}
/**
* 获取所有已注册的策略类型
*/
public Map<DuplicateCheckStrategy, IDuplicatePurchaseChecker> getAllCheckers() {
return new HashMap<>(checkerMap);
}
}

View File

@@ -21,6 +21,13 @@ import com.ycwl.basic.order.mapper.OrderV2Mapper;
import com.ycwl.basic.order.mapper.OrderRefundMapper;
import com.ycwl.basic.order.service.IOrderService;
import com.ycwl.basic.order.event.*;
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
import com.ycwl.basic.order.factory.DuplicatePurchaseCheckerFactory;
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.service.IProductTypeCapabilityService;
import com.ycwl.basic.pricing.dto.DiscountDetail;
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
@@ -69,6 +76,8 @@ public class OrderServiceImpl implements IOrderService {
private final ICouponService couponService;
private final IVoucherService voucherService;
private final IProductConfigService productConfigService;
private final IProductTypeCapabilityService productTypeCapabilityService;
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -767,16 +776,10 @@ public class OrderServiceImpl implements IOrderService {
/**
* 获取商品类型中文名称
* 重构: 从配置驱动替代硬编码
*/
private String getProductTypeName(String productType) {
return switch (productType) {
case "VLOG_VIDEO" -> "Vlog视频";
case "RECORDING_SET" -> "录像集";
case "PHOTO_SET" -> "照相集";
case "PHOTO_PRINT" -> "照片打印";
case "MACHINE_PRINT" -> "一体机打印";
default -> "景区商品";
};
return productTypeCapabilityService.getDisplayName(productType);
}
/**
@@ -850,14 +853,17 @@ public class OrderServiceImpl implements IOrderService {
: getProductTypeName(product.getProductType().name());
// 3. 处理按数量计价的商品类型
if (product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.PHOTO_PRINT ||
product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.MACHINE_PRINT) {
if (product.getQuantity() != null && product.getQuantity() > 0) {
unitPrice = unitPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
if (originalPrice != null) {
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
}
// 重构: 使用商品类型能力配置替代硬编码判断
ProductTypeCapability capability = productTypeCapabilityService.getCapability(productTypeCode);
if (capability.getPricingModeEnum() == PricingMode.QUANTITY_BASED) {
Integer quantity = product.getQuantity() != null && product.getQuantity() > 0
? product.getQuantity() : 1;
unitPrice = unitPrice.multiply(BigDecimal.valueOf(quantity));
if (originalPrice != null) {
originalPrice = originalPrice.multiply(BigDecimal.valueOf(quantity));
}
log.debug("按数量计价: productType={}, quantity={}, unitPrice={}",
productTypeCode, quantity, unitPrice);
}
}
@@ -894,7 +900,8 @@ public class OrderServiceImpl implements IOrderService {
/**
* 检查重复购买
* 防止用户重复购买相同内容
*
* 重构: 使用策略模式替代硬编码的 switch-case
*
* @param userId 用户ID
* @param faceId 人脸ID
* @param scenicId 景区ID
@@ -903,117 +910,44 @@ public class OrderServiceImpl implements IOrderService {
*/
private void checkDuplicatePurchase(Long userId, Long faceId, Long scenicId, List<ProductItem> products) {
for (ProductItem product : products) {
switch (product.getProductType()) {
case VLOG_VIDEO:
checkVideoAlreadyPurchased(userId, faceId, scenicId, product.getProductId());
break;
case RECORDING_SET:
case PHOTO_SET:
checkSetAlreadyPurchased(userId, faceId, scenicId, product.getProductType());
break;
case PHOTO_PRINT:
case PHOTO_PRINT_MU:
case PHOTO_PRINT_FX:
case MACHINE_PRINT:
// 打印类商品允许重复购买,跳过检查
log.debug("跳过打印类商品重复购买检查: productType={}, productId={}",
product.getProductType(), product.getProductId());
break;
default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break;
String productType = product.getProductType().getCode();
// 获取商品类型能力配置
ProductTypeCapability capability = productTypeCapabilityService.getCapability(productType);
// 如果允许重复购买,直接跳过
if (Boolean.TRUE.equals(capability.getAllowDuplicatePurchase())) {
log.debug("商品类型允许重复购买,跳过检查: productType={}, productId={}",
productType, product.getProductId());
continue;
}
// 获取检查策略并执行
DuplicateCheckStrategy strategy = capability.getDuplicateCheckStrategyEnum();
if (strategy != null && strategy != DuplicateCheckStrategy.NO_CHECK) {
try {
IDuplicatePurchaseChecker checker = duplicatePurchaseCheckerFactory.getChecker(strategy);
// 构建检查上下文
DuplicateCheckContext context = new DuplicateCheckContext();
context.setUserId(String.valueOf(userId));
context.setScenicId(String.valueOf(scenicId));
context.setProductType(productType);
context.setProductId(product.getProductId());
context.setProducts(products);
context.addParam("faceId", faceId);
// 执行检查
checker.check(context);
} catch (DuplicatePurchaseException e) {
// 重新抛出重复购买异常
throw e;
} catch (Exception e) {
log.error("重复购买检查失败,策略: {}, productType: {}", strategy, productType, e);
// 检查失败时为了安全起见,默认拒绝
throw new BaseException("重复购买检查失败,请稍后重试");
}
}
}
}
/**
* 检查视频是否已经购买
*
* @param userId 用户ID
* @param faceId 人脸ID
* @param scenicId 景区ID
* @param videoId 视频ID
* @throws DuplicatePurchaseException 如果已购买
*/
private void checkVideoAlreadyPurchased(Long userId, Long faceId, Long scenicId, String videoId) {
// 构建查询条件:查找已支付的有效订单中包含该视频的订单
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
orderQuery.eq("member_id", userId)
.eq("face_id", faceId)
.eq("scenic_id", scenicId)
.eq("payment_status", PaymentStatus.PAID.getCode())
.in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode())
.eq("deleted", 0);
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该视频
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO.name())
.eq("product_id", videoId);
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}",
userId, faceId, scenicId, videoId, order.getId());
throw new DuplicatePurchaseException(
"您已购买过此视频",
order.getId(),
order.getOrderNo(),
com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO,
videoId
);
}
}
log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}",
userId, faceId, scenicId, videoId);
}
/**
* 检查套餐(录像集/照相集)是否已经购买
*
* @param userId 用户ID
* @param faceId 人脸ID
* @param scenicId 景区ID
* @param productType 商品类型
* @throws DuplicatePurchaseException 如果已购买
*/
private void checkSetAlreadyPurchased(Long userId, Long faceId, Long scenicId,
com.ycwl.basic.pricing.enums.ProductType productType) {
// 构建查询条件:查找已支付的有效订单中包含该类型套餐的订单
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
orderQuery.eq("member_id", userId)
.eq("face_id", faceId)
.eq("scenic_id", scenicId)
.eq("payment_status", PaymentStatus.PAID.getCode())
.in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode())
.eq("deleted", 0);
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该类型的套餐
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", productType.name());
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}",
userId, faceId, scenicId, productType, order.getId());
throw new DuplicatePurchaseException(
"您已购买过此类型的套餐",
order.getId(),
order.getOrderNo(),
productType
);
}
}
log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}",
userId, faceId, scenicId, productType);
}
}

View File

@@ -0,0 +1,65 @@
package com.ycwl.basic.order.strategy;
import com.ycwl.basic.pricing.dto.ProductItem;
import lombok.Data;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 重复购买检查上下文
* 封装检查所需的所有参数
*/
@Data
public class DuplicateCheckContext {
/**
* 用户ID
*/
private String userId;
/**
* 景区ID
*/
private String scenicId;
/**
* 商品类型代码
*/
private String productType;
/**
* 商品ID(视频ID、套餐ID等)
*/
private String productId;
/**
* 当前购物车中的所有商品
*/
private List<ProductItem> products;
/**
* 额外参数(扩展用)
*/
private Map<String, Object> additionalParams;
public DuplicateCheckContext() {
this.additionalParams = new HashMap<>();
}
/**
* 添加额外参数
*/
public void addParam(String key, Object value) {
this.additionalParams.put(key, value);
}
/**
* 获取额外参数
*/
@SuppressWarnings("unchecked")
public <T> T getParam(String key) {
return (T) this.additionalParams.get(key);
}
}

View File

@@ -0,0 +1,43 @@
package com.ycwl.basic.order.strategy;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
/**
* 重复购买检查策略接口
*
* 设计原则:
* 1. 单一职责: 每个策略只负责一种检查逻辑
* 2. 开放扩展: 通过实现接口添加新策略
* 3. 自动注册: Spring 自动扫描并注册所有实现类
*/
public interface IDuplicatePurchaseChecker {
/**
* 获取策略类型
* 用于策略工厂的注册和查找
*
* @return 对应的 DuplicateCheckStrategy 枚举值
*/
DuplicateCheckStrategy getStrategyType();
/**
* 执行重复购买检查
*
* @param context 检查上下文,包含用户ID、商品信息等
* @throws DuplicatePurchaseException 如果检测到重复购买
*/
void check(DuplicateCheckContext context) throws DuplicatePurchaseException;
/**
* 是否支持该商品类型
* 默认实现:所有策略都支持所有商品类型
* 可在具体实现中覆盖以限制适用范围
*
* @param productType 商品类型代码
* @return true-支持, false-不支持
*/
default boolean supports(String productType) {
return true;
}
}

View File

@@ -0,0 +1,31 @@
package com.ycwl.basic.order.strategy.impl;
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 不检查重复购买策略
* 适用于:打印类商品等允许重复购买的商品类型
*
* 检查逻辑:
* 不执行任何检查,直接通过
*/
@Slf4j
@Component
public class NoCheckDuplicateChecker implements IDuplicatePurchaseChecker {
@Override
public DuplicateCheckStrategy getStrategyType() {
return DuplicateCheckStrategy.NO_CHECK;
}
@Override
public void check(DuplicateCheckContext context) {
// 不检查,直接通过
log.debug("跳过重复购买检查: productType={}, productId={}",
context.getProductType(), context.getProductId());
}
}

View File

@@ -0,0 +1,101 @@
package com.ycwl.basic.order.strategy.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ycwl.basic.order.entity.OrderItemV2;
import com.ycwl.basic.order.entity.OrderV2;
import com.ycwl.basic.order.enums.OrderStatus;
import com.ycwl.basic.order.enums.PaymentStatus;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.order.mapper.OrderItemMapper;
import com.ycwl.basic.order.mapper.OrderV2Mapper;
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 按套餐ID检查重复购买策略
* 适用于:RECORDING_SET(录像集)、PHOTO_SET(照相集)商品类型
*
* 检查逻辑:
* 1. 查找用户在该景区已支付的有效订单
* 2. 检查订单明细中是否包含相同类型的套餐
* 3. 如果存在,抛出重复购买异常
*/
@Slf4j
@Component
public class SetIdDuplicateChecker implements IDuplicatePurchaseChecker {
@Autowired
private OrderV2Mapper orderV2Mapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Override
public DuplicateCheckStrategy getStrategyType() {
return DuplicateCheckStrategy.CHECK_BY_SET_ID;
}
@Override
public void check(DuplicateCheckContext context) throws DuplicatePurchaseException {
String userId = context.getUserId();
String scenicId = context.getScenicId();
String productType = context.getProductType();
// 获取人脸ID(从扩展参数中)
Long faceId = context.getParam("faceId");
log.debug("执行套餐ID重复购买检查: userId={}, faceId={}, scenicId={}, productType={}",
userId, faceId, scenicId, productType);
// 构建查询条件:查找已支付的有效订单
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
orderQuery.eq("member_id", userId)
.eq("scenic_id", scenicId)
.eq("payment_status", PaymentStatus.PAID.getCode())
.in("order_status",
OrderStatus.PAID.getCode(),
OrderStatus.PROCESSING.getCode(),
OrderStatus.COMPLETED.getCode())
.eq("deleted", 0);
// 如果提供了人脸ID,也作为查询条件
if (faceId != null) {
orderQuery.eq("face_id", faceId);
}
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该类型的套餐
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", productType);
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}",
userId, faceId, scenicId, productType, order.getId());
// 为了兼容旧代码,需要转换为 ProductType 枚举
com.ycwl.basic.pricing.enums.ProductType productTypeEnum =
com.ycwl.basic.pricing.enums.ProductType.fromCode(productType);
throw new DuplicatePurchaseException(
"您已购买过此类型的套餐",
order.getId(),
order.getOrderNo(),
productTypeEnum
);
}
}
log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}",
userId, faceId, scenicId, productType);
}
}

View File

@@ -0,0 +1,100 @@
package com.ycwl.basic.order.strategy.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ycwl.basic.order.entity.OrderItemV2;
import com.ycwl.basic.order.entity.OrderV2;
import com.ycwl.basic.order.enums.OrderStatus;
import com.ycwl.basic.order.enums.PaymentStatus;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.order.mapper.OrderItemMapper;
import com.ycwl.basic.order.mapper.OrderV2Mapper;
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 按视频ID检查重复购买策略
* 适用于:VLOG_VIDEO 商品类型
*
* 检查逻辑:
* 1. 查找用户在该景区已支付的有效订单
* 2. 检查订单明细中是否包含相同的视频ID
* 3. 如果存在,抛出重复购买异常
*/
@Slf4j
@Component
public class VideoIdDuplicateChecker implements IDuplicatePurchaseChecker {
@Autowired
private OrderV2Mapper orderV2Mapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Override
public DuplicateCheckStrategy getStrategyType() {
return DuplicateCheckStrategy.CHECK_BY_VIDEO_ID;
}
@Override
public void check(DuplicateCheckContext context) throws DuplicatePurchaseException {
String userId = context.getUserId();
String scenicId = context.getScenicId();
String productId = context.getProductId();
// 获取人脸ID(从扩展参数中)
Long faceId = context.getParam("faceId");
log.debug("执行视频ID重复购买检查: userId={}, faceId={}, scenicId={}, videoId={}",
userId, faceId, scenicId, productId);
// 构建查询条件:查找已支付的有效订单
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
orderQuery.eq("member_id", userId)
.eq("scenic_id", scenicId)
.eq("payment_status", PaymentStatus.PAID.getCode())
.in("order_status",
OrderStatus.PAID.getCode(),
OrderStatus.PROCESSING.getCode(),
OrderStatus.COMPLETED.getCode())
.eq("deleted", 0);
// 如果提供了人脸ID,也作为查询条件
if (faceId != null) {
orderQuery.eq("face_id", faceId);
}
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该视频
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", "VLOG_VIDEO")
.eq("product_id", productId);
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}",
userId, faceId, scenicId, productId, order.getId());
// 为了兼容旧代码,需要转换为 ProductType 枚举
throw new DuplicatePurchaseException(
"您已购买过此视频",
order.getId(),
order.getOrderNo(),
com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO,
productId
);
}
}
log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}",
userId, faceId, scenicId, productId);
}
}