PAYATHON 2026

预授权线上资金冻结调用 ap.TradePay() 报错误码 4000,如何解决?

支付老李

结论

这段 orderStr 最明显的异常是时间戳:

timestamp=2024-08-27+05%3A35%3A04

订单号 BZJ2408271324230485 表明,订单大概率创建于当天 13:24 左右,但时间戳是 05:35,相差约 8 小时。服务端很可能使用了 UTC 时间,没有按应用实际所在时区生成当前时间。签名请求时间超出支付宝允许的有效范围后,ap.tradePay 可能直接返回 4000,并提示 商家订单参数错误。

建议先修正服务端时区,再重新生成完整的 orderStr。还要确认前端调用的是大小写正确的 ap.tradePay,并且没有对 orderStr 再次编码、解码或重新拼接。

重点排查项

1. 检查服务端时间戳

支付宝开放平台接口要求 timestamp 使用以下格式:

yyyy-MM-dd HH:mm:ss

生成签名时必须使用当前有效时间。不要将时间写死,也不要直接把服务器的 UTC 时间格式化后输出。

Java 示例:

TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
formatter.setTimeZone(timeZone);

String timestamp = formatter.format(new Date());

如果使用支付宝 Java SDK,通常不必手工拼接公共参数,可以让 SDK 直接生成待支付字符串:

AlipayFundAuthOrderAppFreezeRequest request =
        new AlipayFundAuthOrderAppFreezeRequest();

request.setNotifyUrl("https://test5.sxds.com/api/pay/aliPayNotify");
request.setBizContent("""
{
  "out_order_no":"BZJ2408271324230485",
  "out_request_no":"BZJ2408271324230485",
  "order_title":"保证金",
  "amount":"0.01",
  "product_code":"PREAUTH_PAY",
  "timeout_express":"20m"
}
""");

String orderStr = alipayClient.sdkExecute(request).getBody();

每次测试时,都应重新生成订单号、请求号和 orderStr,不要反复使用已经过期的示例字符串。

2. 使用正确的 JSAPI 名称

支付宝 JSAPI 名称区分大小写,正确写法是:

ap.tradePay(...)

不能写成:

ap.TradePay(...)

基本调用方式如下:

<script src="https://appx/web-view.min.js"></script>
<script>
  function freeze(orderStr) {
    ap.tradePay(
      {
        orderStr: orderStr
      },
      function (result) {
        console.log("tradePay result:", result);
      }
    );
  }
</script>

需要引入哪种 JSAPI 桥接脚本,要以页面实际运行环境和支付宝当前接入文档为准。如果支付宝客户端已经向页面注入了 ap 对象,就不要重复加载不匹配的桥接库。

调用前可以先检查:

if (window.ap && typeof ap.tradePay === "function") {
  freeze(orderStr);
} else {
  console.error("当前环境未提供 ap.tradePay");
}

3. 原样传递 orderStr

服务端返回的 orderStr 已经完成参数编码和签名。前端需要将其作为一个完整字符串传入,不能再次执行以下操作:

encodeURIComponent(orderStr);
decodeURIComponent(orderStr);
new URLSearchParams(orderStr).toString();

也不要解析参数后重新排序或拼接字符串。下面这种处理就可能破坏签名:

const params = new URLSearchParams(orderStr);
const rebuiltOrderStr = params.toString();

ap.tradePay({
  orderStr: rebuiltOrderStr
});

建议让服务端返回 JSON:

{
  "orderStr": "app_id=...&biz_content=...&sign=...&sign_type=RSA2..."
}

前端拿到响应后直接使用:

const response = await fetch("/api/alipay/preauth/freeze", {
  method: "POST"
});

const data = await response.json();

ap.tradePay(
  {
    orderStr: data.orderStr
  },
  function (result) {
    console.log(result);
  }
);

如果将 orderStr 放进 HTML 属性、URL 查询参数或模板代码,&、+、% 等字符很容易被转义。通过接口的 JSON 响应传递会更合适。

4. 确认后端生成方式正确

在 alipay.fund.auth.order.app.freeze 场景中,后端需要生成供客户端调用的签名订单串。使用 Java SDK 时,一般通过以下方法取得 orderStr:

alipayClient.sdkExecute(request)

不要先把请求发送到支付宝网关,再将网关响应当作 orderStr 传给前端。普通接口响应中的 JSON 内容也不能代替签名订单串。

5. 检查签名和应用配置

如果修正时间后仍然返回 4000,可以继续检查:

  • app_id 是否属于当前已经签约并开通预授权能力的应用。
  • 应用私钥是否与支付宝开放平台配置的公钥或应用证书匹配。
  • 是否使用 RSA2,签名算法配置是否一致。
  • 是否混用了沙箱应用、正式应用和支付宝客户端环境。
  • product_code 是否仍为 PREAUTH_PAY。
  • out_order_no 和 out_request_no 是否符合接口要求并保持唯一。
  • 服务端生成 orderStr 后,内容是否被网关、模板引擎或前端框架修改。
  • 手机系统时间、服务器时间和时区是否准确。

建议的排查顺序

  1. 打印服务端当前时间、时区以及生成的 timestamp。
  2. 将服务器时区修正为 Asia/Shanghai,重新生成订单。
  3. 使用新的 out_order_no 和 out_request_no。
  4. 确认调用名称是 ap.tradePay。
  5. 将服务端返回的 orderStr 原样传入,不做二次编码。
  6. 对比 签名前的参数 和 前端实际收到的字符串,确认两者逐字符一致。
  7. 如果仍然失败,再检查应用签约、密钥或证书,以及正式环境和沙箱环境是否匹配。

从题目给出的参数来看,应先检查相差约 8 小时的 timestamp。这是当前字符串中最直观的异常,也最值得优先验证。

备注:内容仅供参考。