PAYATHON 2026

Alipay APP支付参数中的notify_url和timestamp显示异常

支付阿杰

结论

¬ify_url 和 ×tamp 基本可以确定是支付参数进入 HTML 解析环境后发生的显示或传输异常:

  • 浏览器把 &notify_url 开头的 &not 识别成了 HTML 实体 ¬
  • 浏览器把 &timestamp 开头的 &times 识别成了 HTML 实体 ×

支付宝 SDK 正常生成的参数应该是:

&notify_url=...
&timestamp=...

问题通常与 RSA2 密钥无关,而是生成结果经过 HTML 页面、富文本组件、模板引擎或前端 DOM 时,被浏览器当作实体解析了。

处理时要区分两种情况:

  • 只在页面上展示参数时,必须进行 HTML 转义。
  • 将参数交给 APP 支付时,不要做 HTML 实体转义或解码。应把 sdkExecute() 返回的原始字符串直接传给 APP。

如果 APP 实际收到的是 ¬ify_url 或 ×tamp,参数名和签名内容都已经被破坏,出现“参数错误”是正常的。

为什么会变成这两个字符

HTML 中有以下命名字符引用:

¬   → ¬
× → ×

部分 HTML 解析场景允许省略实体末尾的分号,所以:

&notify_url

可能会被解析成:

&not + ify_url

页面最终显示为:

¬ify_url

同样:

&timestamp

可能会被解析成:

&times + 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>

转义后,源码中的 & 会变成 &amp;。浏览器仍会显示普通的 &,但不会把 &not 或 &times 当作实体处理。

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));

重点确认原始字符串中是否有:

&notify_url=
&timestamp=

再依次检查以下位置:

  1. sdkExecute() 刚返回时的字符串
  2. 服务端输出 HTTP 响应前的字符串
  3. 浏览器 Network 面板中的原始响应
  4. 前端解析 JSON 后的变量值
  5. 页面最终展示的内容
  6. APP 支付桥接层实际收到的字符串

根据检查结果可以判断问题所在:

  • Network 响应正确,但页面显示错误,说明前端可能使用了 innerHTML 或富文本渲染。
  • PHP 日志正确,但 Network 响应错误,说明服务端模板或响应封装修改了字符串。
  • 页面显示错误,但 APP 收到的原始值正确,说明只是展示异常,不影响支付。
  • APP 收到 ¬ify_url 或 ×tamp,说明订单字符串已经在传输链路中损坏。必须修复链路并重新生成,不能只替换字符后继续使用。

不建议通过下面的方式修复已经发生字符替换的订单字符串:

$orderString = str_replace('¬ify_url', '&notify_url', $orderString);
$orderString = str_replace('×tamp', '&timestamp', $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 页面。
  • 支付宝可能重复发送回调,订单更新必须具备幂等性。

这个问题应先修复订单字符串的输出和传输方式。只有确认原始参数没有被修改,但仍然发生签名失败时,才需要继续检查密钥配置。

备注:内容仅供参考。