织梦DedeCMS升级补丁后无法生成HTML:5步排查修复
这事儿是保哥前几年帮一位老客户排查织梦DedeCMS故障时遇到的:客户在后台点了「在线更新升级」,过了几分钟回来,发现「更新栏目HTML」「更新文档HTML」点下去全部没反应,进度条直接卡死或者一闪而过,前台访问文章页虽然还能看,但生成出来的HTML文件停留在升级前的版本。我之前在自己的几个Dede老站上也遇到过这个症状,今天就把完整的排查思路与修复方法整理出来,希望能帮到同样卡在升级补丁后无法生成HTML的朋友。
故障表现与首次定位
升级补丁后无法生成HTML的典型表现有三个:第一,后台「生成-更新文档HTML」点击后页面空白或者直接刷出一行红色PHP Notice;第二,templets_c目录下生成的编译模板文件大小为0KB;第三,前台访问刚发布的文章会跳到404或者直接返回500。
保哥的第一反应通常是先看PHP错误日志,因为织梦本身的错误提示很简陋,真正能告诉我们「函数未定义」「文件读不到」这种关键信息的还是PHP自己的error_log。打开PHP错误日志(不同主机位置不同,常见的是/www/wwwroot/<站点目录>/php_errors.log或者/var/log/php-fpm/error.log),重点搜索Call to undefined function这一类报错。
如果你看到类似Call to undefined function listtag()或者Call to undefined function flink()之类的提示,那基本可以确定问题就是升级补丁覆盖文件时遗漏了某些自定义函数,导致原本依赖这些函数的模板在生成阶段直接抛错。
为什么补丁会破坏HTML生成
织梦的在线升级机制说白了就是从官方服务器下载差异文件,然后批量覆盖站点对应的PHP文件。问题在于,很多人之前为了实现TAG调用、友情链接调用、自定义字段输出等功能,会手动往include/common.func.php、include/extend.func.php这两个公共函数库里追加自定义函数。
升级补丁覆盖时并不会智能merge,它会把整个common.func.php替换成官方原版,结果就是你之前加的所有自定义函数全部没了。模板里还在{dede:field name='tags' function='listtag(@me)'/}这样调用,引擎在生成时找不到listtag函数,触发fatal error,整个生成流程就被中断。
保哥过去帮客户处理过四五次类似事故,每次的表象都是「升级后无法生成HTML」,但根因百分百都是公共函数库被覆盖。所以这篇排查的核心思路就是:恢复被覆盖的自定义函数,并加固后续不再被覆盖。
修复listtag函数的标准代码
如果错误日志明确指向listtag未定义,那就把下面这段代码追加到include/common.func.php文件的最末尾。这是织梦社区流传度最广的标准实现,保哥自己也在十几个站上验证过,能稳定支持{dede:field name='tags' function='listtag(@me)'/}标签输出。
// TAG 调用函数(注意:将下方的 dede_ 表前缀替换为你站点实际的前缀)
function listtag($aid)
{
$tsql = new DedeSql(false);
$tags = '';
$tsql->SetQuery("Select i.tag From dede_taglist t left join dede_tagindex i on i.id=t.tid where t.aid='$aid'");
$tsql->Execute('t');
while($row = $tsql->GetArray('t', MYSQL_ASSOC)){
$tags .= "".$row['tag']." ";
}
return $tags;
}
几个细节务必注意:
第一,表前缀。Dede默认是dede_,但很多站为了防注入会在安装时改成xx_dede_或者随机字符串,请用phpMyAdmin进数据库确认taglist与tagindex这两张表的真实前缀,再回到代码里替换。
第二,MYSQL_ASSOC这个常量在PHP 7.0之后被废弃,PHP 7.4上还能用但会报deprecated警告,PHP 8.0直接报致命错误。如果你的服务器PHP已经升到8.x,需要把这一行换成$tsql->GetArray('t')不带常量参数的形式,或者改用MYSQLI_ASSOC,否则函数本身又会成为新的报错源头。
第三,建议在函数前面加一个if (!function_exists('listtag'))判断,避免与未来某个补丁里Dede官方自带的同名函数冲突,写成下面这样更稳:
if (!function_exists('listtag')) {
function listtag($aid)
{
// 上面的实现照搬过来
}
}
3个常用自定义函数的完整实现代码
除了listtag,保哥统计了客户Dede站点最常被升级覆盖的另外几个函数,这一节给出可直接复用的实现,避免你升级之后还要满社区翻代码。
第一个是flink友情链接分组调用函数。Dede默认的友链调用不支持按分组输出,运营经常要求「首页只显示行业类友链,栏目页只显示综合类友链」,这种场景就需要自定义flink函数:
if (!function_exists('flink')) {
function flink($typename = '默认分组', $row = 20)
{
global $dsql;
$sql = "SELECT * FROM dede_flink WHERE typename='$typename' ORDER BY sortrank ASC LIMIT $row";
$dsql->SetQuery($sql);
$dsql->Execute();
$out = '';
while($r = $dsql->GetArray()){
$out .= '<a href="'.$r['url'].'" target="_blank">'.$r['webname'].'</a>';
}
return $out;
}
}
第二个是mytypelist自定义栏目列表函数。这个函数被很多企业站用来在首页输出指定栏目的最新文章,覆盖之后会导致首页大半个屏幕空白:
if (!function_exists('mytypelist')) {
function mytypelist($typeid, $row = 10)
{
global $dsql;
$sql = "SELECT id,title,filename FROM dede_archives WHERE typeid='$typeid' AND arcrank>-1 ORDER BY id DESC LIMIT $row";
$dsql->SetQuery($sql);
$dsql->Execute();
$out = '<ul>';
while($r = $dsql->GetArray()){
$url = '/plus/view.php?aid='.$r['id'];
$out .= '<li><a href="'.$url.'">'.$r['title'].'</a></li>';
}
$out .= '</ul>';
return $out;
}
}
第三个是getauthor作者信息函数。多作者站点经常用这个函数在文章页输出作者头像、简介、文章总数等,覆盖后文章页底部会缺一大块:
if (!function_exists('getauthor')) {
function getauthor($writer)
{
global $dsql;
$sql = "SELECT * FROM dede_member WHERE userid='$writer'";
$dsql->SetQuery($sql);
$dsql->Execute();
$r = $dsql->GetArray();
if (!$r) return '';
return '作者:'.$r['uname'].',文章数:'.$r['articles'];
}
}
把这三个函数加上前面的listtag一起放在include/common.func.php末尾,或者更优雅一些,单独放在include/custom.func.php里再require_once进来。保哥强烈推荐后一种做法,下面的「长期防护」一节会展开讲。
系统性排查升级后失效的其他自定义函数
光修一个listtag不一定够。保哥处理过的一个站,补丁覆盖之后总共少了七个自定义函数,包括TAG调用、附件批量统计、友链分组输出等等。一次性排查完,比每次报错再补一个要省心得多。
排查的思路是:把当前的common.func.php、extend.func.php与升级前的备份做diff。Linux服务器上直接用diff命令:
diff /backup/2025-03-30/common.func.php /www/wwwroot/site/include/common.func.php
Windows本地可以用VSCode的Compare Selected或者Beyond Compare。把备份里多出来的、官方版本没有的自定义函数全部提取出来,整段追加回新版的common.func.php末尾。
如果你没有升级前的备份,也可以从模板里反向找。打开templets/default下所有.htm模板,搜索function=',把所有引用过的函数名收集起来,再去common.func.php与extend.func.php里逐个确认是否存在;不存在的就是被覆盖掉的,需要从社区找回标准实现或者按需重写。
修复后回归验证的完整流程
代码追加完之后不要急着关闭文件,按下面的顺序做一次完整回归,避免修了A又坏了B这种连锁反应。
1. 后台-系统-系统基本参数-保存(强制刷新缓存)
2. 后台-生成-更新系统缓存
3. 后台-生成-更新主页 HTML
4. 后台-生成-更新栏目 HTML(勾选 全部栏目)
5. 后台-生成-更新文档 HTML(勾选 全部文档)
6. 前台抽查首页、栏目页、文章页各一篇,确认无 PHP 报错
7. 用 curl -I 抓三个文章页响应头,确认返回 200 且 Content-Type 正常
保哥自己的习惯是在第6步打开浏览器开发者工具的Network面板,刷新文章页,逐项查看是否有4xx或5xx资源;在第7步用命令行curl -I https://example.com/news/123.html检查响应头,避免页面看着正常其实是缓存的假象。
如果某一步还是报错,错误信息会直接回写到PHP错误日志,定位起来比之前容易得多——因为现在大头的listtag已经修好了,剩下的报错往往是另一两个自定义函数没回归到位,按同样思路再补就行。
长期防护:避免下次升级再次踩坑
这件事的根本解法不是「下次再修」,而是「下次不再坏」。保哥结合自己的运维经验给出三条长期防护建议。
第一,把所有自定义函数集中到一个独立文件,比如include/custom.func.php,再在common.func.php末尾通过require_once引入。这样升级补丁覆盖common.func.php时,你的自定义文件原封不动。
// 在 common.func.php 文件末尾追加
if (file_exists(DEDEINC.'/custom.func.php')) {
require_once(DEDEINC.'/custom.func.php');
}
第二,在升级前一定要做版本快照。宝塔面板有「网站快照」,云服务器有「实例快照」,本地也可以用tar -czf命令打包整站。十分钟的备份能省下两小时的修复时间,绝对划算。
第三,认真评估是否真的需要升级。织梦官方早已经停止常规维护,所谓的在线补丁很多时候只是社区或第三方修补的安全补丁。除非补丁明确修复了高危漏洞,否则保哥更倾向于保留稳定版本,只针对漏洞做精准热补,避免大面积覆盖文件带来的连锁问题。
Dede升级补丁的安全策略与替代方案
这一节保哥单独把「补丁是否值得打」这件事讲透。织梦自从2021年商业授权风波之后,官方基本停止了主线维护,市面流传的所谓「在线补丁」绝大多数来自第三方或者社区分支,质量参差不齐。盲目升级带来的破坏力,往往比补丁修复的漏洞本身更严重。
保哥给客户的标准建议是分级处理:高危漏洞(远程代码执行、SQL注入、任意文件上传)必须打,但优先选择官方安全公告里点名修复的精准补丁,而不是整包覆盖;中低危漏洞(XSS、信息泄漏)可以延后,先用Nginx层的WAF规则或宝塔防火墙做临时拦截;非安全类的功能更新(界面优化、新插件)基本不打,因为收益远低于风险。
另外一个替代方案是迁移到现代CMS。如果你的Dede站点流量稳定但维护成本越来越高,可以考虑迁移到Typecho、WordPress或者Headless方案(如Strapi + Next.js)。保哥帮过几位老客户做迁移,平均成本是站点规模100篇文章约5000元、1000篇文章约2万元,迁移之后再也不用每次都和这种「升级即崩」的故事抗争。
但如果实在不想动主站,至少做好以下三件事:站点放进Cloudflare做基础DDoS和WAF防护、PHP版本停在7.4不要升级到8.x(Dede很多代码在8.x上直接报错)、数据库定期异地备份(建议每天一次增量、每周一次全量)。这三件事做齐,老Dede站再战三五年完全没问题。
一个真实排查案例:从凌晨两点到清晨六点的修复全过程
保哥前年帮一位做地方门户的朋友处理过一次比较棘手的事故。事情起因是他听说织梦有一个新的安全补丁可以堵SQL注入漏洞,半夜十一点自己点了在线升级,过了几分钟就发现首页打不开、栏目页502。他凌晨两点联系到我的时候已经焦头烂额,因为站点白天有大量推广流量进来,第二天上午就要开会跟广告主对账。
我接入服务器之后,第一步先把站点切到维护页面,避免持续返回5xx影响搜索引擎抓取频次。第二步打开PHP错误日志,立刻看到一连串Call to undefined function错误,包括listtag、flink、mytypelist三个函数。第三步对比客户提前一个月做的整站快照,把这三个函数的实现从老common.func.php里抠出来,按照本文前面的「集中到custom.func.php」方案重新部署。
第四步是回归生成。我先生成栏目HTML,跑完没有报错;再生成文档HTML,发现还有一个getauthor函数缺失,回头继续从快照里补上。第五步关闭维护页面,前台访问几篇热门文章确认正常,再把站点重新提交搜索引擎抓取。
整个过程从凌晨两点到清晨六点,正好赶在客户开会之前修完。这次事故让我和客户都意识到一件事:不是所有补丁都值得升级,特别是对一个流量已经稳定的老站来说,「不动可能比动更安全」。从那以后他规定:任何升级动作必须先在测试环境跑一周,且必须有完整快照备份,否则一律不上线。这条规矩到今天还在执行。
升级前必须建立的运维清单
借这次案例的教训,保哥后来把Dede升级流程整理成一份运维清单,分享给大家:
1. 升级前 24 小时:完整备份网站文件 + 数据库
2. 升级前 12 小时:克隆一份到测试环境跑全流程
3. 升级前 1 小时:截图记录当前关键页面、关键 SQL 表结构
4. 升级时:开维护模式、记录开始时间
5. 升级后立即:刷错误日志、跑首页 + 三个栏目 + 五篇文章的回归
6. 升级后 30 分钟:观察 PHP-FPM 进程数、MySQL 慢查询日志
7. 升级后 24 小时:检查搜索引擎抓取日志、流量监控数据
按这份清单走一遍,再大的事故都能在可控范围内被发现并回滚,比起「点了升级然后祈祷一切正常」要踏实得多。
常见问题解答
补丁覆盖之后没有备份能恢复成升级前的版本吗
如果服务器开启了快照或者宝塔自动备份,可以直接回滚整站文件。如果什么备份都没有,只能从社区下载与原版本号对应的官方包,再把数据库表结构对照一下;这条路风险高,强烈不建议在生产环境裸跑,最好先在测试环境验证。补救前一定要先把当前损坏状态的文件再次备份一份,万一回滚也失败还能回到现在的「半坏」状态做下一轮排查。
PHP版本是8.1按这篇文章加listtag之后还是报错怎么办
PHP 8.x对Dede这种基于过时写法的代码并不友好,常见的会报MYSQL_ASSOC未定义、each函数已移除等等。建议用MYSQLI_ASSOC替换MYSQL_ASSOC,或者在不影响业务的前提下把站点PHP切回7.2/7.4,这是Dede兼容性最好的版本区间。如果业务上必须停留在PHP 8.x,建议把所有自定义函数里涉及mysql_*的全部改成mysqli_*,工作量不小但是一劳永逸。
升级后文档HTML能生成但栏目HTML还是空白怎么办
这种情况大概率是栏目模板里调用了未定义的自定义函数,比如flink、mytypelist之类。打开对应栏目的模板(默认是templets/default/list_article.htm与index_article.htm),搜索function=,把里面引用的所有函数名挨个核对一遍是否存在于公共函数库。另一种可能是栏目缓存没刷,先去后台「生成-更新系统缓存」跑一遍再重试。
有没有办法一次性看出补丁覆盖了哪些文件
升级前后做一次整站MD5快照对比就能看出来。Linux下命令是find . -type f -name *.php -exec md5sum {} \; | sort > before.txt,升级后再跑一次得到after.txt,diff before.txt after.txt输出的差异行就是被覆盖或新增的文件清单。保哥每次帮客户做大版本升级前都会跑一遍这个流程,省去后续无穷无尽的猜谜。
能不能完全跳过Dede在线升级机制自己手动patch
可以而且推荐。在线升级机制本身就是个隐患,因为它会盲目覆盖文件。手动patch的标准做法是:拿到官方或社区发布的diff文件(通常是一组PHP文件加上一份说明),自己对照备份逐个文件做合并;如果不会合并代码,可以请熟悉PHP的开发协助。手动patch的好处是清楚每一行改了什么,出问题能精准定位。保哥维护的几个核心客户站全部是这种手动patch模式,已经三年没出过一次升级事故。
Dede升级失败之后SEO收录数据会不会受影响
短时间(24小时以内)的5xx错误对SEO影响有限,搜索引擎会自动重试。但如果错误持续超过48小时,Googlebot会大幅降低抓取频率,Bing也会标记网站为不可用。所以发现故障第一时间一定要切维护页面(返回503 Retry-After头而不是502/500),告诉搜索引擎我在维护稍后再来,这样能最大程度保护排名。修复完之后立刻去Google Search Console和必应站长工具提交sitemap强制重新抓取。
写在最后
织梦的故事到今天已经接近尾声,但它留给互联网世界的是大量仍在线的老站点和经验丰富的运维社群。每次遇到「升级补丁后无法生成HTML」这类问题,保哥的态度都是把它当作一次系统加固的机会:不仅修复表面错误,还顺手把自定义函数集中、备份机制完善、回归流程标准化。这样即使将来再遇到类似事故,也能在半小时内定位、一小时内恢复,而不必像最初那样在凌晨四点对着报错信息抓狂。希望这篇排查记录能帮你把同样的弯路走得更短一些。