PAYATHON 2026

支付宝APP内H5支付成功后未回调且跳转异常

支付阿杰

结论

支付成功后的页面跳转,不能只依赖前端的“支付成功回调”,需要同时处理两条链路:

  1. 服务端通过支付宝异步通知确认订单是否支付成功;
  2. 前端根据实际接入方式,通过 tradePay 回调或 WAP 支付的 return_url 进入自定义结果页。

如果支付过程中已经离开原来的 H5 WebView,或者支付宝收银台关闭、页面容器发生切换,原页面中的 JavaScript 回调可能不会执行。H5 也无法直接监听支付宝收银台上的“完成”按钮。遇到这种情况,后续页面应由创建支付订单时配置的返回地址决定,不能继续等待原页面回调。

支付完成后进入另一个支付宝小程序,通常表示支付请求、返回地址或中间重定向链路与该小程序有关。排查时应重点检查服务端生成的支付参数,不能只看前端回调代码。

先确认当前使用的支付方式

支付宝 APP 内的 H5 页面通常有两种支付方式,不能混用。

使用 AlipayJSBridge.tradePay

这种方式不会让页面整体跳转,而是通过支付宝 JSAPI 拉起收银台。支付结束后,结果会回调到当前页面。

function ready(callback) {
  if (window.AlipayJSBridge) {
    callback();
  } else {
    document.addEventListener('AlipayJSBridgeReady', callback, false);
  }
}

ready(function () {
  AlipayJSBridge.call(
    'tradePay',
    {
      tradeNO: '服务端创建并返回的支付宝交易号'
    },
    function (result) {
      if (String(result.resultCode) === '9000') {
        window.location.replace('/payment/result?orderNo=YOUR_ORDER_NO');
        return;
      }

      if (String(result.resultCode) === '6001') {
        // 用户取消支付
        return;
      }

      // 其他结果不要直接判定为支付失败,应向服务端查询订单状态
      checkOrderStatus('YOUR_ORDER_NO');
    }
  );
});

只有原来的 H5 页面仍然存在,回调函数才可能执行。如果支付过程中原 WebView 被销毁、页面发生导航,或者实际打开了另一个小程序页面,原页面的回调就不会继续运行。

使用 alipay.trade.wap.pay

如果服务端调用手机网站支付接口,通常会生成表单或支付 URL。浏览器提交后会离开当前页面,因此不能再用原页面的 JavaScript 回调控制支付完成后的跳转,而要在服务端请求中配置 return_url。

示意代码:

AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();

request.setNotifyUrl("https://example.com/api/alipay/notify");
request.setReturnUrl("https://example.com/payment/result");

request.setBizContent("""
{
  "out_trade_no": "ORDER_202609090001",
  "total_amount": "100.00",
  "subject": "订单支付",
  "product_code": "QUICK_WAP_WAY"
}
""");

用户完成支付并点击收银台中的“完成”后,支付宝通常会通过同步返回链路打开 return_url。实际行为还可能受到支付宝客户端页面容器、支付产品类型和接入方案的影响,最终应以所用接口的官方文档和联调结果为准。

完整处理流程

1. 创建支付订单时明确返回地址

检查服务端调用支付宝接口时传入的以下参数:

  • return_url
  • notify_url
  • app_id
  • out_trade_no
  • product_code
  • 支付接口名称

return_url 应填写可通过 HTTPS 访问的完整地址,例如:

https://example.com/payment/result?orderNo=ORDER_202609090001

不要把订单支付状态直接写入 URL,也不要只根据同步返回参数修改订单状态。

如果接口或 SDK 支持 quit_url,需要注意,它通常是用户中途退出支付时的返回地址,并不等同于支付成功后的 return_url。当前支付产品是否支持该参数及其具体含义,应以对应接口定义为准。

2. 用异步通知确认支付结果

更新订单状态应主要依据支付宝服务端的异步通知。接收通知时,至少要完成以下处理:

  • 验证支付宝签名;
  • 校验 app_id;
  • 校验 out_trade_no;
  • 校验金额和币种;
  • 判断交易状态;
  • 对重复通知进行幂等处理。

伪代码如下:

@PostMapping("/api/alipay/notify")
public String alipayNotify(HttpServletRequest request) {
    Map<String, String> params = extractParams(request);

    boolean verified = verifyAlipaySignature(params);
    if (!verified) {
        return "failure";
    }

    String orderNo = params.get("out_trade_no");
    String tradeStatus = params.get("trade_status");

    validateAppId(params.get("app_id"));
    validateOrderAmount(orderNo, params.get("total_amount"));

    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {
        markOrderPaidIdempotently(orderNo, params.get("trade_no"));
    }

    return "success";
}

