feat(order): 添加支付相关接口和功能

- 新增获取支付参数接口和处理支付回调接口
- 实现支付参数获取和支付回调处理的逻辑
- 添加支付相关数据传输对象(DTO)
- 修改订单服务接口和实现类,增加支付相关方法
This commit is contained in:
2025-08-29 15:32:47 +08:00
parent bc2b2fb10f
commit 5a66856e72
6 changed files with 461 additions and 4 deletions

View File

@@ -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;
}
}

View File

@@ -0,0 +1,17 @@
package com.ycwl.basic.order.dto;
import lombok.Data;
/**
* 获取支付参数请求DTO
* 所有参数都是可选的,系统会自动生成商品名称和描述
*/
@Data
public class PaymentParamsRequest {
// 预留字段,目前所有信息都由系统自动生成
// 可以在未来版本中扩展支持自定义参数
public PaymentParamsRequest() {
}
}

View File

@@ -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<String, Object> paymentParams;
/**
* 支付描述信息
*/
private String description;
/**
* 商品名称
*/
private String goodsName;
/**
* 创建成功的支付参数响应
*/
public static PaymentParamsResponse success(Long orderId, String orderNo, BigDecimal payAmount,
Boolean needPay, Map<String, Object> 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;
}
}

View File

@@ -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<OrderV2ListResponse> 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);
}

View File

@@ -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<String, Object> 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<OrderItemV2> itemQuery = new QueryWrapper<>();
itemQuery.eq("order_id", orderId);
java.util.List<OrderItemV2> orderItems = orderItemMapper.selectList(itemQuery);
if (orderItems.isEmpty()) {
return "景区商品";
}
// 获取所有不同的商品类型
java.util.Set<String> 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 -> "景区商品";
};
}
}