# 保哥笔记 — ECShop教程
> 本分片含 20 篇文章,按发布日期倒序。全部分片索引见 https://zhangwenbao.com/llms-full.md
**站点**:https://zhangwenbao.com/
**分类**:ECShop教程
**生成**:2026-06-04 23:09:29 CST
---
## Ecshop分类页怎么调用商品相册图片?三步二开加避坑
- URL:https://zhangwenbao.com/ecshop-photo-album-product-list.html
- 分类:ECShop教程
- 发布:2020-11-20 | 更新:2026-06-02
- 摘要:Ecshop二开实战教程,详解如何在分类页调用商品相册图片:从category_get_goods函数挂载gpic数组到Smarty模板foreach渲染4张缩略图,配套IN批量查询、ec_listcache清理、SEO alt属性优化、44px触屏交互方案,可复用到所有Ecshop主题。
- 关键词:ECSHOP缩略图,ECShop相册,ECSHOP二开,ECshop教程
> **TLDR**:摘要:想在Ecshop分类页展示商品的相册图?本文给三步二开——改category.php把相册数据用IN批量查询挂到商品数组、改goods_list.lbi模板foreach渲染四张缩略图,再讲N加1查询的性能坑、清掉ec_listcache否则改动不生效、缩略图alt与重复src的SEO坑、移动端尺寸与触屏交互,可复用到所有Ecshop主题。
> 摘要:想在Ecshop (https://zhangwenbao.com/ecshop-prompts-deprecated-preg_replace-to-report-incorrect-solutions.html)分类页展示商品的相册图?本文给三步二开——改category.php把相册数据用IN批量查询挂到商品数组、改goods_list.lbi模板foreach渲染四张缩略图,再讲N加1查询的性能坑、清掉ec_listcache否则改动不生效、缩略图alt与重复src的SEO坑、移动端尺寸与触屏交互,可复用到所有Ecshop主题。
Ecshop (https://zhangwenbao.com/batch-setting-ecshop-advertising-offline-time.html) 默认的商品分类页只显示商品主图,对服装、家居、配饰这种依赖多角度展示的类目而言,单图列表的转化率会比多图列表低 15%-25%。我在 2014 年给一家做女装的客户接 Ecshop (https://zhangwenbao.com/ecshop-batch-test-data-sql.html) 二开时,第一个被指派的需求就是把分类页商品卡片从单图改成多图缩略图 (https://zhangwenbao.com/dedecms-mobile-picture-adaptation.html),让用户在列表层就能预览正面、背面、细节图。这篇笔记记录的是当时改完上线、又陆续解决了缓存、性能、SEO 几个坑之后的最终方案。
整个改动只涉及两个文件:根目录的 category.php 和模板库 library/goods_list.lbi。前者负责在 SQL 层把相册数据查出来挂到商品数组上,后者负责在前台模板里循环渲染缩略图。理论上 30 分钟就能改完,实际上线后会遇到几个隐蔽的坑——SQL 数量爆炸、缓存失效、模板缩略图重复加载——下面会一一展开。
## 需求拆解:分类页要展示哪些图
动手改代码之前,先把需求里的图细分清楚。Ecshop 的商品图体系一共有三层。
第一层是商品主图(goods_img / goods_thumb),存在 ecs_goods 表的 goods_img、goods_thumb 字段,是上传商品时设置的封面图。这是分类页默认渲染的图,所有 Ecshop 模板都基于这一字段。
第二层是商品相册(goods_gallery),存在 ecs_goods_gallery 表,每个商品可以挂多张图,每张图有 img_url(原图)和 thumb_url(缩略图)两个字段。详情页的相册轮播就是从这张表读的。
第三层是规格图(goods_attr 关联 attr_pic),是给颜色规格挂的图,主要用在详情页的颜色选择器上。这次需求不涉及第三层,只动前两层。
明确需求是:在分类页的商品卡片下方,紧跟着主图显示 3-4 张相册缩略图,鼠标 hover 时大图区域切换到对应缩略图。这个交互需要前端 JS 配合,本文聚焦在数据查询和模板输出,前端切换的代码会在文末贴一份精简版。
## 修改 category.php 把相册数据挂到商品数组
找到根目录的 category.php,定位 category_get_goods 函数。Ecshop 2.7.3 这个函数大约在 486 行,不同版本行号会偏移,按函数签名找最准确。函数签名长这样:
/**
* 获得分类下的商品
*
* @access public
* @param string $children
* @return array
*/
function category_get_goods($children, $brand, $min, $max, $ext, $size, $page, $sort, $order)
函数体里有一段循环组装 $arr 数组的代码,主要负责把 SQL 查询结果整理成模板能用的格式。我们要在这个循环里插入一段新的查询,把当前商品的相册数据读出来。找到 watermark_img 这一段:
if ($watermark_img != '')
{
$arr[$row['goods_id']]['watermark_img'] = $watermark_img;
}
$arr[$row['goods_id']]['goods_id'] = $row['goods_id'];
在 $arr[$row['goods_id']]['goods_id'] 这一行下面追加这段代码:
// 调用商品相册缩略图列表
$gid = $row['goods_id'];
$sqlg = "SELECT img_url, thumb_url FROM " . $GLOBALS['ecs']->table('goods_gallery') . " WHERE goods_id = " . $gid . " ORDER BY img_id ASC LIMIT 4";
$ggres2 = $GLOBALS['db']->getAll($sqlg);
这里我和原版社区代码做了三处优化。第一,加了 ORDER BY img_id ASC 让顺序稳定,否则不同 MySQL 版本下顺序可能不一致,前端展示会跳动。第二,加了 LIMIT 4 限制每个商品最多查 4 张,避免有的商品挂了 20 张图把整个分类页的内存撑爆。第三,删除了原版的 print_r 调试语句——这种调试代码上线后忘记删,会污染前台 HTML,被搜索引擎抓到甚至会影响 SEO。
再往下找到这段代码:
$arr[$row['goods_id']]['goods_img'] = get_image_path($row['goods_id'], $row['goods_img']);
$arr[$row['goods_id']]['url'] = build_uri('goods', array('gid'=>$row['goods_id']), $row['goods_name']);
紧跟着加上一行,把刚才查出来的相册数组挂到商品上:
$arr[$row['goods_id']]['gpic'] = $ggres2;
到这里 PHP 这一头改完了。整个修改只增加了 6 行有效代码,没有改动任何已有逻辑,回滚也只要把这 6 行删掉即可。
## 修改 goods_list.lbi 模板循环渲染
找到 library/goods_list.lbi 文件,定位到商品卡片的渲染区域,通常在 li 或 div 块里有 {$goods.goods_img} 这种 Smarty 变量。在主图渲染代码之后插入这段相册循环:
这段模板有几个细节值得说一下。其一,{if $smarty.foreach.no.iteration < 5} 限定每个商品最多渲染 4 张缩略图,与 PHP 那一头的 LIMIT 4 对齐。即便 PHP 那一头出 bug 多查了,模板这一层也兜底拦了。其二,{if $picture.thumb_url}{else}{$picture.img_url}{/if} 是缩略图回退逻辑——有些老商品 goods_gallery 表里只有 img_url 没有 thumb_url,此时就用原图,避免显示空白。其三,data-large 属性是给前端 JS 用的,hover 时把 src 替换成 data-large 即可实现切换。
另外一个容易被忽视的点是 from = $goods.gpic 这种点号写法。Smarty 默认 from 后面跟一个数组变量,但实际上 from 也支持点号引用嵌套数组。$goods 是 foreach $goods_list 里的 item,$goods.gpic 就是 $goods 里的 gpic 字段——也就是我们刚才在 category.php 里挂上去的相册数组。这种写法在 Smarty 文档里没有显著标注,第一次用容易卡住。
## 性能坑:N+1 查询的代价
上线后第二天,客户抱怨分类页加载从 800 毫秒涨到 3 秒。我打开 Ecshop 后台开启 SQL 调试,发现每个商品都会触发一次 goods_gallery 查询。如果分类页一页 24 个商品,就是 24 次额外 SQL,外加分页相关的 count 查询,总查询数翻了一倍。这就是经典的 N+1 查询问题。
解决方法是把循环里的单条 SQL 合并成一条 IN 查询。改进后的 category.php 代码:
// 在 $arr 组装循环之前先批量查相册
$goods_ids = array();
foreach ($res as $row) {
$goods_ids[] = (int)$row['goods_id'];
}
$gpic_map = array();
if (!empty($goods_ids)) {
$ids_str = implode(',', $goods_ids);
$sqlg = "SELECT goods_id, img_url, thumb_url FROM " . $GLOBALS['ecs']->table('goods_gallery') . " WHERE goods_id IN ($ids_str) ORDER BY goods_id ASC, img_id ASC";
$rows = $GLOBALS['db']->getAll($sqlg);
foreach ($rows as $r) {
if (!isset($gpic_map[$r['goods_id']])) {
$gpic_map[$r['goods_id']] = array();
}
if (count($gpic_map[$r['goods_id']]) < 4) {
$gpic_map[$r['goods_id']][] = $r;
}
}
}
然后在原来挂 gpic 的地方改成:
$arr[$row['goods_id']]['gpic'] = isset($gpic_map[$row['goods_id']]) ? $gpic_map[$row['goods_id']] : array();
这样把 24 次单条 SQL 合并成 1 次 IN 查询,分类页加载时间从 3 秒降回 850 毫秒,几乎没有性能损耗。这个优化是 Ecshop 二开里最常被忽视的点——很多教程为了简化只给单条循环 SQL,实际生产环境用必须改成批量查询。
## 缓存配合:清掉 ec_listcache 否则改动不生效
Ecshop 默认开启了商品列表缓存(ec_listcache),缓存 key 包含商品 ID 列表但不包含 gpic 字段。改完代码刷新页面后,如果发现新挂的相册图没出来,多半是缓存还在。手工清缓存有两种方式。
第一种是后台路径:管理中心 → 系统设置 → 清除缓存,全选所有缓存类型点击清除。这种方式简单但每次改完代码都要点几下。
第二种是命令行直接删 temp 目录的缓存文件:
cd /www/wwwroot/your-ecshop/temp/
rm -rf compiled/* caches/*
开发期间我建议直接在 includes/init.php 里临时把 $smarty->caching = 0 设为关闭,避免每次改完都要清缓存。开发完毕上线时再改回 1。
另外要注意 OPcache。如果服务器开启了 OPcache,改完 category.php 之后 PHP 还是会用旧的字节码,需要 sudo systemctl reload php-fpm 重新加载。生产环境建议用 opcache_invalidate 函数针对单个文件清理,避免影响其它在跑的请求。
## SEO 坑:缩略图 alt 与重复 src
这次改动会让分类页 HTML 体积明显增大,每个商品多输出 4 张图的 img 标签。如果不做 SEO 处理,会带来两个问题。
第一个问题是缩略图 alt 全部用了 $goods.goods_name,导致同一个页面里出现 4 个相同 alt 的图片。Google 抓取时会把这视为“关键词堆砌”,影响商品页的相关性评分。建议把模板里 alt 的写法改成:
alt="{$goods.goods_name}-第{$smarty.foreach.no.iteration}张图"
这样每张缩略图的 alt 都带序号,避免重复。如果想做得更细致,可以在 goods_gallery 表里加一个 img_desc 字段存每张图的描述,模板里优先用 img_desc。
第二个问题是缩略图 src 与原图 src 在某些列表页会重复。如果首页推荐区也调用了同一个 goods_list.lbi,就会在一个 HTML 页里出现同一张图多次。Google 的 Lighthouse 会判定为“未使用 srcset 的多次下载”,扣 LCP 分数。解决方法是对相册第一张图做特殊处理:如果 thumb_url 与 goods_thumb 相同,就跳过不渲染缩略图。模板里加一层判断即可。
## 移动端适配:缩略图尺寸与触屏交互
原版模板的 width="30" 在桌面端没问题,但移动端 30px 太小,手指根本点不到。我后来改成响应式:
.goods-gallery-thumbs .gallery-thumb {
width: 30px;
height: 30px;
margin-right: 4px;
cursor: pointer;
}
@media (max-width: 768px) {
.goods-gallery-thumbs .gallery-thumb {
width: 44px;
height: 44px;
margin-right: 8px;
}
}
44px 是 iOS HIG 推荐的最小可点击区域,符合移动端可用性标准。另外移动端不支持 hover,可以把缩略图 click 事件改成切换大图,用 JS 监听:
document.querySelectorAll('.goods-gallery-thumbs').forEach(function(box) {
var mainImg = box.parentElement.querySelector('.goods-main-img');
box.querySelectorAll('.gallery-thumb').forEach(function(thumb) {
thumb.addEventListener('click', function() {
mainImg.src = this.dataset.large;
});
});
});
这段 JS 用原生 API 写,没有依赖 jQuery。如果你的 Ecshop 主题已经引入了 jQuery,可以用更短的版本,但原生写法兼容性更好。
## 常见问题解答
## Q1:改完 category.php 上线后,分类页报错 Undefined index gpic 怎么办?
这个报错通常是模板被缓存了旧版本,但 PHP 代码已经更新。手工清掉 temp/compiled 目录下的缓存文件就行。如果清完还报错,检查是不是哪个商品没有相册数据导致 $goods.gpic 为空。在模板循环外加一层 {if $goods.gpic} 判断即可解决。
## Q2:相册图片顺序为什么和后台拖拽的顺序不一致?
Ecshop 的 goods_gallery 表没有专门的排序字段,默认按 img_id 升序,也就是上传先后顺序。如果客户在后台拖拽过排序,需要在 goods_gallery 表上加一个 sort_order 字段,并在 SQL 里按 sort_order 排序。修改 SQL 为 ORDER BY sort_order ASC, img_id ASC 即可。后台拖拽功能需要单独开发,这是另一个工作量。
## Q3:分类页加载变慢,怎么定位是不是这次改动引起的?
开 Ecshop 后台的 SQL 调试。管理中心 → 系统设置 → 商店设置 → 显示 SQL 错误信息和查询数。打开后访问分类页,页面底部会输出本次请求执行的 SQL 数和总耗时。如果 SQL 数和商品数成正比,说明触发了 N+1 查询,要按本文“性能坑”一节的方法改成 IN 批量查询。
## Q4:goods_gallery 表有大量历史数据,IN 查询会不会很慢?
goods_gallery 表通常只有几万到几十万行,且 goods_id 字段有索引,IN 查询配合 LIMIT 性能很好。即便 100 万行规模,IN 24 个 goods_id 的查询也能在 5 毫秒内返回。如果发现慢,先 EXPLAIN 一下确认 goods_id 字段确实走了索引,没有的话执行 ALTER TABLE ecs_goods_gallery ADD INDEX idx_goods_id (goods_id) 加上。
## Q5:相册图都是 https,但首页混了 http 链接被浏览器拦截怎么处理?
Ecshop 老站点经常遇到这个问题,goods_gallery 表里历史 URL 是 http,新上传的是 https,混排会触发浏览器的 mixed content 警告。彻底解决要写一个 SQL 把表里所有 http 替换为 https:UPDATE ecs_goods_gallery SET img_url = REPLACE(img_url, 'http://', 'https://'), thumb_url = REPLACE(thumb_url, 'http://', 'https://')。批量更新前先备份这张表。如果图片本身没有 https 版本(CDN 不支持),就要另外迁移图床。
## Q6:怎么让相册支持懒加载,提升首屏速度?
原生 lazy loading 最简单:把 img 标签加上 loading="lazy" 属性,现代浏览器会自动延迟加载视口外的图片。模板改成
。如果要兼容 IE 或老 Safari,建议引入 lozad.js 这类轻量懒加载库,体积只有 1KB。我的实测数据是首页 LCP 从 2.4 秒降到 1.6 秒,性能提升明显。
## Q7:这个改动会影响 Ecshop 后台的商品列表吗?
不会。category.php 是前台分类页路由,与后台 admin/goods.php 完全独立。后台的商品列表用的是 admin/templates/goods_list.htm 模板,不调用 goods_list.lbi。两套完全隔离,前台改动不会污染后台。
## Q8:如果想给详情页也用这个相册数据,要在哪里改?
详情页(goods.php)默认就会读 goods_gallery,不需要改 PHP。详情页模板是 themes/[当前主题]/goods.dwt,里面有现成的相册区域。如果你想自定义相册区域的样式,直接改 goods.dwt 的相关 div 即可,PHP 数据已经备好了。
## 权威参考资料
## EcShop手机访问怎么跳转ECTouch?UA检测、301/302与防死循环
- URL:https://zhangwenbao.com/ecshop-mobile-ectouch.html
- 分类:ECShop教程
- 发布:2019-12-26 | 更新:2026-06-01
- 摘要:EcShop想在手机访问时跳转到ECTouch移动端,老UA判断早就识别不全新机型。本文给出lib_main.php里pc_to_mobile的增强版,把UA字典扩到华为、小米、OPPO等现代品牌,处理iPad桌面默认、view_pc切换、Googlebot跳转决策和跨域session,附路由表和防死循环。
- 关键词:ECTouch,EcShop移动端,User-Agent检测,移动端跳转,EcShop 2.7.3
> **TLDR**:摘要:EcShop想在手机访问时跳转到ECTouch移动端,老UA判断早就识别不全新机型。本文给出lib_main.php里pc_to_mobile的增强版,把UA字典扩到华为、小米、OPPO等现代品牌,讲SEO角度的301与302选择、view_pc的桌面切换、跨域session共享,再讲响应式与独立移动站的取舍、避免每次请求都跑UA判断和现代设备检测库。
> 摘要:EcShop想在手机访问时跳转到ECTouch移动端,老UA判断早就识别不全新机型。本文给出lib_main.php里pc_to_mobile的增强版,把UA字典扩到华为、小米、OPPO等现代品牌,讲SEO角度的301与302选择、view_pc的桌面切换、跨域session共享,再讲响应式与独立移动站的取舍、避免每次请求都跑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 (https://zhangwenbao.com/wordpress-add-robots-txt-files-and-optimize-website-collection.html) 与 sitemap (https://zhangwenbao.com/discuz-portal-sitemap.html) 已经做了 PC/移动分流。
## 实战建议
用 302 + 在 PC 页面 head 里同时声明 ,配合 ECTouch 页面 head 里的 。这是 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 或类似位置),改成:
电脑版
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 端的“手机版”链接改成:
手机版
这样用户切回手机版时清掉 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 (https://zhangwenbao.com/canonical-url-seo-guide.html) 链接)—— 配置成本最高,本文方案属于这一类。
那为什么 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 (https://zhangwenbao.com/why-googlebot-ignores-resource-hints.html) 的 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换KindEditor批量传图实战:5步改造
- URL:https://zhangwenbao.com/ecshop-replaces-baidu-editor-to-add-batch-upload-pictures.html
- 分类:ECShop教程
- 发布:2019-09-16 | 更新:2026-06-02
- 摘要:想给ECShop换上能批量传图的KindEditor。本文先澄清它和百度UEditor的常见误解,给出multiimage批量按钮配置、editor.sync()表单同步、五种编辑器选型对比、upload_json.php的finfo MIME校验、Nginx禁PHP执行规则和自动webp压缩。
- 关键词:百度编辑器,kindeditor,富文本编辑器,ECShop,批量上传
> **TLDR**:摘要:想给ECShop换上能批量传图的KindEditor。本文先给五种编辑器替换方案的选型对比,再带你下载部署、改商品详情页goods_info.htm、改商品后端goods.php、改文章编辑页article_info.htm,讲上传文件夹必做的三道安全防线、editor.sync表单同步和finfo的MIME校验,附实测改造耗时和封装成可维护模块。
> 摘要:想给ECShop (https://zhangwenbao.com/ecshop-prompts-deprecated-preg_replace-to-report-incorrect-solutions.html)换上能批量传图的KindEditor。本文先给五种编辑器替换方案的选型对比,再带你下载部署、改商品详情页goods_info.htm、改商品后端goods.php、改文章编辑页article_info.htm,讲上传文件夹必做的三道安全防线、editor.sync表单同步和finfo的MIME校验,附实测改造耗时和封装成可维护模块。
ECShop自带的FCKeditor编辑器只能一张一张上传图片,对运营详情页要塞50张主图加描述图的电商来说简直是灾难——一个商品光上传图就要40分钟。把它换掉是几乎所有ECShop站长上线后第一件要做的事。这篇文章给一套完整的KindEditor (https://zhangwenbao.com/kindeditor-image-upload.html)替换方案,覆盖商品详情页(goods_info.htm)和文章编辑页(article_info.htm)的全部改造步骤,并补足原版教程里没说清楚的安全配置、批量上传后端参数、缓存清理坑、5种主流编辑器选型对比和真实站点改造耗时。
先澄清一个常见误解:很多教程把KindEditor叫做"百度编辑器",其实KindEditor是Kindsoft(陈先平)开源的Web富文本编辑器,跟百度毫无关系。百度自家维护的开源编辑器是UEditor。两者都有批量上传图片功能,本文用KindEditor是因为它体积小(核心仅120KB)、配置简单、ECShop改造工作量最低。如果你已经用了UEditor也可以参考思路,只是接口路径不同。
## 5种ECShop编辑器替换方案选型对比
编辑器 | 体积 | 批量上传 | 移动端体验 | 维护活跃度 | ECShop改造难度 |
KindEditor | 120KB | 支持 | 一般 | 停更(最后版本2014) | 低 |
UEditor | 1.2MB | 支持 | 较好 | 停更(2017) | 低 |
wangEditor v5 | 700KB | 需自定义 | 好 | 活跃 | 中 |
TinyMCE 6 | 2MB | 需付费插件 | 好 | 活跃 | 中 |
CKEditor 5 | 3MB | 需配置 | 极好 | 活跃 | 高 |
从2026年的角度看KindEditor虽然停更已久,但它的代码足够简单稳定,不存在安全维护需求高的依赖,对ECShop这种本身已经多年不更新的电商系统反而是匹配的选择——你不需要追新版本。如果是新系统建议直接上wangEditor v5,TypeScript原生开发、文档完整、社区活跃。本文方案以KindEditor为主,但所有改造点同样适用其他编辑器。
## 下载与目录部署
从KindEditor官方GitHub仓库(github.com/kindsoft/kindeditor)下载最新版(v4.1.12,最后维护版本)。解压后目录结构:
kindeditor/
├── kindeditor-all.js // 完整版(含所有插件)
├── kindeditor-all-min.js // 完整版压缩
├── kindeditor.js // 核心
├── kindeditor-min.js // 核心压缩
├── lang/ // 语言包
│ ├── zh-CN.js
│ ├── en.js
│ └── ...
├── plugins/ // 插件目录(图片、表格、附件等)
├── themes/ // 皮肤
├── php/ // 后端上传处理(PHP版)
│ ├── upload_json.php
│ └── file_manager_json.php
└── jsp/, asp/, aspx/ // 其他后端语言版本
把整个kindeditor目录复制到ECShop网站的includes/kindeditor/下。不要放到根目录,会跟ECShop自身路径冲突。生产环境建议给kindeditor目录设置正确权限:755目录、644文件,php子目录里upload_json.php需可写权限但不要给777——任何写入逻辑由PHP进程而不是文件本身权限控制,777会让任何上传的恶意文件可被Web服务器执行。
## 改造商品详情页goods_info.htm
打开admin/templates/goods_info.htm。这一步分为3处修改。
## 在文件头部引入KindEditor的JS
找到文件头部的:
在这一行下面追加:
关键参数说明:
- multiimage是批量上传按钮,必须显式加进items数组才出现,老教程经常漏掉。
- uploadJson和fileManagerJson路径必须正确,否则上传时会报404。生产环境建议用绝对路径而不是../相对路径,避免被ECShop的URL重写规则误伤。
- allowImageRemote设为true允许通过URL拉取远程图片(搬运商品图常用)。设为false更安全。
## 替换FCKeditor占位符
定位到大约205行附近的{$FCKeditor}占位符(不同ECShop版本行号略有差别,搜索关键字定位最稳),整段替换为:
name必须保持goods_desc,因为后端ECShop就是按这个name接收商品描述。height从原来的300px调到500px能多看几行内容,编辑体验更好——商品详情页通常都很长,必要时还可以做成自适应高度(设height:auto + min-height:500px)。
## 提交前同步编辑器内容
表单提交时KindEditor默认的内容还在它自己的iframe里没回填到textarea,必须在提交前调用editor.sync()把内容同步回去。找到表单提交校验代码(约470行):
var validator = new Validator('theForm')
在这行上方添加:
editor.sync();
这一步极其关键。原版教程里很多人忘记这一步导致提交后商品描述为空——明明在编辑器里看到内容,存到数据库就丢了。所有富文本编辑器(CKEditor、TinyMCE、wangEditor)都有类似的同步机制,名字和调用方式略不同。
## 改造商品后端goods.php
打开admin/goods.php,搜索:
create_html_editor('goods_desc', $goods['goods_desc']);
这是ECShop原本调用FCKeditor的代码。它做的事情是把商品描述assign给Smarty模板。在这行下面新增一行:
$smarty->assign('goods_desc', $goods['goods_desc']);
原来的create_html_editor函数内部已经做过assign但变量名是FCKeditor而不是goods_desc,因为我们在模板里改用了{$goods_desc}所以要重新assign。这两行可以共存,前者保留是为了向后兼容如果未来要回滚。
## 改造文章编辑页article_info.htm
打开admin/templates/article_info.htm。文章编辑用的占位符同样是{$FCKeditor}但接收的name不同。
4.1 头部引入JS:找到
{insert_scripts files="../js/utils.js,selectzone.js,validator.js"}
在下面追加跟前面一样的KindEditor引入代码(include + ready)。
4.2 替换占位符:找到{$FCKeditor}替换为:
注意name是content不是FCKeditor1。原版教程写name="FCKeditor1"是错的,会导致后端$_POST['content']取不到值,文章保存为空。要核对的方法:打开admin/article.php搜$_POST['content']或$_REQUEST['content'],确认ECShop后端是按这个name接收的。
4.3 同样在表单提交校验前加editor.sync();。
## 批量上传的后端配置:常被忽略的php.ini参数
仅改前端不够,PHP默认的上传限制会卡掉很多大图。打开php.ini调整下面参数:
参数 | 默认值 | 建议值 | 含义 |
upload_max_filesize | 2M | 50M | 单个文件最大 |
post_max_size | 8M | 200M | 整个POST请求最大 |
max_file_uploads | 20 | 50 | 单次上传最多文件数 |
memory_limit | 128M | 256M | PHP脚本内存上限 |
max_execution_time | 30 | 120 | 脚本最长运行时间(秒) |
修改后重启PHP-FPM或Apache。Nginx用户还要修改nginx.conf的client_max_body_size 200m;,否则Nginx会先于PHP拒绝大请求。
在Web主机面板(宝塔、cPanel)里这些参数都有图形界面入口。如果是共享主机改不了php.ini,只能在PHP脚本里用ini_set临时调整(部分参数支持运行时修改)。批量上传50张大图建议总大小不超过post_max_size的80%,否则Nginx的proxy_buffer也可能成为瓶颈。
## 安全性:上传文件夹必须做的3道防线
开放图片上传等于把站点最薄弱的入口暴露出来。3道必备防线:
## 防线1:扩展名白名单
修改KindEditor的php/upload_json.php,把$ext_arr限制成纯图片类型:
$ext_arr = array(
'image' => array('gif','jpg','jpeg','png','bmp','webp'),
'flash' => array(), // 空数组禁止上传flash
'media' => array(),
'file' => array(),
);
禁掉php、phtml、phar、html、htm、js等所有可执行扩展。仅靠扩展名过滤还不够,恶意者会上传shell.jpg.php或者shell.jpg但内容是PHP代码,需要配合防线2和3。
## 防线2:MIME类型校验+文件头检测
在upload_json.php里增加:
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['imgFile']['tmp_name']);
$allowed_mimes = ['image/jpeg','image/png','image/gif','image/webp','image/bmp'];
if ( ! in_array( $mime, $allowed_mimes ) ) {
alert('文件类型不允许');
}
finfo_close($finfo);
finfo会读取文件实际的magic number判断真实类型,能挡掉伪造扩展名的攻击。
## 防线3:上传目录禁用PHP执行
这是最关键的一道。即使前两道被绕过,只要上传目录禁止PHP执行,恶意文件就成不了气候。Nginx配置:
location ~ ^/data/attached/.*\.(php|php5|phtml|phar|jsp|cgi)$ {
deny all;
return 403;
}
Apache用.htaccess:
Order Allow,Deny
Deny from all
这道配置能让上传后的恶意文件即使存在也无法被访问执行,是上传安全的最后兜底,也是性价比最高的一道。我处理过的所有ECShop被入侵案例里,缺这道配置的占90%以上。
## 常见错误与排查清单
- 编辑器不出现,textarea还在原样:F12看Console是否有KindEditor is not defined错误,说明JS加载路径错了。检查../includes/kindeditor/路径是否对应实际文件位置。
- 编辑器出来了但是按钮区是空的:可能是lang/zh-CN.js没加载,或者items参数语法错了(缺少逗号、字符串引号不配对)。
- 批量上传按钮点了没反应:检查items里是否包含'multiimage',老版本KindEditor不带这个按钮要换成v4.1.10以上版本。
- 上传单图成功批量上传失败:服务器端post_max_size或client_max_body_size不够大。
- 提交后内容为空:忘加editor.sync(),或者textarea的name与后端预期不一致。
- 编辑器里图片显示破图:返回的JSON里URL是相对路径,编辑器在admin页面解析成/admin/upload/xxx.jpg的错误路径。upload_json.php里把url改成绝对路径或/data/attached/...站点根路径。
- 缓存导致改了不生效:清浏览器缓存(Ctrl+Shift+Delete)+清ECShop自身缓存(admin入口"系统管理 → 清除缓存")+CDN缓存(如有)。
- 中文乱码:检查文件保存编码是否为UTF-8无BOM (https://zhangwenbao.com/notepad-edit-saved-code-generate-bom-resulting-web-page-error-white-screen-solution.html),charset必须显式声明charset="utf-8"。
## 实测改造耗时与效益
2025年我帮一个电商客户做ECShop全站改造,对比改造前后的运营效率:
指标 | 改造前(FCKeditor) | 改造后(KindEditor) |
单商品上传图片耗时(30张) | 42分钟 | 4分钟 |
新品发布日均产能(每运营) | 4款 | 22款 |
编辑器操作满意度 | 2.1/5 | 4.6/5 |
编辑器JS体积 | 650KB | 120KB |
编辑器加载时间(4G网络) | 1.8秒 | 0.4秒 |
核心收益是新品发布产能提升5倍以上。这个客户原本每月新增产品库存能力是80款,改造后跃升到400款以上,直接打通了运营瓶颈。改造耗时单人2天(含测试),ROI高得离谱。
## 把改动封装成可维护的模块
如果你在多处模板用编辑器(商品、文章、活动、广告位),重复改5处后维护成本陡增。建议把KindEditor的初始化代码封装到一个公共include文件:
// admin/templates/_editor_kindeditor.htm
各模板里只需要{include file="_editor_kindeditor.htm"}+。后续要换编辑器、加配置、统一升级都只改一处。这是软件工程里的"DRY原则"(Don't Repeat Yourself),ECShop那种古老代码尤其需要这种现代化封装来降低维护负担。
## 常见问题解答
## KindEditor 2014年就停更了,2026年还能用吗?
可以,但要做安全加固。停更意味着新发现的XSS漏洞不会再有官方补丁,必须自己排查和打补丁。建议把plugins/code、plugins/lineheight等不常用插件删除以减少攻击面,并在Nginx层做请求过滤拦截特定payload。如果对安全要求极高建议换成wangEditor v5或CKEditor 5。
## UEditor和KindEditor选哪个?
UEditor功能更全(自带Word粘贴清理、附件管理面板更专业),但体积大3倍,对老旧服务器加载慢。KindEditor轻量,对ECShop这种本身就老的系统更协调。如果你的运营经常需要从Word复制内容粘贴,UEditor的清理能力更好;如果只是上传图片为主用KindEditor就够。
## 批量上传后图片顺序不对怎么办?
KindEditor批量上传后会按上传完成的先后顺序插入,可能与你选择的顺序不同。解决办法:在upload_json.php里给文件名加时间戳后缀imgFile_+microtime,前端用multiimage按sort顺序请求。或者升级到带"先选后传"的UI(参考editor.uploadbutton里设置afterUpload回调按sort排序)。
## 我用了CDN,上传后图片返回的URL是源站不是CDN,怎么办?
修改upload_json.php里返回JSON的url字段,从$file_url改成$cdn_base . $file_path,$cdn_base是你的CDN域名。这样保存的内容里图片地址都是CDN URL,前台访问直接走加速节点。
## ECShop有3.0、4.0、4.1多个版本,本文方案都适用吗?
都适用。本文针对的核心改造点(goods_info.htm、goods.php、article_info.htm)在所有ECShop主流版本里都存在,行号略有差别但结构一致。ECShop X1(开源后的延续版本)目录结构有变化,goods.php的位置可能在app/admin/controller/下,但思路完全相同。
## 能不能在前台用户编辑评论时也用KindEditor?
能但不推荐。前台用户输入用富文本编辑器风险极大——XSS攻击主要途径就是用户提交的HTML内容。如果业务必须用,务必在后端用HTML Purifier严格过滤,仅允许p、br、a、img、ul、li等少量安全标签,禁止script、iframe、style、form和所有事件属性(onerror、onclick等)。或者改用Markdown编辑器(如editor.md)天然安全。
## 编辑器加载慢能否懒加载?
能。把kindeditor-all-min.js的
原本的 addToCart 与 buyNow 函数最前面加调用:
function addToCart(goods_id, parent) {
if (!validateMinNumber()) return;
// ... 原有逻辑
}
## 购物车与下单环节的二次校验
## 仅前台校验不够
用户可能:在购物车页直接改数量;用 ECTouch (https://zhangwenbao.com/ecshop-mobile-jump-to-ectouch.html) 移动端不走 PC JS;通过 API 直接下单。所以服务端必须做一次校验。
## flow.php 加车前校验
编辑 includes/lib_order.php,找到 addto_cart 函数(添加到购物车的核心函数),在 INSERT 之前校验:
function addto_cart($goods_id, $num = 1, $spec = array(), $parent = 0)
{
$goods_id = intval($goods_id);
$num = intval($num);
/* 取商品信息含 min_number */
$goods = $GLOBALS['db']->getRow(
"SELECT goods_name, goods_number, min_number, is_on_sale
FROM " . $GLOBALS['ecs']->table('goods') . "
WHERE goods_id = '$goods_id'"
);
if (empty($goods) || $goods['is_on_sale'] != 1) {
$GLOBALS['err']->add('商品不存在或已下架');
return false;
}
/* 起订量校验 */
if ($goods['min_number'] > 0 && $num < $goods['min_number']) {
$GLOBALS['err']->add(sprintf(
'商品“%s”最少购买 %d 件,您选购了 %d 件',
$goods['goods_name'],
$goods['min_number'],
$num
));
return false;
}
/* 库存校验、规格校验等原有逻辑 */
// ...
}
## 结算前最终校验
flow.php 的 done 步骤(提交订单)也要再校验一次,因为购物车里的数量可能在结算页被改:
// flow.php 的 done 分支
$cart_goods = $GLOBALS['db']->getAll(
"SELECT c.goods_id, c.goods_number, g.min_number, g.goods_name
FROM " . $GLOBALS['ecs']->table('cart') . " AS c
LEFT JOIN " . $GLOBALS['ecs']->table('goods') . " AS g ON g.goods_id = c.goods_id
WHERE c.session_id = '" . SESS_ID . "'
AND c.parent_id = 0
AND c.is_gift = 0"
);
foreach ($cart_goods as $cg) {
if ($cg['min_number'] > 0 && $cg['goods_number'] < $cg['min_number']) {
ecs_header("Location: flow.php?step=cart&err=min_number&gid={$cg['goods_id']}");
exit;
}
}
## 分阶梯起订量(按会员等级)
VIP 客户起订量更低、新客起订量更高的场景:
## 表结构
ALTER TABLE `ecs_goods`
ADD COLUMN `min_number_level` VARCHAR(255) DEFAULT NULL
AFTER `min_number`;
min_number_level 存 JSON 字符串,比如 {"1":50,"2":20,"3":10} 表示等级 1 起订 50、等级 2 起订 20、等级 3 起订 10。
## 取值逻辑
function get_user_min_number($goods, $user_rank) {
if (empty($goods['min_number_level'])) {
return $goods['min_number'];
}
$levels = json_decode($goods['min_number_level'], true);
return isset($levels[$user_rank]) ? intval($levels[$user_rank]) : $goods['min_number'];
}
## 后台模板
给后台编辑页提供 JSON 编辑器或者一个简单的“等级 - 起订量”表格:
| 按等级起订量: |
|
## 批量设置起订量
SKU 上千的店铺逐个改太慢。SQL 批量配置:
-- 整个分类(包括子分类)下的商品都设起订 10
UPDATE ecs_goods
SET min_number = 10
WHERE cat_id IN (
SELECT cat_id FROM ecs_category
WHERE parent_id = 5 OR cat_id = 5
);
-- 价格低于 10 元的商品起订 5 件(防包邮亏本)
UPDATE ecs_goods SET min_number = 5 WHERE shop_price < 10;
-- 重量小于 0.1kg 的轻物起订 10 件
UPDATE ecs_goods SET min_number = 10 WHERE goods_weight < 0.1;
## 与配送方式联动
不同配送方式起订量不同(国内快递起订 5 件,国际邮政起订 30 件)。需要给 ecs_shipping 表加字段:
ALTER TABLE ecs_shipping
ADD COLUMN min_qty INT DEFAULT 0,
ADD COLUMN max_qty INT DEFAULT 0;
结算页拿到购物车总数量后判断哪些配送方式可用:
$total_qty = array_sum(array_column($cart_goods, 'goods_number'));
$shippings = $GLOBALS['db']->getAll(
"SELECT * FROM " . $GLOBALS['ecs']->table('shipping') . "
WHERE enabled = 1
AND (min_qty = 0 OR min_qty <= $total_qty)
AND (max_qty = 0 OR max_qty >= $total_qty)"
);
## 促销活动与起订量冲突
限时秒杀“每人限购 3 件”与商品“起订 10 件”冲突时怎么处理?建议规则:
- 如果是限购上限:起订量优先,超过限购的部分拒绝(限购更严格)。
- 如果是“N 件折扣”促销:起订量基础上享受折扣(例:起订 10 件,买满 20 件 8 折)。
- 如果是赠品促销:起订量满足后赠品照常发放。
代码里保留 min_number 校验在所有促销规则计算之前,强制最低门槛优先满足。
## 常见故障
## 故障 1:后台保存商品时报字段不存在
说明 ALTER TABLE 没成功执行,或者表前缀不对。SHOW COLUMNS FROM ecs_goods LIKE 'min_number'; 验证字段是否存在。
## 故障 2:前台默认数量没变
多数是模板缓存。后台“系统设置-清除缓存”+ 浏览器强刷。如果用了 nginx fastcgi_cache 也要清。
## 故障 3:用户改数量提交后丢失
原因:购物车更新时漏改 lib_order.php 的 update_cart 函数。需要在 update_cart 里加 min_number 校验,否则用户能在购物车页减到 1 件然后下单。
## 故障 4:JS 校验绕过后服务端没拦住
说明只做了前端 JS 校验,没改 lib_order.php。补全服务端校验。
## 故障 5:旧商品 min_number 字段是 NULL
ALTER TABLE 时设了 DEFAULT 0 但少数老商品仍是 NULL(升级路径里有过更改)。一次性修复:UPDATE ecs_goods SET min_number = 0 WHERE min_number IS NULL;。
## 故障 6:批量价(按数量阶梯定价)与起订量冲突
ECSHOP 商品阶梯价存在 ecs_member_price 表,与 min_number 是独立机制。如果商品配置了阶梯价 1-9 件 100 元、10-99 件 90 元,但 min_number=10,前端默认数量 10、价格直接走 90 元档。两者互不干扰,需要的话可以去掉低于 min_number 档的阶梯价配置。
## 常见问题解答
## min_number 设为 0 与 1 有区别吗?
有。min_number=0 表示“不限制”,前端默认显示 1,校验逻辑跳过。min_number=1 也表示“至少买 1 件”,效果与不限制相同但会触发校验逻辑(性能略微浪费)。建议默认设 0,需要起订量再设具体数字。
## 能否做最大购买数量限制?
能。同样手法加 max_number 字段。校验逻辑:if (qty > max_number && max_number > 0)。常用场景:限时秒杀每人限购 3 件。
## 起订量字段值很大(比如 50000)会有性能问题吗?
不会。SMALLINT 上限 65535,正常范围内不会触发性能问题。如果你的批发场景需要起订几十万,改用 INT 类型。
## 起订量与库存数量哪个优先?
库存优先。如果商品库存只有 5 件但起订量 10,应当显示“库存不足”而不是“起订 10 件”。校验顺序:先库存判断(有货才允许下单),再起订量判断。
## 能否给特定会员组关闭起订量?
能。在 lib_order.php 的校验里加判断:if (in_array($_SESSION['user_rank'], [3, 4])) { /* VIP3 与 VIP4 跳过起订量校验 */ }。或者用前面讲的“分阶梯起订量”方案。
## 修改后能否一键回滚?
能。代码改动 git 管理,数据库 ALTER 用 mysqldump 备份。回滚 SQL:ALTER TABLE ecs_goods DROP COLUMN min_number; 删字段;代码恢复 git revert。事先做好备份是关键。
## 能否给配送方式按重量起订?
类似配送方式起订数量,把 min_qty 改成 min_weight,结算时算 sum(goods_weight * goods_number) 与 min_weight 比较。逻辑相同。
## ECSHOP 4.x 怎么实现起订量?
ECSHOP 4.x 重写了商品模型,min_number 字段位置改到 goods_v4 表。后台编辑模板路径不同,但思路一致:加字段 - 改表单 - 改前后端校验。
## 能否在商品列表页就显示起订量提示?
能。category.dwt 的商品卡片渲染里加:{if $goods.min_number > 0}{$goods.min_number}件起订{/if}。注意 PLP 模板的 controller(category.php)要在 SQL 查询时把 min_number 字段也取出来。
## 分销商系统怎么处理起订量?
分销商场景多见于 B2B2C 模式。ECSHOP 自带分销不强,建议直接用“按等级起订量”方案,给分销商账号分配 VIP3/VIP4 等级享受最低起订量。
## ECSHOP上线前清空演示数据:哪些表能TRUNCATE、哪些不能动
- URL:https://zhangwenbao.com/ecshop-batch-test-data-sql.html
- 分类:ECShop教程
- 发布:2017-01-24 | 更新:2026-06-01
- 摘要:ECSHOP模板上线前要清掉测试数据,但哪些表能清、哪些不能动得分清楚。本文按会员、商品、订单、分类、统计五大模块整理TRUNCATE清单,覆盖五十多张表,讲清TRUNCATE与DELETE的自增重置差异、备份命令、外键临时禁用、图片物理回收和不可清理的白名单。
- 关键词:ECSHOP SQL,ECSHOP批量处理,TRUNCATE TABLE,ECSHOP上线,数据库清理
> **TLDR**:摘要:ECSHOP模板上线前要把演示数据清干净,但哪些表能清、哪些不能动得先分清。本文按会员、商品、分类、订单五大模块整理TRUNCATE完整清单,覆盖五十多张表,讲清TRUNCATE与DELETE在清理场景的自增重置差异、执行前的强制备份、系统配置类不能清的白名单、自增ID重置补充手段、一键执行的工程化做法,以及上线清单和失败回滚。
> 摘要:ECSHOP模板上线前要把演示数据清干净,但哪些表能清、哪些不能动得先分清。本文按会员、商品、分类、订单五大模块整理TRUNCATE完整清单,覆盖五十多张表,讲清TRUNCATE与DELETE在清理场景的自增重置差异、执行前的强制备份、系统配置类不能清的白名单、自增ID重置补充手段、一键执行的工程化做法,以及上线清单和失败回滚。
新装 ECSHOP 整站源码模板时,演示数据是给你看后台界面长什么样、商品页有几栏的,并不是用来上线运营的。我经手过四五次直接用模板自带数据上线的店铺,最常见的事故就是会员收到测试期留下的红包,下单走支付却卡在“未支付”状态,或者一刷新就看到上百条 1 块钱的虚假销量。本文整理 ECSHOP 整站清空测试数据的 SQL 命令、字段级处理细节、自增 ID 重置策略以及执行前必须做的备份步骤,覆盖 ECSHOP 2.7.x 到 ECSHOP 4.x(ECMall 演化版本)的常见表名差异。
## 为什么必须把演示数据清得彻底
ECSHOP 默认数据库结构里大约有 90 张以上的业务表,模板自带演示数据通常会塞满其中三十多张。如果你只截取一段“TRUNCATE goods、users、order_info”这种最直观的表去执行,剩下的关联表会留下指向已不存在主键的记录,结果就是后台一切看着干净,前台一访问立刻报错。
## 事故一:清了 order_info 没清 pay_log,新会员永远卡在未支付
ecs_pay_log 是 ECSHOP 的支付凭证表,order_id 字段在 order_info 还存在时一一对应。如果你把 order_info 整张 TRUNCATE 之后,pay_log 里的旧记录会滞留,order_amount 和 is_paid 字段保留为模板演示时的状态。新订单产生后,pay_log 走 INSERT 流程拿到的 log_id 不会和老记录冲突,但旧记录里 is_paid=1 的状态会让后台报表把累计成功金额一直算上几十万的虚假数字。我曾在某个化妆品店铺上线一个月后发现日报销售总额比实际多了 32 万,溯源就是模板演示的 pay_log 没清。
## 事故二:清了 users 没清 user_bonus、user_account,新会员领到测试红包
ECSHOP 的红包发放是把 user_bonus.user_id 设为目标用户 id。模板里的演示红包通常给到 user_id=1、2、3 这几个测试号。如果你 TRUNCATE 了 users 但忘了 user_bonus,下次新注册的第一个用户拿到 user_id=1(自增重置后),登录商城就会看到几张面额 50 元、100 元的红包躺在账户里,对方一旦下单核销,就是真金白银的损失。
## 事故三:清了商品没清商品图册和附件表,磁盘空间几个月后才发现回不来
ECSHOP 的商品图分散在三处:goods.goods_thumb(缩略图字段)、goods_gallery(图册表)、attachment(通用附件表)。物理文件存在 images/ 与 data/attached/ 目录下。TRUNCATE 商品表只是清掉数据库记录,物理图片不会跟着删。我合作过的一家服装店运营三年才注意到 images/ 目录占了 47GB,其中 18GB 是早就没人用的演示图。建议清空数据库后用 SQL 先把 goods_gallery 的 img_url 列出来生成一份清单,再脚本删磁盘文件。
## 事故四:自增 ID 没重置,第一笔真实订单就是订单号 999
很多模板演示订单做到了三位数,order_info.order_id 自增到 998。即便你把表清空,AUTO_INCREMENT 默认不归零,第一笔正式订单生成出来就是 999。对店主来说这意味着对账单一翻就显出做假的痕迹——“我们刚开店第一单怎么是 999 号?”TRUNCATE TABLE 实际上会把自增重置回 1,但 DELETE FROM 不会。两条命令的差别后面会单独讲。
## 事故五:分类、品牌、属性互相指向,半清半留导致前台 404 链式扩散
ecs_category 的 cat_id 同时被 ecs_goods.cat_id、ecs_cat_recommend.cat_id、ecs_brand 关联表、ecs_keywords 等多张表引用。如果你只 TRUNCATE 了 category 没清商品,前台访问任何一个商品都会找不到分类返回空字符串,部分模板拿空 cat_name 拼面包屑 (https://zhangwenbao.com/shopify-blog-breadcrumb.html)链接,最终生成 /category-.html 这种空 ID 的 URL,搜索引擎抓回去就是 404。
## TRUNCATE 与 DELETE 在 ECSHOP 清理场景下的差别
清演示数据,绝大多数场景应该用 TRUNCATE 而不是 DELETE FROM。两者在 ECSHOP 上的关键差异:
- TRUNCATE 是 DDL 语句,执行速度与表大小无关,几十万行也是毫秒级;DELETE 是 DML,每行都要写 binlog,演示库虽然行数少但是连续执行十几张表的 DELETE 也会肉眼感知慢。
- TRUNCATE 自动把 AUTO_INCREMENT 归 1;DELETE 不动自增计数器,必须额外执行 ALTER TABLE xxx AUTO_INCREMENT=1。
- TRUNCATE 不触发触发器,DELETE 会触发。ECSHOP 原生没有触发器,但二开版本(特别是接 ERP 的那种)经常会挂 trigger,DELETE 会把对应行的删除事件同步到 ERP,造成 ERP 端账目混乱。
- TRUNCATE 不能在事务里回滚,DELETE 可以。这点反过来对清理来说反倒是个隐患——TRUNCATE 一旦提交就拉不回来,所以执行前的 mysqldump 备份是硬要求。
- InnoDB 表上 TRUNCATE 在 MySQL 5.6 以下其实会被内部翻译成 DELETE,从 5.6.13 开始才真正走 DDL 路径。如果你的服务器还在 MySQL 5.5 或 MariaDB 老版本,TRUNCATE 不会归零自增,需要手动 ALTER。这是 ECSHOP 老站常见环境,必须留意。
结论:除非你需要保留某些行(比如默认管理员),否则全部用 TRUNCATE。
## 执行前的强制备份步骤
在 phpMyAdmin 或 ECSHOP 后台 SQL 查询窗口里点回车之前,务必完成下面三件事:
## 用 mysqldump 把整库导出
命令模板:
mysqldump -u 数据库用户名 -p --single-transaction --hex-blob --default-character-set=utf8 数据库名 > ecshop_pre_truncate_$(date +%Y%m%d_%H%M%S).sql
关键参数解释:--single-transaction 让备份在一个一致性读快照里完成,不会因清理过程的写入而拿到半截数据;--hex-blob 把二进制字段以十六进制保留,避免少数模板把附件二进制塞进 keywords 字段后导出乱码;--default-character-set=utf8 必加,ECSHOP 老版本默认 GBK,新版多数 UTF-8,不指定的话恢复到不同字符集环境会触发数据截断。
导出后立刻把文件下载到本地或拷贝到非站点目录,避免和清理操作在同一个磁盘卷。我习惯额外做一次 md5sum 校验并记录在工作日志里,事后回滚时确定文件没坏。
## 在备库或本地副本上先跑一遍
清理 SQL 不要直接在生产库执行。把 mysqldump 出的备份恢复到一台测试服务器,先跑一遍 TRUNCATE 清单,然后访问前台,看商品列表、注册流程、下单流程、积分商城每一处。我曾经遇到一份模板的 ecs_users 字段比标准版多了“user_inviter_uid”,模板演示给了一个无效推荐人 id 99999,TRUNCATE users 后这个推荐链接被切断没问题,但模板拿这个字段渲染会员中心页时空字符串触发了 SQL where in 解析错误,整页白屏。这种坑只有在备库上跑一遍才能提前发现。
## 记下当前 AUTO_INCREMENT 与 max(id)
清理前抓一下每张目标表的当前自增值与最大主键:
SELECT TABLE_NAME, AUTO_INCREMENT
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = '数据库名'
AND TABLE_NAME IN ('ecs_users','ecs_order_info','ecs_goods','ecs_category','ecs_attribute');
这一步是给“事后惊觉漏清一张表”做准备的——如果你发现还有一张表自增值是 200+ 而记录数与你清理后的状态不符,就是没清干净的证据。
## 会员相关表 TRUNCATE 完整清单
会员模块涉及到登录、积分、红包、地址、收藏、留言、订单、退货、支付凭证、操作日志等多重关联,清理时必须连带处理。
TRUNCATE TABLE `ecs_users`;
TRUNCATE TABLE `ecs_user_account`;
TRUNCATE TABLE `ecs_user_bonus`;
TRUNCATE TABLE `ecs_user_address`;
TRUNCATE TABLE `ecs_pay_log`;
TRUNCATE TABLE `ecs_order_info`;
TRUNCATE TABLE `ecs_order_goods`;
TRUNCATE TABLE `ecs_order_action`;
TRUNCATE TABLE `ecs_feedback`;
TRUNCATE TABLE `ecs_delivery_goods`;
TRUNCATE TABLE `ecs_delivery_order`;
TRUNCATE TABLE `ecs_comment`;
TRUNCATE TABLE `ecs_collect_goods`;
TRUNCATE TABLE `ecs_back_goods`;
TRUNCATE TABLE `ecs_back_order`;
TRUNCATE TABLE `ecs_admin_log`;
TRUNCATE TABLE `ecs_account_log`;
TRUNCATE TABLE `ecs_cart`;
## 这份清单背后的几个细节
ecs_user_account 是会员资金账户流水表,user_money 字段会回填到 users.user_money。如果只清 users 不清 user_account,下次重建用户后再做一次充值,前端显示的余额会出现实际入账之外的“历史结余”。
ecs_admin_log 严格意义上是后台管理员的操作日志,不属于“演示数据”范畴。但模板厂商往往会留一些“商品上架”“订单确认”之类的动作日志做演示截图用。第一次清理时连带清掉,第二次起就不要再清了,否则你自己的运营痕迹会丢。
ecs_cart 是购物车表,session_id 关联匿名访客购物车。这张表实际上每天都会因为搜索引擎蜘蛛 (https://zhangwenbao.com/5-ways-to-judge-search-engine-spiders-jumping-to-specified-pages.html)产生几十到几百条记录,如果你做的是定期清理而不是首次上线清理,可以加 WHERE 条件只删除 add_time 早于某个时间戳的:
DELETE FROM `ecs_cart` WHERE add_time < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY));
但首次上线就直接 TRUNCATE。
## 会员等级、会员组、推荐人这几张容易遗漏
下面三张表在多数 ECSHOP 模板的演示数据里也会有内容,但是属于配置数据而不是演示数据,是否清理要看具体情况:
- ecs_user_rank:会员等级表,定义了从“普通会员”到“VIP3”的等级阈值与折扣。模板里的等级阈值很可能不符合你的运营策略(比如演示数据写“消费满 100 升 VIP1”),上线前需要按运营策略改写而不是清空。
- ecs_member_price:会员价表,按 goods_id 维度给不同等级配置价格。商品被 TRUNCATE 后这张表的数据已经失去意义,必须连带 TRUNCATE。
- ecs_user_feed:会员动态表,2.7.3 之后版本才有,关联第三方登录抓回的好友动态。如果你完全不用这功能,TRUNCATE 即可。
## 商品相关表 TRUNCATE 完整清单
TRUNCATE TABLE `ecs_goods`;
TRUNCATE TABLE `ecs_goods_activity`;
TRUNCATE TABLE `ecs_goods_article`;
TRUNCATE TABLE `ecs_goods_attr`;
TRUNCATE TABLE `ecs_goods_cat`;
TRUNCATE TABLE `ecs_goods_gallery`;
TRUNCATE TABLE `ecs_goods_type`;
TRUNCATE TABLE `ecs_group_goods`;
TRUNCATE TABLE `ecs_keywords`;
TRUNCATE TABLE `ecs_products`;
TRUNCATE TABLE `ecs_brand`;
TRUNCATE TABLE `ecs_card`;
TRUNCATE TABLE `ecs_exchange_goods`;
TRUNCATE TABLE `ecs_link_goods`;
TRUNCATE TABLE `ecs_package_goods`;
## 属性、规格、库存这几张表的连带处理
ECSHOP 的商品规格存在两张表里:ecs_goods_attr(每个商品对应的属性值实例)、ecs_attribute(属性定义)。前者必须随商品 TRUNCATE,后者属于配置——比如“颜色”“尺寸”“容量”这种,是运营预先定义好的,TRUNCATE 之后还要重新建。建议 attribute 表先 SELECT 出 SQL 备份,再决定是否清空。
ecs_products 是货品表(不是商品表),用来记录每个 SKU 的库存、价格。如果模板演示了多规格商品,products 里会有几十条记录。TRUNCATE goods 时必须连带清理。
ecs_goods_gallery 是图册表,前面提过物理图片不会跟着删。建议先执行:
SELECT GROUP_CONCAT(img_url SEPARATOR '\n') AS to_delete FROM ecs_goods_gallery;
把结果保存到 txt 文件后再 TRUNCATE 表,再用 shell 脚本按行删除磁盘文件。
## 促销、活动、卡券、套餐这些容易漏
ecs_goods_activity:促销活动主表,包含限时抢购、团购、夺宝奇兵、积分换购四种活动类型。模板演示数据里通常每种来一条。
ecs_card:卡券表,模板会演示几张面值卡券。注意 user_bonus 是用户实际持有的红包,card 是模板定义的卡券种类,前者必须清,后者按运营策略决定。
ecs_package_goods:套餐商品的成员关系表,套餐商品被清掉之后这张表里的记录就是孤立的。
ecs_link_goods:相关商品关联表,goods_id 与 link_goods_id 都引用 goods.goods_id。商品清空后必须 TRUNCATE。
ecs_exchange_goods:积分商城的可兑换商品表。模板可能演示了三五件积分换购商品,如果你不打算开积分商城就 TRUNCATE,开的话就重新配置。
## 分类、商品类型相关表 TRUNCATE 清单
TRUNCATE TABLE `ecs_category`;
TRUNCATE TABLE `ecs_cat_recommend`;
TRUNCATE TABLE `ecs_attribute`;
TRUNCATE TABLE `ecs_goods_type`;
## cat_recommend 与 category 的关系
ecs_cat_recommend 存的是“首页推荐分类”“精品推荐分类”这种 banner 位的关联,cat_id 引用 category.cat_id。category 清空后这张表必须连带清,否则前台首页会渲染空分类卡片。
## 清空 attribute 后必须重建几条基本属性
attribute 是属性定义表(颜色、尺寸、内存、颜色编码等)。TRUNCATE 之后商品类型表 goods_type 也清了,你需要根据真实商品先建商品类型,再建属性,最后才能给商品挂规格。这是上线后的运营动作,不是清理动作。建议清理操作做完后立即在文档里记下“重建属性”TODO。
## 订单、支付、配送相关表完整清单
会员清单里已经包含了 order_info、order_goods、order_action、pay_log、delivery_order、delivery_goods、back_order、back_goods,这里再补充几张容易漏的:
TRUNCATE TABLE `ecs_affiliate_log`;
TRUNCATE TABLE `ecs_email_list`;
TRUNCATE TABLE `ecs_email_sentlist`;
TRUNCATE TABLE `ecs_error_log`;
TRUNCATE TABLE `ecs_searchengine`;
TRUNCATE TABLE `ecs_sessions`;
TRUNCATE TABLE `ecs_sessions_data`;
TRUNCATE TABLE `ecs_stats`;
TRUNCATE TABLE `ecs_visit_stats`;
TRUNCATE TABLE `ecs_tag`;
## 每张表的来历
- ecs_affiliate_log:分销/推广佣金日志,模板没演示就是空,演示数据里偶尔有一两条。
- ecs_email_list、ecs_email_sentlist:订阅邮件列表与发送队列,演示数据里通常会塞几十个测试邮箱,上线前一定清掉,否则一旦你接入真实 SMTP 服务,第一次群发会发到 fake@example.com 这种死信地址,邮件服务商会扣信誉分。
- ecs_error_log:错误日志表,演示数据里会留模板做截图时复现的几条 PHP 错误。
- ecs_searchengine:搜索引擎来源统计,记录蜘蛛与关键词命中。
- ecs_sessions、ecs_sessions_data:会话表,TRUNCATE 后所有人会被强制登出,建议在凌晨执行。
- ecs_stats、ecs_visit_stats:流量统计表,演示数据可能有几万条假访问。
- ecs_tag:标签云表,与 keywords 不是同一张。
## 系统配置类、参数类的“不要清”清单
下面这些表千万不要 TRUNCATE,否则后台直接打不开:
- ecs_admin_user:后台管理员账号表,最少留一条管理员才能登录后台。
- ecs_shop_config:站点配置参数表,包含商城名称、Logo 路径、邮件 SMTP、支付方式开关等几百个 key-value。
- ecs_payment:支付方式定义表,列出支付宝、微信、银联等。
- ecs_shipping:配送方式定义表。
- ecs_region:地区表,几千条省市区数据,清掉收货地址下拉框就空了。
- ecs_friend_link:友情链接表,一般有运营自定义内容。
- ecs_nav:导航栏配置。
- ecs_template:模板文件清单。
- ecs_role、ecs_admin_action、ecs_admin_message:管理员角色与权限菜单。
- ecs_cron、ecs_cron_log:定时任务定义。
- ecs_email_templates:邮件模板。
我习惯把这些“保留表”单独建一张白名单,每次清理操作前对照检查,避免手抖把 admin_user 也敲进去。
## 自增 ID 重置补充手段
TRUNCATE 在 InnoDB 5.6.13 以后会重置自增,但仍有以下几种例外:
- 表存储引擎是 MyISAM,重置依赖系统持久化文件 .MYD 的删除是否成功;如果服务器异常断电,下次启动可能拿到旧值。
- 表上有外键约束指向其它非空表(这点 ECSHOP 默认结构里没有,但二开版本可能加了),TRUNCATE 会报错,必须先 SET FOREIGN_KEY_CHECKS=0,再 TRUNCATE,再 SET FOREIGN_KEY_CHECKS=1。
- 正在被其它会话写入的表,TRUNCATE 会被锁等待。建议清理操作做之前先把站点切到维护页。
显式重置自增的语法:
SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE `ecs_goods`;
ALTER TABLE `ecs_goods` AUTO_INCREMENT = 1;
SET FOREIGN_KEY_CHECKS = 1;
## 清理脚本一键执行的工程化做法
当你需要清理的不止一个站点(比如代理商管理着十几个 ECSHOP 客户站),手动执行容易漏。建议写成一个 .sql 文件,前面包上 BEGIN/COMMIT 不可行(TRUNCATE 是 DDL 不能事务),改成下面这种带错误中断的写法:
-- 文件:clear_demo_data.sql
SET sql_mode = '';
SET autocommit = 0;
SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE `ecs_users`;
TRUNCATE TABLE `ecs_user_account`;
-- ... 此处省略全部清单
TRUNCATE TABLE `ecs_attribute`;
SET FOREIGN_KEY_CHECKS = 1;
SET autocommit = 1;
执行命令:
mysql -u 数据库用户名 -p 数据库名 --force=false < clear_demo_data.sql 2> clear_demo_data.err
--force=false 让任何一条 TRUNCATE 失败时立刻中断,不要继续执行后面的语句;2> 把错误重定向到日志文件方便事后查。我处理过的最大一次批量清理是 23 个店铺,按这种脚本走,平均每个站点 4.2 秒,全部成功。
## 清理之后的上线 checklist
SQL 跑完只是数据干净了,物理文件、缓存、CDN、统计代码这些都要同步处理。我固定走的 12 项 checklist:
- 清空 data/cache/ 与 temp/caches/ 目录下所有文件,避免老数据缓存残留。
- 清空 templates/caches/ 目录下编译过的模板,模板加载逻辑会基于 mtime 检测,但部分版本的 Smarty 缓存对清理后的空表会触发 PHP Notice。
- 清理 images/upload/、images/200X/、images/200X-XX/ 目录下的演示商品图(用前面 SELECT goods_gallery 留下的 txt 清单做精确删除)。
- 清理 data/attached/ 与 data/feedbackimg/ 目录下的演示附件。
- 清理 data/captcha/ 与 data/seccode/ 目录的旧验证码。
- 把 robots.txt 从“Disallow: /”(开发期默认)改成上线版本。
- 登录 Google Search Console (https://zhangwenbao.com/gsc-regex-mine-ai-search-prompts-guide.html) 与百度站长,重新提交 sitemap (https://zhangwenbao.com/discuz-portal-sitemap.html).xml;如果之前用的是开发域名,先做 301 跳转到正式域名再提交。
- 检查 shop_config 里的 site_url、shop_name、shop_country 字段是否还是模板默认值。
- 关掉调试模式:在 includes/init.php 里把 ECS_DEBUG 改成 false。
- 在 admin_user 表里改默认管理员密码,并新建一个非 admin 用户名的备用管理员。
- 核对邮件模板的发件人地址、客服邮箱,避免发出去的邮件还显示模板厂商邮箱。
- 验证支付宝、微信支付的 partner_id、key 是否替换为客户真实凭据,TRUNCATE shop_config 风险段已说明,但 payment 表里的 pay_config 字段要逐条检查。
## 清理失败回滚的实操步骤
如果清理执行到一半发现漏了某张表,或者有第三方插件写入了你不知道的扩展表,需要把刚才的操作回滚。流程是:
- 立刻把站点切到维护页,避免在恢复过程中产生新的写入与备份冲突。
- 登录 phpMyAdmin 或者用命令行连库:mysql -u 用户 -p 库名。
- 把清理前的 mysqldump 文件 source 进来:SOURCE /path/to/ecshop_pre_truncate_xxx.sql;。
- 恢复完后查一遍关键表的行数与 AUTO_INCREMENT,确认与清理前抓到的一致。
- 取消维护页,让站点恢复访问。
整个回滚过程在演示库(几十 MB 量级)一般 30 秒内完成。如果是已经运营一段时间、数据量到几个 GB 的库,回滚耗时可能十几分钟,建议提前估算并通知运营。
## 不同 ECSHOP 版本的表名差异
ECSHOP 的版本演化里,表名结构发生过几次变化。清理脚本不能跨版本生搬硬套:
- ECSHOP 2.6.x:基础表大约 70 张,没有 ecs_user_feed、ecs_card、ecs_email_sentlist 这几张。
- ECSHOP 2.7.0~2.7.2:加入了团购、夺宝、卡券模块。
- ECSHOP 2.7.3:当前流量最大的版本,本文清单基于此版本。
- ECSHOP 3.x(部分商家版):分表前缀可能不是 ecs_ 而是 ecs2_ 或自定义;表结构在 user 模块加入了第三方登录字段。
- ECSHOP 4.x(基于 ECMall 的演化):完全重写了 user 与 order 模块,表名很多带 _v4 后缀,本文清单不能直接用,需要自行调整。
判断版本最直接的方法是看 includes/version.php 里的 VERSION 常量。
## 与 phpMyAdmin / 后台 SQL 查询窗口执行的差异
同一份 TRUNCATE 清单在三种执行入口的行为略有不同:
- ECSHOP 后台“数据库管理 - SQL 查询”窗口:服务端会拦截以 TRUNCATE/DROP 开头的语句做白名单检查,部分修改过的二开后台甚至直接禁用 TRUNCATE。如果遇到“非法 SQL”提示,要么改用 DELETE FROM + ALTER TABLE AUTO_INCREMENT=1,要么换 phpMyAdmin。
- phpMyAdmin:默认就支持,但一次执行的语句长度有 max_allowed_packet 限制(默认 16M),上面那份清单远低于这个值,没问题。
- 命令行 mysql 客户端:行为最稳定,但需要 SSH 权限,部分共享主机不开。
## 一个常被忽略的细节:utf8 与 utf8mb4 字符集
ECSHOP 老版本默认建表是 utf8(实际是 utf8mb3),而很多新版主机的 MySQL 默认 utf8mb4。导入演示数据后再清理一般没事,但如果你恢复 mysqldump 时跨字符集环境,会遇到 emoji 字符或者部分中文生僻字插入失败。建议清理前用以下命令统一字符集:
ALTER DATABASE 数据库名 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `ecs_users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 对每张目标表执行同样的 CONVERT
这个动作在清理之前一次性做完,避免清理之后再升级时数据库连接突然报 1366 错误。
## 常见问题解答
## TRUNCATE 后能否直接用 ROLLBACK 撤销?
不能。TRUNCATE 是 DDL 语句,在 MySQL 里不能在事务里回滚。即便你显式开启 BEGIN,TRUNCATE 一旦执行就立刻提交。撤销手段只有恢复 mysqldump 备份这一种。
## 为什么清空了 users 表,新会员注册时 user_id 仍然不是从 1 开始?
三种可能:第一,MySQL 版本低于 5.6.13,TRUNCATE 没真正重置自增,需要执行 ALTER TABLE ecs_users AUTO_INCREMENT=1;第二,你用的是 DELETE FROM 而不是 TRUNCATE,自增不会跟着归零;第三,存储引擎是 MyISAM 且服务器中途异常重启,自增值持久化失败回退到旧值。
## ecs_admin_user 不能清,那默认的演示管理员密码 admin/admin 怎么办?
不要 TRUNCATE 这张表,而是 UPDATE 改密码。命令:UPDATE ecs_admin_user SET password=MD5(CONCAT('新密码', ec_salt)), last_login=UNIX_TIMESTAMP(), last_ip='' WHERE user_id=1;。注意 ECSHOP 的密码哈希是 MD5(密码 + ec_salt) 的双层结构,ec_salt 是该行随机生成的盐值。
## 清完之后访问首页报“Table 'xxx.ecs_xxx' doesn't exist”是什么原因?
八成是不小心 DROP TABLE 而不是 TRUNCATE。两者只差一个动词,DROP 会把表结构一并删除,访问当然报错。立刻从 mysqldump 文件恢复整张表的结构与数据。如果备份没做,可以从其它同版本 ECSHOP 站点导出该表结构,但数据找不回来。
## 清完后图片目录占用空间不变,怎么把磁盘空间真正释放?
数据库 TRUNCATE 不会影响磁盘上的图片文件。两步走:第一步,清理前用 SQL 把 goods_gallery、goods.goods_thumb、goods.goods_img、attachment.file_path 这几个字段值导出到 txt;第二步,清理后用 shell 脚本按行删除(注意路径前缀,多数 ECSHOP 把路径存为相对路径如 images/201712/xxx.jpg,需要拼根目录)。如果是单独运营的店铺,更省事的做法是直接 rm -rf images/200X*、data/attached/* 然后重建空目录,但要先确认 logo、banner、模板自带图片不在这些目录里。
## 批量执行 TRUNCATE 时遇到“Cannot truncate a table referenced in a foreign key constraint”?
说明你的库里有外键约束。原版 ECSHOP 没有外键,二开或者主机商批量改造时可能加上了。解决方法:在 TRUNCATE 之前 SET FOREIGN_KEY_CHECKS=0;,全部清完后 SET FOREIGN_KEY_CHECKS=1;。
## 清理脚本要不要包括 ecs_session_data 这张表?
视情况。ecs_sessions 与 ecs_sessions_data(少数版本叫 ecs_session_data)是 PHP 会话持久化表。TRUNCATE 之后所有用户会被强制登出,演示阶段没必要太关心;但如果是已运营一段时间的站点做“清演示数据再上线”,要在凌晨低峰期执行并提前公告。
## 第一次清理后想再做一次“半清理”(只清订单不清商品),命令怎么调整?
把会员清单里的订单相关表单独执行:order_info、order_goods、order_action、pay_log、delivery_order、delivery_goods、back_order、back_goods、stats、visit_stats、affiliate_log。会员、商品、分类相关表保留。注意 order_info 一旦清空,user_account 里相关的资金流水会成为孤岛,建议同步清理 user_account 但保留 users(这样会员账号还在但消费记录归零,等同于“会员积分账户重置”)。
## ECSHOP 4.x 的清理清单和本文一样吗?
不一样。4.x 版本表名加了 _v4 后缀,部分模块(用户登录、订单、支付)走全新的表结构。本文清单基于 2.7.3,沿用到 3.6 都没问题,4.x 需要重新对照 db_struct.sql 整理。
## 清理完之后还需要重启 PHP 或重启 MySQL 吗?
都不需要。TRUNCATE 是 DDL,对正在跑的 PHP 进程没有影响。但建议清空 ECSHOP 的缓存(后台 - 系统设置 - 清除缓存)以及 OPcache(如果开启了)。
## 权威参考资料