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.PaymentParamsRequest;
|
||||||
import com.ycwl.basic.order.dto.PaymentParamsResponse;
|
import com.ycwl.basic.order.dto.PaymentParamsResponse;
|
||||||
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
|
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.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
@@ -57,10 +63,11 @@ public class AppOrderV2Controller {
|
|||||||
private final TemplateRepository templateRepository;
|
private final TemplateRepository templateRepository;
|
||||||
private final VideoRepository videoRepository;
|
private final VideoRepository videoRepository;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
private final IProductTypeCapabilityService productTypeCapabilityService;
|
||||||
|
private final DuplicatePurchaseCheckerFactory duplicatePurchaseCheckerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移动端价格计算
|
* 移动端价格计算
|
||||||
* 包含权限验证:验证人脸所属景区与当前用户匹配
|
|
||||||
* 集成Redis缓存机制,提升查询性能
|
* 集成Redis缓存机制,提升查询性能
|
||||||
*/
|
*/
|
||||||
@PostMapping("/calculate")
|
@PostMapping("/calculate")
|
||||||
@@ -102,6 +109,12 @@ public class AppOrderV2Controller {
|
|||||||
Long scenicId = face.getScenicId();
|
Long scenicId = face.getScenicId();
|
||||||
|
|
||||||
request.getProducts().forEach(product -> {
|
request.getProducts().forEach(product -> {
|
||||||
|
// 获取商品的重复检查策略
|
||||||
|
DuplicateCheckStrategy strategy = productTypeCapabilityService
|
||||||
|
.getDuplicateCheckStrategy(product.getProductType().name());
|
||||||
|
|
||||||
|
boolean hasPurchasedFlag;
|
||||||
|
|
||||||
switch (product.getProductType()) {
|
switch (product.getProductType()) {
|
||||||
case VLOG_VIDEO:
|
case VLOG_VIDEO:
|
||||||
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
|
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
|
||||||
@@ -132,6 +145,13 @@ public class AppOrderV2Controller {
|
|||||||
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
||||||
break;
|
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);
|
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
|
// 将计算结果缓存到Redis
|
||||||
String cacheKey = priceCacheService.cachePriceResult(currentUserId, scenicId, request.getProducts(), result);
|
String cacheKey = priceCacheService.cachePriceResult(currentUserId, scenicId, request.getProducts(), result);
|
||||||
|
|
||||||
@@ -355,4 +381,55 @@ public class AppOrderV2Controller {
|
|||||||
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
|
public ApiResponse<Boolean> getDownloadableOrder(@PathVariable("orderId") Long orderId) {
|
||||||
return ApiResponse.success(!redisTemplate.hasKey("order_content_not_downloadable_" + 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;
|
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列表(服务端计算填充,客户端传入会被忽略)
|
* 商品属性Key列表(服务端计算填充,客户端传入会被忽略)
|
||||||
*/
|
*/
|
||||||
private List<String> attributeKeys;
|
private List<String> attributeKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已购买(服务端填充)
|
||||||
|
* 结合 DuplicateCheckStrategy 判断:
|
||||||
|
* - NO_CHECK: 始终为 false(允许重复购买)
|
||||||
|
* - 其他策略: 基于用户已有资源判断
|
||||||
|
*/
|
||||||
|
private Boolean hasPurchased;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user