feat(pay): 新增支付订单模块

- 创建支付订单实体类PaymentOrder,包含订单基本信息和状态字段
- 实现PaymentOrderMapper接口,提供根据商户订单号和交易流水号查询方法
- 定义PaymentOrderService接口,包含创建订单、查询订单和更新订单状态方法
- 实现PaymentOrderServiceImpl类,完成订单创建、查询和状态更新业务逻辑
- 集成PingPong支付服务,支持调用其API创建支付订单
- 添加订单重复性校验,防止相同商户订单号重复创建
- 实现订单状态管理和异步通知处理机制
- 记录支付操作日志和异常情况处理
This commit is contained in:
2025-12-18 18:01:36 +08:00
parent 1cf4914d8b
commit 8f9244e434
4 changed files with 332 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
package com.mtkj.mtpay.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 支付订单实体
*/
@TableName(value = "payment_order", resultMap = "BaseResultMap")
@Data
public class PaymentOrder {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 商户订单号(全局唯一)
*/
@TableField(value = "merchant_transaction_id", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String merchantTransactionId;
/**
* PingPong交易流水号
*/
@TableField(value = "transaction_id", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String transactionId;
/**
* PingPong商户号
*/
@TableField(value = "client_id", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String clientId;
/**
* PingPong商户店铺编号
*/
@TableField(value = "acc_id", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String accId;
/**
* 交易金额
*/
@TableField(value = "amount", jdbcType = org.apache.ibatis.type.JdbcType.DECIMAL)
private BigDecimal amount;
/**
* 交易币种ISO 4217三位币种
*/
@TableField(value = "currency", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String currency;
/**
* 交易类型SALE-直接付款AUTH-预授权
*/
@TableField(value = "payment_type", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String paymentType;
/**
* 支付方式
*/
@TableField(value = "payment_brand", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String paymentBrand;
/**
* 订单状态PENDING-待支付SUCCESS-支付成功FAILED-支付失败REVIEW-审核中CANCELLED-已取消
*/
@TableField(value = "status", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String status = "PENDING";
/**
* PingPong返回的token
*/
@TableField(value = "token", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String token;
/**
* 支付收银台地址
*/
@TableField(value = "payment_url", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String paymentUrl;
/**
* 商户用户ID
*/
@TableField(value = "merchant_user_id", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String merchantUserId;
/**
* 结果重定向URL
*/
@TableField(value = "shopper_result_url", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String shopperResultUrl;
/**
* 取消重定向URL
*/
@TableField(value = "shopper_cancel_url", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String shopperCancelUrl;
/**
* 异步通知地址
*/
@TableField(value = "notification_url", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String notificationUrl;
/**
* 备注
*/
@TableField(value = "remark", jdbcType = org.apache.ibatis.type.JdbcType.VARCHAR)
private String remark;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,37 @@
package com.mtkj.mtpay.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mtkj.mtpay.entity.PaymentOrder;
import org.apache.ibatis.annotations.Mapper;
import java.util.Optional;
/**
* 支付订单Mapper
*/
@Mapper
public interface PaymentOrderMapper extends BaseMapper<PaymentOrder> {
/**
* 根据商户订单号查询
*/
default Optional<PaymentOrder> findByMerchantTransactionId(String merchantTransactionId) {
PaymentOrder order = selectOne(
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper.<PaymentOrder>lambdaQuery()
.eq(PaymentOrder::getMerchantTransactionId, merchantTransactionId)
);
return Optional.ofNullable(order);
}
/**
* 根据PingPong交易流水号查询
*/
default Optional<PaymentOrder> findByTransactionId(String transactionId) {
PaymentOrder order = selectOne(
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper.<PaymentOrder>lambdaQuery()
.eq(PaymentOrder::getTransactionId, transactionId)
);
return Optional.ofNullable(order);
}
}

View File

@@ -0,0 +1,46 @@
package com.mtkj.mtpay.service;
import com.mtkj.mtpay.dto.request.CheckoutRequestDTO;
import com.mtkj.mtpay.entity.PaymentOrder;
import java.util.Optional;
/**
* 支付订单服务接口
*/
public interface PaymentOrderService {
/**
* 创建支付订单
*
* @param request 支付请求
* @return 支付订单
*/
PaymentOrder createPaymentOrder(CheckoutRequestDTO request);
/**
* 根据商户订单号查询订单
*
* @param merchantTransactionId 商户订单号
* @return 支付订单
*/
Optional<PaymentOrder> findByMerchantTransactionId(String merchantTransactionId);
/**
* 根据PingPong交易流水号查询订单
*
* @param transactionId PingPong交易流水号
* @return 支付订单
*/
Optional<PaymentOrder> findByTransactionId(String transactionId);
/**
* 更新订单状态
*
* @param merchantTransactionId 商户订单号
* @param status 订单状态
* @param transactionId PingPong交易流水号
* @return 支付订单
*/
PaymentOrder updateOrderStatus(String merchantTransactionId, String status, String transactionId);
}

View File

@@ -0,0 +1,119 @@
package com.mtkj.mtpay.service.impl;
import com.mtkj.mtpay.dto.request.CheckoutRequestDTO;
import com.mtkj.mtpay.dto.response.CheckoutResponseDTO;
import com.mtkj.mtpay.entity.PaymentOrder;
import com.mtkj.mtpay.entity.PaymentRecord;
import com.mtkj.mtpay.mapper.PaymentOrderMapper;
import com.mtkj.mtpay.mapper.PaymentRecordMapper;
import com.mtkj.mtpay.service.PaymentOrderService;
import com.mtkj.mtpay.service.PingPongPayService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Optional;
/**
* 支付订单服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentOrderServiceImpl implements PaymentOrderService {
private final PaymentOrderMapper paymentOrderMapper;
private final PaymentRecordMapper paymentRecordMapper;
private final PingPongPayService pingPongPayService;
@Override
@Transactional
public PaymentOrder createPaymentOrder(CheckoutRequestDTO request) {
log.info("创建支付订单,商户订单号: {}", request.getMerchantTransactionId());
// 检查订单号是否已存在
Optional<PaymentOrder> existingOrder = paymentOrderMapper.findByMerchantTransactionId(
request.getMerchantTransactionId()
);
if (existingOrder.isPresent()) {
throw new RuntimeException("商户订单号已存在: " + request.getMerchantTransactionId());
}
// 创建订单实体
PaymentOrder order = new PaymentOrder();
order.setMerchantTransactionId(request.getMerchantTransactionId());
order.setAmount(new BigDecimal(request.getAmount()));
order.setCurrency(request.getCurrency());
order.setPaymentType(request.getPaymentType());
order.setPaymentBrand(request.getPaymentBrand());
order.setMerchantUserId(request.getMerchantUserId());
order.setShopperResultUrl(request.getShopperResultUrl());
order.setShopperCancelUrl(request.getShopperCancelUrl());
order.setNotificationUrl(request.getNotificationUrl());
order.setRemark(request.getRemark());
order.setStatus("PENDING");
// 调用PingPong API创建支付
CheckoutResponseDTO response = pingPongPayService.checkout(request);
// 更新订单信息
if ("001000".equals(response.getCode())) {
// transactionId在回调中返回这里先不设置
order.setClientId(response.getClientId());
order.setAccId(response.getAccId());
order.setToken(response.getToken());
order.setPaymentUrl(response.getPaymentUrl());
} else {
order.setStatus("FAILED");
log.warn("PingPong返回错误code: {}, description: {}", response.getCode(), response.getDescription());
}
// 保存订单
paymentOrderMapper.insert(order);
// 记录支付记录
PaymentRecord record = new PaymentRecord();
record.setMerchantTransactionId(order.getMerchantTransactionId());
record.setTransactionId(order.getTransactionId());
record.setRecordType("CHECKOUT");
record.setCode(response.getCode());
record.setDescription(response.getDescription());
record.setStatus(order.getStatus());
paymentRecordMapper.insert(record);
log.info("支付订单创建成功订单ID: {}", order.getId());
return order;
}
@Override
public Optional<PaymentOrder> findByMerchantTransactionId(String merchantTransactionId) {
return paymentOrderMapper.findByMerchantTransactionId(merchantTransactionId);
}
@Override
public Optional<PaymentOrder> findByTransactionId(String transactionId) {
return paymentOrderMapper.findByTransactionId(transactionId);
}
@Override
@Transactional
public PaymentOrder updateOrderStatus(String merchantTransactionId, String status, String transactionId) {
Optional<PaymentOrder> orderOpt = paymentOrderMapper.findByMerchantTransactionId(merchantTransactionId);
if (orderOpt.isEmpty()) {
throw new RuntimeException("订单不存在: " + merchantTransactionId);
}
PaymentOrder order = orderOpt.get();
order.setStatus(status);
if (transactionId != null && !transactionId.isEmpty()) {
order.setTransactionId(transactionId);
}
paymentOrderMapper.updateById(order);
return order;
}
}