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,并使用订单元数据实现幂等控制。
备注:内容仅供参考。
