EasySDK 电脑网站支付订单关闭失败后仍可支付如何处理?
结论
Factory.Payment.Common().Close() 返回 Business Failed,并不表示支付宝侧订单已经关闭。也不能仅凭本地过期时间,将订单改为“已关闭”。只要支付宝侧交易仍可支付,原支付页面或二维码就可能继续完成付款。
应按以下方式处理:
- 检查关单响应中的
code、sub_code和sub_msg; - 立即调用交易查询接口,确认支付宝侧的实际状态;
- 只有关单接口明确返回成功,或查询结果为
TRADE_CLOSED,才能把本地订单标记为已关闭; - 如果查询到
TRADE_SUCCESS,或之后收到有效的支付通知,应按“超时支付”处理,通常需要自动退款或转人工处理; - 关单和支付可能同时发生,必须通过订单状态机和幂等控制避免误判。
为什么关单失败后仍然可以支付
订单“过期”可能只是业务系统中的状态,并不代表支付宝交易已经关闭。电脑网站支付页面生成后,只要支付宝侧交易仍处于 WAIT_BUYER_PAY,客户就可能继续付款。
Business Failed 是 SDK 对业务失败结果的概括,具体原因要查看原始响应字段。常见情况包括:
- 关单请求参数不正确;
out_trade_no或trade_no不匹配;- 支付宝侧尚未创建可关闭的交易;
- 交易已经支付或正在支付;
- 交易已经关闭,本次属于重复关单;
- 关单请求和客户付款同时发生;
- 网络重试、响应丢失等问题导致调用方无法确认最终结果。
因此,调用了关单接口,不等于订单已经关闭。订单状态应以明确的关单结果或后续查询结果为准。
如果已经确认支付宝侧状态为 TRADE_CLOSED,但同一笔支付宝交易后来又显示支付成功,需要核对两次响应中的 trade_no 和 out_trade_no 是否完全一致。正常情况下,同一笔已经关闭的支付宝交易不应再次支付成功。更常见的情况是本地订单已关闭,但支付宝交易并未关闭,或者客户支付的是重新创建的另一笔交易。
建议的处理流程
1. 保存完整的关单响应
不要只记录 Business Failed,至少要保存:
- 商户订单号
out_trade_no - 支付宝交易号
trade_no codemsgsub_codesub_msg- 请求时间和响应时间
- SDK 请求 ID 或支付宝返回的追踪信息(如果响应中存在)
日志中的敏感信息需要脱敏。
2. 关单失败后立即查询交易
关单接口返回失败、超时或结果不明确时,应调用交易查询接口,并根据支付宝侧状态处理:
| 支付宝交易状态 | 建议处理 |
|---|---|
TRADE_CLOSED | 将本地订单更新为已关闭 |
TRADE_SUCCESS | 按已支付处理,再执行超时支付策略 |
TRADE_FINISHED | 按已支付且交易完成处理 |
WAIT_BUYER_PAY | 延迟后重试关单,并继续查询确认 |
| 查询不到交易 | 检查订单是否尚未在支付宝侧创建,短暂延迟后复查 |
| 查询失败或状态不明确 | 保持处理中,不得直接标记为已关闭 |
重试要设置次数上限和退避间隔,避免持续高频调用。
3. 使用明确的本地状态机
建议区分以下状态:
WAIT_PAY
CLOSING
CLOSED
PAID
LATE_PAID
REFUNDING
REFUNDED
MANUAL_REVIEW
订单过期时,先将状态从 WAIT_PAY 原子更新为 CLOSING。支付宝明确返回关单成功后,再更新为 CLOSED。
如果关单过程中查询到支付成功,或收到经过验签的支付成功通知,订单应进入 LATE_PAID,不能再按普通关单成功处理。
4. 妥善处理超时支付
业务订单已经过期,但支付宝侧实际支付成功时,应按照预先设定的规则处理:
- 商品或服务仍可履约:恢复订单并正常履约;
- 已经无法履约:发起原路退款;
- 涉及库存、价格变化或高风险交易:转人工审核。
不能忽略支付成功通知,也不能因为本地状态已经是 CLOSED 就直接拒绝更新。资金发生变化后,系统必须留下可追踪的处理结果。
5. 同时停用业务侧支付入口
订单进入 CLOSING 后,应禁止再次生成支付页面,不再展示二维码,也不能继续跳转到旧支付入口。
这样只能降低客户继续支付的概率,无法替代支付宝关单。客户已经打开的页面不完全受本地前端状态控制,最终仍要根据支付宝侧的关单结果判断。
Java 处理示例
下面的示例用于说明处理原则。不同 EasySDK 版本的响应类型和字段访问方式可能不同,应以项目实际依赖中的接口定义为准。
String outTradeNo = order.getOutTradeNo();
String tradeNo = order.getTradeNo();
// 先通过数据库条件更新或分布式锁,将订单从 WAIT_PAY 切换为 CLOSING。
boolean acquired = orderService.markClosingIfWaiting(outTradeNo);
if (!acquired) {
return;
}
try {
AlipayTradeCloseResponse closeResponse =
Factory.Payment.Common().close(tradeNo, outTradeNo);
log.info(
"close trade result, outTradeNo={}, tradeNo={}, code={}, msg={}, subCode={}, subMsg={}",
outTradeNo,
tradeNo,
closeResponse.code,
closeResponse.msg,
closeResponse.subCode,
closeResponse.subMsg
);
if (closeResponse.isSuccess()) {
orderService.markClosed(outTradeNo);
return;
}
// 关单业务失败不能直接标记为 CLOSED,继续查询真实交易状态。
reconcileTradeStatus(outTradeNo, tradeNo);
} catch (Exception e) {
log.error(
"close trade exception, outTradeNo={}, tradeNo={}",
outTradeNo,
tradeNo,
e
);
// 请求异常时结果未知,同样通过查询进行确认。
reconcileTradeStatus(outTradeNo, tradeNo);
}
交易状态核对逻辑可以这样组织:
private void reconcileTradeStatus(String outTradeNo, String tradeNo) throws Exception {
AlipayTradeQueryResponse queryResponse =
Factory.Payment.Common().query(tradeNo, outTradeNo);
if (!queryResponse.isSuccess()) {
orderService.markClosePending(outTradeNo);
retryService.scheduleCloseReconcile(outTradeNo);
return;
}
String tradeStatus = queryResponse.tradeStatus;
switch (tradeStatus) {
case "TRADE_CLOSED":
orderService.markClosed(outTradeNo);
break;
case "TRADE_SUCCESS":
case "TRADE_FINISHED":
orderService.markLatePaid(
outTradeNo,
queryResponse.tradeNo,
queryResponse.totalAmount
);
latePaymentService.handle(outTradeNo);
break;
case "WAIT_BUYER_PAY":
orderService.markClosePending(outTradeNo);
retryService.scheduleClose(outTradeNo);
break;
default:
orderService.markManualReview(outTradeNo);
break;
}
}
示例中的 markClosingIfWaiting、scheduleCloseReconcile 和 handle 需要由业务系统自行实现,不属于 EasySDK。
如果调用签名要求传入 trade_no 和 out_trade_no,应保证至少有一个有效编号。如果两个编号都提供,它们必须指向同一笔交易。具体参数顺序以当前使用的 EasySDK 版本为准。
支付通知也要做状态兜底
处理异步通知时,必须完成验签并核对以下字段:
app_idseller_idout_trade_notrade_nototal_amounttrade_status
通知处理也要保证幂等:
@Transactional
public void handlePaymentNotify(PaymentNotify notify) {
Order order = orderRepository.lockByOutTradeNo(notify.getOutTradeNo());
verifyMerchantAndAmount(order, notify);
if (order.isPaidOrRefunded()) {
return;
}
if (order.isExpired() || order.isClosing() || order.isClosed()) {
order.markLatePaid(notify.getTradeNo());
latePaymentService.handle(order.getOutTradeNo());
return;
}
order.markPaid(notify.getTradeNo());
}
即使本地订单已经进入关闭流程,也不能直接丢弃合法的支付成功通知。否则可能出现支付宝已经收款,本地却没有相应处理记录的资金差错。
已经发生客诉时如何排查
按时间顺序核对以下数据:
- 支付页面生成时间;
- 本地订单过期时间;
- 关单请求和响应时间;
- 关单响应的
code、sub_code、sub_msg; - 关单后的交易查询结果;
- 支付成功时间;
- 异步通知到达和处理时间;
- 每一步对应的
out_trade_no与trade_no。
如果最终确认支付宝侧已经支付成功,但业务无法履约,应调用退款接口处理并保存退款结果。只修改本地订单状态并不会把资金退还给客户。
注意事项
- 本地超时不代表支付宝订单会自动关闭。
Business Failed不是最终交易状态,需要查看详细错误并补充查询。- 关单成功应以接口成功响应或查询到
TRADE_CLOSED为准。 - 关单失败时,不能直接将订单更新为
CLOSED。 - 关单、查询、支付通知和退款都要保证幂等。
out_trade_no应保持唯一。重新支付时通常应创建新的商户订单号,避免复用旧订单导致状态混乱。- 数据库更新应使用条件更新、事务或行锁,防止关单任务和支付通知相互覆盖。
- 长时间停留在
CLOSING、支付金额不一致或订单号不一致的记录,应触发告警并进入人工核对流程。
备注:内容仅供参考。
