PAYATHON 2026

当面付扫码支付查单、关单后订单状态异常

支付阿杰

结论

这些现象不能简单归结为”关单后订单会自动恢复”。

alipay.trade.precreate 成功返回二维码后,支付宝侧的交易数据可能需要一点时间,才能同步到查询服务、关单服务和支付宝客户端。因此,刚完成预创建就立刻查单或关单,可能暂时查不到交易,也可能出现关单结果尚未生效、客户端显示滞后等情况。

从业务语义看:

  • 只有 alipay.trade.close 明确返回成功,才能认定支付宝交易已经关闭。
  • 同一笔支付宝交易一旦成功关闭,不应在几十分钟后自动恢复为可支付状态。
  • 支付宝账单显示”待支付”,不代表交易服务端仍允许支付。最终状态应以接口查询结果和异步通知为准。
  • 如果后来确实支付成功,需要重点检查系统是否重新调用了 alipay.trade.precreate、是否生成了新的 trade_no,以及 out_trade_no 是否被重复使用或错误映射。

短时间内”查不到”或页面显示不一致,可能是状态同步造成的。但不能把”同一笔已经确认关单的交易重新开放支付”当作正常业务逻辑来依赖。

为什么预创建后会出现”交易不存在”

调用 alipay.trade.precreate 后,商户会先拿到用于展示的 qr_code。交易刚创建且二维码尚未被扫描时,交易数据可能还没有同步到所有服务。

如果此时立即调用:

  • alipay.trade.query
  • alipay.trade.close

接口可能返回”交易不存在”。这时不能直接把商户订单标记为已关闭,因为”没有查询到交易”和”交易已经关闭”是两种不同的状态。

还需要确认预创建是否真正成功。不能只看 HTTP 请求是否成功,还要检查支付宝响应中的业务结果,并保存完整响应。至少应记录:

  • out_trade_no
  • qr_code
  • 返回码及子返回码
  • 请求时间
  • app_id
  • 商户身份信息
  • 后续获得的 trade_no

如果 alipay.trade.precreate 本身没有成功,后续查询不到交易就是预期结果,并非同步延迟。

为什么账单显示待支付,却又不允许支付

支付宝客户端中的账单或待支付入口属于展示层,其刷新时间可能与交易核心状态不同。

短时间内可能出现这样的情况:

  1. 用户扫码时,支付页面判断交易已经关闭;
  2. 支付宝账单仍保留”待支付”记录;
  3. 用户从账单入口继续支付时,交易核心再次校验状态;
  4. 核心状态不允许支付,于是提示”当前订单状态不允许支付”。

最后一次服务端状态校验才是有效判断。账单中的”待支付”只是展示信息,不能用来确定订单是否仍可支付。

客户端稍后刷新、入口消失或展示状态发生变化,也不代表交易出现了逆向状态迁移。

为什么几十分钟后又能支付

如果几十分钟后确实完成了付款,不能只根据客户端页面认定它还是原来的交易。需要同时核对 out_trade_no 和支付宝 trade_no。

可以从以下几个方向排查。

1. 关单实际上没有成功

调用过 alipay.trade.close,不等于关单已经完成。如果接口返回”交易不存在”、系统异常或其他失败结果,支付宝侧并没有确认关闭这笔交易。

如果商户系统在这种情况下直接把订单记为 CLOSED,双方状态就会不一致。

2. 系统再次调用了预创建接口

轮询、补偿任务、页面刷新、收银台重试或人工操作,都可能再次触发 alipay.trade.precreate。

需要检查:

  • 是否针对同一业务订单重复发起预创建;
  • 是否生成了新的 out_trade_no;
  • 是否把新交易错误地显示成原交易;
  • 回调中的 trade_no 是否发生变化。

3. 商户订单号映射错误

如果前端二维码、商户数据库订单和支付宝异步通知没有严格对应,系统可能把两笔交易误认为同一笔。

应建立清晰的唯一映射:

业务订单ID -> out_trade_no -> trade_no

out_trade_no 应由商户生成并保持唯一。订单关闭后,如果业务允许用户重新付款,应创建新的支付尝试编号,不要继续展示旧二维码。

4. 页面展示或本地状态没有及时刷新

“可以点击支付”与”支付成功”不是一回事。只有查询接口明确返回成功,或收到验签通过的支付通知,才能确认付款已经完成。

用户只是重新进入支付页面,并不能证明已关闭的交易被重新打开。

如果确认同一个 trade_no 在成功关单后又支付成功,应保存完整的请求、响应和通知记录,并提交给支付宝技术支持排查。缺少这些证据时,不能把该现象归因于某种确定的官方状态机制。

推荐的处理流程

第一步:预创建成功后保存支付尝试

预创建成功后,应保存二维码和本次支付尝试,不能只保存业务订单状态。

CREATING
  -> WAIT_SCAN
  -> PAYING
  -> SUCCESS
  -> CLOSING
  -> CLOSED

状态只能根据明确结果向前推进。一次查询不到交易,不能直接把状态改为 CLOSED。

