【开发者必读】XIUNOX 插件启用/停用状态管理

管理

关键源码:model/plugin.func.phpplugin_initplugin_paths_enabledplugin_installplugin_unstallplugin_enableplugin_disable)、admin/route/plugin.php


1. 原版设计缺陷与重构背景

Xiuno 原版存在插件状态双数据源冲突的底层设计缺陷:插件安装、启用、停用状态同时存储在 conf.json 静态配置与数据库 bbs_plugin 表两处,两套状态互相干扰,引发批量线上异常。

原版逻辑:数据库无插件记录时,直接读 conf.jsonenable/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 是静态清单文件,不参与运行时状态判断

字段

存储位置

作用

installed

db bbs_plugin.installed

0=未安装,1=已安装(执行过 install.php)

enable

db bbs_plugin.enable

0=已停用,1=已启用(hook/overwrite 生效)

version

db bbs_plugin.version

已安装版本号(与 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 变更

副作用

plugin_install($dir)

installed=1, enable=1, version=conf.json.version

plugin_clear_tmp_dir()

plugin_unstall($dir)

installed=0, enable=0

plugin_clear_tmp_dir()

plugin_enable($dir)

enable=1

plugin_clear_tmp_dir()

plugin_disable($dir)

enable=0

plugin_clear_tmp_dir()

4.1 plugin_clear_tmp_dir() 的作用

清空 tmp/ 编译缓存 + 整站数据缓存 + OPcache,确保新启停的插件状态在所有缓存层即时生效:

  • 数据缓存:Redis/Memcached 驱动下 tmp/cache 不在文件系统,需通过 CacheService::clearByType(['data']) 清理

  • OPcachevalidate_timestamps=1 + revalidate_freq 较大或多 worker 时,旧字节码不会自动重载 → 新启用的插件 hook/Service 类不生效 → 500。必须显式 opcache_reset()


5. 后台路由入口

admin/route/plugin.php 提供以下 action,全部需 POST + CSRF 校验:

action

路由

作用

install

?plugin-install-{dir}.htm

安装插件(执行 install.php + 写 db)

unstall

?plugin-unstall-{dir}.htm

卸载插件(执行 uninstall.php + 写 db)

enable

?plugin-enable-{dir}.htm

启用插件

disable

?plugin-disable-{dir}.htm

停用插件

upgrade

?plugin-upgrade-{dir}.htm

升级插件(执行 upgrade.php + 更新 db.version)

upload

?plugin-upload.htm

上传 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=0

  • plugin_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": {}
}

禁止包含 installedenable 字段。这两个字段是运行时状态,只该存在于 db。历史插件 conf.json 中的这两个字段代码层已忽略,但建议在下次发布插件包时删除。

7.2 install.php / uninstall.php / upgrade.php 约定

  • install.php:首次安装时执行,建表 + 写默认 setting。只在 plugin_install 后调用一次

  • upgrade.php:升级时执行,数据库结构变更(ALTER TABLE)走此机制。幂等(可重复执行),变更后递增 conf.json.version

  • uninstall.php:卸载时执行,删表 + setting_delete('插件名')。统一用此名(旧拼写 unstall.php 向后兼容)

7.3 缓存清理

任何修改 model/*.func.phpview/htm/*.htmroute/*.php 后,必须清理 tmp/ 下对应的编译缓存(_include() 不比较源文件 mtime):

rm -f tmp/route_*.php tmp/model_*.func.php tmp/view_htm_*.htm

插件启停操作通过 plugin_clear_tmp_dir() 自动清理,无需手动处理。

最新回复

请先登录后再回复 登录

uid:1 管理
关注
随遇而安,随缘而行
发帖 126
评论 447
粉丝 13
关注 1
发新帖
目录
【开发者必读】XIUNOX 插件启用/停用状态管理
此广告位招租
此广告位招租
此广告位招租
广告招租
广告招租
广告招租
广告招租
广告招租
广告招租