给一个鹦鹉爱好者论坛(Flarum 2.0-RC)加一套「品种百科」词条 + 卡片首页 + 多维投票。功能最后都上线了,但过程里踩的坑比预想多。这篇不打草稿,按「现象 → 原因 → 解法」记下来,给后来人省点时间。

背景
009.jpg
需求很朴素:

  • 每个鹦鹉品种一个词条,结构化字段(学名、寿命、体长、体重、产地、价格……)+ 配图;
  • 一个卡片式首页,把所有品种铺成一堵墙;
  • 词条底下挂投票(吵不吵 / 咬不咬人 / 掉不掉毛 / 亲不亲人 / 会不会说话)。

技术栈:Flarum 2.0(还在 RC 阶段)+ 宝塔面板 + PHP 8.3 + MySQL 5.7。全程尽量不改核心文件,用扩展和设置解决。

下面是我踩到的坑。

坑 1:Flarum 的帖子内容是「按需渲染」的

我之前改了格式化器(让 Markdown 表格能渲染),然后盯着已有的帖子想:内容没变,要不要「重新保存」一遍让它生效?

结果白折腾。Flarum 的 posts 表里只存 content(原始 Markdown),不存渲染后的 HTML;渲染是每次请求现算的。所以格式化器一改,旧帖子里的表格当场就出来了,不用重存。

教训:别用「静态站点思维」猜 Flarum 的表结构。先 SHOW COLUMNS,再动手。

坑 2:别信脑子(和压缩摘要)里的 ID

批量操作时我一度把词条 ID 记成 17/18/19,实际是 21/22/23。这个错还是翻数据库才发现的。

教训:对线上资源的 ID,一律现查 API/数据库再操作。记忆和摘要都会骗你,现查不会。

坑 3:csrf_token_mismatch 十有八九是 token 不对,不是 CSRF 配置

用 API 建帖子,有几次突然全是:

{"errors":[{"status":400,"code":"csrf_token_mismatch"}]}

第一反应是去翻 CSRF 中间件配置——错。翻到 Flarum 源码才明白:带 Authorization: Token xxx 的请求,只要 token 有效,框架会自动绕过 CSRF(AuthenticateWithHeaderbypassCsrfToken=true)。报这个错,通常意味着这个 token 根本没通过校验(被当成游客了)。

几个关于 Flarum 令牌的冷知识:

  • access_tokens 表里 type = session_remember 的是用户登录会话令牌,会被系统轮换/清掉——拿它做自动化,早晚失效;
  • 想给脚本用,正解是建一个 API Key(api_keys 表),它才同时享受「免 CSRF + 免限流」待遇;
  • 手打 token 极易敲错(尤其是从文档里复制一半),脚本里别硬编码,从文件/环境变量读。

坑 4:宝塔环境下 composer 直接跑不动

composer require 报:

Call to undefined function Composer\XdebugHandler\putenv()

原因是宝塔的 php-cli.ini 默认 disable_functions 里禁了 putenvproc_open 等,而 composer 依赖它们。

解法:调用时覆盖掉:

php -d disable_functions= /usr/local/bin/composer require foo/bar

装完记得把 vendor/ 和扩展目录 chown www:www,不然前台可能读不了。

坑 5:LESS 编译器的两个隐藏坑

用 Flarum 的 custom_less 设置写自定义 CSS,被 LESS 编译器坑了两回:

(1)带斜杠的简写会被吃掉。

grid-column: 1 / -1; /* 编译后变成 grid-column:-1,布局直接乱掉 */

LESS 把 1 / 当除法算没了。解法是用长写法绕开:

grid-column-start: 1;
grid-column-end: -1;

(2)min() / max() 会被当成 LESS 函数。

max-width: min(1400px, 94vw); /* 报 422:incompatible types */

LESS 先按自己的函数去算,单位不同就炸。解法是用固定值,或者转义成 ~"min(1400px, 94vw)"

坑 6:自定义 CSS 压不过扩展的样式

custom_less 覆盖扩展样式,偶尔「不生效」。原因是权重打平时,谁在后面谁赢——而扩展的 CSS 往往排在你的 custom_less 之后。

解法:加前缀提权,或者直接 !important。比如扩展里是 .Pages .Pages-container{max-width:820px},你写 .Pages-container{max-width:1400px} 是压不过的,得写 .Pages .Pages-container{... !important}

