App 支付前端返回 60029 错误如何排查?
结论
该错误是支付参数在前端传递时被解析成了 HTML 实体,导致参数名发生变化:
¬ify_url → ¬ify_url
×tamp → ×tamp
支付宝收到的订单字符串与服务端签名时使用的原始字符串不同,因此可能出现参数缺失、格式错误或验签失败,最终由 App 支付 SDK 返回 60029。
这个问题与请求耗时无关。0.051526s 只表示请求生成或处理所用的时间,不能说明参数是否合法。
为什么参数会变成这样
问题中的订单字符串包含两个异常片段:
...20230209095607562387¬ify_url=...
...paytype=alipay×tamp=2023-02-09...
正确内容应为:
...20230209095607562387¬ify_url=...
...paytype=alipay×tamp=2023-02-09...
支付参数如果被直接写入 HTML,再经过模板、富文本组件或 innerHTML 处理,浏览器可能会把以 ¬、× 开头的内容当作 HTML 实体:
¬ify_url
└─ ¬ 被解析为 ¬
×tamp
└─ × 被解析为 ×
解析后,完整的参数名便分别成了 ¬ify_url 和 ×tamp。
支付宝签名覆盖请求参数。任何字符发生变化,都会造成签名不匹配。即使 sign 本身没有被修改,只要参与验签的参数变了,整个订单字符串就会失效。
正确的排查方法
1. 对比服务端和客户端的原始订单字符串
分别记录以下三个位置的数据:
- 服务端签名前的待签名字符串;
- 服务端签名后返回的完整
orderString; - 客户端调用支付宝 SDK 前的最终字符串。
重点检查这些内容:
notify_url
timestamp
biz_content
sign
+
%
&
=
如果服务端输出的是 ¬ify_url,客户端收到后却变成了 ¬ify_url,问题就出在接口序列化、页面模板渲染或前端取值过程中。
日志应记录原始字符串,不能只看浏览器渲染后的页面内容。浏览器开发者工具的 Elements 面板显示的是解析后的 DOM,不一定与接口的原始响应相同。排查时还要检查 Network 面板中的响应正文。
2. 检查接口响应类型
服务端最好通过 JSON 返回订单字符串,不要直接把它拼进 HTML:
{
"orderString": "app_id=2021003175613781&method=alipay.trade.app.pay&..."
}
响应头应与实际内容一致,例如:
Content-Type: application/json; charset=utf-8
前端从 JSON 字段中读取字符串,然后直接传给支付宝 SDK。
3. 检查是否使用了 innerHTML
下面的写法可能触发 HTML 实体解析,不适合保存或中转支付参数:
element.innerHTML = orderString;
const paymentParams = element.innerHTML;
应直接使用接口返回的字符串:
const response = await fetch('/api/alipay/create-order', {
method: 'POST'
});
const data = await response.json();
const orderString = data.orderString;
// 直接交给 App 支付插件或原生桥接层
payWithAlipay(orderString);
如果确实需要在页面上显示订单字符串,应使用 textContent:
element.textContent = orderString;
显示过的字符串不要再从 DOM 中取出并用作支付参数。
4. 检查模板引擎的输出方式
如果通过服务端模板把订单字符串注入 JavaScript,可能会受到 HTML 转义或反转义的影响。例如,不建议直接这样拼接:
<script>
const orderString = "{{ orderString }}";
</script>
更稳妥的方式是使用 JSON 序列化输出:
<script>
const orderString = JSON.parse('{{ orderStringJson }}');
</script>
具体写法取决于使用的模板引擎,但订单字符串必须作为数据传递,不能先按 HTML 解析,再从页面中提取。
更稳妥的方案是让前端调用接口获取 JSON,不要把支付字符串嵌入页面。
修正后的参数结构
异常部分至少应恢复为:
&return_url=https%3A%2F%2Fzy.nywuji.com%2Faddons%2Fepay%2Findex%2Freturnx%2Fpaytype%2Falipay%2Fout_trade_no%2F20230209095607562387
¬ify_url=https%3A%2F%2Fzy.nywuji.com%2Faddons%2Fepay%2Findex%2Fnotifyx%2Fpaytype%2Falipay
×tamp=2023-02-09+09%3A56%3A07
不能只在现有字符串中手动把 ¬ify_url 和 ×tamp 替换回来。参数发生变化后,原有的 sign 已不再可靠,必须根据修正后的完整参数重新生成签名和订单字符串。
服务端应按以下流程处理:
构造业务参数
→ 按支付宝接口规则生成待签名内容
→ 使用应用私钥执行 RSA2 签名
→ 对各参数执行规定的 URL 编码
→ 返回完整 orderString
→ 客户端原样调用支付宝 SDK
服务端返回示例
以下示例只展示传输方式,签名仍应由支付宝服务端 SDK 生成:
<?php
$orderString = $alipaySdk->sdkExecute($request);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(
['orderString' => $orderString],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
前端收到后直接使用:
async function createAlipayOrder() {
const response = await fetch('/api/alipay/create-order', {
method: 'POST',
headers: {
Accept: 'application/json'
}
});
if (!response.ok) {
throw new Error(`创建支付订单失败:HTTP ${response.status}`);
}
const { orderString } = await response.json();
if (
typeof orderString !== 'string' ||
orderString.includes('¬ify_url') ||
orderString.includes('×tamp')
) {
throw new Error('支付宝订单参数在传输过程中发生了异常转换');
}
return orderString;
}
还需要检查的事项
- 客户端不要重新排序、拆分、拼接或编码签名后的订单字符串。
- 不要对完整的
orderString再执行一次encodeURIComponent()。 sign中的+、/、=必须按照支付宝 SDK 的要求正确编码,避免重复编码或漏编码。biz_content必须是合法 JSON,金额、订单号和product_code应符合接口要求。app_id、应用私钥、支付宝公钥和应用环境必须相互对应,正式环境与沙箱环境不能混用。- App 支付必须使用
alipay.trade.app.pay,product_code通常为QUICK_MSECURITY_PAY。 - 修复传输问题后,必须重新创建订单并重新签名,不能继续使用示例中的旧
sign。 - 示例中包含完整的
app_id、业务域名和签名。对外分享排查日志时应先脱敏。私钥绝不能出现在前端、日志或问题截图中。
本例应先修复订单字符串被解析成 HTML 实体的问题,再重新生成签名。只有客户端调用 SDK 前的字符串与服务端生成的 orderString 逐字符一致,才需要继续排查密钥配置、应用签约状态和支付宝账户环境。
备注:内容仅供参考。
