refactor(order): 重构重复购买检查策略

- 移除SetIdDuplicateChecker和VideoIdDuplicateChecker两个具体策略类
- 更新DuplicateCheckStrategy枚举,将CHECK_BY_SET_ID和CHECK_BY_VIDEO_ID
  替换为更通用的UNIQUE_RESOURCE和PARENT_RESOURCE策略
- 修改ProductTypeCapabilityManagementServiceImpl中的策略分配逻辑
- UNIQUE_RESOURCE适用于照片、视频等独立资源的重复购买检查
- PARENT_RESOURCE适用于套餐类商品的重复购买检查
- 打印类商品现在正确设置为允许重复购买且不检查
- 其他类别商品默认设置为不检查重复购买
This commit is contained in:
2025-11-28 00:54:54 +08:00
parent 4244b42d4b
commit e292a0798d
5 changed files with 76 additions and 43 deletions

View File

@@ -10,6 +10,7 @@ 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.pricing.enums.ProductType;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -18,17 +19,19 @@ import org.springframework.stereotype.Component;
import java.util.List;
/**
* 按套餐ID检查重复购买策略
* 适用于RECORDING_SET录像集PHOTO_SET照相集商品类型
* 父资源/套餐重复购买检查策略
* 适用于套餐类商品RECORDING_SET录像集PHOTO_SET照片集等
*
* 检查逻辑
* 1. 查找用户在该景区已支付的有效订单
* 2. 检查订单明细中是否包含相同类型的套餐
* 2. 检查订单明细中是否包含相同product_type的商品不关心具体productId
* 3. 如果存在抛出重复购买异常
*
* SQL查询: WHERE order_id = ? AND product_type = ?
*/
@Slf4j
@Component
public class SetIdDuplicateChecker implements IDuplicatePurchaseChecker {
public class ParentResourceDuplicateChecker implements IDuplicatePurchaseChecker {
@Autowired
private OrderV2Mapper orderV2Mapper;
@@ -38,19 +41,19 @@ public class SetIdDuplicateChecker implements IDuplicatePurchaseChecker {
@Override
public DuplicateCheckStrategy getStrategyType() {
return DuplicateCheckStrategy.CHECK_BY_SET_ID;
return DuplicateCheckStrategy.PARENT_RESOURCE;
}
@Override
public void check(DuplicateCheckContext context) throws DuplicatePurchaseException {
String userId = context.getUserId();
String scenicId = context.getScenicId();
String productType = context.getProductType();
String productType = context.getProductType(); // "RECORDING_SET""PHOTO_SET"
// 获取人脸ID从扩展参数中
Long faceId = context.getParam("faceId");
log.debug("执行套餐ID重复购买检查: userId={}, faceId={}, scenicId={}, productType={}",
log.debug("执行父资源重复购买检查: userId={}, faceId={}, scenicId={}, productType={}",
userId, faceId, scenicId, productType);
// 构建查询条件查找已支付的有效订单
@@ -72,22 +75,20 @@ public class SetIdDuplicateChecker implements IDuplicatePurchaseChecker {
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该类型的套餐
// 检查订单明细中是否包含该类型的商品仅按productType匹配
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", productType);
.eq("product_type", productType); // 仅按productType匹配
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}",
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);
ProductType productTypeEnum = ProductType.fromCode(productType);
throw new DuplicatePurchaseException(
"您已购买过此类型的套餐",
String.format("您已购买过%s", productTypeEnum.getDescription()),
order.getId(),
order.getOrderNo(),
productTypeEnum
@@ -95,7 +96,7 @@ public class SetIdDuplicateChecker implements IDuplicatePurchaseChecker {
}
}
log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}",
log.debug("父资源重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}",
userId, faceId, scenicId, productType);
}
}

View File

@@ -10,6 +10,7 @@ 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.pricing.enums.ProductType;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -18,17 +19,19 @@ import org.springframework.stereotype.Component;
import java.util.List;
/**
* 按视频ID检查重复购买策略
* 适用于VLOG_VIDEO 商品类型
* 唯一资源重复购买检查策略
* 适用于独立资源类商品PHOTO照片VLOG_VIDEO视频VIDEO视频片段等
*
* 检查逻辑
* 1. 查找用户在该景区已支付的有效订单
* 2. 检查订单明细中是否包含相同的视频ID
* 2. 检查订单明细中是否包含相同product_type和product_id的商品
* 3. 如果存在抛出重复购买异常
*
* SQL查询: WHERE order_id = ? AND product_type = ? AND product_id = ?
*/
@Slf4j
@Component
public class VideoIdDuplicateChecker implements IDuplicatePurchaseChecker {
public class UniqueResourceDuplicateChecker implements IDuplicatePurchaseChecker {
@Autowired
private OrderV2Mapper orderV2Mapper;
@@ -38,20 +41,21 @@ public class VideoIdDuplicateChecker implements IDuplicatePurchaseChecker {
@Override
public DuplicateCheckStrategy getStrategyType() {
return DuplicateCheckStrategy.CHECK_BY_VIDEO_ID;
return DuplicateCheckStrategy.UNIQUE_RESOURCE;
}
@Override
public void check(DuplicateCheckContext context) throws DuplicatePurchaseException {
String userId = context.getUserId();
String scenicId = context.getScenicId();
String productId = context.getProductId();
String productType = context.getProductType();
String productId = context.getProductId(); // 唯一资源ID
// 获取人脸ID从扩展参数中
Long faceId = context.getParam("faceId");
log.debug("执行视频ID重复购买检查: userId={}, faceId={}, scenicId={}, videoId={}",
userId, faceId, scenicId, productId);
log.debug("执行唯一资源重复购买检查: userId={}, faceId={}, scenicId={}, productType={}, productId={}",
userId, faceId, scenicId, productType, productId);
// 构建查询条件查找已支付的有效订单
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
@@ -72,29 +76,30 @@ public class VideoIdDuplicateChecker implements IDuplicatePurchaseChecker {
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该视频
// 检查订单明细中是否包含该资源按product_type和product_id双重匹配
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", "VLOG_VIDEO")
.eq("product_id", productId);
.eq("product_type", productType)
.eq("product_id", productId); // 按productId精确匹配
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}",
userId, faceId, scenicId, productId, order.getId());
log.warn("检测到重复购买唯一资源: userId={}, faceId={}, scenicId={}, productType={}, productId={}, existingOrderId={}",
userId, faceId, scenicId, productType, productId, order.getId());
ProductType productTypeEnum = ProductType.fromCode(productType);
// 为了兼容旧代码需要转换为 ProductType 枚举
throw new DuplicatePurchaseException(
"您已购买过此视频",
String.format("您已购买过此%s", productTypeEnum.getDescription()),
order.getId(),
order.getOrderNo(),
com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO,
productTypeEnum,
productId
);
}
}
log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}",
userId, faceId, scenicId, productId);
log.debug("唯一资源重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}, productId={}",
userId, faceId, scenicId, productType, productId);
}
}

