EcShop 判断手机访问跳转 ECTouch 完整方案:UA 字典升级、SEO 301/302、跨域 session 与防死循环
EcShop 2.7.3 跳转 ECTouch 1.0 的 pc_to_mobile() 函数原版 UA 字典只覆盖功能机,对 iPhone/Android 完全失效。本文给出含国产手机品牌、防死循环、view_pc cookie 切换的增强版函数,并补全 ECTouch 完整路由表,附 SEO 301/302 选择、跨域父域 cookie、nginx 层 UA 判断等实战配置。
EcShop 与 ECTouch 是两套独立的程序:EcShop 跑 PC 端商城,ECTouch 跑移动端商城,前者走传统 PHP 模板,后者走 MVC 风格的轻量框架。两者共享同一个数据库(ecs_users、ecs_goods、ecs_order_info 这套表),但 URL 体系完全不同。手机用户从搜索引擎进来落到 PC 域名时,必须做 User-Agent 检测把他跳到 ECTouch 对应页面,否则用户在小屏上看 PC 商城是灾难级体验。本文给出 EcShop 2.7.3 + ECTouch 1.0 的完整跳转方案,并扩展到现代手机 UA 适配、SEO 角度的 301/302 选择、桌面版切换 cookie、跨域 session 共享等实战配置。
整体架构与流程
跳转链路一共四步:
- 用户从手机浏览器访问 PC 端 URL(比如 example.com/category-5.html)。
- EcShop 的 includes/init.php 在请求生命周期最早期被加载,里面新加的 pc_to_mobile() 函数检测 User-Agent。
- 判定是手机后,按 PC 端文件名(category、goods、group_buy 等)拼出对应的 ECTouch URL。
- ecs_header('Location: ...') 发出 302 重定向,浏览器跳到 ECTouch URL。
这个设计的关键是“在 init.php 早期就触发”——必须在任何 echo 输出之前发出 Location 头,否则 PHP 会报“Cannot modify header information - headers already sent”。
核心函数 pc_to_mobile() 完整实现
编辑 includes/lib_main.php,在文件末尾追加这个函数:
/**
* EcShop 其他页面(商品详情、分类、品牌、团购、优惠、积分商城)
* 判断如果是手机访问跳转到 ECTouch 1.0 对应页面
*
* @access public
*/
function pc_to_mobile()
{
// 已经在手机版的就不再跳
if (strpos(strtolower($_SERVER['REQUEST_URI']), 'mobile') !== false) {
return;
}
// 已经手动选择了桌面版,跳过
if (!empty($_COOKIE['view_pc']) && $_COOKIE['view_pc'] == '1') {
return;
}
// User-Agent 判断
$is_mobile = false;
$ua = strtolower($_SERVER['HTTP_USER_AGENT']);
if ($ua === '') {
return; // 空 UA 不跳
}
$uachar = "/(nokia|sony|ericsson|mot|samsung|sgh|lg|philips|panasonic|alcatel|lenovo|cldc|midp|mobile|iphone|ipod|android|blackberry|windows phone|webos|opera mini|opera mobi|huawei|xiaomi|oppo|vivo|redmi|meizu|honor|nubia)/i";
if (preg_match($uachar, $ua)) {
$is_mobile = true;
}
// iPad 默认走桌面版(屏幕够大),如果你想 iPad 也跳手机版可以注释掉这行
if (strpos($ua, 'ipad') !== false && strpos($ua, 'mobile') === false) {
$is_mobile = false;
}
if (!$is_mobile) {
return;
}
// 取当前文件名
$cur_url = basename(PHP_SELF);
if (intval($GLOBALS['_CFG']['rewrite'])) {
$filename = strpos($cur_url, '-') ? substr($cur_url, 0, strpos($cur_url, '-')) : substr($cur_url, 0, -4);
} else {
$filename = substr($cur_url, 0, -4);
}
// 首页不跳,让 ECTouch 自己的根路径接管
if ($filename === 'index') {
$mobile_url = $GLOBALS['ecs']->url() . 'mobile/';
ecs_header("Location: {$mobile_url}");
exit;
}
$mobile_url = $GLOBALS['ecs']->url() . 'mobile';
$location = '';
// 商品/分类/品牌
if (in_array($filename, array('category', 'goods', 'brand'))) {
$qs = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
$location = $mobile_url . '/?c=' . $filename . '&a=index' . ($qs ? '&' . $qs : '');
} elseif ($filename === 'group_buy') {
$location = $mobile_url . '/?c=groupbuy' . (!empty($_GET['id']) ? '&a=info&id=' . intval($_GET['id']) : '');
} elseif ($filename === 'auction') {
$location = $mobile_url . '/?c=auction' . (!empty($_GET['id']) ? '&a=info&id=' . intval($_GET['id']) : '');
} elseif ($filename === 'snatch') {
$location = $mobile_url . '/?c=snatch' . (!empty($_GET['id']) ? '&a=info&id=' . intval($_GET['id']) : '');
} elseif ($filename === 'wholesale') {
$location = $mobile_url . '/?c=wholesale' . (!empty($_GET['id']) ? '&a=info&id=' . intval($_GET['id']) : '');
} elseif ($filename === 'exchange') {
$location = $mobile_url . '/?c=exchange' . (!empty($_GET['id']) ? '&a=exchange_goods&gid=' . intval($_GET['id']) : '');
} elseif ($filename === 'activity') {
$location = $mobile_url . '/?c=activity';
}
if (!empty($location)) {
ecs_header("Location: {$location}");
exit;
}
}这一版相比原文做的几处增强
- UA 字典补充了 iPhone、Android、各国产手机品牌。原文字典停留在功能机时代(nokia、ericsson)已经几乎不命中。
- iPad 单独处理,默认走桌面版,因为 ECTouch 1.0 的设计是 320-414px 宽度,iPad 跳过去反而比看 PC 版差。
- 引入 view_pc cookie 让用户能手动选择“就要看桌面版”。
- 已经在 mobile 路径下不再触发跳转(原文有这个判断,但只检查 wap 字符串,新代码加 mobile 关键字)。
- QUERY_STRING 用了拼接式而不是 ".$_SERVER['QUERY_STRING']" 直接撞上,避免空值时 URL 末尾出现野生 "&"。
- 所有 GET 参数 intval() 后再拼,避免 SQL 注入向量从这里穿到 ECTouch。
挂载到 init.php 的正确位置
编辑 includes/init.php,找到“商店关闭”判断这段(大概在第 129 行左右):
if (!isset($_CFG['shop_closed']) || $_CFG['shop_closed'] == 1) {
// 商店关闭逻辑
}在它之后立即调用:
pc_to_mobile();不要放在 init.php 的开头——init.php 前 100 行做了 GLOBALS、CFG、URL 重写、$ecs 对象初始化等准备工作,pc_to_mobile() 函数依赖 $GLOBALS['_CFG']、$GLOBALS['ecs']->url() 等,必须在它们就绪之后才能调用。
ECTouch 1.0 路由表速查
跳转目标 URL 的拼写规则要严格匹配 ECTouch 1.0 的 MVC 路由设计。下表整理了 EcShop PC 文件 → ECTouch URL 的对应:
| EcShop 文件 | ECTouch 路由 |
|---|---|
| index.php | /mobile/ |
| category.php?id=N | /mobile/?c=category&a=index&id=N |
| goods.php?id=N | /mobile/?c=goods&a=index&id=N |
| brand.php?id=N | /mobile/?c=brand&a=index&id=N |
| group_buy.php?id=N | /mobile/?c=groupbuy&a=info&id=N |
| auction.php?id=N | /mobile/?c=auction&a=info&id=N |
| snatch.php?id=N | /mobile/?c=snatch&a=info&id=N |
| wholesale.php?id=N | /mobile/?c=wholesale&a=info&id=N |
| exchange.php?id=N | /mobile/?c=exchange&a=exchange_goods&gid=N |
| activity.php | /mobile/?c=activity |
| article.php?id=N | /mobile/?c=article&a=index&id=N |
| flow.php (购物车) | /mobile/?c=flow |
| user.php (会员中心) | /mobile/?c=user |
| search.php?keywords=X | /mobile/?c=search&a=index&keywords=X |
原文给的清单缺少 article、flow、user、search 四个常用入口,我合作过的几家店铺都补上了。
SEO 角度的 301 vs 302 选择
ecs_header() 默认发的是 302 临时跳转。从 SEO 角度有两种选择:
302 的论据
同一份内容在 PC 版与移动版不是“永久迁移”关系,未来用户从手机切桌面版还是要能切回去。Google 对 302 与 301 的“权重传递”差异已经多年模糊(Google 官方在 2016 年明确说 302 不再丢失 PageRank)。所以 302 在大多数场景是合理的。
301 的论据
如果你的 PC 域名与 ECTouch 域名是同一个(example.com 与 example.com/mobile),301 可以让搜索引擎更明确“移动设备应该收录 mobile 版本”。但前提是你的 robots.txt 与 sitemap 已经做了 PC/移动分流。
实战建议
用 302 + 在 PC 页面 head 里同时声明 <link rel="alternate" media="only screen and (max-width: 640px)" href="https://example.com/mobile/对应路径">,配合 ECTouch 页面 head 里的 <link rel="canonical" href="https://example.com/对应PC路径">。这是 Google 官方推荐的“分离站点”配置。改 ecs_header 调用的话:
function ecs_header_301($location) {
if (PHP_SAPI != 'cli') {
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $location);
}
exit;
}替换 pc_to_mobile() 里的 ecs_header() 调用即可。
桌面版切换:view_pc cookie 设计
用户在 ECTouch 看到底部的“电脑版”链接点击后,应该能跳回 PC 版且不再被自动跳回手机版。实现思路:
ECTouch 端设置 cookie
找到 ECTouch 模板的“电脑版”链接(一般在 mobile/themes/default/views/footer.html 或类似位置),改成:
<a href="javascript:void(0);" onclick="setCookie('view_pc', '1', 30); window.location.href='/'">电脑版</a>setCookie 函数定义(如果 ECTouch 还没有的话):
function setCookie(name, value, days) {
var d = new Date();
d.setTime(d.getTime() + days * 24 * 3600 * 1000);
document.cookie = name + '=' + value + ';expires=' + d.toUTCString() + ';path=/';
}PC 端尊重 view_pc cookie
本文 pc_to_mobile() 函数开头已经检查 view_pc cookie,命中就 return。
PC 端“手机版”链接清 cookie
PC 端的“手机版”链接改成:
<a href="javascript:void(0);" onclick="document.cookie='view_pc=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/';window.location.href='/mobile/'">手机版</a>这样用户切回手机版时清掉 cookie,下次再访问 PC URL 时仍会被自动跳。
跨域时的 Session 共享
如果你的 PC 域名与移动域名不同(example.com vs m.example.com),跳转后 cookie 不跨域,登录态丢失。两种方案:
方案 A:父域 cookie
在 EcShop 的 init.php 里把 session.cookie_domain 设置成父域:
ini_set('session.cookie_domain', '.example.com');注意点号开头,让 cookie 在所有子域生效。ECTouch 端做同样设置。两边的 PHPSESSID 共用一个域,登录态自动同步。
方案 B:URL 携带 token
跳转时在 URL 上加一个加密 token:
$token = md5($_SESSION['user_id'] . time() . SECRET);
$location .= '&sso_token=' . $token;ECTouch 端拿到 token 后从 EcShop 的 session 表里反查 user_id 完成登录。这个方案适合两套程序的 session 后端不通用的场景。
响应式设计 vs 独立移动站的取舍
本文方案是“独立移动站”(separate URLs)路线。Google 官方文档列出了三种移动适配方案,按推荐度排序:
- 响应式设计(同一 URL,根据屏幕宽度切样式)—— Google 最推荐。
- 动态服务(同一 URL,服务端按 UA 返回不同 HTML)—— 需要正确的 Vary: User-Agent 头。
- 独立 URL(PC 与移动版用不同 URL,互相通过 alternate/canonical 链接)—— 配置成本最高,本文方案属于这一类。
那为什么 EcShop + ECTouch 还要用独立 URL?因为 EcShop 2.7.3 是 2014 年的代码,基础架构里没有 mobile-first 模板能力,临时改成响应式工程量比保留 ECTouch 大十倍。如果你正在新建一个商城,建议直接用 ShopXO、WooCommerce 这种支持响应式主题的现代电商平台,跳过 EcShop + ECTouch 这套。
实战遇到过的几类故障
故障 1:跳转后 ECTouch 显示“Page not found”
原因是 ECTouch 的 .htaccess 或 nginx rewrite 没启用,导致 /mobile/?c=goods&a=index&id=123 这种 URL 后端没匹配到路由。检查 ECTouch 根目录是否有 .htaccess(apache)或者站点配置里是否加了 ECTouch 专属的 rewrite 块。最简的 nginx 配置:
location /mobile/ {
try_files $uri $uri/ /mobile/index.php?$query_string;
}故障 2:iPhone Chrome 不跳,iOS Safari 正常
iPhone Chrome 的 UA 长这样:Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/116.0.0.0 Mobile/15E148 Safari/604.1。原文的 UA 字典里没有“iphone”,所以匹配失败。本文增强版加了 iphone 关键字。
故障 3:跳转死循环
用户访问 example.com/goods.php?id=1,被跳到 example.com/mobile/?c=goods&a=index&id=1,但 ECTouch 端发现是手机访问又跳回 PC——配置错误导致互相推。两种触发:
- ECTouch 没正确识别“我已经在手机版页面”,例如 ECTouch 的某些跳转模块也调了 PC 端的 init.php。检查 ECTouch 是否独立 init,避免依赖 EcShop。
- 本文 pc_to_mobile() 的“已经在 mobile 路径下不再触发”判断没生效,可能是 REQUEST_URI 没有包含 mobile 关键词(比如你用的是子域名 m.example.com 而不是 /mobile/)。需要把判断条件改成
strpos($_SERVER['HTTP_HOST'], 'm.') === 0这种主机名检查。
故障 4:搜索引擎抓取被自动跳到移动版
Googlebot 的 UA 包含“Mobile”字样(Googlebot for Mobile),会被本文 UA 字典命中触发跳转。如果你希望 Googlebot Mobile 抓取 PC URL(让 Google 看到你的 alternate/canonical 关系),可以在 pc_to_mobile() 函数开头加:
if (strpos($ua, 'googlebot') !== false || strpos($ua, 'baiduspider') !== false || strpos($ua, 'bingbot') !== false || strpos($ua, 'yandexbot') !== false) {
return; // 搜索引擎不跳转
}但更现代的做法是让搜索引擎都跳转——Google 的 mobile-first indexing 默认只索引移动版本,不跳反而错过收录机会。决策的依据是你的 PC 与移动 URL 是否互相做了 alternate/canonical:做了就让搜索引擎跳,没做就不跳。
故障 5:HTTPS / HTTP 混合
EcShop 是 HTTPS,ECTouch 配置成了 HTTP,跳转后浏览器报“不安全”。一定要让两套程序都走 HTTPS。具体操作:在 EcShop 后台改 site_url 为 https://,ECTouch 的配置文件 mobile/data/config.php 里也改成 https://。然后 nginx 强制 80 跳 443:
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}故障 6:手机端用户体验断崖式下跌
跳转到 ECTouch 之后,发现页面渲染慢、图片大、字体小、按钮挤。这不是跳转方案的问题,是 ECTouch 1.0 主题本身已经过时了。补救:升级到 ECTouch 2.7+(重写过模板,响应式);或者让用户长按选择电脑版(前面 cookie 方案);或者干脆放弃 ECTouch,把 EcShop 主题改成响应式。
性能优化:避免每次请求都跑 UA 判断
pc_to_mobile() 在每次请求都执行,对高并发站点是浪费。两个优化:
优化 A:在 nginx 层做 UA 判断
map $http_user_agent $is_mobile {
default 0;
"~*(iphone|ipod|android|blackberry|windows phone|webos|opera mini|opera mobi|huawei|xiaomi|oppo|vivo|redmi|meizu|honor|nubia|mobile)" 1;
}
server {
listen 443 ssl;
server_name example.com;
location ~ \.php$ {
if ($is_mobile = 1) {
set $rewrite_url "";
if ($uri ~ "^/category-(\d+)") {
set $rewrite_url "/mobile/?c=category&a=index&id=$1";
}
if ($uri ~ "^/goods-(\d+)") {
set $rewrite_url "/mobile/?c=goods&a=index&id=$1";
}
if ($rewrite_url != "") {
rewrite ^ $rewrite_url? redirect;
}
}
# ... 正常 PHP-FPM 处理
}
}这样跳转完全不进 PHP,性能最优。
优化 B:用 OPcache 缓存 lib_main.php 编译结果
修改完 lib_main.php 后确保 PHP-FPM 启用 OPcache 且设置 opcache.validate_timestamps=0(生产环境)或 opcache.revalidate_freq=60(开发环境)。这样 pc_to_mobile() 每次调用不会重复编译。
UA 字典维护与现代设备检测库
手动维护 UA 正则字典每年都要更新(新品牌出现、新型号上市)。更稳定的做法是引入开源设备检测库:
Mobile_Detect
开源 PHP 库,单文件,适合插入 EcShop。GitHub 上 serbanghita/Mobile-Detect。用法:
require_once 'includes/Mobile_Detect.php';
$detect = new Mobile_Detect();
if ($detect->isMobile() && !$detect->isTablet()) {
// 跳手机版
}它的 UA 字典覆盖了几乎所有市面手机,且作者每年更新。
WURFL
商业级设备检测库,UA 字典几万条,连具体型号、屏幕尺寸都能识别。EcShop 这种规模一般不需要这么重,了解即可。
常见问题解答
跳转后 URL 后面带了空的 & 符号是什么原因?
原文代码 $mobile_url .'/?c='.$filename.'&a=index&'.$_SERVER["QUERY_STRING"] 在 QUERY_STRING 为空时会产生 ?c=goods&a=index&。修复:用本文增强版的写法 ($qs ? '&' . $qs : '') 显式判断。
用户在手机版选了“电脑版”之后还是被跳回手机版?
三个排查:cookie 是否正确写入(浏览器开发者工具 Application 看 view_pc 是否存在);cookie 的 path 是否设了 /(path 设错会导致 EcShop 端读不到);EcShop 端是否走了缓存导致 PHP 没重新执行(清掉 nginx fastcgi_cache 与浏览器缓存)。
iPad 应该跳手机版还是桌面版?
本文默认让 iPad 走桌面版(屏幕够大)。但如果你的 ECTouch 主题做过 iPad 适配,可以让 iPad 跳手机版。改法是删掉 pc_to_mobile() 里那段 iPad 例外。
跳转方案对 SEO 有没有负面影响?
正确配置(独立 URL + alternate/canonical)的话没有。错误配置(302 死循环、跨域 cookie 丢失、Googlebot 被跳到错误 URL)会有显著负面。每次部署后用 Google Search Console 的“URL 检查”工具测一下 PC 与移动版分别能不能被收录。
ECTouch 2.7+ 是否还需要本文方案?
ECTouch 2.7+ 已经是响应式主题,理论上 PC 与移动可以共享 URL。但如果你已经按本文方案运营了一段时间且搜索引擎已收录 /mobile/ 下的 URL,迁移到响应式会涉及大量 301 跳转。建议根据流量规模与改造成本评估。
UA 字典需要多久更新一次?
每年至少一次。新增品牌(比如荣耀从华为分出之后单独有“Honor”标识,部分新品牌如“真我”“iQOO”)需要单独添加。或者直接接 Mobile_Detect 库省心。
跨域时 ECTouch 拿不到 EcShop 的登录态怎么办?
三种方案:父域 cookie(最简单);URL token SSO(最稳);Redis 共享 session(需要两套程序都改 session_handler)。中型站推荐父域 cookie。
301 跳转后 EcShop 端的统计代码(Google Analytics、百度统计)数据丢失?
301 在跳转前页面没渲染,统计代码不会执行,所以这部分流量不会被 PC 端统计到。如果你希望统计“手机用户访问了哪个 PC URL 然后被跳走”这个数据,可以在 init.php 里 pc_to_mobile() 之前手动记录到自己的统计表,或者把跳转改成 client-side(先返回一段 JS 触发统计再跳)——但 client-side 跳转 SEO 不友好,权衡选择。
nginx 层做 UA 判断与 PHP 层判断哪个更好?
nginx 层性能优、节省 PHP 进程;PHP 层灵活、可以接 cookie 与会话状态。建议组合:nginx 层处理纯 UA 跳转的 95% 场景,PHP 层处理需要看 cookie/会话的 5% 场景。
跳转记录怎么查?日志里看不到 302
nginx access_log 默认会记录 302 响应。如果你看不到,检查 log_format 是否有 $status 字段;access_log 的 buffer 设置可能让记录滞后几分钟才落盘。要实时看 302 跳转,用 tail -f /var/log/nginx/access.log | grep " 302 "。如果 nginx 层做了缓存,跳转可能不进 access_log,需要在 fastcgi_cache 配置里加 X-Cache-Status 头辅助调试。
本文标题:《EcShop 判断手机访问跳转 ECTouch 完整方案:UA 字典升级、SEO 301/302、跨域 session 与防死循环》
本文链接:https://zhangwenbao.com/ecshop-mobile-ectouch.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0