From 5a66856e728d1af109e43a7a41a2232134f5ed22 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 29 Aug 2025 15:32:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(order):=20=E6=B7=BB=E5=8A=A0=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增获取支付参数接口和处理支付回调接口 - 实现支付参数获取和支付回调处理的逻辑 - 添加支付相关数据传输对象(DTO) - 修改订单服务接口和实现类,增加支付相关方法 --- .../mobile/AppOrderV2Controller.java | 66 +++++- .../order/dto/PaymentCallbackResponse.java | 70 ++++++ .../basic/order/dto/PaymentParamsRequest.java | 17 ++ .../order/dto/PaymentParamsResponse.java | 76 +++++++ .../basic/order/service/IOrderService.java | 26 +++ .../order/service/impl/OrderServiceImpl.java | 210 +++++++++++++++++- 6 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/order/dto/PaymentCallbackResponse.java create mode 100644 src/main/java/com/ycwl/basic/order/dto/PaymentParamsRequest.java create mode 100644 src/main/java/com/ycwl/basic/order/dto/PaymentParamsResponse.java diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java b/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java index 6c00ea0..e84ade6 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java @@ -13,10 +13,14 @@ 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import jakarta.servlet.http.HttpServletRequest; import java.math.BigDecimal; /** @@ -142,14 +146,14 @@ public class AppOrderV2Controller { if (cachedResult == null) { log.warn("移动端下单:价格缓存已过期或不存在, userId={}, scenicId={}", currentUserId, scenicId); - return ApiResponse.fail("价格信息已过期,请重新查询价格后再下单"); + 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("价格信息不匹配,请重新查询价格后再下单"); + return ApiResponse.fail("请重新下单!"); } // 验证原价是否匹配(可选) @@ -239,4 +243,62 @@ public class AppOrderV2Controller { return ApiResponse.fail("查询失败:" + e.getMessage()); } } + + // ====== 支付相关接口 ====== + + /** + * 获取订单支付参数 + * 用于小程序调起支付 + */ + @PostMapping("/{orderId}/payment-params") + public ApiResponse 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); + + 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"; + } + } } diff --git a/src/main/java/com/ycwl/basic/order/dto/PaymentCallbackResponse.java b/src/main/java/com/ycwl/basic/order/dto/PaymentCallbackResponse.java new file mode 100644 index 0000000..9ba7164 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/PaymentCallbackResponse.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +/** + * 支付回调响应DTO + */ +@Data +public class PaymentCallbackResponse { + + /** + * 处理是否成功 + */ + private boolean success; + + /** + * 响应消息 + */ + private String message; + + /** + * 订单ID + */ + private Long orderId; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 支付状态变化类型 + */ + private String statusChangeType; + + /** + * 创建成功响应 + */ + public static PaymentCallbackResponse success(Long orderId, String orderNo, String statusChangeType) { + PaymentCallbackResponse response = new PaymentCallbackResponse(); + response.success = true; + response.message = "回调处理成功"; + response.orderId = orderId; + response.orderNo = orderNo; + response.statusChangeType = statusChangeType; + return response; + } + + /** + * 创建失败响应 + */ + public static PaymentCallbackResponse failure(String message) { + PaymentCallbackResponse response = new PaymentCallbackResponse(); + response.success = false; + response.message = message; + return response; + } + + /** + * 创建失败响应(包含订单信息) + */ + public static PaymentCallbackResponse failure(String message, Long orderId, String orderNo) { + PaymentCallbackResponse response = new PaymentCallbackResponse(); + response.success = false; + response.message = message; + response.orderId = orderId; + response.orderNo = orderNo; + return response; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/PaymentParamsRequest.java b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsRequest.java new file mode 100644 index 0000000..8f32f4e --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsRequest.java @@ -0,0 +1,17 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +/** + * 获取支付参数请求DTO + * 所有参数都是可选的,系统会自动生成商品名称和描述 + */ +@Data +public class PaymentParamsRequest { + + // 预留字段,目前所有信息都由系统自动生成 + // 可以在未来版本中扩展支持自定义参数 + + public PaymentParamsRequest() { + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/PaymentParamsResponse.java b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsResponse.java new file mode 100644 index 0000000..9760d68 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsResponse.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 支付参数响应DTO + */ +@Data +public class PaymentParamsResponse { + + /** + * 订单ID + */ + private Long orderId; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 支付金额 + */ + private BigDecimal payAmount; + + /** + * 是否需要支付(false表示免费订单) + */ + private Boolean needPay; + + /** + * 支付参数(微信小程序调起支付所需的参数) + * 包含:appId, timeStamp, nonceStr, package, signType, paySign等 + */ + private Map paymentParams; + + /** + * 支付描述信息 + */ + private String description; + + /** + * 商品名称 + */ + private String goodsName; + + /** + * 创建成功的支付参数响应 + */ + public static PaymentParamsResponse success(Long orderId, String orderNo, BigDecimal payAmount, + Boolean needPay, Map paymentParams) { + PaymentParamsResponse response = new PaymentParamsResponse(); + response.orderId = orderId; + response.orderNo = orderNo; + response.payAmount = payAmount; + response.needPay = needPay; + response.paymentParams = paymentParams; + return response; + } + + /** + * 创建免费订单的响应 + */ + public static PaymentParamsResponse free(Long orderId, String orderNo) { + PaymentParamsResponse response = new PaymentParamsResponse(); + response.orderId = orderId; + response.orderNo = orderNo; + response.payAmount = BigDecimal.ZERO; + response.needPay = false; + response.paymentParams = null; + return response; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/service/IOrderService.java b/src/main/java/com/ycwl/basic/order/service/IOrderService.java index cfa3bed..f41d982 100644 --- a/src/main/java/com/ycwl/basic/order/service/IOrderService.java +++ b/src/main/java/com/ycwl/basic/order/service/IOrderService.java @@ -6,6 +6,11 @@ import com.ycwl.basic.order.dto.*; import com.ycwl.basic.order.entity.OrderV2; import com.ycwl.basic.order.enums.RefundStatus; import com.ycwl.basic.pricing.dto.PriceCalculationResult; +import com.ycwl.basic.order.dto.PaymentParamsRequest; +import com.ycwl.basic.order.dto.PaymentParamsResponse; +import com.ycwl.basic.order.dto.PaymentCallbackResponse; + +import jakarta.servlet.http.HttpServletRequest; /** * 订单服务接口 @@ -116,4 +121,25 @@ public interface IOrderService { * @return 分页结果 */ PageInfo pageOrdersByUser(OrderV2PageRequest request); + + // ====== 支付相关方法 ====== + + /** + * 获取订单支付参数 + * + * @param orderId 订单ID + * @param userId 用户ID(用于权限验证和获取openId) + * @param request 支付参数请求 + * @return 支付参数响应 + */ + PaymentParamsResponse getPaymentParams(Long orderId, Long userId, PaymentParamsRequest request); + + /** + * 处理支付回调 + * + * @param scenicId 景区ID + * @param request HTTP请求对象 + * @return 回调处理结果 + */ + PaymentCallbackResponse handlePaymentCallback(Long scenicId, HttpServletRequest request); } \ 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 c1a02e7..f629a1b 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 @@ -4,6 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.ycwl.basic.dto.MobileOrderRequest; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.mapper.MemberMapper; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.member.resp.MemberRespVO; import com.ycwl.basic.order.dto.*; import com.ycwl.basic.order.entity.OrderDiscountV2; import com.ycwl.basic.order.entity.OrderItemV2; @@ -20,11 +24,19 @@ import com.ycwl.basic.pricing.dto.DiscountDetail; import com.ycwl.basic.pricing.dto.PriceCalculationResult; import com.ycwl.basic.pricing.dto.ProductItem; import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pay.adapter.IPayAdapter; +import com.ycwl.basic.pay.entity.CreateOrderRequest; +import com.ycwl.basic.pay.entity.CreateOrderResponse; +import com.ycwl.basic.pay.entity.PayResponse; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.service.pc.ScenicService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import jakarta.servlet.http.HttpServletRequest; + import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Date; @@ -44,12 +56,15 @@ public class OrderServiceImpl implements IOrderService { private final OrderDiscountMapper orderDiscountMapper; private final OrderRefundMapper orderRefundMapper; private final OrderEventManager orderEventManager; + private final ScenicService scenicService; + private final FaceRepository faceRepository; + private final MemberMapper memberMapper; @Override @Transactional public Long createOrder(MobileOrderRequest request, Long userId, Long scenicId, PriceCalculationResult priceResult) { Date now = new Date(); - + MemberRespVO member = memberMapper.getById(userId); // 1. 生成订单号 String orderNo = generateOrderNo(); @@ -57,7 +72,7 @@ public class OrderServiceImpl implements IOrderService { OrderV2 order = new OrderV2(); order.setOrderNo(orderNo); order.setMemberId(userId); - order.setOpenId(request.getFaceId().toString()); // 临时使用faceId,实际应传入openId + order.setOpenId(member.getOpenId()); // 临时使用faceId,实际应传入openId order.setFaceId(request.getFaceId()); order.setScenicId(scenicId); @@ -527,4 +542,195 @@ public class OrderServiceImpl implements IOrderService { int random = (int) (Math.random() * 900) + 100; return "REFUND" + timestamp + random; } + + // ====== 支付相关方法实现 ====== + + @Override + public PaymentParamsResponse getPaymentParams(Long orderId, Long userId, PaymentParamsRequest request) { + log.info("获取支付参数: orderId={}, userId={}", orderId, userId); + + // 1. 查询订单 + OrderV2 order = orderV2Mapper.selectById(orderId); + if (order == null) { + throw new RuntimeException("订单不存在"); + } + + // 2. 验证订单权限 + if (!userId.equals(order.getMemberId())) { + throw new RuntimeException("无权获取该订单的支付参数"); + } + + // 3. 验证订单状态 + if (order.getPaymentStatus() != PaymentStatus.UNPAID) { + throw new RuntimeException("订单状态不允许支付"); + } + + // 4. 获取用户openId(从订单中获取) + String openId = order.getOpenId(); + if (openId == null || openId.trim().isEmpty()) { + throw new RuntimeException("用户openId不存在,无法发起支付"); + } + + // 5. 检查是否为免费订单 + if (order.getFinalAmount().compareTo(BigDecimal.ZERO) == 0) { + log.info("免费订单,无需支付: orderId={}", orderId); + return PaymentParamsResponse.free(orderId, order.getOrderNo()); + } + + // 6. 获取景区支付适配器 + IPayAdapter payAdapter = scenicService.getScenicPayAdapter(order.getScenicId()); + + try { + // 7. 生成商品名称和描述 + String goodsName = generateGoodsName(orderId); + String description = goodsName; // 直接使用商品名称作为描述 + + // 8. 创建支付订单请求 + CreateOrderRequest payRequest = new CreateOrderRequest() + .setOrderNo(order.getOrderNo()) + .setPriceInCents(order.getFinalAmount().multiply(BigDecimal.valueOf(100)).intValue()) + .setDescription(description) + .setGoodsName(goodsName) + .setUserIdentify(openId) + .setNotifyUrl("https://zhentuai.com/api/mobile/order/v2/payment/callback/" + order.getScenicId()); + + // 9. 调用支付适配器创建订单 + CreateOrderResponse payResponse = payAdapter.createOrder(payRequest); + + // 10. 获取支付参数 + java.util.Map paymentParams = payAdapter.getPaymentParams(payResponse); + + // 11. 构建响应 + PaymentParamsResponse response = PaymentParamsResponse.success( + orderId, + order.getOrderNo(), + order.getFinalAmount(), + !payResponse.isSkipPay(), + paymentParams + ); + response.setDescription(description); + response.setGoodsName(goodsName); + + log.info("支付参数生成成功: orderId={}, needPay={}, amount={}", + orderId, !payResponse.isSkipPay(), order.getFinalAmount()); + + return response; + + } catch (Exception e) { + log.error("获取支付参数失败: orderId={}, error={}", orderId, e.getMessage(), e); + throw new RuntimeException("获取支付参数失败: " + e.getMessage()); + } + } + + @Override + public PaymentCallbackResponse handlePaymentCallback(Long scenicId, HttpServletRequest request) { + log.info("处理支付回调: scenicId={}", scenicId); + + try { + // 1. 获取支付适配器 + IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId); + + // 2. 解析回调数据 + PayResponse callbackResponse = payAdapter.handleCallback(request); + log.info("解析支付回调数据: {}", callbackResponse); + + if (callbackResponse == null || callbackResponse.getOrderNo() == null) { + return PaymentCallbackResponse.failure("回调数据解析失败"); + } + + // 3. 查询订单 + String orderNo = callbackResponse.getOrderNo(); + OrderV2 order = getByOrderNo(orderNo); + if (order == null) { + return PaymentCallbackResponse.failure("订单不存在", null, orderNo); + } + + // 4. 处理不同的支付状态变更 + String statusChangeType = null; + if (callbackResponse.isPay()) { + // 支付成功 + statusChangeType = "PAID"; + updatePaymentStatus(order.getId(), PaymentStatus.PAID.name()); + + // 触发支付成功事件 + orderEventManager.publishPaymentStatusChangeEvent( + new PaymentStatusChangeEvent(order.getId(), PaymentStatus.PENDING, PaymentStatus.PAID) + ); + + } else if (callbackResponse.isCancel()) { + // 支付取消 + statusChangeType = "CANCELLED"; + updatePaymentStatus(order.getId(), PaymentStatus.CANCELLED.name()); + + // 触发支付取消事件 + orderEventManager.publishPaymentStatusChangeEvent( + new PaymentStatusChangeEvent(order.getId(), PaymentStatus.PENDING, PaymentStatus.CANCELLED) + ); + + } else if (callbackResponse.isRefund()) { + // 退款 + statusChangeType = "REFUNDED"; + updateRefundStatus(order.getId(), RefundStatus.COMPLETED.name()); + + // 触发退款事件 + orderEventManager.publishRefundStatusChangeEvent( + new RefundStatusChangeEvent(order.getId(), RefundStatus.PENDING, RefundStatus.COMPLETED) + ); + } + + log.info("支付回调处理成功: orderId={}, orderNo={}, statusChangeType={}", + order.getId(), orderNo, statusChangeType); + + return PaymentCallbackResponse.success(order.getId(), orderNo, statusChangeType); + + } catch (Exception e) { + log.error("处理支付回调失败: scenicId={}, error={}", scenicId, e.getMessage(), e); + return PaymentCallbackResponse.failure("回调处理失败: " + e.getMessage()); + } + } + + /** + * 生成商品名称 + * 根据订单的商品类型生成用于支付页面展示的商品名称 + * 如果是一种商品类型,显示具体类型名称;如果是多种,显示"多项景区商品" + */ + private String generateGoodsName(Long orderId) { + // 查询订单商品明细 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", orderId); + java.util.List orderItems = orderItemMapper.selectList(itemQuery); + + if (orderItems.isEmpty()) { + return "景区商品"; + } + + // 获取所有不同的商品类型 + java.util.Set productTypes = new java.util.HashSet<>(); + for (OrderItemV2 item : orderItems) { + productTypes.add(item.getProductType()); + } + + // 如果只有一种商品类型,返回具体类型名称 + if (productTypes.size() == 1) { + String productType = productTypes.iterator().next(); + return getProductTypeName(productType); + } else { + // 如果有多种商品类型,返回通用名称 + return "多项景区商品"; + } + } + + /** + * 获取商品类型中文名称 + */ + private String getProductTypeName(String productType) { + return switch (productType) { + case "VLOG_VIDEO" -> "Vlog视频"; + case "RECORDING_SET" -> "录像集"; + case "PHOTO_SET" -> "照相集"; + case "PHOTO_PRINT" -> "照片打印"; + case "MACHINE_PRINT" -> "一体机打印"; + default -> "景区商品"; + }; + } } \ No newline at end of file