PAYATHON 2026

订单码支付如何获取支付回调?

支付小周

结论

订单码支付的异步结果应由支付请求中指定的 notify_url 接收,应用网关通常不能替代 notify_url。如果两个地址都没有收到通知,请优先检查:

  1. 创建订单时是否确实传入了 notify_url。
  2. 订单是否已经支付成功,而不只是完成了订单创建或扫码。
  3. notify_url 能否从公网直接访问。
  4. 回调接口是否按 POST 表单方式接收参数。
  5. 服务端完成验签和业务处理后,是否返回纯文本 success。
  6. 请求是否被反向代理、WAF、登录认证或程序异常拦截。

异步通知只应作为状态变更信号。无论有没有收到通知,最终支付状态都应通过主动查询接口确认。

应用网关与 notify_url 的区别

应用网关是开放平台调用应用服务的入口,并不是支付异步通知地址。支付结果通常会发送到本次支付请求中指定的 notify_url。

以支付宝当面付接口为例,使用 SDK 时应在请求对象上设置:

AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setNotifyUrl("https://pay.example.com/callback/alipay");
request.setBizContent(bizContent);

不能只在平台后台配置应用网关,也不要把 notify_url 放进 biz_content。具体的传参位置应以所用支付接口及对应 SDK 的文档为准。

排查步骤

1. 确认订单请求中包含 notify_url

记录创建订单时发送的参数,确认最终请求中确实包含完整的回调地址:

https://pay.example.com/callback/alipay

建议使用固定的 HTTPS 地址,避免使用:

  • localhost、127.0.0.1 或局域网 IP。
  • 临时地址或已经过期的内网穿透域名。
  • 需要登录、验证码或访问鉴权的地址。
  • 会发生多次 301、302 跳转的地址。
  • 必须依赖浏览器 Cookie 才能访问的接口。

如果 SDK 提供 setNotifyUrl 一类方法,应优先通过该方法设置。

2. 确认订单已经支付成功

“生成订单码”、“用户扫码”和”支付成功”是不同的交易状态。异步支付通知通常要等到交易状态发生相应变化后才会发送。

使用创建订单时的 out_trade_no 主动查询订单。支付宝场景可以调用:

alipay.trade.query

查询时重点核对:

  • out_trade_no 是否与创建订单时一致。
  • 使用的应用、商户和运行环境是否正确。
  • 订单是否已经达到支付成功状态。
  • 用户是否中途取消、支付失败,或者订单是否已经关闭。

如果主动查询也找不到订单,应先检查下单请求,不必继续排查回调接口。

3. 从公网验证回调地址

在服务器外部测试 notify_url 能否访问,同时确认网络链路没有拦截 POST 请求:

curl -i -X POST 'https://pay.example.com/callback/alipay' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'out_trade_no=test_order&trade_status=TRADE_SUCCESS'

至少检查以下内容:

  • DNS 是否解析到正确的公网地址。
  • HTTPS 证书是否有效,证书链是否完整。
  • 防火墙、安全组和 WAF 是否允许访问。
  • Nginx、网关或负载均衡是否把请求转发到了正确的服务。
  • 接口是否返回 404、403、405 或 500。
  • Web 框架是否因为 CSRF、防重放或登录认证而拒绝请求。

这条测试请求没有合法签名,只能用来检查网络和路由是否通畅,不能按真实支付通知处理。

4. 按表单方式接收通知

支付回调通常不是 JSON,而是 application/x-www-form-urlencoded 格式的 POST 请求,因此不能只读取 JSON 请求体。

Spring Boot 示例:

