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 响应。
备注:内容仅供参考。
