支付宝沙箱模式 APP支付异常,服务端参数是否正确?
结论
从这组参数来看,URL 编码和 app_id 没有明显问题,但仅凭这些信息还不能确认请求正确。建议先检查两处:
biz_content中缺少 APP 支付通常需要的product_code=QUICK_MSECURITY_PAY。- 参数中的
timestamp是2023-02-14 10:53:59。如果前端一直重复使用这份旧参数,请求会因时间失效或订单号重复而出现异常。
签名是否正确也无法通过最终字符串直接判断,还需要核对沙箱应用公钥、应用私钥、签名前的原文,以及前端是否改动过服务端生成的订单字符串。
参数检查
解码后的 biz_content 为:
{
"out_trade_no": "102023021311172009394564435500",
"subject": "商品描述",
"total_amount": "2.00"
}
建议改成:
{
"out_trade_no": "每次支付新生成的唯一订单号",
"product_code": "QUICK_MSECURITY_PAY",
"subject": "商品描述",
"total_amount": "2.00"
}
其余参数的基本形式没有明显问题:
method=alipay.trade.app.pay:APP 支付接口正确。sign_type=RSA2:签名类型正确。charset=utf-8:字符集正确。version=1.0:接口版本正确。app_id=2021000122612036:与前端提供的沙箱 AppID 一致。notify_url:格式合法,但该地址必须能被支付宝服务器通过公网访问。
alipay_sdk=alipay-sdk-java-dynamicVersionNo 通常是 SDK 自动添加的标识,一般不需要优先怀疑这个参数。
推荐的服务端生成方式
不要自行拼接参数或手工签名。可以使用支付宝 Java SDK 的 sdkExecute,生成需要传给客户端的完整订单字符串:
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.response.AlipayTradeAppPayResponse;
public class AlipayOrderService {
private static final String APP_ID = "2021000122612036";
private static final String PRIVATE_KEY = "沙箱应用私钥";
private static final String ALIPAY_PUBLIC_KEY = "沙箱支付宝公钥";
public String createOrderString() throws AlipayApiException {
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi-sandbox.dl.alipaydev.com/gateway.do",
APP_ID,
PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2"
);
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo(generateUniqueOrderNo());
model.setSubject("商品描述");
model.setTotalAmount("2.00");
model.setProductCode("QUICK_MSECURITY_PAY");
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setBizModel(model);
request.setNotifyUrl(
"https://xspa-server-test.xingdata.com/xspa/sorder/ali/notify"
);
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"生成支付宝订单参数失败:" + response.getSubCode()
+ "," + response.getSubMsg()
);
}
return response.getBody();
}
private String generateUniqueOrderNo() {
return "TEST" + System.currentTimeMillis();
}
}
服务端应将 response.getBody() 原样返回给 APP。客户端不要解析、排序、重新拼接或再次编码其中的参数。
客户端调用示意:
PayTask payTask = new PayTask(activity);
Map<String, String> result = payTask.payV2(orderInfo, true);
这里的 orderInfo 就是服务端调用 sdkExecute 后返回的完整字符串。
排查步骤
1. 每次重新生成支付参数
不要继续使用问题中的这份参数:
timestamp=2023-02-14 10:53:59
out_trade_no=102023021311172009394564435500
每次测试都要重新生成:
- 与当前时间对应的
timestamp - 尚未使用过的
out_trade_no - 新的 RSA2 签名
重复使用订单号可能触发“商户订单号重复”等错误。重复使用旧的订单字符串,还可能造成签名或请求有效期方面的问题。
2. 补充 product_code
检查最终解码后的 biz_content,确认其中包含:
"product_code": "QUICK_MSECURITY_PAY"
不能只确认 Java 对象中已经设置该字段。还要记录 sdkExecute 的返回值,解码后检查最终参数是否确实包含它。
3. 核对沙箱密钥
以下配置必须全部属于同一个沙箱应用:
app_id- 签名所用的应用私钥
- 支付宝开放平台沙箱中配置的应用公钥
- 服务端配置的支付宝公钥
常见的密钥配置错误包括:
- 混用正式环境密钥和沙箱密钥
- 混淆应用公钥和支付宝公钥
- 重新生成密钥后,服务端仍在使用旧私钥
- 后台配置的是证书模式,客户端初始化却仍采用普通公钥模式
如果沙箱后台更换过应用公钥,服务端也要改用与新公钥配套的应用私钥重新签名。
4. 确认客户端没有二次处理
服务端返回的订单字符串中,sign、biz_content 等字段已经完成 URL 编码。客户端应直接把字符串传给支付 SDK,不要再做以下处理:
URLDecoder.decode(...)
URLEncoder.encode(...)
拆成 Map 后重新排序拼接
JSON 转换后再次生成字符串
把 + 替换为空格
签名中的 %2B、%2F、%3D 分别表示 +、/、=。再次编码、解码或替换字符,都可能导致验签失败。
5. 检查沙箱客户端和账号
沙箱支付必须使用与当前沙箱环境匹配的测试客户端和沙箱买家账号。正式支付宝客户端、正式账号不能与沙箱参数混用。
如果异常是从某一天突然开始出现的,还需要重新确认:
- 沙箱买家账号能否正常登录
- 测试客户端是否升级或被替换
- 应用公钥是否重新设置过
- 服务端是否切换过配置文件或环境变量
- SDK 返回的错误码、
sub_code和sub_msg是否发生变化
如何进一步定位
仅凭订单参数,无法确定问题究竟出在签名、订单号、环境还是客户端。排查时应同时收集以下信息:
客户端 payV2 返回的 resultStatus
客户端返回的 memo
客户端返回的 result
服务端生成订单字符串时的日志
支付宝异步通知日志
可以根据错误信息重点检查以下方向:
isv.invalid-signature:检查签名原文、私钥和编码。isv.invalid-app-id:检查 AppID 是否正确,以及当前环境是否匹配。isv.missing-method、参数缺失类错误:检查订单字符串是否被截断或错误解析。- 商户订单号重复:检查
out_trade_no是否重复使用。 - 系统繁忙或沙箱异常:使用新的订单号重新请求,再结合完整错误码判断,不能只看页面提示。
建议先补充 QUICK_MSECURITY_PAY,使用沙箱配置调用 sdkExecute,重新生成一份全新的订单字符串,再由客户端原样传给支付 SDK。如果仍然失败,应根据 payV2 返回的 resultStatus、memo 和服务端错误码继续定位。
备注:内容仅供参考。
