ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从FilamentPHP到自研面板:Teanary V1.2.1后台开发复盘与选型思考

从FilamentPHP到自研面板:Teanary V1.2.1后台开发复盘与选型思考 Teanary V1.2.1 发版好几天了一直憋着没写总结。这个版本对我和团队来说有点特殊因为它被我们内部标记为“最后一个基于 FilamentPHP 的稳定版本”。也就是说下一次大版本迭代Teanary 会彻底换掉现役的后台技术底座进入一套自研面板时代。趁着周末整理版本发布前因后果的间隙把这段基于 FilamentPHP 的开发经历、踩过的坑、优化过的资源、以及最终决定“分手”的真实原因都摊开聊聊希望能对正在用 FilamentPHP 或正打算选型后台框架的同行有所帮助。1. 项目定位与选型背后的考量1.1 Teanary 到底解决什么问题Teanary 是一个面向小团队的自托管式客户支持与工单管理系统。团队内部一共 4 个人要维护一个面向付费客户的 SaaS 服务还要兼顾工单流转、客户信息沉淀、邮件回复、数据报表这些东西。一开始也想用现成的开源工单系统但调研了一圈发现传统开源工单系统的问题在于一是界面老化严重二是二次开发成本不低三是权限模型跟我们的业务对不上。我们真正需要的不是一个大而全的 helpdesk而是一个能按我们团队协作方式自由修改的“工单客户管理”后台。于是决定自研。但说实话一个小团队连专职前端都没有要从零写一个完整的管理面板界面这在资源上是不现实的。后端我们很熟Laravel 用了很多年所以当时的第一想法是在 Laravel 生态里找一个后台框架。那时候摆在桌面上的选项有 Laravel Nova、AdminLTE 套模板、还有 2022 年开始在社区里热度明显上升的 FilamentPHP。选 FilamentPHP 不是没有理由的。先说 Nova功能确实成熟但价格不低一个项目授权要几百美元而且它是一个闭源商业包自定义起来总觉得不透彻。AdminLTE 之类的模板属于“给你一个壳子”表格、表单、弹窗全部要自己一个个页面的写代码量非常惊人。FilamentPHP 的定位不一样它把后台开发抽象成了“面板”和“资源”两个核心概念开发者只需要声明资源就能得到一套完整的表格、表单、筛选、操作、详情页体系。对当时我们这种“后端为主前端能力有限”的小团队来说这几乎是最优解了。1.2 为什么一个小团队会押注 FilamentPHPFilamentPHP 的核心价值用一句话总结就是“把 80% 的管理后台代码量减到 10%。”它不是模板也不是组件库而是一个完整的后台应用框架。你定义好数据模型之后甚至不需要写一行 Vue 或者 React就能得到一套交互完善、响应快速的管理界面。当时我们做了一个很小的 PoC用一个下午把 Team 模型和 Ticket 模型做成 Resource定义了表单字段、表格列、过滤器并且加了两个自定义操作。下班之前一套可以用的简单工单后台就跑起来了。这个速度放在传统开发流程里是不可想象的。如果自己从零写后台光是把用户管理、角色权限、列表筛选、表单验证这些基础设施做完估计要一到两周。而且 FilamentPHP 的组件体系非常齐全。它内置了文本输入框、下拉选择器、日期时间选择器、文件上传、RichEditor 富文本、ColorPicker、Toggle、TagsInput 这类常见表单组件表格方面有排序、搜索、筛选、批量操作、行操作、导出表格、列排序、表格卡片视图等除此之外还有通知系统、模态框、抽屉、全局搜索、活动日志等能力。对普通后台来说基本不需要再引入额外的前端 UI 库。另一个让我放心的点是它的架构。FilamentPHP 基于 Laravel 的组件系统构建所有面板、资源、页面本质上都是 Livewire 组件。而 Livewire 是 Laravel 官方生态中做动态交互的主力方案它可以在不写 JavaScript 的情况下实现前端交互。可以说这是“少数人维护高复杂度后台”这条路上的两个重要支撑点Livewire 负责交互FilamentPHP 负责后台应用本身的封装。2. 核心实现与开发过程中的实操要点2.1 基于 Resource 的快速建模实践FilamentPHP 中最核心的概念是 Resource。一个 Resource 对应一个 Eloquent 模型它同时包含列表页、创建页、编辑页、详情页以及相关的 Form Schema 和 Table Schema。也就是说你只需要在一个类里配置好“这个模型要显示哪些字段、表单要录入哪些字段、支持哪些筛选、提供哪些操作”Filament 会自动为你生成所有配套页面和路由。以 Teanary 中的工单模型为例protected function getHeaderActions(): array { return [ Actions\CreateAction::make() -label(新建工单) -icon(heroicon-o-plus), ]; } public static function table(Table $table): Table { return $table -columns([ Tables\Columns\TextColumn::make(ticket_no) -label(工单编号) -searchable() -sortable(), Tables\Columns\TextColumn::make(customer.name) -label(客户) -searchable(), Tables\Columns\TextColumn::make(subject) -label(主题) -limit(50), Tables\Columns\TextColumn::make(status) -label(状态) -badge() -colors([ success resolved, warning pending, danger closed, ]), ]) -filters([ Tables\Filters\SelectFilter::make(status) -options(TicketStatus::options()), ]) -defaultSort(created_at, desc); }这段配置写完列表页就拥有了搜索、排序、状态徽章和筛选能力。如果按照传统方式实现这部分至少需要写一个 blade 模板文件、一个控制器方法、一段前端 JS 和一堆 CSS 类名而 FilamentPHP 只需要展示声明即可。但这里也有个容易踩的坑Resource 的自动化程度高不代表业务逻辑可以乱塞。我见过很多团队把复杂的业务判断逻辑直接写在 Form Schema 里或者把 API 调用放在 Resource 的 mutateFormDataBeforeCreate 方法里做。这种写法在前期看起来省事但后期维护会非常痛苦。Resource 应该保持“哑”——只负责数据的结构化和页面呈现真正的业务决策、事务处理、事件触发都应该放到 Service 层或者独立的 Action 类里。在 Teanary 中我们所有涉及状态流转的操作比如“将工单标记为已解决”都不是直接改数据库而是走一个自定义 ActionAction::make(resolve) -requiresConfirmation() -action(function (Ticket $record) { app(TicketResolutionService::class)-resolve($record, auth()-user()); }) -after(function ($record) { Notification::make() -title(工单已解决) -success() -send(); });这样页面上的按钮逻辑轻了状态变更的副作用发送邮件、记录日志、通知相关责任人统一放在服务层处理测试也方便很多。这是我们在初期没有注意到的架构细节后来踩了几次坑才回头调整建议新项目一开始就按这个原则划分边界。2.2 权限、多语言与主题定制的实操细节FilamentPHP 有完整的 Authorize 机制基于 Laravel Policy 来做资源访问控制。在 Teanary 里我们定义了三种角色管理员、坐席、观察者。通过 Policy 控制谁能创建工单、谁能批量操作、谁能看到客户联系方式实现起来非常干净。有一点容易被忽略FilamentPHP 的多租户模式。如果你要做一个 SaaS 产品让多个客户团队各自登录后台默认的 Filament 并不直接支持多租户的数据隔离。官方有 Multi-Tenancy 文档但实现核心理念是让每个 Panel 下的资源都绑定当前 Tenant并且通过全局作用域来限定数据可见范围。Teanary 早期没有做多租户因为团队内部协作不需要但 V2.0 规划中多租户会是核心能力之一。在做选型判断时对多租户的支持程度也要纳入考量范围。多语言方面FilamentPHP 对 Laravel 本地化做了很好的集成。发布语言包资源文件后后台 UI 的文字会随 locale 切换。Teanary 从 V1.1 开始支持英文和中文界面V1.2.1 还新增了西班牙语。实际操作中除了 lang 目录下的 JSON 翻译键还要注意自定义字段 label、Action label、Notification 消息这些地方的翻译一致性。我建议团队维护一份统一的翻译名词表定义好核心概念在不同语言下的说法避免后期出现同一个字段在列表页和表单页显示不同文案的问题。主题定制是 FilamentPHP 3 的一个重点能力。从 Filament v3 开始面板支持自定义主题但它不是简单的“改个 CSS 文件”而是要走一次构建流程。你需要用 npm 安装 Tailwind CSS 和必要的依赖然后修改主题入口文件再编译出来。这一步对小团队来说有点门槛但好处是主题可以深度定制不像 v2 时代那样只能跟着框架默认样式走。我们最终配置了一套符合 Teanary 品牌色的深色主题侧边栏换成深蓝底色主按钮使用了我们的品牌绿色表格每一行之间的边框颜色调成了更浅的灰色。编译需要几分钟但效果值得。如果你第一次做建议先把 Filament 官方主题构建文档过一遍特别注意 tailwind.config.js 里 content 路径的配置漏掉任何一层路径都可能让自定义样式没有生效。2.3 列表页性能调优与避免 N1 查询FilamentPHP 让表格开发方便了很多但方便不等于可以乱来。如果列表页直接展示关联模型的字段而不做预加载很容易出 N1 查询问题。这一点在所有 ORM 后台框架中都很常见Filament 也不除外。Teanary 的工单列表页最开始显示“客户名称”时我们是直接用了 customer.name。结果首页加载发出了一百多条查询。后来做了两个优化一是在 Resource 的 getEloquentQuery 方法里强制 with([customer])二是在表头增加一个加载状态提示避免用户重复点击。public static function getEloquentQuery(): Builder { return parent::getEloquentQuery() -with([customer, assignee]) -when(!Auth::user()-isAdmin(), function (Builder $query) { return $query-where(assignee_id, Auth::id()); }); }另外Filament 表格中的搜索功能默认会对所有 searchable 字段做 LIKE 查询。如果字段太多或者表数据量很大搜索会变得很慢。我的处理方式是对搜索频率最高的字段工单编号、客户邮箱启用搜索其他字段只允许筛选过滤不做全文搜索。再搭配 MySQL 的索引列表页的响应时间基本都维持在 200ms 以内。不要觉得这些都是小问题。后台系统表面上使用人数少但操作频率高、数据密度大一个慢查询被点击几百次对服务端资源的消耗也是非常可观的。尤其是当你打算把 Teanary 部署在低配服务器上时性能优化优先级还要更高。3. V1.2.1 版本做了哪些事3.1 版本内容概览与发布前规划V1.2.1 不是一个功能大版本而是一个“平滑过渡版本”。它的核心目标有三个把 FilamentPHP 时代的体验打磨到最佳状态为 V2.0 的框架切换做数据与功能层面的收尾同时修复已知问题。我们刻意控制了这个版本的范围没有引入新功能因为这种“最后一版稳定版”的意义是提供可长期运行的稳定基线而不是冒险加入更多变数。这个版本我们一共合入了 23 个提交主要涵盖四个方面表格交互优化、多语言完善、权限细化、稳定性修复。表格交互方面主要优化了批量操作的用户引导。以前批量选择工单后操作菜单是折叠的用户需要多一步点击才看到“标记为已解决”或“分配给某人”。V1.2.1 里我们把高频操作直接放到了工具栏低频操作才收起。这个改动看起来小实际对坐席日常使用的速度提升很明显。权限模型也做了细化。原本“观察者”角色也可以查看客户邮件内容V1.2.1 加了一层字段级权限控制让“观察者”看不到敏感联系方式。FilamentPHP 在表单和表格字段上支持 -visible(fn (): bool auth()-user()-can(...)) 的闭包控制配合 Policy 使用非常灵活。但闭包不要乱用如果同一类权限判断散落在多个字段建议封装成一个集合方法或者 Policy避免日后调整权限时翻遍整个类。3.2 发布前处理的问题清单每次发布前我们都会跑一轮自己的“发布前检查清单”。V1.2.1 在做回归时遇到过几个有意思的问题这里挑两个典型的说说。第一个问题是 Composer 依赖版本锁定。因为 FilamentPHP 是一个更新非常频繁的框架小版本之间也偶尔会有不兼容的变更为了避免发布日手忙脚乱我们会在正式发布前两周锁定所有依赖版本到精确版本号并单独跑一次 composer update 创建一组 CI 用的版本快照。也就是说生产环境和 CI 环境用的 composer.lock 是完全一致的。这看起来是常识但确实有不少团队因为本地和 CI 的依赖不一致导致发布的版本与测试版本行为不一致。第二个问题是 Livewire 组件的缓存问题。Livewire 在开发模式和生产模式下的组件缓存策略不同有时改了自定义组件类的属性或方法名生产环境还会跑去旧的 Livewire 组件缓存导致页面交互异常。遇到这种诡异的“改了代码没反应”先执行一下日志清理和缓存重建大概率能解决。php artisan livewire:publish --assets php artisan view:clear php artisan config:clear php artisan route:clear php artisan optimize:clear这些命令基本是每次发布后的固定动作。如果订单量或流量敏感建议放在发布脚本中自动跑别手敲真的会漏。3.3 回归测试与兼容性检查清单我们团队没有专职测试所以发布前的回归测试靠的是清单加人工过一遍核心流程。下面是 V1.2.1 发布前实际执行的测试表格你可以直接参考测试项测试路径预期结果备注工单创建与分配新建工单 - 表单校验 - 自动分配坐席创建成功分配关系正确注意表单校验中文提示邮件通知工单状态变更 - 触发通知动作客户收到邮件内容无乱码邮件模板多语言切换批量操作表格多选 - 批量标记已解决所选记录状态一致更新无遗漏大数量集下需检查超时权限隔离观察者访问工单列表无法查看客户联系方式字段字段级权限生效多语言切换面板 locale 从英文切到中文全部资源、通知、表单字段同步切换注意缓存后偶发翻译不更新主题切换用户偏好切换浅色/深色主题所有页面样式正常无白屏首次切换时会编译需稍等数秒发布当天我们预留了 4 小时窗口但实际上核心回归只用了 2 个多小时就完成了。剩下的时间用来观察线上日志和监控告警。发布后第一个工作日我们把坐席使用日志翻了一遍没有发现异常情况。这里也要提醒一句如果你的项目团队没有配置日志链路发布前至少要把 Laravel 的日志级别调为 info并确认监控面板看得到请求量和错误率否则线上出问题后排查非常被动。4. 为什么决定告别 FilamentPHP转向自研面板4.1 FilamentPHP 让我心存感激的几个点在吐槽之前得先把 FilamentPHP 的巨大价值说清楚。它是那种“用了之后才知道原来后台能做得这么快”的框架。Teanary 能在三个月内从仓库里的一个 Laravel 项目变成可用的内部工具FilamentPHP 是最大功臣。最让我满意的是它的表单体系。Filament 的 Form Schema 写起来非常流畅字段可以很方便地设置规则、默认值、联动显示、依赖注入。比如在多语言支持里让“选择语言”字段切换时对应字段自动变成不同的校验规则这一点在传统模板开发中要写很多 JS而 FilamentPHP 只需要在 Schema 里用 reactive() 方法声明关联关系。另外FilamentPHP 的社区质量也很高。它的文档、Discord 频道、GitHub Issues 活跃度都很好。遇到一个比较冷门的问题比如自定义表格列排序时如何与模型访问器协同也很快有人给出靠谱的答案。对一个开源项目来说社区响应速度会直接影响采用者的稳定性信心FilamentPHP 在这方面表现优秀。而且正如我前文讲的FilamentPHP 在快速原型阶段几乎无可替代。你不需要考虑前端技术栈、不需要维护 Webpack 或 Vite 配置没有独特的 UI 约束就能交付一个体验完整的管理后台。这对于独立开发者、外包团队、初期创业团队来说都是一个非常务实的选择。4.2 真实推动我们离开的四个原因但 Teanary 的产品轨迹渐渐发生了变化。当它从“内部工具”变成“要卖给更多小团队的产品”时我们的技术栈限制开始显现。第一个原因是复杂交互的定制成本。FilamentPHP 确实很强但它的交互模型是“表格 表单 操作”这套后台模式。像拖拽排序、看板视图、富文本与消息流混合排版、实时协同编辑这类更面向终端的交互形式在 Filament 里做起来就要绕很多路。Teanary 下一阶段要做一个带看板视图的团队工单面板Filament 对“看板”这种高度定制化 UI 的支持本质上还是通过古早的 HTML 组件堆叠开发成本反而不如从零写前端。第二个原因是前端可控性需求。FilamentPHP 生成的页面里 HTML 结构和 CSS 类是它自己约定的虽然可以覆盖样式但覆盖的深度和安全性有限。做 Saas 产品客户对品牌一致性、页面设计的要求远高于内部工具。我们需要对最终展示给用户看的东西有百分之百的控制权Filament 的抽象层级在这种需求下变成了一层“不要跨越的边界”。第三个原因是版本升级的破坏性变更。FilamentPHP 的迭代节奏快v3 到 v3.x 之间偶尔也有程度不等的 Breaking Change。我们的代码库维护了两年每次上游升级都要重新检查一遍自定义组件和主题兼容这部分成本说实话不低。如果把技术栈锁定在一个版本不升级又会越来越落后最终陷入“要么推翻重来要么永远停在旧版”的困境。第四个原因是团队技能结构的演进。随着项目的推进团队里专职前端的人加入了。既然有人可以从容驾驭 Vue 和 Tailwind那我们就不应该继续把产品的前端能力限制在框架的可配置范围内而是直接把它交到擅长的人手里。这本质上是一个“技术栈与人员能力匹配”的选择题每个人情况不同结论可能也会不同。4.3 V2.0 的技术栈规划与迁移思路V2.0 我们决定采用“Laravel Inertia.js Vue 3 TailwindCSS”的组合。Laravel 后端保持不变数据模型和业务服务全部保留前端换成 Inertia 驱动页面由 Vue 组件渲染。模板方面用 TailwindCSS shadcn-vue 风格的组件自己搭一套轻量组件库。为什么选 Inertia因为它是目前 Laravel 全栈开发中最接近“前后端一体但前端可控”的方案。不需要自己维护 API 路由和认证重定向服务端依然可以用 Laravel 的 Controller 和 Middleware前端拿到的是一个 JSON 页面描述而不是上一整包 HTML。对我们这种熟悉 Laravel 的团队来说过渡成本很低。数据层我们打算一块都不动。Ticket、Customer、Message、User 这些表结构已经在生产环境稳定运行了近一年没有任何理由为了框架换代去动数据。Service 层的方法设计得也比较独立不依赖 Filament 的任何类所以平移压力不大。真正的工作量在“把 T1.2.1 中的表单、列表、筛选、操作逻辑用 Vue 组件重写一遍”我们预估需要两个月左右。迁移期间我们也不会直接抛弃 FilamentPHP。V1.2.1 会作为稳定维护分支继续存在一段时间公司内部还在用它管理和处理存量工单。V2.0 做到可以横向对比的数据阶段后才会把内部链路切换过去这段时间两个版本并行跑降低突然迁移的风险。5. 从 Teanary 迁移经验中提炼的选型建议5.1 哪些人仍然适合用 FilamentPHP我在不同场合被问过同一个问题“FilamentPHP 适合什么样的人”我现在的答案清晰了很多如果你做的是一个内部的、以数据新增/编辑/展示为核心的管理后台FilamentPHP 是一个非常值得推荐的选择。典型场景包括后台订单管理、内容审核后台、客户关系系统、运营数据报表、内部权限配置、运维工单系统等。如果你是独立开发者或小微企业一个人要负责后端、部署、客服甚至还要写文档FilamentPHP 能帮你省掉的开发时间是以“周”为单位的。你的目标客户是内部人员对页面设计不敏感对交互复杂度要求不高更加看重的是“能用、稳定、好改”。FilamentPHP 直接把后台变成一个“配置化的东西”要调字段就改 Schema 配一下不需要开一个庞大的前端子项目。另外外包团队也可以积极考虑。FilamentPHP 的统一架构模式非常适合外包合作多套项目之间是同一套逻辑模式一套团队的经验可以快速复制到下一个项目。版本升级统一管理文档也齐全外包交付后的维护成本要比纯手写后台低不少。5.2 关于后台框架选型的几个务实忠告先想清楚你的产品阶段再做决定。如果你也在做一款面向终端客户的 SaaS 产品建议一开始就认真评估前台个性化需求的技术边界不要因为“先快速上线”而在技术栈上背负太久的债。快速上线确实重要但快速上线之后你一定会经历一个“技术债偿还期”这个周期可能比你想象中更长。如果你的产品是一个内部工具那 FilamentPHP 这类框架真的是好东西不需要用一套复杂的 Vue 前端去“炫技”。内部工具的考核点是流程效率和可维护性而不是视觉新鲜感。我曾经见过一个团队给内部工单系统上微前端方案折腾了三个月最后还是回来用现成后台框架成本全浪费了。框架锁定问题要提前看。任何后台框架都有锁定效应区别只是深浅不同。你用 FilamentPHP 做后台短期内开发效率极高但精细交互要自己拓展你用 Vue 自己做后台什么都能掌控但前期工期至少是 FilamentPHP 的 3~5 倍。选择哪个都合理关键是团队必须能回答一个问题“三年后我们还会不会对这个产品的交互有更高要求”答案若是“会有”那就要为定制空间预留更多位置。5.3 常见问题速查表整理一份我们开发期间遇到的典型问题你可以对照排查问题现象可能原因解决办法表格列表页加载缓慢SQL 日志显示大量关联查询缺少预加载触发了 N1Resource 的 getEloquentQuery 方法中 -with() 关联模型创建表单的字段不显示默认值表单 Schema 没有配置 -default()在字段定义中使用 -default($value)或在页面 mount 中赋值自定义按钮不触发动作Action 未绑定到表头或行操作确认按钮添加到了 headerActions() 或 table()-actions()多语言切换后部分字段没翻译自定义组件未加载资源翻译文件检查 lang/ 下对应 translation key调用 -label(__(...))生产环境修改主题不生效主题未被重新编译重新执行 npm run build 主题脚本并清理缓存单条操作后页面没有刷新缺失 -refreshable() 或 did it manually使用 -after(function ($record) { $this-resetTable(); }) 强制刷新批量更新数据到一半超时数据量大单次操作过重分批处理或转换为队列任务去执行文件上传后访问 404存储磁盘权限或 URL 链接未配置检查 storage:link 是否执行确认 filesystem disk 配置正确这里面我觉得最容易栽跟头的还是 N1 查询问题因为它不会立刻让页面崩掉只会在数据量变大后慢慢变卡。建议在本地开发时打开 Laravel Debugbar跑一遍主要页面看到查询次数超过 50 就要警惕认真过一遍预加载逻辑。Teanary V1.2.1 对我个人而言不只是“一个版本的发布”更像是一段技术攻坚期收缴了。最后一次用 FilamentPHP 做完一次完整的功能迭代与排查心里反而踏实了。这个框架的抽象层级是真的高帮我省掉了无数基础工作让我能把精力放在产品逻辑本身。但产品要往前走技术栈就要跟着走没有哪个框架能陪你到永远关键在于你拥有什么敢于在今天把旧东西放下来。如果你也在一个自研项目里遇到了“快速发展但交互受限”的纠结希望这篇复盘能给你一些确定感。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进