PAYATHON 2026

支付宝当面付扫码支付时 SDK 打印日志后无后续响应

支付小周

结论

code="10003" 不表示 SDK 卡死,也不表示支付失败。它说明支付宝已经受理订单,但用户尚未完成付款,交易当前处于“等待用户支付”状态。

alipay.trade.pay 返回该结果后,商户系统应主动调用 alipay.trade.query 查询交易状态,不要继续等待当前 SDK 调用返回其他结果。如果超过业务设定的等待时间仍未支付,应调用 alipay.trade.cancel 撤销交易,再根据撤销结果决定是否继续查询。

为什么打印日志后没有后续响应

日志中已经包含了本次接口的同步响应:

{
  "code": "10003",
  "msg": "order success pay inprocess",
  "out_trade_no": "110031667093219182",
  "trade_no": "2022103022001418021410998340",
  "buyer_pay_amount": "0.00",
  "total_amount": "1882.33"
}

各字段含义如下:

  • 10003:订单已受理,支付仍在处理中。
  • buyer_pay_amount="0.00":返回响应时还没有完成扣款。
  • out_trade_no:商户订单号,后续查询、撤销和对账都需要使用,应妥善保存。
  • trade_no:支付宝交易号,可以用于后续查询。
  • 日志末尾的 8ms,630ms,nullms 通常是 SDK 或业务代码记录的耗时信息,不能用来判断支付结果。
  • 日志中的换行只是输出格式问题。只要原始响应可以正常解析和验签,一般不会影响交易。

条码支付本身就需要处理“用户支付中”这一状态。用户可能还没有输入密码或确认付款,也可能是支付渠道处理较慢,或者首次同步请求结束时,支付结果还没有确定。

正确的处理流程

1. 区分成功、处理中和明确失败

不要只用 response.isSuccess() 判断整个支付流程。处理 alipay.trade.pay 的返回结果时,应区分以下情况:

  • code=10000:接口调用成功,还需要结合响应内容确认业务结果。
  • code=10003:支付处理中,应进入查询流程。
  • 其他错误码:结合 sub_code 和 sub_msg,判断是明确失败还是可以重试的异常。

尤其不能把 10003 直接更新为“支付失败”。如果用户稍后完成付款,商户订单状态就会与支付宝交易状态不一致。

2. 使用原订单号查询支付结果

收到 10003 后,调用 alipay.trade.query。查询时优先使用原来的 out_trade_no,不要为同一次付款重新生成订单号。

可以根据查询结果中的 trade_status 进行处理:

trade_status处理建议
TRADE_SUCCESS支付成功,以幂等方式更新订单并发货
TRADE_FINISHED交易完成,按成功状态处理
WAIT_BUYER_PAY用户尚未完成付款,稍后继续查询
TRADE_CLOSED交易已关闭,不能按支付成功处理
查询不到交易短暂等待后重新查询;如果持续查不到,再进入异常处理流程

商户应根据收银场景设置查询间隔和总等待时间,既不要无间隔地高频循环,也不要无限查询。

3. 超时后撤销交易

如果超过收银台允许的等待时间,仍然无法确认支付结果,应调用:

alipay.trade.cancel

撤销时继续使用同一个 out_trade_no。由于撤销接口可能遇到交易状态尚未稳定等情况,不能因为一次网络异常就认定撤销成功。

可以按以下原则处理:

  1. 撤销明确成功:将本地订单更新为已撤销或已关闭。
  2. 撤销失败,但查询结果显示支付成功:按支付成功处理。
  3. 撤销结果不明确:继续调用 alipay.trade.query 核实最终状态。
  4. 无法自动确认:记录订单号、支付宝交易号和接口响应,转入人工核查。

Java 处理示例

下面的示例展示了核心状态处理逻辑。客户端初始化以及证书模式或公钥模式的配置,应继续使用项目当前的支付宝 SDK 配置。

import com.alipay.api.AlipayClient;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.response.AlipayTradePayResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;

public class AlipayTradeHandler {

    private final AlipayClient alipayClient;

