PAYATHON 2026

WooCommerce thank you page AJAX 桌面端正常但移动端失效

支付老李

结论

问题不在于 WooCommerce 为桌面端和移动端加载了不同的 JavaScript,而在于代码会根据 User-Agent 改变 AJAX 的执行方式:

if($browser != "safariOrFirefox")

浏览器被识别为 Safari 或 Firefox 后,请求会改用同步 AJAX:

async: false

移动端与桌面端浏览器的 User-Agent 并不相同。iPhone 和 iPad 上的 Chrome 使用 CriOS 标识,通常仍包含 Safari,却不包含字符串 Chrome,所以可能被误判为 Safari。同步 XMLHttpRequest 已被弃用,在移动浏览器、页面跳转和结账回调场景中尤其不可靠。

发送订单邮件本质上是服务端业务,不应依赖感谢页面中的 JavaScript。用户关闭页面、网络切换、支付网关跳转、缓存或脚本加载失败,都可能造成邮件漏发。更可靠的做法是监听 WooCommerce 的订单状态 Hook,在服务器端发送邮件或调用 SendGrid。

为什么移动端更容易失效

1. 浏览器判断可能误判

当前判断逻辑如下:

if (strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') && !strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome')) {
    $browserSafari = 'Safari';
}

这段逻辑有几个问题:

  • iOS Chrome 通常使用 CriOS,而不是 Chrome。
  • iOS Firefox 通常使用 FxiOS,不一定包含 Firefox。
  • strpos() 可能返回位置 0,直接用作判断并不严谨,应使用 !== false。
  • $browserSafari、$browserFirefox、$browser 和 $agent 没有预先初始化,可能产生 PHP notice。
  • 浏览器标识可以被修改或伪造,不适合用来决定是否发送订单邮件。

因此,即使请求来自同一台服务器,最终输出的 JavaScript 也可能因设备而异。

2. async: false 不可靠

移动端进入 Safari 分支后,会执行以下代码:

$.ajax({
    type: "POST",
    async: false,
    url: ".../emailDownloadFdd.php",
    data: {array: myJSON, email: email}
});

同步 XHR 会阻塞页面主线程,现代浏览器已不建议使用。页面即将跳转、关闭或进入后台时,请求也可能被取消。

这里没有使用同步 AJAX 的必要。即使暂时保留前端请求,也应删除浏览器判断,并统一使用异步请求。

3. 感谢页面不是可靠的业务触发点

WooCommerce 的 thank-you page 只是展示页面,无法保证每次都被访问。例如:

  • 用户付款后直接关闭浏览器;
  • 支付网关没有正常跳回网站;
  • 移动端切换应用后,页面被系统回收;
  • 页面被缓存;
  • jQuery 或第三方 CDN 加载失败;
  • 用户刷新页面,导致请求重复执行。

所以,当前实现既可能漏发邮件,也可能重复发送。

4. 还可能存在跨域或重定向问题

如果 AJAX 地址与结账页面使用不同的协议、域名或子域名,请求会受到同源策略限制。此时,emailDownloadFdd.php 必须正确响应 CORS 预检和实际请求。

代码还在已经输出 HTML 和 <script> 后调用了:

header("Location: https://hidden/franchise_dashboard/model/insertNewFunds.php");

这可能触发 headers already sent,也可能让浏览器在 AJAX 完成前离开页面。即使启用输出缓冲后桌面端看似可以运行,也不能说明这种写法可靠。

推荐方案:在 WooCommerce 服务端发送邮件

不要再从感谢页面调用 emailDownloadFdd.php。可以在插件或子主题的 functions.php 中监听订单状态,并直接执行邮件发送逻辑。

例如,订单进入处理中状态时调用现有发送程序:

add_action(
    'woocommerce_order_status_processing',
    'send_fdd_download_email',
    10,
    1
);

function send_fdd_download_email($order_id) {
    $order = wc_get_order($order_id);

    if (!$order) {
        return;
    }

    // 防止 Hook 重复触发而重复发送。
    if ($order->get_meta('_fdd_download_email_sent')) {
        return;
    }

    $email = $order->get_billing_email();

    if (!is_email($email)) {
        return;
    }

    /*
     * 建议将 emailDownloadFdd.php 中的发送逻辑重构为 PHP 函数,
     * 然后在此处直接调用,而不是通过 HTTP/AJAX 请求自己的网站。
     *
     * send_fdd_email_for_order($order_id, $email);
     */

    $order->update_meta_data('_fdd_download_email_sent', current_time('mysql'));
    $order->save();
}