前端没有收到 9000,不代表订单支付失败。前端回调、同步返回和异步通知的到达顺序可能不同,也可能因为网络或页面生命周期问题而缺失。

3. 自定义结果页主动查询订单状态

无论页面是通过 tradePay 回调跳转,还是通过 return_url 返回,结果页都应向业务服务端查询订单状态:

async function checkOrderStatus(orderNo) {
  const response = await fetch(
    `/api/orders/${encodeURIComponent(orderNo)}/payment-status`,
    {
      credentials: 'include',
      cache: 'no-store'
    }
  );

  const result = await response.json();

  if (result.status === 'PAID') {
    renderPaymentSuccess();
  } else if (result.status === 'PENDING') {
    setTimeout(() => checkOrderStatus(orderNo), 1500);
  } else {
    renderPaymentPending();
  }
}

结果页可以先显示“支付结果确认中”,并进行短时间轮询,以免异步通知尚未到达时误报支付失败。轮询必须设置次数或超时时间,不能无限执行。

为什么成功时不回调,失败时却能回调

这种现象通常表示支付成功和支付失败使用了不同的页面链路。

支付失败或取消时,支付宝可能直接关闭收银台并返回原 H5,因此 tradePay 回调还能执行。支付成功后,如果支付宝根据支付订单信息、返回地址或客户端路由打开了另一个页面,原 H5 就不再处于活动状态,回调也无法完成。

还要检查以下情况:

  • 同时使用了表单提交和 AlipayJSBridge.call('tradePay');
  • 支付前执行了 location.href、location.replace 或表单跳转;
  • return_url 指向旧域名、旧活动页或小程序跳转链接;
  • 返回页面的服务端又执行了一次 302 重定向;
  • 支付订单使用了错误的 app_id、商户配置或支付产品;
  • 中间页根据 User-Agent 自动跳转到支付宝小程序;
  • 页面绑定了 visibilitychange、pageshow 等事件,并在恢复时触发错误跳转;
  • 前端重复发起支付,实际完成的是另一笔订单;
  • CDN、网关或服务端缓存仍在返回旧的支付参数。

排查跳入其他小程序的问题

可以保存并对比一笔正常订单和一笔异常订单的完整支付链路,但日志中不能记录私钥、完整签名或用户敏感信息。

重点检查:

  1. 创建订单时实际使用的 app_id 是否属于当前应用;
  2. return_url 是否确实指向自定义页面的 HTTPS 地址;
  3. return_url 是否经过短链接、网关或活动页重定向;
  4. 返回地址是否包含 alipays://、小程序 Scheme 或小程序跳转参数;
  5. 支付参数是否由旧服务、旧配置中心或另一套环境生成;
  6. 支付订单号是否与前端当前展示的业务订单一致;
  7. 支付宝开放平台配置、应用授权或支付产品接入方式是否发生变化。

可以在自定义结果页临时记录以下信息,用于确认该页面是否被打开:

navigator.sendBeacon(
  '/api/payment/return-page-log',
  JSON.stringify({
    orderNo: new URLSearchParams(location.search).get('orderNo'),
    href: location.href,
    referrer: document.referrer,
    time: Date.now()
  })
);

日志中不要写入支付宝私钥、签名密钥、用户身份凭证或完整支付请求内容。

注意事项

  • H5 无法可靠监听支付宝收银台内部“完成”按钮的点击事件。
  • tradePay 的前端回调只能用于页面交互,不能作为最终入账依据。
  • return_url 是同步返回地址,同样不能作为确认支付成功的唯一依据。
  • 服务端应结合异步通知和主动查询确认订单状态。
  • 不要只判断 resultCode === '9000'。如果回调缺失或结果不明确,应查询服务端订单状态。
  • 如果代码没有变化,但问题最近才出现,还应检查支付订单参数、开放平台应用配置、网关重定向和客户端容器变化。
  • 如果支付参数和业务跳转都没有异常,但页面仍然稳定进入无关小程序,需要整理异常订单号、支付宝交易号、发生时间、支付宝客户端版本和复现路径,提交给支付宝开放平台技术支持进一步核查。

备注:内容仅供参考。