You've already forked FrameTour-BE
- 集成重复购买检查策略工厂和上下文管理 - 实现基于商品类型的重复购买验证机制 - 添加价格计算结果中是否已购买的标识字段 - 扩展商品项目DTO以支持已购买状态标记 - 实现异常捕获方式的购买状态检测逻辑 - 集成Redis缓存提升重复购买检查性能
436 lines
19 KiB
Java
436 lines
19 KiB
Java
package com.ycwl.basic.controller.mobile;
|
|
|
|
import com.github.pagehelper.PageInfo;
|
|
import com.ycwl.basic.constant.BaseContextHandler;
|
|
import com.ycwl.basic.mapper.SourceMapper;
|
|
import com.ycwl.basic.mapper.VideoMapper;
|
|
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
|
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
|
|
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
|
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
|
import com.ycwl.basic.pricing.enums.ProductType;
|
|
import com.ycwl.basic.repository.TemplateRepository;
|
|
import com.ycwl.basic.repository.VideoRepository;
|
|
import com.ycwl.basic.repository.VideoTaskRepository;
|
|
import com.ycwl.basic.service.pc.OrderService;
|
|
import com.ycwl.basic.utils.ApiResponse;
|
|
import com.ycwl.basic.pricing.dto.*;
|
|
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
|
import com.ycwl.basic.service.pc.FaceService;
|
|
import com.ycwl.basic.service.PriceCacheService;
|
|
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
|
|
import com.ycwl.basic.dto.MobileOrderRequest;
|
|
import com.ycwl.basic.order.service.IOrderService;
|
|
import com.ycwl.basic.order.dto.OrderV2DetailResponse;
|
|
import com.ycwl.basic.order.dto.OrderV2ListResponse;
|
|
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;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
|
import java.util.List;
|
|
|
|
/**
|
|
* 移动端订单控制器V2
|
|
* 包含价格查询和订单管理功能
|
|
*/
|
|
@Slf4j
|
|
@RestController
|
|
@RequestMapping("/api/mobile/order/v2")
|
|
@RequiredArgsConstructor
|
|
public class AppOrderV2Controller {
|
|
|
|
private final IPriceCalculationService priceCalculationService;
|
|
private final FaceService faceService;
|
|
private final PriceCacheService priceCacheService;
|
|
private final IOrderService orderService;
|
|
private final OrderService oldOrderService;
|
|
private final SourceMapper sourceMapper;
|
|
private final VideoMapper videoMapper;
|
|
private final VideoTaskRepository videoTaskRepository;
|
|
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")
|
|
public ApiResponse<PriceCalculationResult> calculatePrice(@RequestBody MobilePriceCalculationRequest request) {
|
|
// 获取当前登录用户ID
|
|
String currentUserIdStr = BaseContextHandler.getUserId();
|
|
if (currentUserIdStr == null) {
|
|
log.warn("移动端价格计算:用户未登录");
|
|
return ApiResponse.fail("用户未登录");
|
|
}
|
|
|
|
Long currentUserId = Long.valueOf(currentUserIdStr);
|
|
log.info("移动端价格计算请求: userId={}, faceId={}, products={}",
|
|
currentUserId, request.getFaceId(), request.getProducts().size());
|
|
|
|
// 验证faceId参数
|
|
if (request.getFaceId() == null) {
|
|
log.warn("移动端价格计算:faceId参数缺失");
|
|
// return ApiResponse.fail("faceId参数不能为空");
|
|
// 兼容:兼容旧版本
|
|
ProductItem productItem = request.getProducts().getFirst();
|
|
switch (productItem.getProductType()) {
|
|
case VLOG_VIDEO -> {
|
|
VideoEntity video = videoRepository.getVideo(Long.valueOf(productItem.getProductId()));
|
|
request.setFaceId(video.getFaceId());
|
|
}
|
|
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
|
|
}
|
|
}
|
|
|
|
// 查询人脸信息进行权限验证
|
|
ApiResponse<FaceRespVO> faceResponse = faceService.getById(request.getFaceId());
|
|
if (!faceResponse.isSuccess() || faceResponse.getData() == null) {
|
|
log.warn("移动端价格计算:人脸信息不存在, faceId={}", request.getFaceId());
|
|
return ApiResponse.fail("人脸信息不存在");
|
|
}
|
|
|
|
FaceRespVO face = faceResponse.getData();
|
|
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()));
|
|
if (videoEntities != null && !videoEntities.isEmpty()) {
|
|
product.setQuantity(videoTaskRepository.getTaskLensNum(videoEntities.getFirst().getTaskId()));
|
|
} else {
|
|
product.setQuantity(1);
|
|
}
|
|
break;
|
|
case RECORDING_SET:
|
|
case PHOTO_SET:
|
|
SourceReqQuery sourceReqQuery = new SourceReqQuery();
|
|
sourceReqQuery.setMemberId(currentUserId);
|
|
sourceReqQuery.setType(product.getProductType() == ProductType.RECORDING_SET ? 1 : 2);
|
|
sourceReqQuery.setFaceId(face.getId());
|
|
Integer count = sourceMapper.countUser(sourceReqQuery);
|
|
product.setQuantity(count);
|
|
break;
|
|
case AI_CAM_PHOTO_SET:
|
|
SourceReqQuery aiPhotoSetReqQuery = new SourceReqQuery();
|
|
aiPhotoSetReqQuery.setMemberId(currentUserId);
|
|
aiPhotoSetReqQuery.setType(13);
|
|
aiPhotoSetReqQuery.setFaceId(face.getId());
|
|
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
|
|
product.setQuantity(_count);
|
|
break;
|
|
default:
|
|
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
|
|
break;
|
|
}
|
|
|
|
// 使用 DuplicatePurchaseChecker 检查是否已购买
|
|
hasPurchasedFlag = checkIfPurchased(strategy, currentUserId, String.valueOf(scenicId),
|
|
product.getProductType().name(), product.getProductId(), face.getId());
|
|
|
|
// 设置是否已购买标识
|
|
product.setHasPurchased(hasPurchasedFlag);
|
|
});
|
|
|
|
// 转换为标准价格计算请求
|
|
PriceCalculationRequest standardRequest = request.toStandardRequest(currentUserId, scenicId);
|
|
|
|
// 执行价格计算
|
|
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);
|
|
|
|
log.info("移动端价格计算完成: userId={}, scenicId={}, originalAmount={}, finalAmount={}, cacheKey={}",
|
|
currentUserId, scenicId, result.getOriginalAmount(), result.getFinalAmount(), cacheKey);
|
|
|
|
return ApiResponse.success(result);
|
|
}
|
|
|
|
/**
|
|
* 移动端下单接口
|
|
* 验证价格缓存有效性,确保5分钟内使用缓存价格下单
|
|
*/
|
|
@PostMapping("/add")
|
|
public ApiResponse<String> addOrder(@RequestBody MobileOrderRequest request) {
|
|
// 获取当前登录用户ID
|
|
String currentUserIdStr = BaseContextHandler.getUserId();
|
|
if (currentUserIdStr == null) {
|
|
log.warn("移动端下单:用户未登录");
|
|
return ApiResponse.fail("用户未登录");
|
|
}
|
|
|
|
Long currentUserId = Long.valueOf(currentUserIdStr);
|
|
log.info("移动端下单请求: userId={}, faceId={}, products={}, expectedFinalAmount={}",
|
|
currentUserId, request.getFaceId(), request.getProducts().size(), request.getExpectedFinalAmount());
|
|
|
|
// 验证必填参数
|
|
if (request.getFaceId() == null) {
|
|
log.warn("移动端下单:faceId参数缺失");
|
|
return ApiResponse.fail("faceId参数不能为空");
|
|
}
|
|
|
|
if (request.getProducts() == null || request.getProducts().isEmpty()) {
|
|
log.warn("移动端下单:商品列表为空");
|
|
return ApiResponse.fail("商品列表不能为空");
|
|
}
|
|
|
|
if (request.getExpectedFinalAmount() == null) {
|
|
log.warn("移动端下单:预期价格参数缺失");
|
|
return ApiResponse.fail("预期价格不能为空");
|
|
}
|
|
|
|
// 查询人脸信息进行权限验证
|
|
ApiResponse<FaceRespVO> faceResponse = faceService.getById(request.getFaceId());
|
|
if (!faceResponse.isSuccess() || faceResponse.getData() == null) {
|
|
log.warn("移动端下单:人脸信息不存在, faceId={}", request.getFaceId());
|
|
return ApiResponse.fail("人脸信息不存在");
|
|
}
|
|
|
|
FaceRespVO face = faceResponse.getData();
|
|
Long scenicId = face.getScenicId();
|
|
|
|
// 验证并消费价格缓存(一次性使用)
|
|
PriceCalculationResult cachedResult = priceCacheService.validateAndConsumePriceCache(
|
|
currentUserId, scenicId, request.getProducts());
|
|
|
|
if (cachedResult == null) {
|
|
log.warn("移动端下单:价格缓存已过期或不存在, userId={}, scenicId={}", currentUserId, scenicId);
|
|
return ApiResponse.fail("请重新下单!");
|
|
}
|
|
|
|
// 验证价格是否匹配
|
|
if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) {
|
|
log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}",
|
|
cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId);
|
|
return ApiResponse.fail("价格信息变化,请退出后重新查询价格!");
|
|
}
|
|
|
|
// 验证原价是否匹配(可选)
|
|
if (request.getExpectedOriginalAmount() != null &&
|
|
cachedResult.getOriginalAmount().compareTo(request.getExpectedOriginalAmount()) != 0) {
|
|
log.warn("移动端下单:原价不匹配, cached={}, expected={}, userId={}, scenicId={}",
|
|
cachedResult.getOriginalAmount(), request.getExpectedOriginalAmount(), currentUserId, scenicId);
|
|
return ApiResponse.fail("原价信息不匹配,请重新查询价格后再下单");
|
|
}
|
|
|
|
log.info("价格缓存验证通过: userId={}, scenicId={}, finalAmount={}",
|
|
currentUserId, scenicId, cachedResult.getFinalAmount());
|
|
|
|
// 使用旧版创建订单逻辑
|
|
try {
|
|
Long orderId = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
|
|
return ApiResponse.success(String.valueOf(orderId));
|
|
} catch (Exception e) {
|
|
log.warn("移动端下单:订单创建失败, userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
|
|
return ApiResponse.fail("订单创建失败,请稍后重试");
|
|
}
|
|
|
|
// 创建订单
|
|
// try {
|
|
// Long orderId = orderService.createOrder(request, currentUserId, scenicId, cachedResult);
|
|
//
|
|
// log.info("移动端订单创建成功: orderId={}, userId={}, scenicId={}, finalAmount={}",
|
|
// orderId, currentUserId, scenicId, cachedResult.getFinalAmount());
|
|
//
|
|
// return ApiResponse.success(orderId.toString());
|
|
//
|
|
// } catch (Exception e) {
|
|
// log.error("订单创建失败: userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e);
|
|
// return ApiResponse.fail("订单创建失败,请稍后重试");
|
|
// }
|
|
}
|
|
|
|
// ====== 新增移动端订单查询功能 ======
|
|
|
|
/**
|
|
* 用户分页查询自己的订单列表
|
|
*/
|
|
@PostMapping("/page")
|
|
public ApiResponse<PageInfo<OrderV2ListResponse>> pageUserOrders(@RequestBody OrderV2PageRequest request) {
|
|
String currentUserIdStr = BaseContextHandler.getUserId();
|
|
if (currentUserIdStr == null) {
|
|
log.warn("用户未登录");
|
|
return ApiResponse.fail("用户未登录");
|
|
}
|
|
|
|
Long currentUserId = Long.valueOf(currentUserIdStr);
|
|
request.setMemberId(currentUserId); // 设置当前用户ID,确保只查询自己的订单
|
|
|
|
log.info("用户查询订单列表: userId={}, request={}", currentUserId, request);
|
|
|
|
try {
|
|
PageInfo<OrderV2ListResponse> pageInfo = orderService.pageOrdersByUser(request);
|
|
return ApiResponse.success(pageInfo);
|
|
} catch (Exception e) {
|
|
log.error("查询用户订单列表失败: userId={}", currentUserId, e);
|
|
return ApiResponse.fail("查询失败:" + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 查询订单详情
|
|
*/
|
|
@GetMapping("/detail/{orderId}")
|
|
public ApiResponse<OrderV2DetailResponse> getUserOrderDetail(@PathVariable("orderId") Long orderId) {
|
|
log.info("查询订单详情: orderId={}", orderId);
|
|
|
|
try {
|
|
OrderV2DetailResponse detail = orderService.getOrderDetail(orderId);
|
|
if (detail == null) {
|
|
return ApiResponse.fail("订单不存在");
|
|
}
|
|
|
|
return ApiResponse.success(detail);
|
|
} catch (Exception e) {
|
|
log.error("查询订单详情失败: orderId={}", orderId, e);
|
|
return ApiResponse.fail("查询失败:" + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// ====== 支付相关接口 ======
|
|
|
|
/**
|
|
* 获取订单支付参数
|
|
* 用于小程序调起支付
|
|
*/
|
|
@PostMapping("/{orderId}/payment-params")
|
|
public ApiResponse<PaymentParamsResponse> getPaymentParams(
|
|
@PathVariable("orderId") Long orderId,
|
|
@RequestBody PaymentParamsRequest request) {
|
|
|
|
String currentUserIdStr = BaseContextHandler.getUserId();
|
|
if (currentUserIdStr == null) {
|
|
log.warn("用户未登录");
|
|
return ApiResponse.fail("用户未登录");
|
|
}
|
|
|
|
Long currentUserId = Long.valueOf(currentUserIdStr);
|
|
|
|
log.info("获取支付参数: userId={}, orderId={}", currentUserId, orderId);
|
|
|
|
return oldOrderService.getPaymentParams(orderId, currentUserId, request);
|
|
|
|
//
|
|
// try {
|
|
// PaymentParamsResponse response = orderService.getPaymentParams(orderId, currentUserId, request);
|
|
// return ApiResponse.success(response);
|
|
// } catch (Exception e) {
|
|
// log.error("获取支付参数失败: userId={}, orderId={}", currentUserId, orderId, e);
|
|
// return ApiResponse.fail(e.getMessage());
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* 支付回调处理接口
|
|
* 供第三方支付平台回调使用
|
|
*/
|
|
@PostMapping("/payment/callback/{scenicId}")
|
|
public String handlePaymentCallback(
|
|
@PathVariable("scenicId") Long scenicId,
|
|
HttpServletRequest request) {
|
|
|
|
log.info("接收支付回调: scenicId={}", scenicId);
|
|
|
|
try {
|
|
PaymentCallbackResponse response = orderService.handlePaymentCallback(scenicId, request);
|
|
|
|
if (response.isSuccess()) {
|
|
log.info("支付回调处理成功: scenicId={}, orderId={}, statusChangeType={}",
|
|
scenicId, response.getOrderId(), response.getStatusChangeType());
|
|
return "SUCCESS"; // 返回给第三方支付平台的成功标识
|
|
} else {
|
|
log.error("支付回调处理失败: scenicId={}, message={}", scenicId, response.getMessage());
|
|
return "FAIL"; // 返回给第三方支付平台的失败标识
|
|
}
|
|
} catch (Exception e) {
|
|
log.error("支付回调异常: scenicId={}", scenicId, e);
|
|
return "FAIL";
|
|
}
|
|
}
|
|
|
|
@GetMapping("/downloadable/{orderId}")
|
|
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;
|
|
}
|
|
}
|
|
}
|