diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java b/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java index cdb60cff..8e3372db 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java @@ -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 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 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 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; + } + } } diff --git a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java index c23e8bbe..4af39f32 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/PriceCalculationResult.java @@ -55,4 +55,15 @@ public class PriceCalculationResult { * 商品明细列表 */ private List productDetails; + + /** + * 是否已购买标识(结合商品重复购买策略判断) + * true: 至少有一个不允许重复购买的商品(DuplicateCheckStrategy != NO_CHECK)的 quantity > 0 + * false: 所有不允许重复购买的商品的 quantity 都为 0 或 null + * + * 说明: + * - 对于允许重复购买的商品(如打印类,策略为 NO_CHECK),即使 quantity > 0 也不影响此标识 + * - 对于需要检查的商品(UNIQUE_RESOURCE、PARENT_RESOURCE),quantity > 0 表示已购买 + */ + private Boolean isPurchased; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java index 533294df..44ce94a4 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/ProductItem.java @@ -56,4 +56,12 @@ public class ProductItem { * 商品属性Key列表(服务端计算填充,客户端传入会被忽略) */ private List attributeKeys; + + /** + * 是否已购买(服务端填充) + * 结合 DuplicateCheckStrategy 判断: + * - NO_CHECK: 始终为 false(允许重复购买) + * - 其他策略: 基于用户已有资源判断 + */ + private Boolean hasPurchased; }