Realex payments API 如何避免模拟 click event 触发付款弹窗?
结论
无法把 $('#rhpp').click() 变成浏览器认可的真实用户点击。无论使用 .click()、trigger('click') 还是 dispatchEvent(),由 JavaScript 触发的点击都是合成事件,不能伪造浏览器的用户激活状态。
要稳定避开 popup blocker,付款弹窗必须在真实鼠标点击、触摸或键盘操作触发的同步调用链中打开。AJAX 是异步操作,等请求完成后再打开 lightbox,通常已经失去了这次用户激活。可行的做法只有调整流程:
- 在用户点击付款按钮前完成订单预处理,再由真实点击直接打开 lightbox;
- 在真实点击发生时同步打开一个窗口,随后异步跳转,但这要求 Realex HPP 接口支持相应的集成方式;
- AJAX 完成后,让用户再次点击付款按钮。
如果既不改变交互流程,也不要求用户执行任何真实操作,就无法可靠地绕过浏览器的弹窗拦截机制。
为什么 Promise 不能保留用户点击状态
问题不在 Promise,而在异步边界。
即使 AJAX 请求由用户点击触发,等 .then() 回调执行时,请求也已经在后续任务中完成。浏览器授予的临时用户激活通常只在很短的同步调用期间有效,无法可靠地跨越网络请求:
$('#checkout').on('click', function () {
// 这里仍处于真实用户操作的同步调用链中。
$.ajax({
method: 'POST',
url: 'processOrder.php',
data: $('form').serialize()
}).done(function (response) {
// 执行到这里时,用户激活通常已经失效。
$('#rhpp').click();
});
});
下面这些写法同样无法创建真实点击:
$('#rhpp').click();
$('#rhpp').trigger('click');
document.getElementById('rhpp').click();
document.getElementById('rhpp').dispatchEvent(
new MouseEvent('click', { bubbles: true })
);
这些事件可以执行对应的事件监听器,但其 isTrusted 仍然是 false,脚本也不能将其改成可信事件。
推荐方案:先准备付款数据,再接受真实点击
最稳妥的做法是提前调用 processOrder.php,获取 Realex HPP 所需的数据。准备完成后,再允许用户执行最终付款操作。这样,打开 lightbox 的代码可以直接在真实点击的处理函数中运行,无需等待 AJAX。
示例:
let paymentData = null;
let paymentPreparing = false;
function preparePayment() {
if (paymentPreparing || paymentData) {
return;
}
paymentPreparing = true;
$.ajax({
method: 'POST',
url: 'processOrder.php',
data: $('form').serialize()
}).done(function (response) {
paymentData = typeof response === 'string'
? JSON.parse(response)
: response;
RealexHpp.setHppUrl(
'https://pay.sandbox.realexpayments.com/pay'
);
RealexHpp.lightbox.init(
'rhpp',
paymentData.return,
typeof paymentData.hpp === 'string'
? JSON.parse(paymentData.hpp)
: paymentData.hpp
);
}).fail(function () {
paymentData = null;
console.error('Response failed');
}).always(function () {
paymentPreparing = false;
});
}
可以在用户确认购物车内容、进入结算页或完成表单时调用:
preparePayment();
付款数据准备好后,应由用户真实点击绑定了 Realex lightbox 的元素。元素叫什么并不重要,重要的是打开弹窗的动作必须发生在可信的用户事件中。
如果订单内容在付款数据准备完成后发生变化,必须废弃旧数据并重新生成:
$('form').on('change input', function () {
paymentData = null;
preparePayment();
});
实际项目中还要加入防抖,避免用户每次输入都提交一次订单请求。
可选方案:在真实点击中预先打开窗口
如果 Hosted Payment Page 接口支持跳转,可以在用户点击时同步打开一个空白窗口,等 AJAX 完成后再修改该窗口的地址:
$('#checkout').on('click', function (event) {
event.preventDefault();
const paymentWindow = window.open('', '_blank');
if (!paymentWindow) {
console.error('付款窗口被浏览器阻止');
return;
}
paymentWindow.document.write(
'<p>正在准备安全付款页面,请稍候……</p>'
);
$.ajax({
method: 'POST',
url: 'processOrder.php',
data: $('form').serialize()
}).done(function (response) {
const data = typeof response === 'string'
? JSON.parse(response)
: response;
// 这里必须使用支付接口实际返回或允许构造的跳转 URL。
paymentWindow.location.replace(data.paymentUrl);
}).fail(function () {
paymentWindow.close();
console.error('Response failed');
});
});
这种方式可能有效,是因为 window.open() 在真实点击的处理函数中同步执行,没有等到 AJAX 完成后才调用。
但不能直接假定 RealexHpp.lightbox.init() 可以接管预先打开的窗口。只有当前 Realex/Global Payments HPP 集成方式能提供跳转 URL,或者其 API 明确支持传入已有窗口时,才能这样实现。不要自行拼接付款 URL,也不要把签名密钥或其他敏感信息放进前端代码。
如果订单只能在点击后创建
如果业务规则要求用户点击”付款”后才能创建订单,而 Realex lightbox 又必须等订单创建成功后才能初始化,那么一次点击无法可靠地完成这两个阶段:
真实点击
→ 发起 AJAX
→ 等待服务器
→ 初始化 HPP
→ 打开 lightbox
用户激活通常会在”等待服务器”期间失效。此时,可靠的交互流程是:
第一次点击
→ 创建订单
→ 页面显示"订单已准备好,请继续付款"
第二次真实点击
→ 立即打开 lightbox
第二次点击可以继续使用原有的付款按钮,不一定要增加新的页面元素,但仍然必须由用户亲自执行。
注意事项
- 不要尝试通过延迟、Promise、
async/await或其他事件触发方式伪造用户点击。这些方法都不能创建可信的用户激活。 - 某种合成点击即使偶尔能在部分浏览器中生效,也不代表它可靠。浏览器类型、隐私设置和弹窗策略都可能影响结果。
- 不建议反复调用
window.open(),也不要使用隐藏按钮或透明覆盖层规避限制。这些做法可能被浏览器视为欺骗性交互。 - 应防止用户重复提交订单。服务器端需要使用订单状态或幂等键,避免多次 AJAX 请求生成多个待支付订单。
- HPP 请求参数和签名必须由服务器生成,并在付款前验证金额、币种、订单号及购物车状态。
- 使用
JSON.parse(data.hpp)前,应先确认data.hpp确实是字符串。如果 AJAX 已经自动解析了 JSON,再次调用JSON.parse()会报错。 - 沙箱地址仅用于测试环境。生产环境地址及具体初始化方式,应以当前使用的 Realex/Global Payments HPP 文档为准。
备注:内容仅供参考。
