保哥笔记

Discuz X3.5 标题省略号去除:2 种修复方法

保哥昨天把一个地方论坛升级到最新的 Discuz X3.5,没想到落了一个挺烦人的坑——门户发布文章时只要标题字数超过限制,系统就自动在标题末尾追加一段省略号"…",而且每编辑保存一次就追加一次,几次之后标题尾巴变成"……………",活活把文章列表搞得像得了帕金森症的电报机。

这种情况在 Discuz X3.4 时代是直接改代码里写死的 80 解决,X3.5 升级后底层把这个数字抽出来变成了 $_G['setting']['maxsubjectsize'] 全局变量,原来熟悉的修改路径找不到了。本文记录保哥从踩坑到搞清楚 X3.5 整套"标题最大字数"机制的全过程,附两种修复方案、升级踩坑提醒、相关配置项的连带影响、以及完整 FAQ。

一、问题表现与影响范围

升级到 Discuz X3.5 之后,门户发布文章流程里会出现这些症状:

如果是新装站点这个问题不容易发现,因为多数测试文章标题不会超过 80 字。但升级到 X3.5 的老站很容易踩坑——尤其是从 X3.2/X3.4 一路升级、原本设置过更大标题长度的论坛,会发现升级后所有长标题集体"被截断"。

二、根因:maxsubjectsize 配置项的演变

翻看 Discuz 源码可以发现,标题截断逻辑在 source/function/function_core.php 里的 getstr() 函数中实现。函数原型大致是:

function getstr($string, $length = 0, $in_slashes = 0, $out_slashes = 0, $censor = 0, $html = 0) {
    // ...截断与省略号追加...
}

调用点在 source/include/portalcp/portalcp_article.php

$_POST['title'] = getstr(trim($_POST['title']), $_G['setting']['maxsubjectsize']);
$_POST['pagetitle'] = getstr(trim($_POST['pagetitle']), $_G['setting']['maxsubjectsize']);

X3.4 时代这个 maxsubjectsize 是写死的 80,所以解决方法就是直接把 80 改成你想要的值。X3.5 把它抽成了全局变量 $_G['setting']['maxsubjectsize'],理论上可以从后台配置——但这个配置项的入口在初版 X3.5 里被开发人员漏掉了 UI,导致一段时间内只能改源码。

幸运的是 X3.5 后续小版本里把这个 UI 补上去了。下面的方案 A 是推荐做法(用后台 UI 改),方案 B 是兜底(直接改源码),保哥都讲清楚。

三、方案 A(推荐):后台全局配置

登录 Discuz 后台 → 全局 → 用户权限 → 标题最大字数,把默认值 80 改成 255(或者你需要的任意小于 255 的数值),保存。

这一项有几个细节需要留意:

这套方案适合 99% 的场景,简单、安全、可逆,不需要改代码。如果你的 Discuz 版本后台找不到这个选项,再走方案 B。

四、方案 B:直接改源码

找到 source/include/portalcp/portalcp_article.php,定位到这两行:

$_POST['title'] = getstr(trim($_POST['title']), $_G['setting']['maxsubjectsize']);
$_POST['pagetitle'] = getstr(trim($_POST['pagetitle']), $_G['setting']['maxsubjectsize']);

把两处 $_G['setting']['maxsubjectsize'] 都改成具体数字,比如 255

$_POST['title'] = getstr(trim($_POST['title']), 255);
$_POST['pagetitle'] = getstr(trim($_POST['pagetitle']), 255);

保存上传,立刻生效。

方案 B 的几个注意点:

五、X3.5 升级踩坑回顾

保哥这次从 X3.4 升级到 X3.5 一共踩了 5 个坑,顺便记录一下避免后人重蹈覆辙:

  1. 覆盖式升级会清掉模板自定义。如果你直接把 X3.5 的全部文件解压覆盖到老站根目录,template/default/ 下的自定义模板会被覆盖。务必先把 template 目录单独备份。
  2. UCenter 同步配置丢失。X3.5 把 UCenter 的部分配置项搬到了新位置,升级后第一件事是检查 config/config_ucenter.php 是不是还指向正确的 UC_API。
  3. 插件兼容。一些 X3.4 时代的插件(特别是涉及到 showmessage()getsubject() 等核心函数的)在 X3.5 上需要重新打补丁,建议升级前到插件作者页面看 X3.5 适配版本。
  4. HTTPS 跳转。X3.5 加强了 HTTPS 处理,老站如果之前是 HTTP/HTTPS 混合状态,升级后可能出现大量"混合内容"警告,需要把站点配置里 $_config['cookie']['cookiedomain'] 等条目同步更新。
  5. maxsubjectsize 截断(本文重点)。这是覆盖升级带来的最隐蔽的体验回退之一。

建议升级前完整备份数据库 + 全部代码文件,备份脚本:

tar czf discuz_backup_$(date +%Y%m%d).tar.gz /www/wwwroot/your-discuz/
mysqldump -uroot -p discuz_db > discuz_db_backup_$(date +%Y%m%d).sql

