feat(mobile): 新增移动端订单V2接口

- 添加 AppOrderV2Controller 控制器,实现移动端价格计算和下单功能
- 新增 MobilePriceCalculationRequest DTO 类,用于移动端价格计算请求- 集成 Redis 缓存机制,提升价格查询性能- 实现人脸权限验证和价格缓存验证逻辑
- 优化日志记录和异常处理
This commit is contained in:
2025-08-28 18:13:59 +08:00
parent 798ff3b9b5
commit 5c2629237e
2 changed files with 238 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.constant.BaseContextHandler;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
/**
* 移动端订单控制器V2
* 包含价格查询和订单管理功能
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/order/v2")
@RequiredArgsConstructor
public class AppOrderV2Controller {
private final IPriceCalculationService priceCalculationService;
private final FaceService faceService;
private final PriceCacheService priceCacheService;
/**
* 移动端价格计算
* 包含权限验证:验证人脸所属景区与当前用户匹配
* 集成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参数不能为空");
}
// 查询人脸信息进行权限验证
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();
// 先尝试从Redis缓存获取价格计算结果
PriceCalculationResult cachedResult = priceCacheService.getCachedPriceResult(
currentUserId, scenicId, request.getProducts());
if (cachedResult != null) {
log.info("命中价格缓存: userId={}, scenicId={}, finalAmount={}",
currentUserId, scenicId, cachedResult.getFinalAmount());
return ApiResponse.success(cachedResult);
}
// 转换为标准价格计算请求
PriceCalculationRequest standardRequest = request.toStandardRequest(currentUserId, scenicId);
// 执行价格计算
PriceCalculationResult result = priceCalculationService.calculatePrice(standardRequest);
// 将计算结果缓存到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-order")
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());
// TODO: 实现订单创建逻辑
// 1. 生成订单号
// 2. 创建订单基础信息
// 3. 保存订单商品明细
// 4. 记录使用的优惠券和券码信息
// 5. 更新优惠券和券码状态
// 6. 初始化订单状态为待支付
// 7. 发送订单创建成功通知
String orderId = "ORDER_" + System.currentTimeMillis(); // 临时订单号生成逻辑
log.info("移动端订单创建成功: orderId={}, userId={}, scenicId={}, finalAmount={}",
orderId, currentUserId, scenicId, cachedResult.getFinalAmount());
return ApiResponse.success(orderId);
}
}

View File

@@ -0,0 +1,62 @@
package com.ycwl.basic.pricing.dto;
import lombok.Data;
import java.util.List;
/**
* 移动端价格计算请求DTO
*/
@Data
public class MobilePriceCalculationRequest {
/**
* 商品列表
*/
private List<ProductItem> products;
/**
* 人脸ID(必填,用于权限验证)
*/
private Long faceId;
/**
* 是否自动使用优惠券
*/
private Boolean autoUseCoupon = true;
/**
* 用户输入的券码
*/
private String voucherCode;
/**
* 是否自动使用券码优惠
*/
private Boolean autoUseVoucher = true;
/**
* 是否仅预览优惠(不实际使用)
*/
private Boolean previewOnly = false;
/**
* 转换为标准价格计算请求
*
* @param userId 用户ID
* @param scenicId 景区ID
* @return 标准价格计算请求
*/
public PriceCalculationRequest toStandardRequest(Long userId, Long scenicId) {
PriceCalculationRequest request = new PriceCalculationRequest();
request.setProducts(this.products);
request.setUserId(userId);
request.setScenicId(scenicId);
request.setFaceId(this.faceId);
request.setAutoUseCoupon(this.autoUseCoupon);
request.setVoucherCode(this.voucherCode);
request.setAutoUseVoucher(this.autoUseVoucher);
request.setPreviewOnly(this.previewOnly);
return request;
}
}