用户中途退出支付后,如何发起二次支付?
结论
不要重新创建业务订单,也不要给业务订单改名。用户再次点击「去支付」时,先查询本地订单,再向支付宝确认支付状态:
- 已支付:更新本地状态,进入支付成功流程。
- 待支付,且原支付请求仍然有效:继续使用原支付交易。
- 已关闭、已超时或无法继续支付:保留原业务订单,创建新的支付尝试,并使用新的
out_trade_no发起支付。
这样可以避免产生重复订单、重复付款以及库存统计混乱。至于同一个 out_trade_no 能否再次唤起支付,需要根据所用支付宝产品及接口规则判断,不能只看本地订单状态。
业务订单和支付流水要分开
业务订单记录用户购买的内容,支付流水记录每一次付款尝试。两者不应共用同一套生命周期。
例如:
业务订单号:ORDER_202609130001
第一次支付流水:PAY_202609130001_01
第二次支付流水:PAY_202609130001_02
用户退出支付页面,不等于业务订单失效,也不能直接判定支付失败。此时通常只能说明系统还没有拿到明确的支付结果。
建议至少保存以下数据:
order
- order_no
- amount
- status
- expire_time
payment
- payment_no
- order_no
- channel
- out_trade_no
- gateway_trade_no
- status
- expire_time
传给支付宝的 out_trade_no 最好使用支付流水号,不要直接使用业务订单号。这样既能保留原订单,也可以在需要时发起新的支付尝试。
二次支付的处理流程
1. 查询本地业务订单
先检查订单是否存在,并确认:
- 当前用户是否有权支付该订单;
- 金额是否仍然有效;
- 商品、库存和优惠是否仍然有效;
- 订单是否已经取消或超过业务有效期。
不要采用前端提交的金额。支付金额必须从服务端订单数据中读取。
2. 检查是否已经支付
如果本地订单已经处于支付成功状态,直接返回成功结果,不要再次发起支付。
如果本地仍显示未支付,但此前已经创建过支付流水,应调用 alipay.trade.query 查询支付宝交易状态。常见状态包括:
TRADE_SUCCESS:支付成功;TRADE_FINISHED:交易结束且不可退款,具体含义需要结合所用产品确认;WAIT_BUYER_PAY:等待用户付款;TRADE_CLOSED:交易已关闭;- 查询不到交易:交易可能尚未成功创建,也可能是请求参数或商户订单号不一致。
如果支付宝已经显示支付成功,而本地状态没有更新,应先补偿本地订单状态,再向用户展示支付成功页面。
3. 决定继续原支付还是创建新支付流水
支付宝返回 WAIT_BUYER_PAY 时,可以优先继续原交易,但需要确认当前支付产品支持再次唤起,而且原支付链接或支付参数仍然有效。
如果原交易已经关闭、超时,或者原支付参数无法再次使用,则按以下步骤处理:
- 关闭仍在等待支付的旧交易,避免多笔交易同时处于可支付状态;
- 创建新的支付流水;
- 为新流水生成新的
out_trade_no; - 按原业务订单的应付金额重新发起支付。
业务订单号保持不变,只新增一条支付尝试记录。
Java 示例
以下代码只用于说明处理思路,省略了签名配置、数据库事务、异常分类和具体返回对象。接口字段需要以项目实际使用的支付宝 SDK 版本为准。
public String retryPay(String orderNo, Long userId) throws AlipayApiException {
Order order = orderRepository.findByOrderNoForUpdate(orderNo);
if (order == null || !order.getUserId().equals(userId)) {
throw new IllegalArgumentException("订单不存在");
}
if (order.getStatus() == OrderStatus.PAID) {
return "/pay/success?orderNo=" + orderNo;
}
if (order.getStatus() == OrderStatus.CANCELLED
|| order.getExpireTime().isBefore(LocalDateTime.now())) {
throw new IllegalStateException("订单已失效");
}
Payment oldPayment =
paymentRepository.findLatestByOrderNo(orderNo, "ALIPAY");
if (oldPayment != null) {
AlipayTradeQueryRequest queryRequest = new AlipayTradeQueryRequest();
queryRequest.setBizContent("""
{
"out_trade_no": "%s"
}
""".formatted(oldPayment.getOutTradeNo()));
AlipayTradeQueryResponse queryResponse =
alipayClient.execute(queryRequest);
if (queryResponse.isSuccess()) {
String tradeStatus = queryResponse.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
confirmPaid(
order,
oldPayment,
queryResponse.getTradeNo()
);
return "/pay/success?orderNo=" + orderNo;
}
if ("WAIT_BUYER_PAY".equals(tradeStatus)
&& oldPayment.getExpireTime().isAfter(LocalDateTime.now())) {
/*
* 此处可以重新生成用于唤起原交易的支付响应。
* 是否允许重复提交同一 out_trade_no,以及响应是否可以直接复用,
* 需要按照实际使用的支付宝产品和接口规则处理。
*/
return buildAlipayPagePay(order, oldPayment);
}
}
closeOldPaymentIfNecessary(oldPayment);
}
Payment newPayment = createPaymentAttempt(order);
return buildAlipayPagePay(order, newPayment);
}
创建新支付流水时,需要生成唯一的 out_trade_no:
private Payment createPaymentAttempt(Order order) {
int attemptNo =
paymentRepository.countByOrderNo(order.getOrderNo()) + 1;
String outTradeNo = order.getOrderNo()
+ "_"
+ String.format("%02d", attemptNo);
Payment payment = new Payment();
payment.setOrderNo(order.getOrderNo());
payment.setOutTradeNo(outTradeNo);
payment.setAmount(order.getPayAmount());
payment.setStatus(PaymentStatus.CREATED);
payment.setExpireTime(order.getExpireTime());
paymentRepository.save(payment);
return payment;
}
发起网页支付的示意代码如下:
private String buildAlipayPagePay(Order order, Payment payment)
throws AlipayApiException {
AlipayTradePagePayRequest request =
new AlipayTradePagePayRequest();
request.setNotifyUrl(alipayNotifyUrl);
request.setReturnUrl(alipayReturnUrl);
request.setBizContent("""
{
"out_trade_no": "%s",
"total_amount": "%s",
"subject": "%s",
"product_code": "FAST_INSTANT_TRADE_PAY"
}
""".formatted(
payment.getOutTradeNo(),
order.getPayAmount().toPlainString(),
escapeJson(order.getSubject())
));
AlipayTradePagePayResponse response =
alipayClient.pageExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"发起支付宝支付失败:" + response.getSubMsg()
);
}
return response.getBody();
}
支付回调必须保证幂等
不能把同步跳转页作为支付成功的最终依据。用户可能关闭页面,网络也可能中断,支付宝异步通知还可能晚于页面跳转到达。
服务端收到异步通知后,需要完成以下校验和处理:
- 验证支付宝签名;
- 校验
app_id、商户身份等字段; - 根据
out_trade_no查找支付流水; - 校验通知金额是否与数据库金额一致;
- 校验交易状态;
- 在事务中更新支付流水和业务订单;
- 对已经处理过的通知直接返回成功,避免重复发货或重复增加余额。
示意逻辑:
@Transactional
public void handleAlipayNotify(AlipayNotify notify) {
Payment payment =
paymentRepository.findByOutTradeNoForUpdate(
notify.getOutTradeNo()
);
if (payment == null) {
throw new IllegalStateException("支付流水不存在");
}
if (payment.getStatus() == PaymentStatus.SUCCESS) {
return;
}
if (notify.getTotalAmount().compareTo(payment.getAmount()) != 0) {
throw new IllegalStateException("支付金额不一致");
}
if ("TRADE_SUCCESS".equals(notify.getTradeStatus())
|| "TRADE_FINISHED".equals(notify.getTradeStatus())) {
payment.markSuccess(
notify.getTradeNo(),
notify.getGmtPayment()
);
orderRepository.markPaidIfUnpaid(
payment.getOrderNo(),
notify.getGmtPayment()
);
}
}
需要特别注意的问题
防止用户连续点击
「去支付」接口需要加锁或使用数据库唯一约束,防止用户连续点击时同时创建多条有效支付流水。可以限制同一业务订单在同一时刻只能有一条状态为 WAIT_BUYER_PAY 的支付宝流水。
不要直接覆盖原支付流水
旧流水可能会在稍后收到异步通知。如果直接修改旧流水的 out_trade_no 或删除记录,回调到达后就无法正确关联订单。因此,每次支付尝试都要保留独立记录。
新支付前处理旧交易
如果旧支付宝交易仍可付款,新旧交易同时存在就可能导致重复支付。创建新支付交易前,应根据接口能力关闭旧交易,或者确认旧交易已经自然过期。
服务端还要处理极端情况下的重复支付。如果同一业务订单收到第二笔成功付款,应转入异常处理或退款流程,不能再次发货。
以服务端状态为准
用户退出前端页面、同步跳转失败或接口超时,都不等于支付失败。只要支付结果不明确,就应通过异步通知和 alipay.trade.query 确认状态。
较稳妥的处理方式是保留原业务订单,二次支付前先查单。原交易还能继续就继续使用,无法继续时再创建新的支付流水。
备注:内容仅供参考。
