支付宝电脑网站支付返回链接无法打开,如何排查?
结论
这个链接不能作为长期有效的支付地址反复使用。链接中的请求时间是 2024-06-03 18:52:15,订单过期时间是 2024-06-18 18:52:15,现在已经失效。需要重新生成订单号、当前时间和过期时间,再通过支付宝 SDK 生成支付表单或跳转地址。
如果重新生成的链接仍然无法打开,可以重点检查以下问题:
- 沙箱网关、沙箱应用和沙箱账号是否配套。
app_id、应用私钥和应用证书是否属于同一个沙箱应用。app_cert_sn、alipay_root_cert_sn是否根据当前证书正确计算。- 签名前使用的参数是否与最终发送的参数完全一致。
timestamp、time_expire和服务器时区是否正确。- URL 是否被二次编码、解码或手工拼接。
total_amount、out_trade_no等业务参数是否符合要求。
当前链接中可以直接确认的问题
链接包含以下时间参数:
timestamp=2024-06-03 18:52:15
time_expire=2024-06-18 18:52:15
这个链接现在肯定无法继续用于支付。即使浏览器还能访问网关,也可能收到请求过期、订单关闭、验签失败或其他错误页面。
timestamp 表示接口请求的发起时间,不是支付链接的创建日期备注。每次重新发起支付都应使用服务器当前时间,不能保存旧链接后反复访问。
建议同时更换 out_trade_no:
2413929508223315545
同一个商户订单号不应作为全新订单重复创建。如果需要继续支付原订单,应先查询订单状态,再决定重新唤起支付、关闭后重建,还是直接提示订单已经完成。
先确认“无法打开”发生在哪一层
排查前,先记录浏览器或网关返回的具体结果:
- 浏览器提示域名无法解析、连接超时或 TLS 错误时,优先检查网络、DNS、代理和沙箱域名。
- 支付宝页面可以打开,但显示请求错误时,检查公共参数和业务参数。
- 返回
isv.invalid-signature或类似验签错误时,检查密钥、证书、字符集、参数排序和编码。 - 返回应用、证书或权限相关错误时,检查沙箱应用配置和签约产品。
- 页面进入收银台后无法付款时,检查沙箱买家账号、订单金额和账号余额。
- 商户页面拿到一段
<form>时,这是正常结果。应将表单输出给浏览器,不要把整段 HTML 当作 URL 打开。
仅记录“页面打不开”很难定位问题。支付宝返回的错误码、sub_code、sub_msg 和请求流水号通常更有用。
检查沙箱环境是否混用
沙箱调用应使用沙箱网关:
https://openapi-sandbox.dl.alipaydev.com/gateway.do
还要确认以下内容全部来自同一个沙箱应用:
app_id- 应用私钥
- 应用公钥证书
- 支付宝公钥证书
- 支付宝根证书
- 沙箱买家账号
- 沙箱卖家账号及对应应用配置
常见情况是使用了沙箱网关,却配置了正式环境的应用私钥或证书。也可能在切换应用时只修改了 app_id,没有同步更换私钥和证书。这些配置通常会造成验签失败或证书序列号校验失败。
沙箱与正式环境的账号、数据、证书和订单不能混用。
检查证书模式配置
示例链接包含:
app_cert_sn=07db9b3c029b168cc5a78a039d6071cf
alipay_root_cert_sn=687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6
这表明请求使用了证书模式。通常需要正确配置以下内容:
- 应用私钥
- 应用公钥证书
- 支付宝公钥证书
- 支付宝根证书
配置时需要注意:
- 应用私钥不是应用公钥,也不是证书文件中的公钥。
- 支付宝公钥证书不能用普通的“支付宝公钥”文本代替。
- 更新应用证书后,需要同步更新本地证书文件和相关序列号。
- 不建议手工填写
app_cert_sn和alipay_root_cert_sn。 - 根证书序列号可能由多个证书序列号组成,不能只读取一个证书的序列号再自行拼接。
- 证书文件路径、内容或部署环境发生变化后,需要重新核对程序实际加载的证书。
最好让官方 SDK 根据证书文件计算并添加证书序列号,以免手工计算造成不一致。
检查签名和 URL 编码
请求使用:
sign_type=RSA2
charset=utf-8
这两个值本身很常见,但能否通过验签,取决于签名前后的参数是否完全一致。
签名时通常应遵循这些原则:
- 排除
sign参数本身。 - 排除值为空的参数,具体以所用 SDK 的实现为准。
- 按参数名排序。
- 使用未经 URL 编码的参数值组成待签名字符串。
- 使用应用私钥执行 RSA2 签名。
- 完成签名后再对参数进行 URL 编码。
- 请求发出后,不要再次修改
biz_content、timestamp或 URL。
示例链接中的签名含有 +、/、= 等字符,放入 URL 时必须正确编码:
+ -> %2B
/ -> %2F
= -> %3D
如果先生成签名,再由框架对整条 URL 解码或二次编码,支付宝最终收到的参数可能与签名原文不同。
下面这些做法很容易造成验签失败:
对整个 SDK 返回地址再次 URLEncoder.encode(...)
或者:
先拼接已编码参数,再使用编码后的字符串计算签名
应尽量通过支付宝 SDK 生成请求,不要手工拼接网关 URL。
检查 biz_content
当前请求的主要业务参数是:
{
"body": "购买 WebOffice 开放平台软件服务",
"out_trade_no": "2413929508223315545",
"product_code": "FAST_INSTANT_TRADE_PAY",
"subject": "WebOffice开放平台账户充值",
"time_expire": "2024-06-18 18:52:15",
"total_amount": "11111.00"
}
可以重点检查以下参数。
product_code
电脑网站支付通常使用:
FAST_INSTANT_TRADE_PAY
不要误用手机网站支付或其他产品的 product_code。
out_trade_no
订单号由商户生成,并且要在对应商户应用范围内保持唯一。重新测试时建议生成新的订单号,例如:
TEST202609091530001
不要直接重复提交已经成功、关闭或过期的订单。
total_amount
11111.00 的小数位格式没有明显问题。不过,测试时可以先改用较小金额,例如:
0.01
这样可以减少沙箱买家余额不足、测试账户支付能力或风控限制等因素的干扰。具体金额限制要以当前支付宝接口文档和实际返回的错误码为准,不能仅凭页面打不开就判断金额超限。
time_expire
重新请求时,需要根据当前时间计算未来的过期时间。例如:
timestamp=2026-09-09 15:30:00
time_expire=2026-09-09 16:00:00
过期时间必须晚于请求时间,并处于接口允许的有效期范围内。不要继续使用历史请求中的固定日期。
检查服务器时间和时区
服务器时间不准确会直接影响 timestamp 和 time_expire。
需要确认:
- 系统时间是否准确。
- 是否启用了时间同步。
- 应用使用的时区是否符合预期。
- 容器时间是否与宿主机时间一致。
- 生成
timestamp和time_expire时是否使用了同一个时区。
例如,应用按 UTC 生成时间,却直接将其格式化为不带时区的字符串,实际时间就可能相差 8 小时。
Java 中可以显式指定时区:
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
LocalDateTime now = LocalDateTime.now(zoneId);
String timestamp = now.format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
String timeExpire = now.plusMinutes(30).format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
接口接收的是格式化后的时间字符串,字段内容本身不含时区。因此,生成时间前必须确认应用实际使用的时区。
推荐的 Java SDK 调用方式
下面是证书模式的示意代码。支付宝 Java SDK 的版本不同,类名和构造方式可能略有差异,请以项目实际依赖的版本为准。
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class AlipayPagePayExample {
public static void main(String[] args) throws Exception {
AlipayConfig config = new AlipayConfig();
config.setServerUrl(
"https://openapi-sandbox.dl.alipaydev.com/gateway.do"
);
config.setAppId("你的沙箱APP_ID");
config.setPrivateKey("你的沙箱应用私钥");
config.setFormat("json");
config.setCharset("UTF-8");
config.setSignType("RSA2");
config.setAppCertPath("/path/to/appCertPublicKey.crt");
config.setAlipayPublicCertPath("/path/to/alipayCertPublicKey_RSA2.crt");
config.setRootCertPath("/path/to/alipayRootCert.crt");
AlipayClient alipayClient = new DefaultAlipayClient(config);
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
LocalDateTime now = LocalDateTime.now(zoneId);
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo("TEST" + System.currentTimeMillis());
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setSubject("沙箱电脑网站支付测试");
model.setBody("支付接口连通性测试");
model.setTotalAmount(new BigDecimal("0.01").toPlainString());
model.setTimeExpire(now.plusMinutes(30).format(formatter));
AlipayTradePagePayRequest request =
new AlipayTradePagePayRequest();
request.setBizModel(model);
request.setReturnUrl(
"https://example.com/alipay/return"
);
request.setNotifyUrl(
"https://example.com/alipay/notify"
);
AlipayTradePagePayResponse response =
alipayClient.pageExecute(request);
if (response.isSuccess()) {
// 通常是自动提交到支付宝网关的 HTML form。
System.out.println(response.getBody());
} else {
System.out.println("code: " + response.getCode());
System.out.println("msg: " + response.getMsg());
System.out.println("subCode: " + response.getSubCode());
System.out.println("subMsg: " + response.getSubMsg());
System.out.println("body: " + response.getBody());
}
}
}
在 Web 服务中,应将 response.getBody() 作为 HTML 返回给浏览器,例如:
@GetMapping(value = "/pay", produces = "text/html;charset=UTF-8")
@ResponseBody
public String pay() throws Exception {
AlipayTradePagePayResponse response =
alipayClient.pageExecute(buildPayRequest());
if (!response.isSuccess()) {
throw new IllegalStateException(
response.getSubCode() + ": " + response.getSubMsg()
);
}
return response.getBody();
}
不要把返回的 <form> 当作普通 URL,也不要对整段表单进行 URL 编码。
return_url 和 notify_url 的检查重点
示例中配置了:
notify_url=https://weboffice-test.wps.cn/api/console/order/alipay/notify
return_url=https://solution.wps.cn/console/callback
两者的用途不同:
return_url:用户完成支付后,浏览器跳回的前端地址。notify_url:支付宝服务器发送异步支付通知的后端地址。
支付链接无法进入收银台,通常不是这两个地址导致的,但仍需确认:
- 地址是公网可以访问的 HTTPS 地址。
notify_url不依赖浏览器登录状态。- 地址不会被登录跳转、WAF、IP 白名单或 CSRF 校验拦截。
- 服务端能够正确接收支付宝提交的方法和参数。
- 异步通知验签使用支付宝公钥或支付宝公钥证书,不能使用应用公钥。
- 商户订单状态应以异步通知或主动查询结果为准,不能只依赖
return_url。
建议的最小化排查顺序
先构造一笔最简单的沙箱订单:
{
"out_trade_no": "全新的唯一订单号",
"product_code": "FAST_INSTANT_TRADE_PAY",
"subject": "沙箱支付测试",
"total_amount": "0.01",
"time_expire": "当前时间之后30分钟"
}
然后按以下顺序检查:
- 确认使用的是沙箱网关和沙箱
app_id。 - 确认 SDK 实际加载了该应用对应的私钥和三份证书。
- 校准服务器时间,并确认使用
Asia/Shanghai时区。 - 使用全新的
out_trade_no。 - 先用
0.01元测试。 - 直接输出 SDK 的
pageExecute结果。 - 不要手工修改或二次编码返回内容。
- 记录支付宝返回的
code、sub_code、sub_msg和完整响应。 - 收银台能够打开后,再逐项恢复实际金额、标题、通知地址等业务配置。
如果最小请求可以正常打开,说明网关、证书和签名这条主链路基本正常。接下来逐个恢复业务参数,就能找出触发问题的具体配置。
备注:内容仅供参考。