@PostMapping(
    value = "/callback/alipay",
    consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public ResponseEntity<String> alipayNotify(
        @RequestParam Map<String, String> params) {

    // 1. 验签
    // 2. 校验订单号、金额、商户身份和应用身份
    // 3. 幂等更新订单
    // 4. 成功后返回纯文本 success

    return ResponseEntity.ok()
            .contentType(MediaType.TEXT_PLAIN)
            .body("success");
}

如果框架获取到的是 Map<String, String[]>,验签前需要按照支付平台的要求转换参数,但不能擅自修改参数值、字符集或签名字段。

5. 正确执行验签

以支付宝 Java SDK 的 RSA2 验签方式为例:

boolean verified = AlipaySignature.rsaCheckV1(
        params,
        alipayPublicKey,
        "UTF-8",
        "RSA2"
);

验签通常使用支付宝公钥,或者采用与支付宝公钥证书相对应的验签方式,而不是应用私钥。普通公钥模式和证书模式的配置及验签方法可能不同,必须与应用当前使用的密钥模式一致。

验签失败时应重点检查:

  • 是否使用了错误环境的公钥。
  • 是否误把应用公钥当成支付宝公钥。
  • charset 和 sign_type 是否与通知参数一致。
  • 验签前是否对参数做过 URL 解码、拼接、裁剪或字符替换。
  • 是否遗漏了参数,或者错误处理了空值和多值参数。
  • 应用是否更换过密钥,而服务器仍在使用旧配置。

即使请求来源 IP 看起来可信,也不能跳过验签。

6. 验签后继续校验业务数据

验签成功并不表示可以直接把任意订单改成已支付。至少还要校验:

  • out_trade_no 是否存在,并且属于当前商户。
  • 通知中的金额是否与本地订单金额一致。
  • 收款方、应用或商户标识是否符合预期。
  • 当前交易状态是否属于业务认可的成功状态。
  • 订单是否已经处理过,以免重复发货或重复记账。

金额建议使用 BigDecimal 比较,不要使用 double:

BigDecimal expectedAmount = order.getAmount();
BigDecimal notifiedAmount = new BigDecimal(params.get("total_amount"));

if (expectedAmount.compareTo(notifiedAmount) != 0) {
    throw new IllegalStateException("支付金额不一致");
}

7. 返回平台要求的成功响应

业务处理完成后,应返回以下纯文本:

success

不要返回 JSON、HTML 页面或其他带包装的内容,例如:

{"code": 200, "data": "success"}

如果响应不符合要求,支付平台可能把这次通知判定为失败并重试。具体的重试策略以对应支付产品的当前文档为准。

推荐的完整处理结构

@PostMapping("/callback/alipay")
public ResponseEntity<String> handleNotify(
        @RequestParam Map<String, String> params) {

    try {
        boolean verified = AlipaySignature.rsaCheckV1(
                params,
                alipayPublicKey,
                "UTF-8",
                "RSA2"
        );

        if (!verified) {
            return ResponseEntity.ok("failure");
        }

        String outTradeNo = params.get("out_trade_no");
        String tradeStatus = params.get("trade_status");
        String totalAmount = params.get("total_amount");

        Order order = orderService.findByOutTradeNo(outTradeNo);
        if (order == null) {
            return ResponseEntity.ok("failure");
        }

        if (order.getAmount().compareTo(new BigDecimal(totalAmount)) != 0) {
            return ResponseEntity.ok("failure");
        }

        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            orderService.markPaidIdempotently(
                    outTradeNo,
                    params.get("trade_no")
            );
        }

        return ResponseEntity.ok("success");
    } catch (Exception e) {
        // 记录异常和通知标识,避免日志中泄露敏感信息
        return ResponseEntity.ok("failure");
    }
}

示例中的交易状态适用于常见的支付宝交易通知场景,实际处理时仍要以所调用接口的状态定义为准。生产环境还应校验商户、应用和收款方等身份字段。

仍收不到通知时的处理方式

如果公网访问、参数配置和订单状态都正常,但服务器日志里完全没有请求,可以继续检查:

  1. 从创建订单的请求日志中确认最终生效的 notify_url。
  2. 查看 CDN、WAF、负载均衡和 Nginx 的访问日志。
  3. 检查平台控制台是否提供异步通知记录或重发功能。
  4. 确认测试订单与应用、商户账号以及沙箱或生产环境完全对应。
  5. 使用订单查询接口补偿未收到的通知。

还可以增加定时补偿任务,主动查询长时间处于”待支付”状态的订单。查询到支付成功后,使用与回调相同的幂等逻辑更新订单。这样即使异步通知丢失、延迟或处理失败,已支付订单也不会长期停留在未支付状态。

备注:内容仅供参考。