有了备份回滚成本就低,遇到任何升级翻车都能 5 分钟内恢复到升级前状态。

六、相关配置项的连带影响

把"标题最大字数"从 80 调到 255 之后,建议同步检查以下相关配置:

七、为什么默认设 80 而不是 255

有同学好奇 Discuz 为什么默认值是 80。保哥跟几个老 Discuz 开发者聊过,原因主要是历史遗留:

所以保哥的建议是:除非你确定业务真的需要长标题(比如一些专业知识库类站点),否则保留 80 默认值反而对站点整体质量更友好。如果你只是为了消掉省略号,可以把限制改为 100~120 字,既能避免大多数标题被截,又能维持基础规范。

八、防止升级被覆盖的插件化方案

如果你不打算用后台 UI 配置(方案 A),又怕方案 B 改源码被升级覆盖,可以做成 hook 插件。Discuz 的 plugin 机制允许在不改核心文件的前提下注入逻辑:

// source/plugin/your_plugin/portalcp_article.inc.php
class plugin_your_plugin_portalcp_article {
    public function global_filter_title($param) {
        // 在 getstr 截断前拦截,绕过 maxsubjectsize 限制
        global $_G;
        $_G['setting']['maxsubjectsize'] = 255;
        return $param;
    }
}

然后在后台 → 应用 → 插件,新建插件并指定钩子 portalcp_article_global_filter_title。这样升级核心文件时插件目录不会被覆盖,配置一次永久生效。

这个方案适合管理多个 Discuz 站点的运维同学,把这一类小修补全部插件化,迁移和升级都更省心。

九、最佳实践与最终建议

把上面所有方案串起来,保哥给出的优先级排序是:

  1. 第一优先:后台 → 全局 → 用户权限 → 标题最大字数,改成 120 或 255。这是 99% 场景的最佳解。
  2. 第二优先:如果后台找不到选项(早期 X3.5 版本),先升级到最新小版本(X3.5 R20240101 之后都已经修复 UI)。
  3. 第三优先:实在不能升级且必须改的,用 hook 插件方案保住升级安全。
  4. 不推荐:直接改源码方案 B 仅作应急用,每次升级后都要重新打补丁,长期维护成本高。

同时建议把"标题最大字数"列入升级 Checklist,每次 Discuz 版本升级后立刻验证一次门户发布、长标题保存是否正常,避免下次再被同样的坑卡几小时。

十、Discuz 标题截断逻辑深度解析

很多同学只是把 80 改成 255 就完事,其实理解一下 Discuz 内部是怎么处理标题的,能帮你在更复杂的场景里举一反三。保哥这里把核心逻辑讲清楚。

Discuz 的 getstr() 函数承担了非常多职责,远不止"截断字符串"。打开 source/function/function_core.php 可以看到,这个函数其实做了五件事:第一是 HTML 实体编码(htmlspecialchars 风格),第二是字符长度截取,第三是省略号追加,第四是反斜杠转义控制,第五是敏感词过滤(censor 参数)。当你只是想"标题别被截断"时,看似只需要改第二步,实际上整个函数链都在工作。

截断的核心是 cutstr() 子函数,它根据传入的 $length 参数和当前字符集(一般是 utf-8)逐字符判断字节宽度,超过阈值就插入 。这里有个被忽视的细节:utf-8 编码下中文字符占 3 字节,英文字符占 1 字节,所以"字符数"和"字节数"是两码事。Discuz 内部默认是按"字符数"计算(也就是 80 个汉字 = 80 字符 = 240 字节),但在某些早期分支里曾经按字节数计算,迁移老站时偶尔能碰到这个坑。

另一个值得关注的点是,maxsubjectsize 还会被用来限制其他地方:

所以"修改 maxsubjectsize"这一个动作,其实在做"全站标题长度策略"的一次性调整。这也是为什么保哥强烈推荐方案 A 后台 UI 改——一处改动全站统一生效,不用分别去每个文件打补丁。

十一、与 SEO 工作流的整合

Discuz 站点的 SEO 主战场是搜索词、长尾词与论坛主题集群,标题字数限制对 SEO 的影响主要在以下几个层面:

第一层:title 标签的有效长度。Google 搜索结果页的 <title> 显示宽度大约是 600 像素,对应中文约 30 字。即使后台允许 255 字标题,搜索结果展示出来后超过 30 字部分就会被搜索结果页截断成省略号。所以"后台 255 字限制"和"前端 SEO 友好"是两件事,需要分开优化。

第二层:列表页 H2/H3 长度。门户首页、文章列表里的标题往往用 <h2><h3> 渲染,CSS 控制单行显示。如果你允许 255 字标题,但 CSS 用的是 white-space: nowrap; overflow: hidden;,前端依然会截断显示——只是数据库里完整保存了。这种"数据库长 + 前端短"的策略最稳。

