PAYATHON 2026

周期扣款H5方式签约成功后如何关闭过渡页面

支付小周

结论

周期扣款签约完成后,商户通常无法主动关闭最初打开的“免密支付安全验证”页面。

这是支付宝签约流程中的页面,商户 H5 无法控制。普通浏览器页面通常也不能直接通过 window.close() 将其关闭。签约后的 returnUrl 只能接收页面跳转,无法关闭原浏览器页面,也不能让用户自动回到原 H5 页面。

建议保留原 H5 页面。用户从支付宝返回浏览器后,再查询服务端的签约状态,并根据查询结果自动刷新当前页面或跳转到签约结果页。

原因

从 H5 唤起支付宝 APP 后,通常会同时存在以下页面:

  1. 商户原 H5 页面。
  2. 浏览器中的“免密支付安全验证”页面。
  3. 支付宝 APP 内的签约页面。
  4. 签约完成后打开的 returnUrl 页面。

这些页面可能运行在系统浏览器、支付宝 WebView 或不同的浏览器标签页中。商户只能控制自己域名下的页面,无法通过 JavaScript 关闭支付宝提供的中间页面。

浏览器通常只允许脚本关闭由同一脚本通过 window.open() 创建的窗口。因此,即使执行:

window.close();

页面也可能不会关闭,不能把这种方式当作可靠方案。

推荐处理方式

1. 保留原 H5 页面

如果业务流程允许,可以通过新窗口或新标签页提交签约表单,让原业务页面保持打开:

<form
  id="signForm"
  method="post"
  action="服务端返回的提交地址"
  target="_blank"
>
  <!-- 服务端返回的表单字段 -->
</form>

<script>
  document.getElementById('signForm').submit();
</script>

能否使用 target="_blank",需要根据服务端返回的完整 form 内容和支付宝接口要求判断。如果接口返回的是不可修改的完整表单,应遵循实际接口约束。

2. 用户返回原 H5 后查询签约状态

原页面可以监听 visibilitychange 和 pageshow 事件。页面重新可见时,向商户服务端查询签约结果:

<script>
  let checking = false;

  async function checkSignStatus() {
    if (checking) return;

    checking = true;

    try {
      const response = await fetch(
        '/api/agreement/status?requestId=YOUR_REQUEST_ID',
        {
          method: 'GET',
          credentials: 'include',
          cache: 'no-store'
        }
      );

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const result = await response.json();

      if (result.status === 'SIGNED') {
        location.replace('/agreement/success');
      } else if (result.status === 'FAILED') {
        location.replace('/agreement/failed');
      }
    } catch (error) {
      console.error('查询签约状态失败:', error);
    } finally {
      checking = false;
    }
  }

  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
      checkSignStatus();
    }
  });

  window.addEventListener('pageshow', () => {
    checkSignStatus();
  });
</script>

requestId 可以使用商户生成的业务请求号、签约申请号,或其他能唯一定位本次签约的数据。具体使用哪个字段,取决于实际接入的接口。

3. 将 returnUrl 作为结果承接页

建议把 returnUrl 配置为商户自己的 HTTPS 页面,并在页面中清楚说明用户接下来需要做什么,例如:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>签约结果</title>
</head>
<body>
  <p id="message">正在确认签约结果,请稍候……</p>

  <script>
    async function loadResult() {
      try {
        const response = await fetch(
          '/api/agreement/status?requestId=YOUR_REQUEST_ID',
          {
            credentials: 'include',
            cache: 'no-store'
          }
        );

        const result = await response.json();

        document.getElementById('message').textContent =
          result.status === 'SIGNED'
            ? '签约已完成,请返回原页面继续操作。'
            : '签约结果正在处理中,请稍后返回原页面查看。';
      } catch (error) {
        document.getElementById('message').textContent =
          '暂时无法确认签约结果,请返回原页面稍后重试。';
      }
    }

    loadResult();
  </script>
</body>
</html>

页面中可以提供“返回业务首页”按钮,但不要依赖页面自动关闭:

<button type="button" onclick="location.replace('/agreement')">
  返回业务页面
</button>

如果该页面确实是商户通过 window.open() 打开的,可以尝试:

window.close();

不过,不同浏览器、支付宝 WebView 和系统版本的处理方式可能不同,因此还要准备页面无法关闭时的提示。

签约状态应以服务端结果为准

不能因为页面跳转到了 returnUrl,就直接认定签约成功。用户可能在签约过程中退出,页面参数也可能遭到重复提交或篡改。

建议按以下流程处理:

  1. 发起签约前生成唯一的业务请求号。
  2. 接收并验证支付宝服务端发送的异步通知。
  3. 必要时由服务端调用签约查询接口确认结果。
  4. 将最终状态保存到商户数据库。
  5. 原 H5 页面和 returnUrl 页面都从商户服务端读取状态。

注意事项

  • returnUrl 无法保证重新激活最初的 H5 页面,也无法关闭系统浏览器中的中间页。
  • 商户页面不能控制支付宝域名下的页面,也不能控制支付宝 APP 的页面栈。
  • 不要在固定延时后直接显示“签约成功”,因为异步通知可能尚未到达。
  • 状态查询接口需要校验当前用户和业务请求号的归属关系,防止越权查询。
  • 前端轮询需要设置查询间隔和超时时间,避免不断发送请求。
  • 除非业务拥有原生 APP,并且已经完整处理未安装、拦截失败和不同系统的兼容问题,否则不建议依赖自定义 Scheme 强制返回原页面。
  • 如果页面无法自动关闭,应给出清楚的提示,例如“签约已完成,请关闭当前页面并返回原页面”。

备注:内容仅供参考。