PAYATHON 2026

SpringBoot支付宝沙箱支付成功创建订单后页面报错

支付小周

明确结论

这个提示并不是“网关地址失效”特有的错误。既然已经使用:

https://openapi-sandbox.dl.alipaydev.com/gateway.do

就不必继续只排查网关地址。

常见原因包括:

  • 混用了沙箱环境和正式环境的 appId、应用私钥或支付宝公钥;
  • 调用的不是电脑网站支付接口,或者 product_code 填写错误;
  • pageExecute() 生成的 HTML 表单被 Spring 当作视图名处理,或被转义、二次包装;
  • 提交给支付宝的参数缺失、编码有误,或者签名后又修改了参数;
  • “订单创建成功”只是本地数据库写入成功,并不表示支付宝已经受理订单。

“您所访问的内容不存在或出现故障”是支付收银台的通用错误页面,仅凭这句话无法判断具体原因。排查时应以支付宝接口返回的 code、sub_code、sub_msg 和浏览器实际提交的请求参数为准。

先确认“订单创建成功”指什么

很多项目会按以下流程处理支付:

  1. 向本地数据库插入订单;
  2. 调用 alipay.trade.page.pay 生成自动提交表单;
  3. 浏览器提交表单后,支付宝校验参数并创建交易。

所以,本地订单插入成功,或者 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;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 修改订单状态。

推荐的排查顺序

  1. 确认当前访问的是沙箱网关,不是正式网关。
  2. 确认 appId、应用私钥和支付宝公钥全部来自同一个沙箱应用。
  3. 确认调用的方法是 alipay.trade.page.pay。
  4. 确认 product_code 是 FAST_INSTANT_TRADE_PAY。
  5. 确认 Spring Boot 直接以 text/html 返回 pageExecute() 的结果。
  6. 在浏览器 Network 面板中检查实际发送给支付宝的参数。
  7. 记录并检查 code、sub_code 和 sub_msg。
  8. 调用 alipay.trade.query 查询 out_trade_no,确认支付宝侧是否存在这笔交易。
  9. 如果重新生成过密钥,重新核对密钥的配对关系并重启应用,以免程序仍在使用旧配置。

如果业务参数都没有问题,但页面仍然显示通用错误,最有用的信息是浏览器提交的完整参数结构,以及支付宝返回的 sub_code 和 sub_msg。缺少这些信息时,不能仅凭错误页面判断某个固定原因。

备注:内容仅供参考。