Alipay APP支付参数中的notify_url和timestamp显示异常
结论
¬ify_url 和 ×tamp 基本可以确定是支付参数进入 HTML 解析环境后发生的显示或传输异常:
- 浏览器把
¬ify_url开头的¬识别成了 HTML 实体¬ - 浏览器把
×tamp开头的×识别成了 HTML 实体×
支付宝 SDK 正常生成的参数应该是:
¬ify_url=...
×tamp=...
问题通常与 RSA2 密钥无关,而是生成结果经过 HTML 页面、富文本组件、模板引擎或前端 DOM 时,被浏览器当作实体解析了。
处理时要区分两种情况:
- 只在页面上展示参数时,必须进行 HTML 转义。
- 将参数交给 APP 支付时,不要做 HTML 实体转义或解码。应把
sdkExecute()返回的原始字符串直接传给 APP。
如果 APP 实际收到的是 ¬ify_url 或 ×tamp,参数名和签名内容都已经被破坏,出现“参数错误”是正常的。
为什么会变成这两个字符
HTML 中有以下命名字符引用:
¬ → ¬
× → ×
部分 HTML 解析场景允许省略实体末尾的分号,所以:
¬ify_url
可能会被解析成:
¬ + ify_url
页面最终显示为:
¬ify_url
同样:
×tamp
可能会被解析成:
× + tamp
最终显示为:
×tamp
这不是 URL 编码错误,也不是支付宝修改了参数名,而是字符串进入 HTML 环境后产生的解析结果。
正确处理方式
1. 服务端直接把参数返回给 APP
在 APP 支付场景中,sdkExecute() 返回的是供客户端支付宝 SDK 使用的 orderString。不要把它当作普通网关接口的响应对象处理,也不要先做 URL 解码、HTML 解码或手动重新拼接。
可以通过 JSON 返回:
<?php
$aop = new AopClient();
$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = 'your_app_id';
$aop->rsaPrivateKey = 'your_private_key';
$aop->alipayrsaPublicKey = 'alipay_public_key';
$aop->apiVersion = '1.0';
$aop->signType = 'RSA2';
$aop->postCharset = 'UTF-8';
$aop->format = 'json';
$object = new stdClass();
$object->out_trade_no = '202609090001';
$object->total_amount = '0.01';
$object->subject = '测试商品';
$object->product_code = 'QUICK_MSECURITY_PAY';
$object->time_expire = '2026-09-09 23:00:00';
$request = new AlipayTradeAppPayRequest();
$request->setNotifyUrl('https://example.com/alipay/notify');
$request->setBizContent(
json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
$orderString = $aop->sdkExecute($request);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(
[
'code' => 0,
'order_string' => $orderString,
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
客户端解析 JSON 后,直接把 order_string 交给支付宝 APP SDK,不要执行这些操作:
htmlspecialchars_decode($orderString);
urldecode($orderString);
parse_str($orderString, $params);
也不要拆分、排序或重新编码参数。签名对应的是 SDK 生成订单字符串时的参数形式,改动后可能无法匹配。
2. 只在 HTML 页面中展示
如果只是为了调试和展示,应先转义 HTML 特殊字符:
<pre><?= htmlspecialchars(
$orderString,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
) ?></pre>
转义后,源码中的 & 会变成 &。浏览器仍会显示普通的 &,但不会把 ¬ 或 × 当作实体处理。
htmlspecialchars() 只能用于 HTML 输出层。转义后的字符串不能直接交给支付宝 APP SDK。
错误示例:
echo '<div>' . $orderString . '</div>';
正确示例:
echo '<pre>';
echo htmlspecialchars(
$orderString,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
echo '</pre>';
3. 前端写入页面时使用 textContent
如果支付参数由接口返回给浏览器,调试展示时不要使用 innerHTML:
// 错误:会触发 HTML 实体解析
document.querySelector('#result').innerHTML = orderString;
// 正确:按纯文本显示
document.querySelector('#result').textContent = orderString;
实际拉起支付时,直接使用接口返回的原始字段:
const response = await fetch('/create-alipay-order');
const data = await response.json();
const orderString = data.order_string;
// 将 orderString 原样交给对应的 APP 支付桥接层
原代码中还需要修正的地方
sdkExecute() 的返回值处理不适用于 APP 支付
原代码把结果当成普通网关接口的响应对象处理:
$result = $aop->sdkExecute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
生成 APP 支付参数时,sdkExecute() 的主要返回值是已经签名的订单字符串,不是包含 alipay_trade_app_pay_response->code 的响应对象。这里应直接接收并返回字符串:
$orderString = $aop->sdkExecute($request);
不能在生成订单字符串时通过检查 code == 10000 判断支付是否成功。最终支付结果需要结合以下信息确认:
- APP SDK 的同步返回结果
- 支付宝异步通知
- 必要时主动调用交易查询接口
setNotifyUrl('') 不会产生有效回调地址
原代码中写的是:
$request->setNotifyUrl('');
空字符串不是有效的异步通知地址,应换成支付宝服务器可以访问的完整公网 URL:
$request->setNotifyUrl('https://example.com/alipay/notify');
回调地址不能是本地地址,也不能依赖登录状态、Cookie、浏览器跳转或前端 JavaScript。该接口必须能够接收支付宝发来的服务端 POST 请求。
统一字符集
示例代码设置了:
$aop->postCharset = 'GBK';
但问题中的实际参数显示为:
charset=utf-8
字符集不一致未必会造成 ¬ify_url 和 ×tamp,却可能导致中文参数、验签或日志比对出现其他问题。除非现有系统明确要求 GBK,否则建议整个链路统一使用 UTF-8:
$aop->postCharset = 'UTF-8';
header('Content-Type: application/json; charset=UTF-8');
检查订单过期时间
示例参数中有:
time_expire=2022-08-01 22:00:00
timestamp=2022-11-30 09:53:06
订单失效时间早于请求时间,这组数据本身就是无效的。虽然它与 HTML 实体解析无关,但同样会导致订单无法正常创建或支付。测试时应使用晚于当前请求时间的有效值,也可以暂时不传 time_expire。
如何定位字符串在哪一层被修改
先在 sdkExecute() 返回后立即记录原始结果。完整签名和业务参数可能包含敏感信息,不要长期保存在生产日志中:
$orderString = $aop->sdkExecute($request);
error_log($orderString);
error_log(bin2hex($orderString));
重点确认原始字符串中是否有:
¬ify_url=
×tamp=
再依次检查以下位置:
sdkExecute()刚返回时的字符串- 服务端输出 HTTP 响应前的字符串
- 浏览器 Network 面板中的原始响应
- 前端解析 JSON 后的变量值
- 页面最终展示的内容
- APP 支付桥接层实际收到的字符串
根据检查结果可以判断问题所在:
- Network 响应正确,但页面显示错误,说明前端可能使用了
innerHTML或富文本渲染。 - PHP 日志正确,但 Network 响应错误,说明服务端模板或响应封装修改了字符串。
- 页面显示错误,但 APP 收到的原始值正确,说明只是展示异常,不影响支付。
- APP 收到
¬ify_url或×tamp,说明订单字符串已经在传输链路中损坏。必须修复链路并重新生成,不能只替换字符后继续使用。
不建议通过下面的方式修复已经发生字符替换的订单字符串:
$orderString = str_replace('¬ify_url', '¬ify_url', $orderString);
$orderString = str_replace('×tamp', '×tamp', $orderString);
字符串一旦经过解析、解码或重新编码,其他字符也可能已经发生变化,原来的 RSA2 签名未必还能匹配。应保留并传递 sdkExecute() 的原始返回值。
回调验签也要与订单参数问题分开排查
如果支付可以正常拉起,但异步通知验签或业务处理报错,需要单独检查回调逻辑。回调参数来自支付宝发送的 POST 请求,不能用创建订单时的参数字符串代替。
典型处理方式如下:
<?php
$aop = new AopClient();
$aop->alipayrsaPublicKey = 'alipay_public_key';
$aop->signType = 'RSA2';
$params = $_POST;
$verified = $aop->rsaCheckV1(
$params,
$aop->alipayrsaPublicKey,
'RSA2'
);
if (!$verified) {
echo 'failure';
exit;
}
if (($params['trade_status'] ?? '') === 'TRADE_SUCCESS') {
// 核对 app_id、out_trade_no、total_amount、seller_id 等业务字段。
// 确认订单未重复处理后,再更新本地订单状态。
}
echo 'success';
具体验签方法的签名可能因 SDK 包装方式而异,应以当前安装版本提供的方法定义为准。处理时还要注意:
- 使用支付宝公钥验签,不要使用应用公钥。
- 验签前不要对
$_POST整体执行htmlspecialchars()。 - 不要随意执行
urldecode(),也不要改动参数值。 - 验签通过后,仍需核对金额、商户、应用和订单号。
- 成功处理后返回纯文本
success,不要返回 HTML 页面。 - 支付宝可能重复发送回调,订单更新必须具备幂等性。
这个问题应先修复订单字符串的输出和传输方式。只有确认原始参数没有被修改,但仍然发生签名失败时,才需要继续检查密钥配置。
备注:内容仅供参考。
