You've already forked FrameTour-BE
feat(order): 添加商品重复购买检查功能
- 集成重复购买检查策略工厂和上下文管理 - 实现基于商品类型的重复购买验证机制 - 添加价格计算结果中是否已购买的标识字段 - 扩展商品项目DTO以支持已购买状态标记 - 实现异常捕获方式的购买状态检测逻辑 - 集成Redis缓存提升重复购买检查性能
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
// 转换为标准价格计算请求
|
||||
@@ -140,6 +160,12 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -56,4 +56,12 @@ public class ProductItem {
|
||||
* 商品属性Key列表(服务端计算填充,客户端传入会被忽略)
|
||||
*/
|
||||
private List<String> attributeKeys;
|
||||
|
||||
/**
|
||||
* 是否已购买(服务端填充)
|
||||
* 结合 DuplicateCheckStrategy 判断:
|
||||
* - NO_CHECK: 始终为 false(允许重复购买)
|
||||
* - 其他策略: 基于用户已有资源判断
|
||||
*/
|
||||
private Boolean hasPurchased;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user