关键源码:
model/plugin.func.php(plugin_init、plugin_paths_enabled、plugin_install、plugin_unstall、plugin_enable、plugin_disable)、admin/route/plugin.php
1. 原版设计缺陷与重构背景
Xiuno 原版存在插件状态双数据源冲突的底层设计缺陷:插件安装、启用、停用状态同时存储在 conf.json 静态配置与数据库 bbs_plugin 表两处,两套状态互相干扰,引发批量线上异常。
原版逻辑:数据库无插件记录时,直接读 conf.json 的 enable/installed 字段同步写入数据库作为初始状态。该逻辑在单机本地部署勉强可用,但在插件分发、团队协作、环境迁移、重装卸载等场景全面失效,衍生 4 大类高频故障:
1.1 插件包分发污染
开发者本地测试开启插件后,打包 zip 时 conf.json 留存 enable:1。其他用户下载部署时数据库无记录,代码直接读取配置文件状态写入数据库,未执行安装流程的插件被标记为已启用,提前加载 hook、overwrite,引发模板报错、功能冲突、页面 500。
1.2 Git 团队协作状态错乱
团队将插件目录提交代码仓库,conf.json 随代码同步。A 开发者卸载插件仅改数据库,配置文件 enable:1 未动;B 开发者拉取代码后本地数据库无记录,配置文件的启用状态重新生效,两端后台显示状态完全不符,团队环境无法统一。
1.3 数据库记录丢失后状态自动回退
bbs_plugin 表数据会因数据库重置、站点重装、误删记录、数据库故障等场景丢失。原版无兜底隔离机制,库内记录消失后代码重新读 conf.json 的启用标识,已卸载停用的插件自动变回"已启用、已安装",废弃插件重新加载,拖慢站点、产生未知兼容性 Bug。
1.4 状态修改逻辑割裂
所有 install/unstall/enable/disable 操作仅更新数据库,不同步修改 conf.json。配置文件状态与数据库长期割裂,只要数据库记录消失,旧的静态状态就会覆盖当前业务状态,属于永久性底层隐患,只能临时修复数据,无法彻底杜绝复发。
1.5 XiunoX 重构方案
2026-07-23 XiunoX 完成插件状态底层重构,确立数据库为插件状态唯一权威源,从根源解决原版遗留的状态错乱问题:
plugin_init()else 分支:db 无记录时默认installed=0/enable=0,不再用 conf.json 平移plugin_paths_enabled():区分 db 不可用(异常回退 conf.json)和 db 无记录(默认 0)conf.json 的
installed/enable字段从代码层彻底剥离,存量字段不强制删除但不再读取
2. 设计原则:db 为唯一权威
Xiuno 的插件启用/停用状态以数据库 bbs_plugin 表为唯一权威来源,conf.json 是静态清单文件,不参与运行时状态判断。
字段 | 存储位置 | 作用 |
|---|---|---|
| db | 0=未安装,1=已安装(执行过 install.php) |
| db | 0=已停用,1=已启用(hook/overwrite 生效) |
| db | 已安装版本号(与 conf.json.version 对比判断是否需升级) |
1.1 为什么 conf.json 不能存状态
conf.json 是插件包发布时携带的静态清单,会被打包进 zip 分发给其他用户。若 conf.json 包含 enable:1:
新用户首次部署:db 无该插件记录,若代码信任 conf.json,会把
enable=1平移到 db,导致未安装的插件自动"已启用"卸载后 db 记录丢失:conf.json 的
enable=1重新生效,插件状态回退到"已启用"跨环境同步代码:开发者 A 卸载某插件(仅改 db),git push 后开发者 B pull,B 的 db 无记录,conf.json 的
enable=1让 B 误以为插件已启用
因此:installed/enable 必须只存在于 db,conf.json 的这两个字段被代码层忽略。
3. 状态读取流程
3.1 plugin_init() — 后台/管理场景
plugin_init() 在每次进入 admin/route/plugin.php 时调用,负责加载本地所有插件并合并 db 状态:
// 简化逻辑
foreach ($plugin_paths as $path) {
$dir = basename($path);
$plugins[$dir] = json_decode(file_get_contents($path.'/conf.json'), true);
}
$db_list = plugin_db_get_all(); // 一次性读全表,以 dir 为 key
foreach ($plugins as $dir => $unused) {
if (isset($db_list[$dir])) {
// ✅ db 有记录:权威覆盖
$plugins[$dir]['installed'] = (int)$db_list[$dir]['installed'];
$plugins[$dir]['enable'] = (int)$db_list[$dir]['enable'];
} else {
// ⚠️ db 无记录:默认未安装未启用(忽略 conf.json 的 installed/enable)
$plugins[$dir]['installed'] = 0;
$plugins[$dir]['enable'] = 0;
plugin_db_init($dir, $plugins[$dir]); // 创建空记录
}
}
关键点:db 无记录时不读 conf.json 的 installed/enable,直接置 0,避免被污染的 conf.json 把状态带偏。
3.2 plugin_paths_enabled() — 前台/编译场景
前台每次请求编译模板时,plugin_compile_srcfile_callback 调用 plugin_paths_enabled() 获取"已启用插件列表",用于决定哪些 hook/overwrite 要被合并到编译产物。
function plugin_paths_enabled() {
$db_list = plugin_db_get_all();
$db_available = TRUE;
try {
$db_list = plugin_db_get_all();
} catch (\Throwable $e) {
$db_list = array();
$db_available = FALSE; // db 异常(install 阶段/数据库故障)
}
foreach ($plugin_paths as $path) {
$dir = basename($path);
if (isset($db_list[$dir])) {
// db 权威
$enable = !empty($db_list[$dir]['enable']);
$installed = !empty($db_list[$dir]['installed']);
} elseif (!$db_available) {
// db 不可用:回退 conf.json(仅 install 阶段/数据库故障时兜底)
$enable = !empty($pconf['enable']);
$installed = !empty($pconf['installed']);
} else {
// db 可用但无记录:默认未启用(忽略 conf.json)
$enable = FALSE;
$installed = FALSE;
}
if (!$enable || !$installed) continue;
$return_paths[$path] = $pconf;
}
}
三种分支含义:
分支 | 条件 | 行为 |
|---|---|---|
db 权威 | db 可用且有记录 | 读 db 的 enable/installed |
db 不可用 | db 抛异常(install 阶段、表不存在) | 回退 conf.json,避免 install 阶段无法加载插件 |
db 无记录 | db 可用但无该插件行 | 默认未启用(忽略 conf.json) |
4. 状态变更 API
所有状态变更操作只改 db,不读写 conf.json 的 installed/enable 字段。
函数 | db 变更 | 副作用 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
4.1 plugin_clear_tmp_dir() 的作用
清空 tmp/ 编译缓存 + 整站数据缓存 + OPcache,确保新启停的插件状态在所有缓存层即时生效:
数据缓存:Redis/Memcached 驱动下
tmp/cache不在文件系统,需通过CacheService::clearByType(['data'])清理OPcache:
validate_timestamps=1+revalidate_freq较大或多 worker 时,旧字节码不会自动重载 → 新启用的插件 hook/Service 类不生效 → 500。必须显式opcache_reset()
5. 后台路由入口
admin/route/plugin.php 提供以下 action,全部需 POST + CSRF 校验:
action | 路由 | 作用 |
|---|---|---|
|
| 安装插件(执行 install.php + 写 db) |
|
| 卸载插件(执行 uninstall.php + 写 db) |
|
| 启用插件 |
|
| 停用插件 |
|
| 升级插件(执行 upgrade.php + 更新 db.version) |
|
| 上传 zip 安装/升级 |
5.1 安装流程
POST ?plugin-install-{dir}.htm
↓
PluginScanner 预扫描(force_blocked 不可被 force 跳过)
↓
plugin_check_dependency($dir, 'install') // 依赖检查
↓
plugin_install($dir) // 写 db: installed=1, enable=1, version
↓
include install.php // 建表、写默认 setting
↓
plugin_clear_tmp_dir() // 清缓存
↓
admin_log_create('plugin_install', ...) // 记日志
↓
message(0, '安装成功', redirect_url)
5.2 卸载流程
POST ?plugin-unstall-{dir}.htm
↓
plugin_check_dependency($dir, 'unstall') // 反向依赖检查(被依赖则不能卸载)
↓
plugin_unstall($dir) // 写 db: installed=0, enable=0
↓
include uninstall.php // 删表、删 setting
↓
plugin_clear_tmp_dir()
↓
admin_log_create('plugin_uninstall', ...)
5.3 上传升级流程(状态保持)
上传 zip 升级已有插件时,需保持原启用状态:
1. 记录原状态 $wasEnabled = !empty($plugins[$dir]['enable'])
2. rename 旧版本到 plugin/{dir}.bak/
3. rmove_dir 新版本到 plugin/{dir}/
4. if ($wasEnabled) plugin_disable($dir) // 升级前禁用,避免 upgrade.php 冲突
5. plugin_install($dir) // 写 db(不执行 install.php)
6. include upgrade.php // 执行数据库迁移
7. if 升级失败: 回滚 bak + 保持禁用
8. if 升级成功: 删 bak + if ($wasEnabled) plugin_enable($dir)
6. 常见问题与排查
6.1 已卸载的插件仍显示"已启用"
根因:旧版本代码在 db 无记录时用 conf.json 的 enable=1 平移到 db,导致:
新部署的插件自动"已启用"
卸载后 db 记录因任何原因丢失,conf.json 的
enable=1重新生效
修复方案(2026-07-23 已实施):
plugin_init()else 分支:db 无记录时默认installed=0/enable=0plugin_paths_enabled():区分 db 不可用(回退 conf.json)和 db 无记录(默认 0)
现有污染数据修复:
方案 A(推荐):在后台插件列表手动点"卸载"按钮,db 记录会被写为
installed=0/enable=0方案 B(批量):直接 SQL
UPDATE bbs_plugin SET enable=0, installed=0 WHERE dir IN ('xnx_oauth', 'xnx_webp', ...),然后清缓存
6.2 启用插件后前台不生效
可能原因:OPcache 未清理,旧字节码未重载。
排查:
# 检查 OPcache 配置
php -r 'echo ini_get("opcache.validate_timestamps");'
php -r 'echo ini_get("opcache.revalidate_freq");'
修复:plugin_clear_tmp_dir() 已调用 CacheService::clearByType(['data', 'opcache'])。若仍不生效,手动访问后台「系统工具 → 清缓存」。
6.3 升级失败后插件状态异常
升级失败会自动回滚,但状态被设为禁用(不恢复启用),需用户手动确认后启用。这是设计意图:让用户检查回滚后的版本能否正常工作,再决定是否启用。
6.4 依赖检查失败
安装时:
plugin_dependencies()检查conf.json.dependencies声明的依赖插件是否已启用,未启用则阻止安装卸载时:
plugin_by_dependencies()反向检查,若被其他已启用插件依赖则阻止卸载
依赖版本约束支持 npm 风格:>=1.0.2、^1.0.2、~1.0.2、*(任意)。
7. 开发规范
7.1 conf.json 字段约定
{
"name": "插件名",
"version": "1.0.0",
"bbs_version": "1.0",
"brief": "插件简介",
"icon": "icon.png",
"type": "0",
"have_setting": 1,
"dependencies": {},
"hooks_rank": {},
"overwrites_rank": {}
}
禁止包含 installed 和 enable 字段。这两个字段是运行时状态,只该存在于 db。历史插件 conf.json 中的这两个字段代码层已忽略,但建议在下次发布插件包时删除。
7.2 install.php / uninstall.php / upgrade.php 约定
install.php:首次安装时执行,建表 + 写默认 setting。只在plugin_install后调用一次upgrade.php:升级时执行,数据库结构变更(ALTER TABLE)走此机制。幂等(可重复执行),变更后递增conf.json.versionuninstall.php:卸载时执行,删表 +setting_delete('插件名')。统一用此名(旧拼写unstall.php向后兼容)
7.3 缓存清理
任何修改 model/*.func.php、view/htm/*.htm、route/*.php 后,必须清理 tmp/ 下对应的编译缓存(_include() 不比较源文件 mtime):
rm -f tmp/route_*.php tmp/model_*.func.php tmp/view_htm_*.htm
插件启停操作通过 plugin_clear_tmp_dir() 自动清理,无需手动处理。
