feat(order): 添加重复购买检测功能

- 新增 DuplicatePurchaseException 类用于处理重复购买异常
- 在 OrderServiceImpl 中实现重复购买检查逻辑
- 更新 CustomExceptionHandle 以处理新的重复购买异常
-优化订单创建流程,在生成订单号前增加重复购买检查
This commit is contained in:
2025-08-30 10:52:26 +08:00
parent 93a424058a
commit 792deb5c4d
4 changed files with 212 additions and 7 deletions

View File

@@ -0,0 +1,58 @@
package com.ycwl.basic.order.exception;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.pricing.enums.ProductType;
/**
* 重复购买异常
* 当用户尝试购买已经购买过的内容时抛出此异常
*/
public class DuplicatePurchaseException extends BaseException {
private final Long existingOrderId;
private final String existingOrderNo;
private final ProductType productType;
private final String productId;
public DuplicatePurchaseException(String message, Long existingOrderId, String existingOrderNo,
ProductType productType, String productId) {
super(message);
this.existingOrderId = existingOrderId;
this.existingOrderNo = existingOrderNo;
this.productType = productType;
this.productId = productId;
}
public DuplicatePurchaseException(String message, Long existingOrderId, String existingOrderNo,
ProductType productType) {
this(message, existingOrderId, existingOrderNo, productType, null);
}
public Long getExistingOrderId() {
return existingOrderId;
}
public String getExistingOrderNo() {
return existingOrderNo;
}
public ProductType getProductType() {
return productType;
}
public String getProductId() {
return productId;
}
/**
* 获取友好的错误消息
*/
public String getFriendlyMessage() {
String productDesc = productType != null ? productType.getDescription() : "商品";
if (productId != null) {
return String.format("您已购买过该%s(商品ID:%s),订单号:%s", productDesc, productId, existingOrderNo);
} else {
return String.format("您已购买过%s,订单号:%s", productDesc, existingOrderNo);
}
}
}

View File

