SpringBoot支付宝沙箱支付成功创建订单后页面报错
明确结论
这个提示并不是“网关地址失效”特有的错误。既然已经使用:
https://openapi-sandbox.dl.alipaydev.com/gateway.do
就不必继续只排查网关地址。
常见原因包括:
- 混用了沙箱环境和正式环境的
appId、应用私钥或支付宝公钥; - 调用的不是电脑网站支付接口,或者
product_code填写错误; pageExecute()生成的 HTML 表单被 Spring 当作视图名处理,或被转义、二次包装;- 提交给支付宝的参数缺失、编码有误,或者签名后又修改了参数;
- “订单创建成功”只是本地数据库写入成功,并不表示支付宝已经受理订单。
“您所访问的内容不存在或出现故障”是支付收银台的通用错误页面,仅凭这句话无法判断具体原因。排查时应以支付宝接口返回的 code、sub_code、sub_msg 和浏览器实际提交的请求参数为准。
先确认“订单创建成功”指什么
很多项目会按以下流程处理支付:
- 向本地数据库插入订单;
- 调用
alipay.trade.page.pay生成自动提交表单; - 浏览器提交表单后,支付宝校验参数并创建交易。
所以,本地订单插入成功,或者 pageExecute() 返回了一段 HTML,都不能证明支付宝订单已经创建成功。
可以调用 alipay.trade.query 查询 out_trade_no。如果查不到交易,问题出在收银台请求提交或支付宝验签阶段,而不是支付后的回调阶段。
检查环境配置是否完全一致
沙箱环境中的配置必须全部来自同一个沙箱应用:
alipay.gateway-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do
alipay.app-id=沙箱应用APPID
alipay.merchant-private-key=应用私钥
alipay.alipay-public-key=沙箱应用对应的支付宝公钥
alipay.charset=UTF-8
alipay.sign-type=RSA2
alipay.format=json
逐项检查以下内容:
appId必须来自沙箱应用,不能使用正式应用的appId。- 应用私钥必须和上传到该沙箱应用的应用公钥配对。
alipayPublicKey指支付宝公钥,不是应用公钥,也不是与应用私钥配对的公钥。- 如果重新生成过应用密钥,代码中的私钥和开放平台中的公钥都要同步更新。
sign_type通常使用RSA2,并且必须与应用配置一致。- 配置文件中的密钥不能带有多余的引号、空格或转义字符。
如果使用的是证书,而不是普通公钥,就需要按证书模式初始化 SDK,不能继续使用普通公钥模式的客户端构造方式。两种模式不能混用。
确认支付接口和业务参数
电脑网页支付应调用:
alipay.trade.page.pay
典型业务参数如下:
{
"out_trade_no": "ORDER_202609090001",
"total_amount": "0.01",
"subject": "测试订单",
"product_code": "FAST_INSTANT_TRADE_PAY"
}
检查这些参数时需要注意:
out_trade_no在商户侧必须唯一。total_amount应是合法的十进制金额字符串,例如"0.01",不能传入null、负数或包含货币符号的内容。subject不能为空。- 电脑网站支付的
product_code应为FAST_INSTANT_TRADE_PAY。 - 手机网站支付、当面付和 App 支付的参数不能直接用于电脑网站支付接口。
biz_content必须是合法 JSON。不要先手动进行 URL 编码,再交给 SDK 处理。
Spring Boot 正确返回支付表单
支付宝 Java SDK 的 pageExecute() 通常会返回一段自动提交的 HTML 表单。Controller 必须将它作为 HTML 响应返回给浏览器,不能让 Spring 将其解释为模板名称。
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AlipayController {
@GetMapping(value = "/pay", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> pay() throws Exception {
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi-sandbox.dl.alipaydev.com/gateway.do",
"沙箱应用APPID",
"应用私钥",
"json",
"UTF-8",
"支付宝公钥",
"RSA2"
);
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("http://localhost:8080/alipay/return");
request.setNotifyUrl("https://可被公网访问的域名/alipay/notify");
request.setBizContent("""
{
"out_trade_no": "ORDER_202609090001",
"total_amount": "0.01",
"subject": "支付宝沙箱支付测试",
"product_code": "FAST_INSTANT_TRADE_PAY"
}
""");
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"支付宝请求失败,code=" + response.getCode()
+ ", subCode=" + response.getSubCode()
+ ", subMsg=" + response.getSubMsg()
);
}
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
.body(response.getBody());
}
}
如果使用 @Controller,需要在方法上添加 @ResponseBody:
@Controller
public class AlipayController {
@ResponseBody
@GetMapping(value = "/pay", produces = "text/html;charset=UTF-8")
public String pay() throws Exception {
// ...
return alipayClient.pageExecute(request).getBody();
}
}
不要将返回的表单放进 JSON:
// 不推荐
return Map.of("form", response.getBody());
前端也不要先发起普通 JSON 请求,再把整段 HTML 当成 URL 跳转。更简单可靠的做法是让浏览器直接访问后端 /pay 接口,由后端返回支付表单。
检查浏览器实际提交的请求
打开浏览器开发者工具,在 Network 面板中找到发往以下域名的请求:
openapi-sandbox.dl.alipaydev.com
然后检查:
- 请求方法是否正常;
- 是否包含
app_id; method是否为alipay.trade.page.pay;- 是否包含
sign、sign_type、timestamp、charset和biz_content; biz_content是否完整;- 请求经过前端框架、反向代理或网关时是否被截断;
- HTML 表单中的
&是否被重复转义为&amp;; - SDK 完成签名后,是否还有参数被修改。
签名完成后,即使只改动一个空格、字符编码或业务参数,也可能导致支付宝验签失败。不要自行拆解、拼接或重新编码 SDK 生成的表单。
检查返回的错误明细
日志中不能只记录“页面出错”,应该保留 SDK 返回的完整结果和异常信息,但不要记录私钥或完整签名。
try {
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
System.out.println("code=" + response.getCode());
System.out.println("msg=" + response.getMsg());
System.out.println("subCode=" + response.getSubCode());
System.out.println("subMsg=" + response.getSubMsg());
return response.getBody();
} catch (Exception e) {
e.printStackTrace();
throw e;
}
如果 response.getBody() 是 HTML 表单,只能说明 SDK 已经生成页面支付请求。支付宝是否接受该请求,还要看浏览器提交后的校验结果。此时需要结合浏览器 Network 面板中的请求继续排查。
回调地址的注意事项
return_url 是用户完成支付后,浏览器跳转的地址。notify_url 是支付宝服务器发送异步通知的地址。
两者的要求不同:
- 本机调试时,
return_url可以暂时使用localhost,因为访问者是当前浏览器。 notify_url必须能被支付宝服务器从公网访问,不能使用localhost、127.0.0.1或局域网地址。- 回调地址配置错误通常会影响支付后的跳转或通知,不一定会导致收银台刚打开就报错。
- 收到回调后必须验签,不能只根据
trade_status修改订单状态。
推荐的排查顺序
- 确认当前访问的是沙箱网关,不是正式网关。
- 确认
appId、应用私钥和支付宝公钥全部来自同一个沙箱应用。 - 确认调用的方法是
alipay.trade.page.pay。 - 确认
product_code是FAST_INSTANT_TRADE_PAY。 - 确认 Spring Boot 直接以
text/html返回pageExecute()的结果。 - 在浏览器 Network 面板中检查实际发送给支付宝的参数。
- 记录并检查
code、sub_code和sub_msg。 - 调用
alipay.trade.query查询out_trade_no,确认支付宝侧是否存在这笔交易。 - 如果重新生成过密钥,重新核对密钥的配对关系并重启应用,以免程序仍在使用旧配置。
如果业务参数都没有问题,但页面仍然显示通用错误,最有用的信息是浏览器提交的完整参数结构,以及支付宝返回的 sub_code 和 sub_msg。缺少这些信息时,不能仅凭错误页面判断某个固定原因。
备注:内容仅供参考。
