预授权线上资金冻结调用 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后,内容是否被网关、模板引擎或前端框架修改。 - 手机系统时间、服务器时间和时区是否准确。
建议的排查顺序
- 打印服务端当前时间、时区以及生成的
timestamp。 - 将服务器时区修正为
Asia/Shanghai,重新生成订单。 - 使用新的
out_order_no和out_request_no。 - 确认调用名称是
ap.tradePay。 - 将服务端返回的
orderStr原样传入,不做二次编码。 - 对比
签名前的参数和前端实际收到的字符串,确认两者逐字符一致。 - 如果仍然失败,再检查应用签约、密钥或证书,以及正式环境和沙箱环境是否匹配。
从题目给出的参数来看,应先检查相差约 8 小时的 timestamp。这是当前字符串中最直观的异常,也最值得优先验证。
备注:内容仅供参考。
