PAYATHON 2026

支付宝异步通知报错,但 HTTP 状态码为 200,如何排查?

支付小周

结论

HTTP 200 OK 只能说明请求到达了 Web 服务,服务器也完成了 HTTP 层的响应。它不代表支付宝已经确认通知处理成功。

从给出的 curl -v 结果来看,响应头后面没有正文,接口很可能返回了空内容。支付宝异步通知通常要求商户完成验签和业务处理后,在响应正文中明确返回以下纯文本:

success

如果接口返回空字符串、HTML 页面、JSON、报错信息、重定向结果,或者在 success 前后夹带调试内容,即使 HTTP 状态码是 200,支付宝仍可能认为通知失败并继续重试。

为什么 200 OK 仍然会报错

异步通知成功通常要同时满足两个条件:

  1. HTTP 请求能够正常到达,接口返回可接受的 HTTP 状态码。
  2. HTTP 响应正文符合支付宝要求,通常应为纯文本 success。

当前测试只能证明以下几点:

  • 域名可以解析;
  • 18880 端口可以连接;
  • nginx 和 PHP 可以处理请求;
  • 接口返回了 HTTP 200。

但输出中没有正文:

< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
<

这说明 notify.php 处理 test=ok 时很可能没有输出任何内容。状态码为 200,不会让空响应自动成为成功确认。

另外,这次 curl 是从当前服务器主动访问接口,只验证了这一条网络路径,不能完全排除支付宝通知服务器访问时遇到以下情况:

  • 来源 IP 被防火墙、WAF 或安全组拦截;
  • 请求受到限流或命中反爬规则;
  • IPv4、IPv6 或不同 DNS 节点的解析结果不一致;
  • 接口处理时间过长,支付宝侧已经超时;
  • HTTP 请求被重定向到 HTTPS、登录页或错误页;
  • 真实通知参数触发了 PHP 异常,而 test=ok 没有进入相同的代码分支。

先检查真实响应正文

不要只看状态码,还要检查响应头和响应体:

curl -sS \
  -D /tmp/alipay_headers.txt \
  -o /tmp/alipay_body.txt \
  -w '\nHTTP_CODE=%{http_code}\nTIME_TOTAL=%{time_total}\n' \
  'http://alipay.example.com:18880/alipay/notify.php' \
  --data 'test=ok'

cat /tmp/alipay_headers.txt
xxd -g 1 /tmp/alipay_body.txt

理想情况下,响应体的字节内容应接近:

00000000: 73 75 63 63 65 73 73                             success

末尾换行通常未必会导致失败,但最稳妥的方式是只输出 success,不要附加 HTML、JSON、日志、空格或调试文本。

还要检查接口是否发生了重定向:

curl -sS -o /dev/null -D - \
  'http://alipay.example.com:18880/alipay/notify.php' \
  --data 'test=ok'

如果返回 301、302、307 或 308,应把支付宝通知地址直接配置为最终处理地址,不要依赖重定向。

正确的通知处理流程

接口收到通知后,应按以下顺序处理:

  1. 读取支付宝实际提交的表单参数。
  2. 使用支付宝 SDK 和支付宝公钥进行 RSA2 验签。
  3. 校验 app_id、商户身份、订单号、金额和交易状态。
  4. 根据 out_trade_no 进行幂等处理。
  5. 持久化本地业务处理结果。
  6. 所有步骤成功后,返回纯文本 success。
  7. 如果验签或业务处理失败,不要返回 success,以便继续排查或等待重试。

不能只看到 trade_status=TRADE_SUCCESS 就更新订单。签名、订单归属和金额都必须验证。

PHP 处理示例

下面的代码只展示处理结构。SDK 加载路径、公钥配置和业务字段需要按实际项目调整,不能直接照搬占位值:

<?php

declare(strict_types=1);

header('Content-Type: text/plain; charset=UTF-8');

require_once __DIR__ . '/vendor/autoload.php';

$params = $_POST;

if (empty($params)) {
    http_response_code(400);
    echo 'failure';
    exit;
}

try {
    $aop = new AopClient();
    $aop->alipayrsaPublicKey = '支付宝公钥内容';
    $aop->signType = 'RSA2';

    // 使用 SDK 验签,避免自行拼接待验签字符串。
    $verified = $aop->rsaCheckV1($params, null, 'RSA2');

    if (!$verified) {
        error_log('Alipay notify signature verification failed');
        echo 'failure';
        exit;
    }

    $expectedAppId = '实际应用 app_id';
    if (($params['app_id'] ?? '') !== $expectedAppId) {
        error_log('Alipay notify app_id mismatch');
        echo 'failure';
        exit;
    }

    $outTradeNo = $params['out_trade_no'] ?? '';
    $tradeNo = $params['trade_no'] ?? '';
    $tradeStatus = $params['trade_status'] ?? '';
    $totalAmount = $params['total_amount'] ?? '';

    if ($outTradeNo === '' || $tradeNo === '') {
        error_log('Alipay notify missing order identifier');
        echo 'failure';
        exit;
    }

    // 从数据库查询订单,并校验订单归属和金额。
    // 金额应使用十进制定点数或以“分”为单位比较,不要使用 float 直接比较。
    $order = findOrderByOutTradeNo($outTradeNo);

    if ($order === null) {
        error_log('Alipay notify order not found: ' . $outTradeNo);
        echo 'failure';
        exit;
    }

    if (!amountEquals($order['total_amount'], $totalAmount)) {
        error_log('Alipay notify amount mismatch: ' . $outTradeNo);
        echo 'failure';
        exit;
    }

    if ($tradeStatus === 'TRADE_SUCCESS' || $tradeStatus === 'TRADE_FINISHED') {
        // 必须幂等:同一通知重复到达时不能重复发货、重复记账。
        markOrderPaidIdempotently($outTradeNo, $tradeNo, $params);
    }

    echo 'success';
} catch (Throwable $e) {
    error_log('Alipay notify exception: ' . $e->getMessage());

    // 未完成业务处理时不要返回 success。
    http_response_code(500);
    echo 'failure';
}

