PAYATHON 2026

支付宝沙箱支付失败时,notify 回调的 request 包含什么?

支付小周

结论

支付宝异步通知没有通用的“支付失败回调”。

用户输错密码、余额不足、主动取消支付、关闭收银台,或者支付渠道暂时不可用时,通常不会收到 trade_status 为 TRADE_FAILED 之类的 notify。订单可能继续处于 WAIT_BUYER_PAY,之后再因超时或主动关单变为 TRADE_CLOSED。实际会通知哪些状态,取决于使用的支付产品和接口,需以对应文档为准。

因此,不能靠等待失败通知来判断支付是否失败。处理时应遵循以下原则:

  • 同步接口只用于判断请求是否被成功受理;
  • 异步通知用于确认交易状态变化;
  • 没有收到通知或结果不明确时,调用 alipay.trade.query 主动查询;
  • 分别通过关单、超时、取消、同步接口报错和本地 Mock 测试失败分支;
  • 沙箱维护属于系统异常,不能直接视为订单支付失败。

支付失败时 notify 包含什么

需要先区分两种情况。

支付尝试没有成功

用户余额不足、密码错误、退出收银台或支付渠道异常时,可能根本不会产生异步通知。商户订单通常会继续保持待支付状态,用户之后仍可重新支付。

这种情况下,并不存在一个固定的“失败 notify request”可供处理。

交易被关闭

订单超时关闭,或者商户成功调用 alipay.trade.close 关闭交易后,异步通知中可能出现:

trade_status=TRADE_CLOSED

是否发送这类通知,以及通知中包含哪些字段,应以具体支付产品的文档为准。不同接口的行为不一定完全相同。

常见通知参数包括:

notify_time
notify_type
notify_id
app_id
charset
version
sign_type
sign
trade_no
out_trade_no
buyer_id
seller_id
trade_status
total_amount
receipt_amount
buyer_pay_amount
gmt_create
gmt_payment
gmt_close

这些字段不一定会在每次通知中全部出现。例如,未支付订单通常没有有效的 gmt_payment 和 receipt_amount。业务代码应按照官方文档区分必填字段与可选字段,不能仅因某个可选字段缺失就判定通知非法。

常见的 trade_status 包括:

  • WAIT_BUYER_PAY:交易已创建,等待买家付款;
  • TRADE_CLOSED:交易已关闭;
  • TRADE_SUCCESS:付款成功;
  • TRADE_FINISHED:交易完成,通常不可再退款。

除非当前支付产品的官方文档明确提供了 TRADE_FAILED,否则不要自行增加这个状态分支。

如何测试失败场景

1. 测试同步接口失败

可以使用无效参数、错误签名或不符合接口约束的请求,测试支付宝接口返回错误时的处理逻辑。

这类错误发生在请求阶段,通常通过同步响应中的 code、sub_code、msg 和 sub_msg 表达,不属于异步支付失败通知。

测试结束后应恢复正确配置,避免故意设置的错误参数影响其他测试。

2. 用户进入收银台后取消

创建订单并进入沙箱收银台后,不完成付款,直接退出或返回商户页面。

可以借此验证:

  • 前端返回页面不会直接将订单标记为成功;
  • 商户订单仍处于待支付状态;
  • 没有收到异步通知时,系统不会误判为失败或成功;
  • 用户可以在订单有效期内重新支付。

退出收银台只表示本次付款没有完成,不一定说明支付宝交易已经关闭。

3. 主动关闭未支付订单

先创建一笔未支付订单,再调用:

alipay.trade.close

然后调用:

alipay.trade.query

确认交易状态是否变为 TRADE_CLOSED。这种方法适合测试商户侧的关单逻辑,但不能代替银行卡扣款失败等真实的渠道异常。

4. 测试订单超时

如果当前接口支持设置超时时间,可以让订单在未付款状态下自然超时,再查询最终状态。

是否会收到 TRADE_CLOSED 通知、订单多久关闭,以及超时参数有哪些限制,都与具体支付产品有关。应以当前接口文档和沙箱实际支持的能力为准。

5. 在测试环境 Mock 状态

渠道级失败通常难以在沙箱中稳定复现。更可靠的做法是在商户测试环境中增加支付网关适配层,由测试桩返回不同结果,例如:

public enum PaymentResult {
    SUCCESS,
    PENDING,
    CLOSED,
    REQUEST_ERROR,
    UNKNOWN
}

业务测试时分别注入这些结果,检查以下逻辑:

  • 支付成功后更新订单;
  • 待支付时允许用户重新发起支付;
  • 订单关闭后禁止继续支付;
  • 请求异常时进入重试流程;
  • 状态未知时主动查询;
  • 收到重复通知时不会重复记账。

