PAYATHON 2026

Alipay Payment Gateway 的 notify URL 未收到响应

支付阿杰

结论

notify_url 是由支付宝服务器发起的异步 HTTP 回调,不依赖用户浏览器跳转。要正常接收通知,需要同时满足以下条件:

  • 创建支付请求时传入了完整的 notify_url。
  • 该 URL 可以从公网直接访问,不能使用 localhost、局域网 IP,也不能指向需要登录的页面。
  • 接口允许支付宝发送 POST 请求,并且不会被 CSRF、防火墙、反向代理或访问控制拦截。
  • 回调程序完成验签和业务处理后,直接返回纯文本 success。

支付成功只能说明交易已经完成,不能证明异步通知接口可以正常访问。

首先检查支付请求

以常见的 PHP RSA2 SDK 为例,notify_url 通常设置在支付请求对象上,不应放在业务参数 biz_content 中:

<?php

$request = new AlipayTradePagePayRequest();

$request->setReturnUrl('https://www.example.com/payment/return');
$request->setNotifyUrl('https://www.example.com/payment/notify');

$request->setBizContent(json_encode([
    'out_trade_no' => 'ORDER_202608310001',
    'product_code' => 'FAST_INSTANT_TRADE_PAY',
    'total_amount' => '99.00',
    'subject' => '订单支付',
], JSON_UNESCAPED_UNICODE));

$response = $aopClient->pageExecute($request);
echo $response;

不同产品和 SDK 版本使用的类名可能不同,具体应以当前 SDK 的支付接口为准。不过有一点不变:请求中必须包含一个能从公网访问的绝对 URL,例如:

https://www.example.com/payment/notify

以下写法不能使用:

http://localhost/payment/notify
http://127.0.0.1/payment/notify
http://192.168.1.10/payment/notify
/payment/notify

return_url 和 notify_url 的用途并不相同:

  • return_url:用户完成支付后,浏览器跳转到的页面。
  • notify_url:支付宝服务器主动调用的后台接口。

所以,即使用户关闭浏览器,也不会影响 notify_url。

编写异步通知接口

下面是一段简化的 PHP 示例。AlipaySignature::rsaCheckV1() 适用于部分常见的 PHP SDK。如果当前 SDK 提供了其他验签方法,需要换成对应的实现。

<?php

require_once __DIR__ . '/alipay-sdk/AopSdk.php';

// 不要输出调试信息、HTML 或 JSON。
$params = $_POST;

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

$alipayPublicKey = '支付宝公钥内容';

$verified = AlipaySignature::rsaCheckV1(
    $params,
    $alipayPublicKey,
    'RSA2'
);

if (!$verified) {
    http_response_code(400);
    echo 'failure';
    exit;
}

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

if ($outTradeNo === '') {
    http_response_code(400);
    echo 'failure';
    exit;
}

// 必须从数据库读取订单,并校验:
// 1. out_trade_no 对应的订单真实存在
// 2. total_amount 与订单金额一致
// 3. app_id、seller_id 等信息与商户配置一致
$order = findOrderByNumber($outTradeNo);

if (
    !$order ||
    bccomp((string) $order['amount'], (string) $totalAmount, 2) !== 0 ||
    $appId !== '你的支付宝应用 APPID'
) {
    http_response_code(400);
    echo 'failure';
    exit;
}

if ($tradeStatus === 'TRADE_SUCCESS' || $tradeStatus === 'TRADE_FINISHED') {
    // 使用事务或条件更新保证幂等,避免重复通知造成重复入账。
    markOrderPaidIfUnpaid($outTradeNo, $tradeNo);
}

// 处理成功后必须返回纯文本 success。
echo 'success';

其中,findOrderByNumber() 和 markOrderPaidIfUnpaid() 是业务层函数,需要根据实际使用的数据库实现。不要用回调参数直接拼接 SQL,应使用参数化查询。

排查回调没有到达的原因

1. URL 无法从公网访问

可以在另一台公网机器上测试:

curl -i -X POST https://www.example.com/payment/notify \
  -d "test=1"

需要逐项检查:

  • DNS 是否解析到了正确的服务器。
  • HTTPS 证书是否有效,证书链是否完整。
  • 80 或 443 端口是否开放。
  • CDN、WAF、安全组或防火墙是否拦截了请求。
  • URL 是否发生了不必要的 301、302 跳转。
  • 路由是否只接受 GET,没有开放 POST。
  • 接口是否要求登录、Cookie、验证码或固定的来源页面。

本地开发时,应通过受控的公网测试地址进行联调,不要把临时隧道地址直接用于生产环境。

2. 被 CSRF 或鉴权中间件拦截

支付宝不会携带网站用户的登录状态或 CSRF Token。通常需要将回调路由排除在浏览器登录验证和 CSRF 校验之外,但支付宝签名验证仍然必须保留。

例如,可以在框架中只为明确的通知路由配置 CSRF 例外:

/payment/notify

不要关闭整个网站的 CSRF 防护。

3. 程序读取参数的方式不正确

支付宝异步通知通常以表单形式通过 POST 发送。在 PHP 中一般这样读取:

$params = $_POST;

如果框架只读取 JSON 请求体,程序可能拿到空参数。需要确认接口支持:

application/x-www-form-urlencoded

验签时必须使用收到的完整通知参数。在验签之前,不要随意修改、重新编码或格式化字段值。

4. 验签配置错误

常见问题包括:

  • 把应用私钥误当成支付宝公钥。
  • 使用了其他环境的公钥,例如混用了沙箱和生产环境。
  • 签名方式配置为 RSA,实际请求却使用 RSA2,或者反过来。
  • 公钥内容复制不完整。
  • 验签前修改了参数值或字符编码。

只有验签成功后,才能更新订单。旧版接口可能还需要校验 notify_id,是否需要这一步取决于具体产品和 SDK,不能拿旧版示例代替当前接口文档。

5. 没有正确返回 success

业务处理成功后,响应体应保持简单:

success

不要返回:

{"status":"success"}

也不要输出 HTML、调试日志、PHP Warning、BOM 或其他字符。如果支付宝没有识别到成功响应,通常会按照相应接口的机制继续尝试通知,所以数据库更新必须具备幂等性。

建议的处理顺序

回调接口可以按下面的顺序处理:

  1. 接收完整的通知参数。
  2. 验证支付宝签名。
  3. 根据 out_trade_no 查询本地订单。
  4. 校验订单金额、app_id、收款方身份等重要字段。
  5. 检查 trade_status。
  6. 使用事务或条件更新,将订单标记为已支付。
  7. 记录 trade_no 和必要的审计信息。
  8. 返回纯文本 success。

不能只根据 trade_status 更新数据库,也不能在验签前信任通知中的订单号和金额。

日志与补偿机制

可以记录以下信息,但不要把私钥等敏感内容写入日志:

error_log(json_encode([
    'time' => date('c'),
    'out_trade_no' => $_POST['out_trade_no'] ?? null,
    'trade_no' => $_POST['trade_no'] ?? null,
    'trade_status' => $_POST['trade_status'] ?? null,
    'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
], JSON_UNESCAPED_UNICODE));

如果日志中完全没有收到请求,应先检查 notify_url 配置和网络可达性。如果日志中有请求,但处理结果失败,则需要检查路由、中间件、验签配置和程序异常。

异步通知也不应是唯一的支付状态确认方式。对于长时间停留在“待支付”状态的订单,可以调用对应的支付宝交易查询接口主动核实支付状态,用于补偿通知丢失或业务处理失败的情况。

备注:内容仅供参考。