如果下载内容应在付款完成后发送,也可以使用:

add_action('woocommerce_payment_complete', 'send_fdd_download_email', 10, 1);

具体选择哪个 Hook,要看实际业务规则:

  • 付款完成后立即发送:woocommerce_payment_complete
  • 订单进入处理中状态时发送:woocommerce_order_status_processing
  • 虚拟或可下载商品进入已完成状态后发送:woocommerce_order_status_completed

部分支付方式不会立即完成付款,因此不能假设所有订单都会按照相同顺序触发这些事件。

只有确认邮件发送成功后,才能写入发送标记。如果 SendGrid 调用失败,应记录错误,不要提前把订单标记为已发送。

如果暂时必须保留 AJAX

作为过渡方案,可以删除 User-Agent 判断和 async: false,并使用 WordPress 自带的 jQuery。不要在 WooCommerce 模板中再次从 Google CDN 加载另一个 jQuery 版本。

PHP 输出数据时应使用 wp_json_encode(),避免邮箱中的引号等字符破坏 JavaScript:

<script>
jQuery(function ($) {
    const requestData = {
        array: <?php echo wp_json_encode($randomID); ?>,
        email: <?php echo wp_json_encode($phpEmail); ?>
    };

    $.ajax({
        type: 'POST',
        url: <?php echo wp_json_encode(
            'https://hidden/SendGrid-Starter-Kit-master/SendGrid-API/emailDownloadFdd.php'
        ); ?>,
        data: requestData,
        timeout: 15000
    })
    .done(function (response) {
        console.log('Download email request completed:', response);
    })
    .fail(function (xhr, textStatus, errorThrown) {
        console.error(
            'Download email request failed:',
            xhr.status,
            textStatus,
            errorThrown,
            xhr.responseText
        );
    });
});
</script>

如果请求地址与当前页面不同源,还要在服务端配置允许访问的来源。对于包含敏感订单数据的接口,不要简单使用:

Access-Control-Allow-Origin: *

应将来源限制为实际商城域名,并为接口增加身份校验。

更符合 WordPress 结构的方式是请求 admin-ajax.php 或自定义 REST API,并使用 nonce:

wp_localize_script('your-script-handle', 'fddEmail', [
    'ajaxUrl' => admin_url('admin-ajax.php'),
    'nonce'   => wp_create_nonce('send_fdd_email'),
]);

前端代码如下:

jQuery(function ($) {
    $.post(fddEmail.ajaxUrl, {
        action: 'send_fdd_email',
        nonce: fddEmail.nonce,
        order_id: orderId
    }).fail(function (xhr) {
        console.error(xhr.responseText);
    });
});

即使采用这种方式,关键邮件仍不能只依赖前端请求。

同时需要修正的安全问题

当前 SQL 通过字符串拼接写入邮箱和随机 ID:

$sql = "INSERT INTO fddDownloadInfo (email, time, randomid)
VALUES ('$billingEmail', NOW(), $randomID)";

应改用 $wpdb->insert() 或预处理语句。例如:

global $wpdb;

$table_name = $wpdb->prefix . 'fddDownloadInfo';

$wpdb->insert(
    $table_name,
    [
        'email'    => $order->get_billing_email(),
        'time'     => current_time('mysql'),
        'randomid' => (int) $randomID,
    ],
    [
        '%s',
        '%s',
        '%d',
    ]
);

还要处理以下问题:

  • 不要在 WooCommerce 模板中手动调用 session_start(),应优先使用订单元数据保存业务状态。
  • 不要把邮箱和订单标识暴露给没有鉴权的公共 PHP 接口。
  • 不要用模板覆盖文件承载核心业务,WooCommerce 模板更新后可能需要同步维护。
  • 不要在页面内容已经输出后调用 header()。
  • 感谢页面刷新时,必须防止重复写入数据库和重复发送邮件。
  • 记录 SendGrid 请求的响应码和错误信息,才能判断问题出在前端没有发起请求、CORS 拦截,还是邮件服务调用失败。

删除 async: false 和浏览器判断,可以解决明显的移动端差异,但只能算临时修补。要让邮件发送可靠运行,应将这部分逻辑迁移到 WooCommerce 服务端 Hook,并使用订单元数据实现幂等控制。

备注:内容仅供参考。