feat(order): 添加商品重复购买检查功能

- 集成重复购买检查策略工厂和上下文管理
- 实现基于商品类型的重复购买验证机制
- 添加价格计算结果中是否已购买的标识字段
- 扩展商品项目DTO以支持已购买状态标记
- 实现异常捕获方式的购买状态检测逻辑
- 集成Redis缓存提升重复购买检查性能
This commit is contained in:
2026-01-26 11:06:45 +08:00
parent 85d0fc0996
commit e87e38be03
3 changed files with 98 additions and 2 deletions

View File

@@ -27,6 +27,12 @@ import com.ycwl.basic.order.dto.OrderV2PageRequest;
import com.ycwl.basic.order.dto.PaymentParamsRequest;
import com.ycwl.basic.order.dto.PaymentParamsResponse;
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.order.factory.DuplicatePurchaseCheckerFactory;
import com.ycwl.basic.order.strategy.DuplicateCheckContext;
import com.ycwl.basic.order.strategy.IDuplicatePurchaseChecker;
import com.ycwl.basic.product.capability.DuplicateCheckStrategy;
import com.ycwl.basic.product.service.IProductTypeCapabilityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
@@ -57,10 +63,11 @@ public class AppOrderV2Controller {
private final TemplateRepository templateRepository;
private final VideoRepository videoRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final IProductTypeCapabilityService productTypeCapabilityService;
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
/**
* 移动端价格计算
* 包含权限验证:验证人脸所属景区与当前用户匹配
* 集成Redis缓存机制,提升查询性能
*/
@PostMapping("/calculate")
@@ -102,6 +109,12 @@ public class AppOrderV2Controller {
Long scenicId = face.getScenicId();
request.getProducts().forEach(product -> {
// 获取商品的重复检查策略
DuplicateCheckStrategy strategy = productTypeCapabilityService
.getDuplicateCheckStrategy(product.getProductType().name());
boolean hasPurchasedFlag;
switch (product.getProductType()) {
case VLOG_VIDEO:
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
@@ -132,6 +145,13 @@ public class AppOrderV2Controller {
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break;
}
// 使用 DuplicatePurchaseChecker 检查是否已购买
hasPurchasedFlag = checkIfPurchased(strategy, currentUserId, String.valueOf(scenicId),
product.getProductType().name(), product.getProductId(), face.getId());
// 设置是否已购买标识
product.setHasPurchased(hasPurchasedFlag);
});
// 转换为标准价格计算请求
@@ -139,7 +159,13 @@ public class AppOrderV2Controller {
// 执行价格计算
PriceCalculationResult result = priceCalculationService.calculatePrice(standardRequest);
// 设置是否已购买标识(基于请求中的商品 hasPurchased 判断)
// 只要有一个商品 hasPurchased = true,则整体 isPurchased = true
boolean isPurchased = request.getProducts().stream()
.anyMatch(product -> Boolean.TRUE.equals(product.getHasPurchased()));
result.setIsPurchased(isPurchased);
// 将计算结果缓存到Redis
String cacheKey = priceCacheService.cachePriceResult(currentUserId, scenicId, request.getProducts(), result);
@@ -355,4 +381,55 @@ public class AppOrderV2Controller {
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
return ApiResponse.success(!redisTemplate.hasKey("order_content_not_downloadable_" + orderId));
}
/**
* 检查商品是否已购买
* 使用 DuplicatePurchaseChecker 通过异常捕获判断
*
* @param strategy 重复检查策略
* @param userId 用户ID
* @param scenicId 景区ID
* @param productType 商品类型
* @param productId 商品ID
* @param faceId 人脸ID
* @return true-已购买, false-未购买
*/
private boolean checkIfPurchased(DuplicateCheckStrategy strategy, Long userId, String scenicId,
String productType, String productId, Long faceId) {
// NO_CHECK 策略表示允许重复购买,直接返回 false
if (strategy == DuplicateCheckStrategy.NO_CHECK) {
return false;
}
try {
// 获取对应的检查器
IDuplicatePurchaseChecker checker = duplicatePurchaseCheckerFactory.getChecker(strategy);
// 构建检查上下文
DuplicateCheckContext context = new DuplicateCheckContext();
context.setUserId(String.valueOf(userId));
context.setScenicId(scenicId);
context.setProductType(productType);
context.setProductId(productId);
context.addParam("faceId", faceId);
// 执行检查,如果抛出异常则表示已购买
checker.check(context);
// 没有抛出异常,表示未购买
return false;
} catch (DuplicatePurchaseException e) {
// 捕获到重复购买异常,表示已购买
log.debug("检测到已购买: userId={}, scenicId={}, productType={}, productId={}",
userId, scenicId, productType, productId);
return true;
} catch (Exception e) {
// 其他异常,记录日志并返回 false(保守处理)
log.warn("检查是否已购买时发生异常: userId={}, scenicId={}, productType={}, productId={}, error={}",
userId, scenicId, productType, productId, e.getMessage(), e);
return false;
}
}
}

View File

@@ -55,4 +55,15 @@ public class PriceCalculationResult {
* 商品明细列表
*/
private List<ProductItem> productDetails;
/**
* 是否已购买标识(结合商品重复购买策略判断)
* true: 至少有一个不允许重复购买的商品(DuplicateCheckStrategy != NO_CHECK)的 quantity > 0
* false: 所有不允许重复购买的商品的 quantity 都为 0 或 null
*
* 说明:
* - 对于允许重复购买的商品(如打印类,策略为 NO_CHECK),即使 quantity > 0 也不影响此标识
* - 对于需要检查的商品(UNIQUE_RESOURCE、PARENT_RESOURCE),quantity > 0 表示已购买
*/
private Boolean isPurchased;
}

View File

@@ -56,4 +56,12 @@ public class ProductItem {
* 商品属性Key列表(服务端计算填充,客户端传入会被忽略)
*/
private List<String> attributeKeys;
/**
* 是否已购买(服务端填充)
* 结合 DuplicateCheckStrategy 判断:
* - NO_CHECK: 始终为 false(允许重复购买)
* - 其他策略: 基于用户已有资源判断
*/
private Boolean hasPurchased;
}