PAYATHON 2026

Alipay 移动支付成功后未向 notify_url 发送通知

支付小周

结论

最值得先排查的是 notify_url。当前地址使用了非标准端口 8080,URL 中还带有路由查询参数:

http://publicFacingIP:8080/index.php?route=payment/alipay/callback

浏览器或本地 cURL 能访问,不代表支付宝的通知服务器也能访问。对于这类旧版 mobile.securitypay.pay 接口,建议先把异步通知地址改成公网可访问的标准 HTTP/HTTPS 地址,使用 80 或 443 端口,并尽量设置为不带查询参数的独立路径,例如:

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

resultStatus=9000 只说明客户端支付流程返回成功,不能代替服务器异步通知,也不能直接作为订单入账依据。

首先修改通知地址

可以通过 Nginx、Apache 或负载均衡器,将标准端口收到的请求转发到应用的 8080 端口:

server {
    listen 443 ssl;
    server_name pay.example.com;

    location = /alipay/notify {
        proxy_pass http://127.0.0.1:8080/index.php?route=payment/alipay/callback;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}

然后将支付参数中的地址改为:

notify_url="https://pay.example.com/alipay/notify"

同时确认以下事项:

  • 域名可以通过公网 DNS 解析。
  • 支付宝可以从公网直接访问服务器,不需要登录、Cookie、验证码或客户端证书。
  • 防火墙、安全组、WAF 和反向代理没有拦截外部 POST 请求。
  • HTTPS 证书链完整且有效。
  • 回调请求不会被强制跳转到登录页、错误页面或其他域名。
  • URL 没有使用内网 IP、临时域名、自签名证书或只能在本机解析的主机名。
  • 回调处理时间足够短,不要在返回响应前执行耗时任务。

旧接口对自定义端口、重定向和复杂 URL 的实际支持,可能受当时支付宝网关策略的影响。因此,使用标准端口和独立路径通常最稳妥。

检查 notify_url 是否真正进入签名请求

异步通知地址必须包含在提交给支付宝的业务参数中,并参与待签名字符串的生成。签名完成后不能再追加或修改该参数。

建议在生成订单信息的服务器上保存以下内容:

1. 签名前的参数集合
2. 排序后的待签名字符串
3. 最终交给移动端的完整字符串
4. 实际使用的 partner、seller_id、service 和 notify_url

生成待签名字符串时,通常需要:

  • 排除 sign、sign_type。
  • 排除空值参数。
  • 按接口规定的参数名顺序排序。
  • 保证参与签名的字符内容与最终提交的内容一致。
  • 遵循对应接口文档对 URL 编码、引号和字符集的要求。

不要只根据移动端日志中打印出的格式判断实际发送内容。日志里的 \" 可能只是字符串转义,也可能说明反斜杠被当作真实字符发送。需要检查原始字节或实际 HTTP 参数才能确认。

示例中的 partner、seller_id、订单号和商品描述看起来也有不一致之处。如果不是脱敏造成的,应确保:

partner      = 签约该移动支付产品的支付宝商户 PID
seller_id    = 与该签约账户匹配的收款方
out_trade_no = 商户系统内唯一且稳定的订单号
service      = mobile.securitypay.pay

正确记录回调请求

var_dump($_POST) 会写入回调响应,未必能在现有日志中看到。应将请求时间、请求头、原始请求体和解析后的参数写入服务器日志。

<?php

$log = [
    'time'        => date('c'),
    'method'      => $_SERVER['REQUEST_METHOD'] ?? '',
    'contentType' => $_SERVER['CONTENT_TYPE'] ?? '',
    'remoteAddr'  => $_SERVER['REMOTE_ADDR'] ?? '',
    'rawBody'     => file_get_contents('php://input'),
    'post'        => $_POST,
];

error_log(
    'Alipay notify: ' .
    json_encode($log, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);

除了 PHP 日志,还要检查接入层日志:

Nginx/Apache access log
反向代理日志
WAF 日志
云安全组或防火墙日志
PHP-FPM 日志
应用路由日志

如果接入层完全没有请求记录,问题通常出在 DNS、端口、防火墙、TLS,或者支付宝没有接受该 notify_url。如果接入层有请求,而 PHP 没有记录,则需要检查反向代理、路由、请求体大小限制和应用异常。

回调处理和验签

旧版接口通常通过 application/x-www-form-urlencoded POST 参数发送通知。处理程序应先保存原始通知,再进行验签和业务校验。

下面的代码只展示处理结构。具体签名算法必须以商户当时签约产品的接口文档为准。旧版接口常见 RSA/SHA1,但不能直接套用新版支付宝开放平台接口的处理方式。

<?php

$params = $_POST;

if (!$params) {
    http_response_code(400);
    echo 'fail';
    exit;
}

$sign = $params['sign'] ?? '';
unset($params['sign'], $params['sign_type']);

$params = array_filter(
    $params,
    static fn ($value) => $value !== '' && $value !== null
);

ksort($params);

$pairs = [];
foreach ($params as $key => $value) {
    $pairs[] = $key . '=' . $value;
}
$contentToVerify = implode('&', $pairs);

$alipayPublicKey = <<<KEY
-----BEGIN PUBLIC KEY-----
支付宝公钥内容
-----END PUBLIC KEY-----
KEY;

$verified = openssl_verify(
    $contentToVerify,
    base64_decode($sign, true),
    $alipayPublicKey,
    OPENSSL_ALGO_SHA1
);

if ($verified !== 1) {
    http_response_code(400);
    echo 'fail';
    exit;
}

$tradeStatus = $params['trade_status'] ?? '';
$outTradeNo  = $params['out_trade_no'] ?? '';
$totalFee    = $params['total_fee'] ?? '';
$partner     = $params['partner'] ?? '';
$sellerId    = $params['seller_id'] ?? '';

/*
 * 还必须从数据库读取订单,并校验:
 * - out_trade_no 是否存在
 * - 金额和币种是否一致
 * - partner、seller_id 是否属于本商户
 * - 订单是否已经处理
 * - trade_status 是否属于允许入账的成功状态
 *
 * 更新订单应放在数据库事务中,并以支付宝交易号或商户订单号保证幂等。
 */

echo 'success';

这里的签名拼接方式只是旧接口中的常见形式。不同 SDK 对参数解码、字符集和签名内容的处理可能不同。应优先使用与该接口版本匹配的支付宝官方服务端 SDK 或示例代码,不要自行猜测编码规则。

验签必须使用支付宝公钥,不能使用商户公钥或商户私钥。

必须返回纯文本 success

服务器完成验签和幂等处理后,应直接返回:

success

不要返回 HTML、JSON、调试信息,也不要附带多余空白。例如,下面的响应可能让支付宝认为通知处理失败:

<pre>array(...)</pre>
success

返回响应前也不要执行发送邮件、生成报表等耗时操作。推荐的处理顺序是:

接收通知 → 验签 → 校验订单 → 幂等更新 → 返回 success → 异步处理附加任务

如果没有按要求返回成功标记,支付宝通常会按照对应接口的重试策略再次发送通知。具体次数和间隔取决于接口版本。

不要依赖客户端的 9000

客户端返回值可能受到网络中断、客户端篡改和重复回调等因素影响,因此:

  • resultStatus=9000 可以用来更新客户端界面。
  • 不能只凭该值将服务端订单标记为已支付。
  • 服务端应以验签通过的异步通知,或主动查询到的交易状态为准。
  • 如果迟迟没有收到异步通知,应通过对应接口的订单查询能力进行补偿,不能让客户端直接上报”支付成功”。

还要确认商户账户确实签约并启用了 mobile.securitypay.pay 对应产品。钱包中的资金变化只能证明交易发生过,不能证明当前商户配置中的异步通知地址已经正确生效。

安全注意事项

不要将商户私钥打包进 Android 或 iOS 应用。更安全的做法是由服务端生成业务参数并完成签名,移动端只接收最终订单字符串,再调用支付宝 SDK。否则,攻击者可能在反编译应用后取得私钥,并伪造商户请求。

建议先从对外通知地址中移除 :8080 和查询参数,改用标准端口上的独立 HTTPS 路径。接着确认该地址在签名前已经写入参数,并通过接入层日志判断支付宝请求是否到达。完成这两项检查后,再继续排查验签、业务校验和 success 响应。

备注:内容仅供参考。