    public AlipayTradeHandler(AlipayClient alipayClient) {
        this.alipayClient = alipayClient;
    }

    public PayResult handlePayResponse(AlipayTradePayResponse payResponse)
            throws Exception {

        String code = payResponse.getCode();

        if ("10000".equals(code)) {
            return PayResult.SUCCESS;
        }

        if ("10003".equals(code)) {
            return queryTrade(payResponse.getOutTradeNo());
        }

        // 保存 code、subCode、subMsg,便于定位明确失败的原因
        return PayResult.FAILED;
    }

    private PayResult queryTrade(String outTradeNo) throws Exception {
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        request.setBizContent(
            "{\"out_trade_no\":\"" + escapeJson(outTradeNo) + "\"}"
        );

        AlipayTradeQueryResponse response = alipayClient.execute(request);

        if (!"10000".equals(response.getCode())) {
            // 接口异常不等于交易失败,交给重试或人工核查流程
            return PayResult.UNKNOWN;
        }

        String tradeStatus = response.getTradeStatus();

        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            return PayResult.SUCCESS;
        }

        if ("WAIT_BUYER_PAY".equals(tradeStatus)) {
            return PayResult.PAYING;
        }

        if ("TRADE_CLOSED".equals(tradeStatus)) {
            return PayResult.CLOSED;
        }

        return PayResult.UNKNOWN;
    }

    private String escapeJson(String value) {
        return value.replace("\\", "\\\\")
                    .replace("\"", "\\\"");
    }

    enum PayResult {
        SUCCESS,
        PAYING,
        CLOSED,
        FAILED,
        UNKNOWN
    }
}

实际项目中,建议使用 SDK 提供的业务模型类或可靠的 JSON 序列化库生成 biz_content,避免手工拼接 JSON。

轮询次数和间隔可以由业务层控制:

int maxAttempts = 10;
long intervalMillis = 2000L;

for (int attempt = 0; attempt < maxAttempts; attempt++) {
    PayResult result = queryTrade(outTradeNo);

    if (result == PayResult.SUCCESS) {
        markOrderPaidIdempotently(outTradeNo);
        break;
    }

    if (result == PayResult.CLOSED || result == PayResult.FAILED) {
        markOrderFailed(outTradeNo);
        break;
    }

    Thread.sleep(intervalMillis);
}

示例中的查询次数和间隔只用于说明流程,并非支付宝的官方要求。生产环境需要结合收银台超时时间、接口限流和用户体验进行配置,同时避免长期占用 Web 请求线程。后续查询更适合交给后台任务或延迟队列处理。

还需要检查的项目

如果业务代码打印日志后完全没有进入查询逻辑,可以重点排查以下问题:

  1. 代码是否只处理了 code=10000,导致 10003 进入未处理分支。
  2. 是否调用了 SDK 示例中的轮询方法,但线程提前结束,或者异常被吞掉。
  3. 是否在异步线程中执行查询,却没有记录线程异常。
  4. HTTP、线程池或业务超时时间是否设置得过短。
  5. out_trade_no 是否已正确保存和传递。
  6. 是否把 10003 当作普通错误直接 return。
  7. 是否完整记录了 code、sub_code、sub_msg、out_trade_no 和 trade_no。
  8. 查询接口返回后,代码是否正确读取了 trade_status,而不是只检查 isSuccess()。

注意事项

支付状态更新必须具备幂等性。同一个订单可能通过主动查询、异步通知或补偿任务被多次确认,但重复确认不能导致重复发货、重复记账或重复增加余额。

网络超时同样不表示支付失败。只要支付请求可能已经到达支付宝,就不能直接创建新的商户订单再次扣款,必须先查询原订单。

如果把异步通知作为补充,还需要验签,并核对 app_id、out_trade_no、金额和收款方信息。不过,收银台不能只依赖异步通知,还要通过主动查询确认最终交易状态,形成完整闭环。

这段日志是正常的“支付处理中”响应。需要修正的是业务状态机:收到 10003 后进入查询流程,查询超时后撤销交易。只要结果还不明确,就应继续核实,不能直接按支付失败处理。

备注:内容仅供参考。