View File

@@ -13,16 +13,27 @@ public enum DuplicateCheckStrategy {
NO_CHECK("NO_CHECK", "不检查"),
/**
* 按视频ID检查(VLOG_VIDEO)
* 同一个视频不允许重复购买
* 检查唯一资源
* 检查用户是否已购买过相同的独立资源
* 查询逻辑:WHERE product_type = ? AND product_id = ?
*
* 适用商品:
* - VLOG_VIDEO:检查是否购买过同一个视频
* - PHOTO:检查是否购买过同一张照片
* - 未来的VIDEO片段:检查是否购买过同一段视频
*/
CHECK_BY_VIDEO_ID("CHECK_BY_VIDEO_ID", "按视频ID检查"),
UNIQUE_RESOURCE("UNIQUE_RESOURCE", "唯一资源检查"),
/**
* 按套餐ID检查(RECORDING_SET, PHOTO_SET)
* 同一个套餐不允许重复购买
* 检查父资源/套餐
* 检查用户是否已购买过该类型的任何商品
* 查询逻辑:WHERE product_type = ?
*
* 适用商品:
* - RECORDING_SET:购买过任意录像集后不能再购买
* - PHOTO_SET:购买过任意照片集后不能再购买
*/
CHECK_BY_SET_ID("CHECK_BY_SET_ID", "按套餐ID检查"),
PARENT_RESOURCE("PARENT_RESOURCE", "父资源检查"),
/**
* 自定义策略(通过扩展实现)

View File

@@ -229,17 +229,33 @@ public class ProductTypeCapabilityManagementServiceImpl implements IProductTypeC
capability.setDisplayName(productType.getDescription());
capability.setCategory(productType.getCategoryCode());
// 根据分类设置默认的定价模式
// 根据分类设置默认的定价模式和去重策略
if (ProductCategory.PRINT == productType.getCategory()) {
// 打印类:基于数量定价,允许重复购买
capability.setPricingMode(PricingMode.QUANTITY_BASED.getCode());
capability.setSupportsTierPricing(true);
capability.setAllowDuplicatePurchase(true);
capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.NO_CHECK.getCode());
} else {
} else if (ProductCategory.PHOTO == productType.getCategory()
|| ProductCategory.VIDEO == productType.getCategory()
|| ProductCategory.VLOG == productType.getCategory()) {
// 照片类、视频类、Vlog类:固定价格,检查唯一资源
capability.setPricingMode(PricingMode.FIXED.getCode());
capability.setSupportsTierPricing(false);
capability.setAllowDuplicatePurchase(false);
capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.CHECK_BY_SET_ID.getCode());
// 判断是套餐类还是独立资源类
if (productType.getCode().endsWith("_SET")) {
capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.PARENT_RESOURCE.getCode());
} else {
capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.UNIQUE_RESOURCE.getCode());
}
} else {
// 其他类:默认不检查
capability.setPricingMode(PricingMode.FIXED.getCode());
capability.setSupportsTierPricing(false);
capability.setAllowDuplicatePurchase(true);
capability.setDuplicateCheckStrategy(DuplicateCheckStrategy.NO_CHECK.getCode());
}
// 优惠能力默认全部开启

View File

@@ -33,7 +33,7 @@ public class ProductTypeCapabilityServiceTest {
assertEquals("VIDEO", capability.getCategory());
assertEquals(PricingMode.FIXED, capability.getPricingModeEnum());
assertEquals(false, capability.getAllowDuplicatePurchase());
assertEquals(DuplicateCheckStrategy.CHECK_BY_VIDEO_ID, capability.getDuplicateCheckStrategyEnum());
assertEquals(DuplicateCheckStrategy.UNIQUE_RESOURCE, capability.getDuplicateCheckStrategyEnum());
}
@Test