第二步:查询不到时进行有限重试

预创建刚完成后,如果查单返回交易不存在,可以按一定间隔进行有限次数的重试,例如使用递增退避:

long[] delays = {500L, 1000L, 2000L, 4000L};

for (long delay : delays) {
    TradeQueryResult result = queryTrade(outTradeNo);

    if (result.isSuccess()) {
        handleTradeStatus(result.getTradeStatus());
        break;
    }

    if (!result.isTradeNotExist()) {
        handleQueryFailure(result);
        break;
    }

    Thread.sleep(delay);
}

这里的时间只是一种重试策略示例,不代表支付宝承诺的数据同步时限。具体重试次数和间隔应按业务超时要求配置。

第三步:关单必须确认结果

不能把”已经发出关单请求”直接视为关单成功。

CloseResult closeResult = closeTrade(outTradeNo);

if (closeResult.isSuccess()) {
    markPaymentAttemptClosed(outTradeNo);
    disableQrCode(outTradeNo);
} else if (closeResult.isTradeNotExist()) {
    markClosePending(outTradeNo);
    scheduleQueryAndCloseRetry(outTradeNo);
} else {
    markCloseFailed(outTradeNo, closeResult.getCode(), closeResult.getSubCode());
}

如果首次关单返回交易不存在,并且已经确认预创建成功,可以稍后查询并重试关单。补偿任务必须保证幂等,同时设置明确的截止时间。

第四步:关单期间禁止继续展示二维码

商户决定取消支付后,应立即让自己页面上的二维码失效,即使支付宝侧仍在确认关单结果。

处理顺序可以是:

  1. 商户订单进入 CLOSING;
  2. 前端停止展示二维码;
  3. 调用 alipay.trade.close;
  4. 查询并确认支付宝状态;
  5. 成功后进入 CLOSED;
  6. 失败或状态不明时,转入人工处理或定时补偿流程。

这样能减少用户扫码与商户关单同时发生的竞态情况。

第五步:支付通知到达时重新核验

关单和支付可能并发发生。收到支付成功通知后,不能因为商户本地状态是 CLOSED 就直接忽略。

需要:

  • 验证通知签名;
  • 校验 app_id、seller_id、金额和币种;
  • 核对 out_trade_no;
  • 保存 trade_no;
  • 必要时调用 alipay.trade.query 做最终确认;
  • 如果出现”本地已关闭但支付宝已支付”,将订单转入退款或人工处理流程。

伪代码如下:

public void handleNotify(AlipayNotify notify) {
    verifySignature(notify);
    verifyMerchantAndAmount(notify);

    PaymentAttempt attempt = findByOutTradeNo(notify.getOutTradeNo());

    if (notify.isTradeSuccess()) {
        if (attempt.isClosedOrClosing()) {
            markPaymentConflict(attempt, notify.getTradeNo());
            triggerRefundOrManualReview(attempt);
            return;
        }

        markPaidIdempotently(attempt, notify.getTradeNo());
    }
}

查询时应关注的交易状态

调用 alipay.trade.query 后,应按返回的 trade_status 处理,不能根据支付宝客户端页面推测状态。常见状态包括:

  • WAIT_BUYER_PAY:交易创建,等待买家付款;
  • TRADE_SUCCESS:支付成功;
  • TRADE_FINISHED:交易结束,不可退款;
  • TRADE_CLOSED:交易关闭。

如果查询接口调用失败,应结合返回码、子返回码和错误信息处理,不能把调用失败当作某一种 trade_status。

日志中必须保留的信息

为了区分同步延迟、重复建单和状态映射错误,应把每次调用记录到同一条支付链路中:

{
  "business_order_id": "ORDER_10001",
  "out_trade_no": "PAY_10001_01",
  "trade_no": null,
  "operation": "alipay.trade.close",
  "request_time": "2026-09-09T10:30:00+08:00",
  "response_code": "...",
  "response_sub_code": "...",
  "response_message": "...",
  "trade_status": null
}

排查时按时间顺序对齐以下记录:

precreate 请求与响应
-> query 请求与响应
-> close 请求与响应
-> 用户扫码时间
-> 支付宝异步通知
-> 后续 query 结果

只凭手机端截图,通常无法确认前后是否为同一笔支付宝交易。

注意事项

  • “交易不存在”不等于”关单成功”。
  • 只有关单接口明确返回成功,才能把支付宝交易视为已关闭。
  • 商户主动取消订单时,应同时撤下二维码,避免用户继续扫码。
  • 不要用支付宝账单页面的显示状态驱动商户订单状态。
  • 同一个 out_trade_no 不应被用作可以反复创建、关闭和重新支付的普通会话标识。
  • 用户重新支付时,应生成新的支付尝试编号,并保存它与原业务订单的关联。
  • 查询、关单、异步通知和退款处理都必须具备幂等性。
  • 如果同一个 trade_no 在关单成功后又支付成功,应把完整接口日志提交给支付宝排查,不能自行认定这是正常的”自动恢复”机制。

备注:内容仅供参考。