不同版本的 SDK 可能采用不同的方法签名,应以项目实际安装版本的接口定义为准。验签时要使用 SDK 和原始通知参数,不要修改参数后再验签。

检查参数接收方式

支付宝异步通知一般通过 application/x-www-form-urlencoded 提交,因此应优先读取:

$params = $_POST;

不要只读取 URL 查询参数:

$params = $_GET; // 通常不适合作为异步通知的唯一参数来源

题目中的「通知内容」以完整 URL 加查询字符串的形式展示,但这可能只是平台日志的展示方式,不能据此认定支付宝真实请求使用了 GET。应结合 nginx 访问日志、应用日志或抓包结果,确认实际的请求方法、请求头和请求体。

如果框架统一按 JSON 解析请求,也要留意支付宝通知通常不是 JSON 请求:

$json = json_decode(file_get_contents('php://input'), true);

如果只按 JSON 读取,结果可能是 null。程序随后仍可能返回 200,却没有完成验签和订单处理。

检查 PHP 是否报错但仍返回 200

以下问题都可能让接口返回 HTTP 200,同时产生空响应或异常页面:

  • display_errors 已关闭,PHP 错误只写入日志;
  • 代码提前执行 return 或 exit,没有输出 success;
  • 验签失败的分支没有任何输出;
  • 数据库异常被捕获后,框架仍统一返回 200;
  • nginx 使用 error_page 把后端错误转换成 200;
  • 输出缓冲被清理;
  • PHP 文件含有 BOM、调试输出或框架模板包装;
  • 业务处理超时,支付宝侧断开后程序才执行完毕。

应检查通知时间附近的日志:

grep '11/Aug/2025:22:32' /var/log/nginx/access.log
grep '2025-08-11 22:32' /var/log/nginx/error.log

PHP-FPM 和应用日志的具体位置取决于实际部署配置。检查时需要确认:

  • 请求是否确实到达;
  • 请求方法和状态码是什么;
  • 返回了多少响应字节;
  • 总处理时间是多少;
  • PHP 是否出现异常;
  • 验签是否成功;
  • 订单更新是否提交成功。

日志中不要保存完整签名、用户标识或未经脱敏的支付数据。

用真实请求结构复现

test=ok 只能测试最基本的连通性,无法触发真实的验签、金额校验和订单更新逻辑。应在测试环境中使用完整且未脱敏的原始通知参数复现问题。

参数中包含 &、$、中文或 JSON 字符串时,不要直接拼接未经转义的 Shell 命令。可以使用单引号和 --data-urlencode:

curl -i -sS \
  'http://alipay.example.com:18880/alipay/notify.php' \
  --data-urlencode 'out_trade_no=测试订单号' \
  --data-urlencode 'trade_status=TRADE_SUCCESS' \
  --data-urlencode 'total_amount=0.01' \
  --data-urlencode 'sign_type=RSA2' \
  --data-urlencode 'sign=完整签名'

手工修改任何已签名参数都会导致验签失败。因此,要真正验证验签流程,必须使用同一次通知的完整原始参数。题目中已经打码的内容不能用于验签,也不能重新排列参数或错误解码参数值。

推荐的定位顺序

按以下顺序排查通常更快:

  1. 在 nginx 访问日志中找到 2025-08-11 22:32:00 附近的真实请求。
  2. 确认请求方法、状态码、响应字节数和处理耗时。
  3. 检查真实响应正文是否为纯文本 success。
  4. 排查重定向、WAF 拦截、超时和来源限制。
  5. 确认程序读取的是 POST 表单参数,而不是只读取 GET 或 JSON。
  6. 记录验签结果和失败原因。
  7. 校验 app_id、订单号、金额和交易状态。
  8. 检查数据库事务是否提交,业务逻辑是否抛出异常。
  9. 确认程序可以幂等处理重复通知,并再次返回 success。

根据现有信息,最明显的问题是接口虽然返回了 200 OK,但测试结果中没有出现 success 响应正文。应先修正并验证响应内容,再查看真实通知时刻的访问日志和应用日志,继续排查验签与业务处理流程。

备注:内容仅供参考。