diff --git a/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java b/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java index 568e056..1070bb7 100644 --- a/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java +++ b/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java @@ -1,6 +1,7 @@ package com.ycwl.basic.exception; import com.ycwl.basic.enums.BizCodeEnum; +import com.ycwl.basic.order.exception.DuplicatePurchaseException; import com.ycwl.basic.utils.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException; @@ -106,4 +107,18 @@ public class CustomExceptionHandle { public ApiResponse handle(SizeLimitExceededException sizeLimitExceededException) { return ApiResponse.buildResponse(415, "文件过大,请重新上传"); } + + /** + * 重复购买异常处理 + */ + @ExceptionHandler(value = DuplicatePurchaseException.class) + public ApiResponse handle(HttpServletResponse response, DuplicatePurchaseException exception) { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + LOGGER.warn("检测到重复购买: productType={}, productId={}, existingOrderId={}, existingOrderNo={}", + exception.getProductType(), exception.getProductId(), + exception.getExistingOrderId(), exception.getExistingOrderNo()); + + // 返回友好的错误信息给前端 + return ApiResponse.buildResponse(4001, exception.getFriendlyMessage()); + } } diff --git a/src/main/java/com/ycwl/basic/order/exception/DuplicatePurchaseException.java b/src/main/java/com/ycwl/basic/order/exception/DuplicatePurchaseException.java new file mode 100644 index 0000000..b0e2569 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/exception/DuplicatePurchaseException.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java index 1c78eb0..fa14b0a 100644 --- a/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java +++ b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java @@ -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 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 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 existingOrders = orderV2Mapper.selectList(orderQuery); + + for (OrderV2 order : existingOrders) { + // 检查订单明细中是否包含该视频 + QueryWrapper 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 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 existingOrders = orderV2Mapper.selectList(orderQuery); + + for (OrderV2 order : existingOrders) { + // 检查订单明细中是否包含该类型的套餐 + QueryWrapper 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); + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java index 19d178c..d74a4dc 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java @@ -238,6 +238,5 @@ public class VoucherPrintServiceImpl implements VoucherPrintService { content += "赠品兑换码"; content += "有效期:"+sdf2.format(new Date())+""; FeiETicketPrinter.doPrint("550519002", content, 1); - // 小票配对码、赠品兑换码、抵扣兑换码 } } \ No newline at end of file