PAYATHON 2026

App 支付前端返回 60029 错误如何排查?

支付老李

结论

该错误是支付参数在前端传递时被解析成了 HTML 实体,导致参数名发生变化:

&notify_url  →  ¬ify_url
&timestamp   →  ×tamp

支付宝收到的订单字符串与服务端签名时使用的原始字符串不同,因此可能出现参数缺失、格式错误或验签失败,最终由 App 支付 SDK 返回 60029。

这个问题与请求耗时无关。0.051526s 只表示请求生成或处理所用的时间,不能说明参数是否合法。

为什么参数会变成这样

问题中的订单字符串包含两个异常片段:

...20230209095607562387¬ify_url=...
...paytype=alipay×tamp=2023-02-09...

正确内容应为:

...20230209095607562387&notify_url=...
...paytype=alipay&timestamp=2023-02-09...

支付参数如果被直接写入 HTML,再经过模板、富文本组件或 innerHTML 处理,浏览器可能会把以 &not、&times 开头的内容当作 HTML 实体:

&notify_url
└─ &not 被解析为 ¬

&timestamp
└─ &times 被解析为 ×

解析后,完整的参数名便分别成了 ¬ify_url 和 ×tamp。

支付宝签名覆盖请求参数。任何字符发生变化,都会造成签名不匹配。即使 sign 本身没有被修改,只要参与验签的参数变了,整个订单字符串就会失效。

正确的排查方法

1. 对比服务端和客户端的原始订单字符串

分别记录以下三个位置的数据:

  1. 服务端签名前的待签名字符串;
  2. 服务端签名后返回的完整 orderString;
  3. 客户端调用支付宝 SDK 前的最终字符串。

重点检查这些内容:

notify_url
timestamp
biz_content
sign
+
%
&
=

如果服务端输出的是 &notify_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
&notify_url=https%3A%2F%2Fzy.nywuji.com%2Faddons%2Fepay%2Findex%2Fnotifyx%2Fpaytype%2Falipay
&timestamp=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 逐字符一致,才需要继续排查密钥配置、应用签约状态和支付宝账户环境。

备注:内容仅供参考。