周期扣款H5方式签约成功后如何关闭过渡页面
结论
周期扣款签约完成后,商户通常无法主动关闭最初打开的“免密支付安全验证”页面。
这是支付宝签约流程中的页面,商户 H5 无法控制。普通浏览器页面通常也不能直接通过 window.close() 将其关闭。签约后的 returnUrl 只能接收页面跳转,无法关闭原浏览器页面,也不能让用户自动回到原 H5 页面。
建议保留原 H5 页面。用户从支付宝返回浏览器后,再查询服务端的签约状态,并根据查询结果自动刷新当前页面或跳转到签约结果页。
原因
从 H5 唤起支付宝 APP 后,通常会同时存在以下页面:
- 商户原 H5 页面。
- 浏览器中的“免密支付安全验证”页面。
- 支付宝 APP 内的签约页面。
- 签约完成后打开的
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,就直接认定签约成功。用户可能在签约过程中退出,页面参数也可能遭到重复提交或篡改。
建议按以下流程处理:
- 发起签约前生成唯一的业务请求号。
- 接收并验证支付宝服务端发送的异步通知。
- 必要时由服务端调用签约查询接口确认结果。
- 将最终状态保存到商户数据库。
- 原 H5 页面和
returnUrl页面都从商户服务端读取状态。
注意事项
returnUrl无法保证重新激活最初的 H5 页面,也无法关闭系统浏览器中的中间页。- 商户页面不能控制支付宝域名下的页面,也不能控制支付宝 APP 的页面栈。
- 不要在固定延时后直接显示“签约成功”,因为异步通知可能尚未到达。
- 状态查询接口需要校验当前用户和业务请求号的归属关系,防止越权查询。
- 前端轮询需要设置查询间隔和超时时间,避免不断发送请求。
- 除非业务拥有原生 APP,并且已经完整处理未安装、拦截失败和不同系统的兼容问题,否则不建议依赖自定义 Scheme 强制返回原页面。
- 如果页面无法自动关闭,应给出清楚的提示,例如“签约已完成,请关闭当前页面并返回原页面”。
备注:内容仅供参考。