坑 7:Flarum 2.0 加「自定义前端 JS 扩展」会把整个论坛搞崩

这个最坑,也最值得记。

我想在左侧导航加一个入口,就写了个极小的论坛前端扩展(Frontend('forum')->js(...))。一启用——整个论坛前端崩了,页面变成「加载论坛时出错」的兜底页。站长正好在群里,问我论坛是不是挂了,我只能先回一句「马上好」。

排查到后面发现:连只写一句 console.log() 的空扩展,照样崩。也就是说,不是我的代码问题,是这台环境上 Flarum 2.0 的 JS 打包管道在「新增前端扩展」时产出的 bundle 是坏的(bundle 能过语法校验、内容也看着正确,但 boot 就是失败)。

试过:启用前后都 assets:publish、删掉编译产物重建、清缓存、各种顺序……都不行。

结论 + 教训:

  • Flarum 2.0(RC 阶段)的扩展 JS 资产管道还不够稳,别在线上反复 toggle 前端扩展,尤其是生产环境;
  • 每次启用都可能把线上前端短暂打挂——要有秒级回滚(flarum extension:disable xxx + 清缓存,实测几秒恢复);
  • 想加前端 JS 又不想冒险,先想想有没有「不改 bundle」的替代方案(见坑 9)。

坑 8:「游客也能投票」没那么简单——权限 ≠ 能投

投票用的是 fof/polls 扩展。想让不登录的游客也能投,我以为给 Guest 组授个「投票」权限就行了。

实测:不行。

给了权限后,游客看投票确实变成「能点的样子」了(canVote=true);但一点,弹出登录框,票没记上。翻源码才看清,有两道硬拦:

  • 接口层:投票端点在代码里写死了 ->authenticated()(必须登录);
  • 前端层:投票组件的点击处理里写死了「没登录就弹 LogInModal」。

权限管的是「你能不能投这个票」,接口和前端管的是「你是不是登录用户」——后两者在源码里,权限说了不算。

要让游客真能投,得改扩展源码,还得解决一个隐藏问题:游客在系统里都算同一个「0 号」身份,直接放开会导致游客之间串票(你投了、别人一投就把你顶掉)。我的做法是给 poll_votes 表加一列 voter_key,按浏览器会话 id 的哈希给每个游客一个独立身份(不存原始会话 id),登录用户仍按 user_id 走。改完游客投票、改票都正常,且彼此不串。

代价:改的是第三方扩展源码 + 前端 dist,扩展一升级就被覆盖,得留好补丁、升级后重打。

教训:遇到「能不能做 X」的需求,先分清是权限问题还是源码问题。权限问题改配置,源码问题改代码——别拿权限的钥匙去开源码的锁。

坑 9(正面的):用 FoF Pages + 默认路由做首页,两步缺一不可

想把卡片页设成论坛首页,用 fof/pages 扩展。踩完坑 7 后,我改用纯设置实现,零代码、可秒回滚:

-- 1) 指定「首页页面」
pages_home = 1
-- 2) 关键:把默认路由指到 pages 的 home 路由
default_route = /pages/home

只设第一步不够——论坛 / 还是走讨论列表。Flarum 的 setDefaultRoute() 是把 / 的处理器换成「默认路由」对应的处理器,所以第二步才让 / 真正渲染卡片页。

顺带两个小发现:

  • 侧栏的「全部讨论」指向 /all(不是 /),所以换首页不会丢讨论列表;
  • 卡片页容器默认被扩展限死在 820px,显得「小气」——放宽 max-width 后一排能放 5 个卡片。

收尾

一句话总结今天的收获:

  • Flarum 2.0 还在 RC,扩展生态和资产管道都有毛刺——多用「设置/数据」解决,少碰「前端 bundle」;真要碰,先把回滚命令备好。
  • 报错先怀疑最小、最可能的变量(token、权限、权重),别一上来就怀疑系统配置。
  • 改第三方源码前先备份,并且记住它会被升级覆盖。

功能最后都上线了:48 个品种词条(带配图)、卡片首页、5 维投票(游客可投),全程零核心文件改动。

如果你也在折腾 Flarum 2.0,希望这几条能帮你少踩两个。反正我那晚是把心提到嗓子眼了好几次。