给一个鹦鹉爱好者论坛(Flarum 2.0-RC)加一套「品种百科」词条 + 卡片首页 + 多维投票。功能最后都上线了,但过程里踩的坑比预想多。这篇不打草稿,按「现象 → 原因 → 解法」记下来,给后来人省点时间。
背景
需求很朴素:
- 每个鹦鹉品种一个词条,结构化字段(学名、寿命、体长、体重、产地、价格……)+ 配图;
- 一个卡片式首页,把所有品种铺成一堵墙;
- 词条底下挂投票(吵不吵 / 咬不咬人 / 掉不掉毛 / 亲不亲人 / 会不会说话)。
技术栈: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(AuthenticateWithHeader 里 bypassCsrfToken=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 里禁了 putenv、proc_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,希望这几条能帮你少踩两个。反正我那晚是把心提到嗓子眼了好几次。
评论