Zsens Admin 插件上线前七步巡检:我因漏掉第4项导致生产环境菜单404的复盘
上周把一个新插件丢到测试服,一切正常,上线后运营反馈后台菜单点进去白屏。根因是 admin_menu 表里的 path 带了前缀斜杠,而测试服路由配置宽容,生产服严格匹配直接 404。这件事让我把上线前的检查清单从 3 项扩到了 7 项,今天把踩过的坑和检查方法摊开聊。
一、权限节点:注册容易,回收难
插件里新增权限节点,install 时写进 admin_rule,uninstall 时你删不删?我早期图省事全删,结果用户之前分配好的角色突然丢权限,客服炸了。现在的做法:
// install 时标记来源
INSERT IGNORE INTO admin_rule (name, title, plugin_mark, created_at)
VALUES ('plugin.order/export', '订单导出', 'my_plugin', NOW());
// uninstall 时只删未绑定的
DELETE FROM admin_rule
WHERE plugin_mark = 'my_plugin'
AND id NOT IN (
SELECT rule_id FROM admin_role_rule
);
上线前跑一遍 SELECT * FROM admin_rule WHERE plugin_mark = 'xxx',确认节点名没和现有系统冲突,尤其是 user/*、order/* 这种大路货命名。
二、路由:大小写、斜杠、重复注册三重雷
Zsens Admin 的路由在 Linux 下严格区分大小写,我 Windows 本机开发时 OrderController 和 orderController 都能进,上线后直接 500。现在提交前必做:
// 路由定义文件里强制小写+中划线
Route::get('plugin/my-plugin/order-list', [OrderController::class, 'index']);
// 用 artisan 命令扫一遍重复
php artisan route:list | grep my-plugin | sort
重点看有没有和其他插件或系统路由撞车,特别是 api/、admin/ 前缀的。
三、菜单:父子级与排序号的隐形契约
后台菜单不是单纯插一条记录就行,pid 指向的父节点如果未启用或权限不足,子菜单整个消失。我的检查项:
- 父节点
status = 1且当前角色有权限 sort值别和现有插件撞,建议用 1000 以上的区间段icon字段如果引用了未加载的图标库,直接显示方块
上线前在干净环境(全新安装,非开发环境)走一遍完整 install 流程,肉眼确认菜单层级和图标正常。
四、配置项:默认值陷阱与类型漂移
插件配置存到 system_config 或独立表,常见问题:
- 布尔值存成字符串
"0",前端 checkbox 反显异常 - JSON 配置项没做
json_validate,用户手抖输错格式,整个配置页崩溃 - 配置键名和系统保留字冲突,比如叫
app_debug直接覆盖系统配置
我的防御代码:
public function getConfig(string $key, mixed $default = null): mixed
{
$raw = $this->configModel->where('key', 'my_plugin_' . $key)->value('value');
// 强制前缀隔离 + 类型还原
return match($this->getType($key)) {
'bool' => filter_var($raw, FILTER_VALIDATE_BOOLEAN),
'json' => json_validate($raw) ? json_decode($raw, true) : $default,
'int' => (int) $raw,
default => $raw ?? $default,
};
}
五、数据库迁移:install.sql 之外的增量脚本
v1.0 到 v1.1 加字段,不能改 install.sql 就完事,老用户升级时不会重新执行完整 SQL。我的版本迁移文件结构:
database/
├── install.sql # 全新安装
├── uninstall.sql # 卸载清理
└── upgrades/
├── 1.0.1.sql # 1.0 → 1.0.1
├── 1.1.0.sql # 1.0.1 → 1.1.0
└── 1.1.0.php # 复杂迁移用 PHP 脚本
上线前用两个库验证:一个全新安装,一个从旧版本升级,结构必须一致。
六、钩子注册:顺序依赖与重复挂载
多个插件监听同一个 hook,执行顺序不可控。如果我的插件依赖另一个插件的数据,上线前确认:
// 注册时声明优先级,数字小的先执行
Hook::listen('order_paid', [MyListener::class, 'handle'], 10);
// 上线前临时加日志,看实际挂载顺序
Log::debug('hook_fired: ' . $hookName, ['plugins' => Hook::getListeners($hookName)]);
还有重复安装导致的重复挂载,uninstall 时记得 Hook::remove(),别只删数据库。
七、文件权限:缓存目录与上传路径
最后一条最简单也最容易忘:插件生成的缓存文件、日志、用户上传的临时文件,目录权限是否 755 或 775,属主是不是 www-data。我有一次上线后图片上传成功但读取 403,查了半天是 storage/plugin/my-plugin/ 目录用 root 跑的 CLI 命令生成,web 进程没权限读。
现在我的 deploy 脚本里固定带一段:
chown -R www-data:www-data storage/plugin/my-plugin
chmod -R 775 storage/plugin/my-plugin
find storage/plugin/my-plugin -type f -exec chmod 664 {} \;
附:我的上线前速查表(复制到备忘录)
□ 权限节点命名无冲突,uninstall 策略已测试
□ 路由全小写,无重复,Linux 环境验证通过
□ 菜单父子级、排序、图标在干净环境正常显示
□ 配置项有前缀隔离,类型处理完备
□ 数据库迁移脚本覆盖全新安装 + 旧版升级两种路径
□ 钩子优先级和卸载清理逻辑已确认
□ 文件目录权限、属主、缓存清理已处理
这份清单从 3 项扩到 7 项,每项背后都是真实故障。你们上线前还有啥固定检查项?或者有没有漏掉某一项导致翻车的经历,欢迎补刀。

