PAYATHON 2026

Realex payments API 如何避免模拟 click event 触发付款弹窗?

支付阿杰

结论

无法把 $('#rhpp').click() 变成浏览器认可的真实用户点击。无论使用 .click()、trigger('click') 还是 dispatchEvent(),由 JavaScript 触发的点击都是合成事件,不能伪造浏览器的用户激活状态。

要稳定避开 popup blocker,付款弹窗必须在真实鼠标点击、触摸或键盘操作触发的同步调用链中打开。AJAX 是异步操作,等请求完成后再打开 lightbox,通常已经失去了这次用户激活。可行的做法只有调整流程:

  1. 在用户点击付款按钮前完成订单预处理,再由真实点击直接打开 lightbox;
  2. 在真实点击发生时同步打开一个窗口,随后异步跳转,但这要求 Realex HPP 接口支持相应的集成方式;
  3. 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 文档为准。

备注:内容仅供参考。