支付宝扫码付未支付订单如何查询、关闭并拒绝超时支付?
结论
要让支付宝拒绝超时后的付款,仅在商户系统中把订单标记为”已超时”还不够,还需要控制支付宝侧的交易状态:
- 创建二维码交易时,通过
timeout_express设置支付宝侧的交易有效期。 - 商户订单到期后,调用
alipay.trade.close主动关闭支付宝交易。 - 关单前后调用
alipay.trade.query查询最终状态,同时处理关单与支付并发发生的情况。 - 如果支付宝已经返回支付成功,关单无法撤销这笔支付。业务无法接受超时付款时,只能调用
alipay.trade.refund退款。
所谓”未支付就查询不到订单”,通常不代表支付宝没有生成交易。只要 alipay.trade.precreate 成功返回二维码,交易一般已经进入支付宝系统,未付款时通常处于 WAIT_BUYER_PAY 状态。如果查询结果显示交易不存在,应检查查询参数、应用和商户身份是否一致,也要考虑短暂的数据同步延迟。
创建交易时设置有效期
扫码支付通常使用 alipay.trade.precreate 创建交易。创建时建议传入 timeout_express,让支付宝侧明确交易的失效时间,不能只依赖商户系统自己的倒计时。
{
"out_trade_no": "ORDER_20260909_0001",
"total_amount": "99.00",
"subject": "商品订单",
"timeout_express": "10m"
}
timeout_express 支持的格式、最小值和最大值可能因接口产品及签约能力而异,应以支付宝开放平台当前的对应接口文档为准,不要自行假定任意秒数都有效。
设置该参数后,商户系统仍应保存自己的过期时间,例如:
expire_at = created_at + 10 minutes
业务判断应以服务端时间为准,不能依赖扫码页面中的 JavaScript 倒计时。
查询未支付交易
使用商户订单号查询时,需要传入创建交易时使用的同一个 out_trade_no:
{
"out_trade_no": "ORDER_20260909_0001"
}
查询接口为:
alipay.trade.query
需要处理的交易状态主要包括:
WAIT_BUYER_PAY:交易已创建,正在等待用户付款。TRADE_SUCCESS:付款成功。TRADE_FINISHED:交易已完成,通常不可退款,具体以接口规则为准。TRADE_CLOSED:交易已关闭,无法继续付款。
如果查询返回类似 ACQ.TRADE_NOT_EXIST 的结果,可以依次检查:
out_trade_no是否与预创建时完全一致。- 查询请求是否使用了同一个
app_id、商户账号和签约主体。 alipay.trade.precreate是否真正执行成功,而非只在商户本地生成了订单。- 是否在预创建成功后立即发起了查询。必要时可以短暂重试,但不能无限重试。
- 是否混用了商户订单号、支付宝交易号
trade_no或其他流水号。
如果预创建请求失败,支付宝侧就没有可关闭的交易。此时只需关闭商户本地订单,并确保页面不再展示或使用原二维码。
主动关闭订单
订单到期后,调用:
alipay.trade.close
使用商户订单号关单的请求参数示例:
{
"out_trade_no": "ORDER_20260909_0001"
}
如果已经取得支付宝交易号,也可以使用 trade_no。实际实现时,应保存每次预创建请求的结果、out_trade_no、二维码内容,以及后续查询得到的 trade_no。
下面是一个简化的 Java SDK 示例。具体类名和调用方式需要根据项目使用的支付宝 SDK 版本核对:
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
request.setBizContent("""
{
"out_trade_no": "ORDER_20260909_0001"
}
""");
AlipayTradeCloseResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
// 关单请求成功,随后仍应查询一次最终交易状态
} else {
// 记录 code、sub_code、msg、sub_msg,按错误类型处理
}
关单接口返回失败时,不能直接把支付宝交易视为已关闭。失败可能是因为交易已经支付、正在处理或已经关闭,也可能是请求参数或商户身份不匹配。无论哪种情况,都必须再通过 alipay.trade.query 确认状态。
处理”最后几秒付款”的竞态
用户扫码、输入密码和支付宝完成扣款涉及多个步骤。商户的超时任务可能与支付宝支付请求同时发生,因此会出现以下竞态:
用户提交支付 ───────┐
├── 支付先成功:关单不能撤销支付
商户执行关单 ───────┘
└── 关单先成功:后续支付应被拒绝
建议使用明确的订单状态机:
PENDING -> PAID
PENDING -> CLOSING -> CLOSED
PAID -> REFUNDING -> REFUNDED
超时处理可以采用以下逻辑:
void expireOrder(String outTradeNo) {
// 使用数据库条件更新或分布式锁,防止支付通知与超时任务同时改状态
MerchantOrder order = orderRepository.findByOutTradeNo(outTradeNo);
if (!order.isPending() || !order.isExpired()) {
return;
}
TradeStatus status = queryAlipay(outTradeNo);
if (status == TradeStatus.TRADE_SUCCESS
|| status == TradeStatus.TRADE_FINISHED) {
markPaidAtomically(outTradeNo);
handleLatePayment(outTradeNo);
return;
}
if (status == TradeStatus.TRADE_CLOSED) {
markClosedAtomically(outTradeNo);
return;
}
CloseResult closeResult = closeAlipay(outTradeNo);
// 无论关单调用结果如何,都再次查询最终状态
TradeStatus finalStatus = queryAlipayWithLimitedRetry(outTradeNo);
if (finalStatus == TradeStatus.TRADE_CLOSED) {
markClosedAtomically(outTradeNo);
} else if (finalStatus == TradeStatus.TRADE_SUCCESS
|| finalStatus == TradeStatus.TRADE_FINISHED) {
markPaidAtomically(outTradeNo);
handleLatePayment(outTradeNo);
} else {
markForManualOrScheduledReconciliation(outTradeNo, closeResult);
}
}
handleLatePayment 应按照业务规则处理。如果订单超时后绝对不能履约,应在确认支付成功后调用 alipay.trade.refund。这种处理属于”支付成功后退款”,无法在原支付链路中拒绝付款。
支付通知也要检查订单状态
不能只依赖主动查询。收到支付宝异步通知后,应先验签,再核对以下信息:
app_id- 商户身份信息
out_trade_notrade_nototal_amount- 交易状态
如果本地订单已经进入 CLOSING 或 CLOSED,但通知显示交易成功,不能直接丢弃通知。应将其记录为”超时支付”,再次查询支付宝的最终状态,再根据业务规则履约或退款。
更新订单状态时,必须使用数据库事务、条件更新或其他并发控制。例如,只有当前状态仍为 PENDING,才能更新为 PAID 或 CLOSING,以免关单任务和支付通知相互覆盖。
实施时的注意事项
out_trade_no应全局唯一。订单关闭后,不要复用原订单号创建新交易。- 二维码过期后,页面应立即将其设为失效状态,停止轮询,也不要继续展示旧二维码。
- 页面倒计时结束不代表支付宝交易已经关闭。
- 关单接口调用成功也不等于本地处理完成,之后仍需查询和对账。
- 查询重试应限制次数并设置间隔。状态持续不明确的订单,应进入定时补偿或人工核对队列。
- 异步通知、主动查询和账单对账应组成三层确认机制。
- 如果业务要求严格拒绝超时付款,应为关单任务预留提前量。例如,页面展示的有效期为 10 分钟,可以提前停止使用二维码并开始关单。具体提前多久,需要根据实际接口耗时和业务容忍度确定。
- 即使提前关单,也无法在技术上消除所有并发窗口。因此,系统仍需准备”支付已成功但商户订单已过期”时的退款补偿流程。
备注:内容仅供参考。