@@ -14,6 +14,7 @@ import com.ycwl.basic.order.entity.OrderItemV2;
import com.ycwl.basic.order.entity.OrderRefundV2;
import com.ycwl.basic.order.entity.OrderV2;
import com.ycwl.basic.order.enums.*;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.order.mapper.OrderDiscountMapper;
import com.ycwl.basic.order.mapper.OrderItemMapper;
import com.ycwl.basic.order.mapper.OrderV2Mapper;
@@ -74,10 +75,18 @@ public class OrderServiceImpl implements IOrderService {
public Long createOrder(MobileOrderRequest request, Long userId, Long scenicId, PriceCalculationResult priceResult) {
Date now = new Date();
MemberRespVO member = memberMapper.getById(userId);
// 1. 生成订单号
// 1. 检查重复购买
log.info("开始检查重复购买: userId={}, faceId={}, scenicId={}, productCount={}",
userId, request.getFaceId(), scenicId, request.getProducts().size());
checkDuplicatePurchase(userId, request.getFaceId(), scenicId, request.getProducts());
log.info("重复购买检查通过: userId={}, faceId={}, scenicId={}",
userId, request.getFaceId(), scenicId);
// 2. 生成订单号
String orderNo = generateOrderNo();
// 2. 创建订单基础信息
// 3. 创建订单基础信息
OrderV2 order = new OrderV2();
order.setOrderNo(orderNo);
order.setMemberId(userId);
@@ -107,7 +116,7 @@ public class OrderServiceImpl implements IOrderService {
log.info("订单基础信息创建成功: orderId={}, orderNo={}", orderId, orderNo);
// 3. 保存订单商品明细
// 4. 保存订单商品明细
for (ProductItem product : request.getProducts()) {
// 重新计算商品价格信息并获取商品名称
ProductPriceAndNameInfo priceInfo = calculateProductItemPriceAndName(product);
@@ -129,7 +138,7 @@ public class OrderServiceImpl implements IOrderService {
log.info("订单商品明细保存成功: orderId={}, itemCount={}", orderId, request.getProducts().size());
// 4. 记录使用的优惠券信息并标记为已使用
// 5. 记录使用的优惠券信息并标记为已使用
if (priceResult.getUsedCoupon() != null) {
OrderDiscountV2 couponDiscount = new OrderDiscountV2();
couponDiscount.setOrderId(orderId);
@@ -165,7 +174,7 @@ public class OrderServiceImpl implements IOrderService {
}
}
// 5. 记录使用的券码信息并标记为已使用
// 6. 记录使用的券码信息并标记为已使用
if (priceResult.getUsedVoucher() != null) {
VoucherInfo voucherInfo = priceResult.getUsedVoucher();
OrderDiscountV2 voucherDiscount = new OrderDiscountV2();
@@ -195,7 +204,7 @@ public class OrderServiceImpl implements IOrderService {
}
}
// 6. 记录其他优惠信息(如限时立减等)
// 7. 记录其他优惠信息(如限时立减等)
if (priceResult.getDiscountDetails() != null) {
for (DiscountDetail discount : priceResult.getDiscountDetails()) {
// 跳过已经记录的优惠券和券码
@@ -881,4 +890,128 @@ public class OrderServiceImpl implements IOrderService {
return new ProductPriceAndNameInfo(defaultPrice, defaultPrice, fallbackName);
}
}
/**
* 检查重复购买
* 防止用户重复购买相同内容
*
* @param userId 用户ID
* @param faceId 人脸ID
* @param scenicId 景区ID
* @param products 商品列表
* @throws DuplicatePurchaseException 如果检测到重复购买
*/
private void checkDuplicatePurchase(Long userId, Long faceId, Long scenicId, List<ProductItem> products) {
for (ProductItem product : products) {
switch (product.getProductType()) {
case VLOG_VIDEO:
checkVideoAlreadyPurchased(userId, faceId, scenicId, product.getProductId());
break;
case RECORDING_SET:
case PHOTO_SET:
checkSetAlreadyPurchased(userId, faceId, scenicId, product.getProductType());
break;
case PHOTO_PRINT:
case MACHINE_PRINT:
// 打印类商品允许重复购买,跳过检查
log.debug("跳过打印类商品重复购买检查: productType={}, productId={}",
product.getProductType(), product.getProductId());
break;
default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break;
}
}
}
/**
* 检查视频是否已经购买
*
* @param userId 用户ID
* @param faceId 人脸ID
* @param scenicId 景区ID
* @param videoId 视频ID
* @throws DuplicatePurchaseException 如果已购买
*/
private void checkVideoAlreadyPurchased(Long userId, Long faceId, Long scenicId, String videoId) {
// 构建查询条件:查找已支付的有效订单中包含该视频的订单
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
orderQuery.eq("member_id", userId)
.eq("face_id", faceId)
.eq("scenic_id", scenicId)
.eq("payment_status", PaymentStatus.PAID.getCode())
.in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode())
.eq("deleted", 0);
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该视频
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO.name())
.eq("product_id", videoId);
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}",
userId, faceId, scenicId, videoId, order.getId());
throw new DuplicatePurchaseException(
"您已购买过此视频",
order.getId(),
order.getOrderNo(),
com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO,
videoId
);
}
}
log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}",
userId, faceId, scenicId, videoId);
}
/**
* 检查套餐(录像集/照相集)是否已经购买
*
* @param userId 用户ID
* @param faceId 人脸ID
* @param scenicId 景区ID
* @param productType 商品类型
* @throws DuplicatePurchaseException 如果已购买
*/
private void checkSetAlreadyPurchased(Long userId, Long faceId, Long scenicId,
com.ycwl.basic.pricing.enums.ProductType productType) {
// 构建查询条件:查找已支付的有效订单中包含该类型套餐的订单
QueryWrapper<OrderV2> orderQuery = new QueryWrapper<>();
orderQuery.eq("member_id", userId)
.eq("face_id", faceId)
.eq("scenic_id", scenicId)
.eq("payment_status", PaymentStatus.PAID.getCode())
.in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode())
.eq("deleted", 0);
List<OrderV2> existingOrders = orderV2Mapper.selectList(orderQuery);
for (OrderV2 order : existingOrders) {
// 检查订单明细中是否包含该类型的套餐
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", order.getId())
.eq("product_type", productType.name());
long count = orderItemMapper.selectCount(itemQuery);
if (count > 0) {
log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}",
userId, faceId, scenicId, productType, order.getId());
throw new DuplicatePurchaseException(
"您已购买过此类型的套餐",
order.getId(),
order.getOrderNo(),
productType
);
}
}
log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}",
userId, faceId, scenicId, productType);
}
}