Mock 只用于测试商户内部逻辑。不要伪造支付宝签名并将请求发送到正式回调地址。

notify 的推荐处理方式

以下 Java Servlet 风格代码展示了核心处理流程。SDK 类名和方法可能因版本不同而变化,应以项目实际使用的支付宝 SDK 为准。

public void handleNotify(HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((key, values) -> {
        params.put(key, String.join(",", values));
    });

    boolean signVerified;
    try {
        signVerified = AlipaySignature.rsaCheckV1(
            params,
            ALIPAY_PUBLIC_KEY,
            "UTF-8",
            params.getOrDefault("sign_type", "RSA2")
        );
    } catch (AlipayApiException e) {
        response.getWriter().write("failure");
        return;
    }

    if (!signVerified) {
        response.getWriter().write("failure");
        return;
    }

    String appId = params.get("app_id");
    String outTradeNo = params.get("out_trade_no");
    String tradeNo = params.get("trade_no");
    String tradeStatus = params.get("trade_status");
    String totalAmount = params.get("total_amount");
    String sellerId = params.get("seller_id");

    Order order = orderService.findByOutTradeNo(outTradeNo);
    if (order == null) {
        response.getWriter().write("failure");
        return;
    }

    // 除验签外,还应核对通知是否属于当前商户和当前订单。
    if (!EXPECTED_APP_ID.equals(appId)
            || !EXPECTED_SELLER_ID.equals(sellerId)
            || order.getAmount().compareTo(new BigDecimal(totalAmount)) != 0) {
        response.getWriter().write("failure");
        return;
    }

    switch (tradeStatus) {
        case "TRADE_SUCCESS":
        case "TRADE_FINISHED":
            // 需要使用事务或唯一约束保证幂等,防止重复通知造成重复记账。
            orderService.markPaidIfUnpaid(
                outTradeNo,
                tradeNo,
                new BigDecimal(totalAmount)
            );
            break;

        case "TRADE_CLOSED":
            // 只有未支付订单才能直接关闭。
            // 若本地已经是已支付状态,应主动查询支付宝交易状态。
            orderService.markClosedIfUnpaid(outTradeNo);
            break;

        case "WAIT_BUYER_PAY":
            orderService.keepPending(outTradeNo);
            break;

        default:
            // 未识别状态不要直接更新订单,记录后通过查询接口确认。
            orderService.recordUnknownStatus(outTradeNo, tradeStatus);
            break;
    }

    // 必须在业务处理成功后返回纯文本 success。
    response.getWriter().write("success");
}

即使验签成功,也不能直接更新订单。至少还要核对:

  • app_id 是否属于当前应用;
  • seller_id 是否属于当前商户;
  • out_trade_no 是否存在;
  • total_amount 是否与商户订单金额一致;
  • trade_no 是否与已保存的支付宝交易号冲突;
  • 当前订单状态是否允许本次状态转换。

支付宝可能重复发送异步通知,因此支付状态更新必须具备幂等性。

没有 notify 时如何确认结果

支付请求超时、页面关闭或长时间没有收到通知时,应调用查询接口:

alipay.trade.query

查询时可以使用 out_trade_no,也可以使用已经取得的 trade_no。商户订单状态可以设计为:

PENDING
PAID
CLOSED
UNKNOWN

发生网络异常、支付宝维护或响应无法解析时,可以先将订单记为 UNKNOWN,或者继续保留 PENDING,之后再由定时任务查询确认。不能因为一次请求超时就立即把订单标记为支付失败,因为可能出现商户没有收到响应,但用户实际上已经付款的情况。

沙箱维护导致失败时怎么处理

沙箱维护表示测试环境暂时不可用,不是能够稳定复现和依赖的业务失败场景。处理方式与生产环境中的网络异常相近:

  1. 保存商户订单和请求流水;
  2. 不要立即认定订单未支付;
  3. 对明确可重试的查询请求进行有限次数的重试;
  4. 调用 alipay.trade.query 确认状态;
  5. 长时间无法确认时保留 UNKNOWN,待环境恢复后对账;
  6. 不要盲目重新创建相同的业务订单,以免产生重复交易;
  7. 查询确认未支付且不再需要收款时,再调用 alipay.trade.close。

最终支付结果应以验签通过的异步通知、主动查询结果和后续对账结果为准。沙箱页面提示、前端跳转结果或单次接口超时,都不能作为最终判断依据。

备注:内容仅供参考。