支付宝沙箱 alipay.trade.app.pay 提示商家订单参数异常
明确结论
从题目列出的 biz_content 来看,各字段没有明显违反 alipay.trade.app.pay 的要求:
product_code使用QUICK_MSECURITY_PAY,配置正确。total_amount为"0.01",格式正常。out_trade_no、subject、body和timeout_express也没有明显问题。
问题更可能出在业务参数以外的环节:
- 请求没有正确生成或携带
sign。 - 使用证书模式时缺少
app_cert_sn或alipay_root_cert_sn。 - 应用私钥与应用公钥证书不属于同一密钥对。
- 服务端返回的 App 支付订单串被解析、重新拼接或重复 URL 编码。
- App 端没有将完整订单串原样传给支付宝客户端 SDK。
- 沙箱应用没有正确开通或配置 App 支付能力。
- 如果仍在使用示例中的
2023-09-12 16:53:35,该时间戳已经失效,必须换成当前时间。
预创建接口能够成功,只能说明网关地址、app_id 和部分签名配置基本可用,不能证明 App 支付订单串的生成和客户端调用流程没有问题。
首先检查完整请求参数
最终提交给支付宝的请求不能只有题目列出的字段,至少还要包含 sign。如果采用证书模式,请求中还会带有证书序列号参数。
典型的证书模式请求参数如下:
app_id
method
format
charset
sign_type
timestamp
version
biz_content
app_cert_sn
alipay_root_cert_sn
sign
各参数的作用如下:
sign:使用应用私钥对待签名字符串进行 RSA2 签名。app_cert_sn:应用公钥证书序列号。alipay_root_cert_sn:支付宝根证书序列号。- 支付宝公钥证书用于验签,并不是应用请求签名时使用的私钥。
如果日志只是手工打印了签名前的参数,没有看到 sign 不一定代表实际请求缺少签名。需要查看 SDK 最终生成的完整订单串才能确认。
正确理解四个证书或密钥文件
证书模式通常需要以下内容:
应用私钥
应用公钥证书
支付宝公钥证书
支付宝根证书
应用私钥本身不是证书。它必须与生成应用公钥证书时使用的公钥属于同一密钥对。
常见的配置错误有:
- 下载了新证书,但程序仍在使用旧的应用私钥。
- 重新生成密钥后只替换了私钥,没有重新上传公钥或下载证书。
- 混用了”支付宝公钥”字符串模式和”支付宝公钥证书”模式。
- 混用了沙箱证书和正式环境证书。
- 配置文件路径指向另一套应用的证书。
- 私钥头尾、空格或换行处理有误。
使用证书模式时,建议由支付宝官方 SDK 计算证书序列号,不要自行拼接 app_cert_sn 和 alipay_root_cert_sn。
使用 SDK 生成 App 支付订单串
以 Java SDK 的常见写法为例。使用证书模式时,应先完成证书配置,再调用 sdkExecute:
CertAlipayRequest certRequest = new CertAlipayRequest();
certRequest.setServerUrl("https://openapi-sandbox.dl.alipaydev.com/gateway.do");
certRequest.setAppId("9021000127609612");
certRequest.setPrivateKey(appPrivateKey);
certRequest.setFormat("json");
certRequest.setCharset("UTF-8");
certRequest.setSignType("RSA2");
certRequest.setCertPath("/path/to/appCertPublicKey.crt");
certRequest.setAlipayPublicCertPath("/path/to/alipayCertPublicKey_RSA2.crt");
certRequest.setRootCertPath("/path/to/alipayRootCert.crt");
AlipayClient alipayClient = new DefaultAlipayClient(certRequest);
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("AC20123444");
model.setProductCode("QUICK_MSECURITY_PAY");
model.setSubject("测试01");
model.setTotalAmount("0.01");
model.setBody("描述1233455");
model.setTimeoutExpress("30m");
request.setBizModel(model);
// 如需异步通知,可设置公网可访问的 HTTPS 地址
// request.setNotifyUrl("https://example.com/alipay/notify");
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
if (response.isSuccess()) {
String orderInfo = response.getBody();
System.out.println(orderInfo);
} else {
System.out.println(response.getCode());
System.out.println(response.getSubCode());
System.out.println(response.getMsg());
System.out.println(response.getSubMsg());
}
不同版本的支付宝 Java SDK 在类构造方式上可能略有差异,应以项目当前使用的 SDK 接口为准。需要确认的是:
- 使用沙箱网关。
- 使用当前沙箱应用对应的私钥和证书。
- 通过证书模式初始化客户端。
- 使用
sdkExecute()生成客户端支付订单串。
不要把 alipay.trade.app.pay 当作普通服务端接口,通过 execute() 获取 JSON 后再自行拼接支付参数。
App 端必须原样使用订单串
服务端生成的 response.getBody() 通常是一整段已签名、已编码的查询字符串,例如:
app_id=...&biz_content=...&charset=UTF-8&method=alipay.trade.app.pay&sign=...&sign_type=RSA2×tamp=...&version=1.0
客户端需要将这段字符串完整传给支付宝 SDK。
Android 示例:
PayTask payTask = new PayTask(activity);
Map<String, String> result = payTask.payV2(orderInfo, true);
不要对订单串做以下处理:
不要把订单串解析成 Map 后重新排序
不要自行修改 biz_content
不要删除 sign 中的字符
不要对整个订单串再次 URLEncoder.encode
不要先 URLDecoder.decode 再传给支付宝 SDK
不要把字符串中的 + 自动替换为空格
不要只把 biz_content 传给 payV2
sign 通常已经经过 URL 编码。再次编码会把 % 变成 %25,最终造成签名校验失败。此时客户端往往只会显示”商家订单参数异常”之类的概括性提示。
如果订单串通过 JSON 返回给 App,需要确保传输过程中只发生了正常的 JSON 字符串转义。App 解析 JSON 后得到的内容,应与服务端的 response.getBody() 完全一致。
推荐的排查顺序
1. 检查环境是否一致
确认以下内容全部来自同一个沙箱应用:
app_id
应用私钥
应用公钥证书
支付宝公钥证书
支付宝根证书
沙箱网关地址
沙箱网关通常应配置为:
https://openapi-sandbox.dl.alipaydev.com/gateway.do
不要混用正式环境的网关或证书。
2. 检查时间戳
示例中的时间戳:
2023-09-12 16:53:35
只能作为历史日志,不能继续写死在请求中。实际请求必须动态生成当前时间:
String timestamp =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
还要检查服务器的时区和系统时间是否正确。
3. 检查最终订单串
不要只记录 biz_content,还应检查 SDK 生成的完整订单串,确认其中包含:
method=alipay.trade.app.pay
product_code=QUICK_MSECURITY_PAY
sign_type=RSA2
sign=...
在证书模式下,还要确认 SDK 已正确处理:
app_cert_sn
alipay_root_cert_sn
私钥和完整签名属于敏感信息,不应写入生产日志或对外提供。
4. 对比服务端与 App 收到的内容
分别记录以下信息:
- 服务端
sdkExecute()返回内容的长度和摘要。 - App 收到的订单串长度和摘要。
- 调用
payV2()前的订单串长度和摘要。
可以通过 SHA-256 摘要进行比对,避免打印完整支付参数:
String digest = DigestUtils.sha256Hex(orderInfo);
如果三处摘要不同,说明订单串在传输或处理过程中发生了变化。
5. 查看客户端 SDK 的完整返回结果
不要只展示弹窗文案,还应记录 payV2() 返回的完整 Map,重点检查:
resultStatus
memo
result
例如:
Map<String, String> result = payTask.payV2(orderInfo, true);
Log.d("Alipay", "resultStatus=" + result.get("resultStatus"));
Log.d("Alipay", "memo=" + result.get("memo"));
Log.d("Alipay", "result=" + result.get("result"));
这些信息通常比”商家订单参数异常”更有助于定位问题。记录日志时要做好脱敏,避免泄露完整签名和交易信息。
6. 检查沙箱账号和应用能力
App 支付需要使用支付宝沙箱提供的买家账号和对应的沙箱客户端环境。同时还要在沙箱应用后台确认 App 支付能力已经开通,签约状态正常。
如果预创建成功,但 App 支付始终失败,应重点比较两个产品的能力开通状态。预创建接口可用,并不代表 App 支付能力也一定可用。
参数本身的建议
当前 biz_content 可以整理为:
{
"out_trade_no": "AC20123444",
"product_code": "QUICK_MSECURITY_PAY",
"subject": "测试01",
"total_amount": "0.01",
"body": "描述1233455",
"timeout_express": "30m"
}
这些字段没有明显问题,但还需要保证:
out_trade_no在当前商户应用范围内唯一。- 金额使用字符串形式,单位为元。
subject不为空。- JSON 使用 UTF-8 编码。
- JSON 由序列化库生成,不要手工处理引号和转义字符。
- 每次重新测试时最好使用新的
out_trade_no,以排除旧订单状态的干扰。
最可能的修正方案
建议按以下顺序调整:
- 使用官方 SDK 的证书模式客户端。
- 确认应用私钥与应用公钥证书匹配。
- 使用当前时间,不要写死
timestamp。 - 通过
sdkExecute()生成 App 支付订单串。 - 将
response.getBody()原样返回给 App。 - App 端直接把完整订单串传给
payV2(),不要重复编码或重新拼接。 - 检查沙箱应用的 App 支付能力和沙箱买家环境。
如果完成这些检查后仍然失败,需要结合客户端 payV2() 的完整返回值、SDK 生成的订单串参数名,以及支付宝开放平台侧的请求日志继续定位。仅凭当前弹窗还无法确定具体是哪个字段出错,但从现有业务参数来看,应优先检查签名、证书序列号和订单串的传递过程。
备注:内容仅供参考。