第三层:RSS / API 输出。Discuz 的 RSS 输出和 JSON API 都是直接读数据库 title 字段。如果你把字段放长了,记得给 RSS 模板加一道前端层 substr 截断,避免在 RSS 阅读器里显示异常。

第四层:sitemap 生成。多数 sitemap 插件不输出 title 字段,影响不大。但如果你用的是带 title 的特殊 sitemap 协议(比如新闻 sitemap),需要注意 Google 对 news sitemap 的 title 长度限制是 80 个字符。

第五层:全文搜索引擎索引。XunSearch、Elasticsearch 等外接搜索引擎对长 title 字段没有长度上限,但建议分配一个独立 field 而不是跟正文挤在一起,便于做 boost 加权。

十二、用户教育与编辑器层校验

从根本上来讲,与其无声地截断标题,不如在用户输入时就给提示。保哥推荐的做法是:

在门户文章发布页的 JavaScript 里加一个实时计数器:

<script>
const $title = document.querySelector('input[name=title]');
const $counter = document.createElement('span');
$counter.style.cssText = 'margin-left:10px;color:#999;';
$title.parentNode.appendChild($counter);
$title.addEventListener('input', () => {
    const len = $title.value.length;
    $counter.textContent = `${len} / 80`;
    $counter.style.color = len > 80 ? '#e74c3c' : len > 60 ? '#f39c12' : '#27ae60';
});
</script>

这样用户输入到 60 字时变橙色提醒,超过 80 字变红,提前知道会被截断而不是发布后才发现。如果你的站点是技术爱好者用户多,他们会感谢你提供这种细节。

更进一步可以在前端层主动阻止超长输入:用 maxlength="80" 属性,浏览器会直接限制输入框最长字符数。但要注意 maxlength 计算的是字符数,对中英文混排可能有差异,复杂场景下还是用 JavaScript 监听 input 事件再处理更可靠。

十三、不同 Discuz 分支版本的差异

Discuz 在过去十几年里出现过多个分支:康盛官方版(X3.x)、社区维护版(如 Discuz! Q)、第三方分叉(FreeDz、HuaiYi 改版等)。这些分支对 maxsubjectsize 的处理略有不同,迁移或合站时需要留意:

跨版本迁移时建议先把所有现有标题导出(SELECT title FROM pre_portal_article_title;),看看实际有多少标题超过新版本的默认上限,预先决定是抬升上限还是手动改写部分超长标题,能避免迁移后批量出现截断显示。

常见问题解答

Q1:改完之后老文章的省略号还在怎么办?

已经被截断的标题不会自动恢复——因为数据库里存的就是带省略号的版本。需要手动 SQL 批量清理:UPDATE pre_portal_article_title SET title = REPLACE(title, '…', '') WHERE title LIKE '%…%';。注意先备份数据库,省略号字符在不同字符集下编码可能不同,必要时改成具体的 unicode 码点匹配。

Q2:标题最大字数能调到 255 以上吗?

不能直接调。Discuz 数据库表 pre_portal_article_title.titlepre_forum_thread.subject 字段类型都是 varchar(255),超过 255 字符会被 MySQL 截断。如果业务真的需要更长,得手动 ALTER TABLE 把字段类型改成 varchar(500)text,并同步修改后台配置项的上限校验逻辑。

Q3:改了后台配置但前端还是省略号?

大概率是缓存问题。Discuz 用文件缓存保存配置项,进入后台 → 工具 → 更新缓存 → 全部更新,再清一次浏览器缓存。如果还不行,检查 data/cache/setting.php 文件里的 maxsubjectsize 值是否已经更新到新数值,没更新就手动删掉这个文件让 Discuz 重建。

Q4:除了门户和论坛,群组、问答、家园日志会受影响吗?

会。maxsubjectsize 是全局变量,所有用 getstr() 函数处理标题的地方都会受影响,包括群组主题、日志、问答、记录、状态。后台改完一次,全站统一生效。

Q5:管理员发的标题也被截断了,怎么解?

管理组在论坛模块可以通过用户组设置里的"发帖不受限制"权限绕过截断,但门户和日志没有这个豁免。如果你希望管理组在门户也不受限,需要在 portalcp_article.php 里加一段判断:检测到当前用户在管理组($_G['groupid'] == 1)就跳过 getstr 调用。

Q6:改这个值会影响 Discuz 升级吗?

不会。后台 → 全局 → 用户权限 的配置存在数据库 pre_common_setting 表里,升级核心文件不会清空数据库设置。只有方案 B 直接改源码会被升级覆盖。

Q7:Discuz X3.4 用户能不能用同样方法?

X3.4 没有 maxsubjectsize 全局配置项,需要走方案 B 直接改源码,把 portalcp_article.php 里写死的 80 改成你想要的值。等到从 X3.4 升级到 X3.5 后再切到方案 A 即可。

把这个坑彻底填平后,Discuz X3.5 站点的门户体验就能回到正常水平。下次保哥会接着写 Discuz 升级到 X3.5 的完整 Checklist,敬请期待。