3.2 安装/升级/卸载脚本与 runquery

让插件的表结构跟着版本走,装、升、卸都有脚本兜底。

六个脚本与它们的调用时机

插件 XML 清单里可以声明六个生命周期脚本,文件名任意,写 install_u8.php 也行。

XML 节点 脚本职责 什么时候被调用
installfile 建表、灌初始数据 站长导入插件、点「安装」时
uninstallfile 清数据、删表、注销注册 点「卸载」时
upgradefile 增量改结构 导入新版本插件时
checkfile 环境与权限检测 安装、卸载、升级之前
enablefile 开启时的准备工作 插件被设为「可用」时
disablefile 关闭时的收尾 插件被设为「停用」时

后两个是 Discuz! X3.1 新增的。价值体现在「停用/启用」有实际动作要做的插件上——比如停用时要停掉定时任务、清理临时文件,只靠删表不够。

声明方式是在 XML 里对应位置写:

    <item id="installfile"><![CDATA[install.php]]></item>
    <item id="uninstallfile"><![CDATA[uninstall.php]]></item>
    <item id="upgradefile"><![CDATA[upgrade.php]]></item>
    <item id="checkfile"><![CDATA[check.php]]></item>
    <item id="enablefile"><![CDATA[enable.php]]></item>
    <item id="disablefile"><![CDATA[disable.php]]></item>

插件当前版本号写在 XML 的 plugin 分支里,它同时也是兼容性声明——兼容多版本用逗号分隔,如 X3,X3.5,X5.0。X5.0 不再兼容 X3.5 及更早版本,标记兼容前务必确认代码在 PHP 8.0+ 下真能跑。

runquery() 与 cdb_ 前缀

安装、升级、开启、关闭脚本里用 runquery() 执行 SQL。关键便利是:表名可以直接写 cdb_ 前缀,系统会自动替换成实际前缀。站长装的时候前缀可能是 discuz_,你的脚本不用改。

runquery("CREATE TABLE cdb_myplugin_log (
    logid mediumint(8) unsigned NOT NULL auto_increment,
    uid mediumint(8) unsigned NOT NULL default '0',
    action varchar(20) NOT NULL default '',
    dateline int(10) unsigned NOT NULL default '0',
    PRIMARY KEY (logid)
)");

前缀这个约定只在 runquery() 里成立,写进业务代码不能这么省事,具体见 3.3。

$finish = TRUE; 与卸载脚本的变更

安装、升级脚本可以随意设计页面跳转,但结束前必须输出 $finish = TRUE;。它的作用是告诉系统「脚本已收尾,不要再往下走默认流程」,少了这一行页面会继续渲染出不该出现的东西。

<?php

if(!defined('IN_DISCUZ')) {
    exit('Access Denied');
}

$finish = TRUE;

从 Discuz! X3.1 起,卸载脚本不再支持页面跳转,也不再支持 $finish 变量。 所以 uninstall.php 里别写跳转,也别指望 $finish 生效。X2 时代的老卸载脚本通常有一句 $finish = TRUE;,现在它只是行无害的普通赋值。

完整示例:install.php

一个合格的安装脚本按建表 → 建索引 → 灌初始数据三步走。索引依赖表结构,数据依赖索引,顺序反了会报错。

<?php

if(!defined('IN_DISCUZ')) {
    exit('Access Denied');
}

// 1. 建表
runquery("CREATE TABLE cdb_myplugin_log (
    logid mediumint(8) unsigned NOT NULL auto_increment,
    uid mediumint(8) unsigned NOT NULL default '0',
    action varchar(20) NOT NULL default '',
    note varchar(255) NOT NULL default '',
    dateline int(10) unsigned NOT NULL default '0',
    PRIMARY KEY (logid)
)");

// 2. 建索引
runquery("CREATE INDEX idx_uid ON cdb_myplugin_log (uid)");
runquery("CREATE INDEX idx_dateline ON cdb_myplugin_log (dateline)");

// 3. 灌初始数据
runquery("INSERT INTO cdb_myplugin_log (uid, action, note, dateline) VALUES (0, 'install', '插件安装完成', ".TIMESTAMP.")");

$finish = TRUE;

两点说明:表名用 myplugin_log 这种「插件标识 + 表名」的写法,前缀由系统加,避免和别的插件撞名;字段风格沿用官方数据字典,与内核表一致。TIMESTAMP 是 Discuz! 的常量。

配套的 uninstall.php 就简单多了:

<?php

if(!defined('IN_DISCUZ')) {
    exit('Access Denied');
}

runquery("DROP TABLE IF EXISTS cdb_myplugin_log");
runquery("DELETE FROM cdb_common_plugin WHERE identifier='myplugin'");

第二句清理插件自身的注册记录是常备动作。X5.0 里通过插件注册的第三方登录、账号、hook 嵌入点,也要在卸载脚本里对应注销。

完整示例:upgrade.php 做增量升级

升级脚本能用 $fromversion 和 $toversion 两个变量判断从哪个版本升到哪个版本。这是做增量升级的关键——不要每次升级都重跑全量 DDL,那样大表上会锁很久。

<?php

if(!defined('IN_DISCUZ')) {
    exit('Access Denied');
}

// 1.0 -> 1.1:新增 note 字段并建索引
if($fromversion == '1.0' && $toversion == '1.1') {
    runquery("ALTER TABLE cdb_myplugin_log ADD COLUMN note varchar(255) NOT NULL default ''");
    runquery("CREATE INDEX idx_action ON cdb_myplugin_log (action)");
}

// 1.1 -> 2.0:清掉早期版本写入的脏数据
if($fromversion == '1.1' && $toversion == '2.0') {
    runquery("DELETE FROM cdb_myplugin_log WHERE action='' LIMIT 10000");
}

$finish = TRUE;

这段代码按版本逐段判断,每段只做增量动作。别只认 fromversion 的精确值做一长串 else if,站长跳版本安装时容易漏执行。

check.php:安装前的环境检测

checkfile 在安装、卸载、升级之前被调用,拦下注定失败的安装:PHP 版本太低、缺扩展、目录不可写。检测不通过就输出原因,比装到一半白屏强得多。

<?php

if(!defined('IN_DISCUZ')) {
    exit('Access Denied');
}

$errors = array();

// PHP 版本:X5.0 开源版要求 PHP 8.0+
if(version_compare(PHP_VERSION, '8.0.0', '<')) {
    $errors[] = '本插件需要 PHP 8.0 及以上版本,当前为 '.PHP_VERSION;
}

// 必需扩展
if(!extension_loaded('curl')) {
    $errors[] = '缺少 curl 扩展,无法调用远程接口';
}

// 目录可写
if(!is_writable(DISCUZ_ROOT.'source/plugin/myplugin/')) {
    $errors[] = 'source/plugin/myplugin/ 目录不可写';
}

if($errors) {
    echo '&lt;ul&gt;';
    foreach($errors as $err) {
        echo '&lt;li&gt;'.$err.'&lt;/li&gt;';
    }
    echo '&lt;/ul&gt;';
    exit;
}

这段放在 source/plugin/myplugin/check.php。可抄的检测项大致就这几类:

检测项 判断方式 不通过的后果
PHP 版本 version_compare(PHP_VERSION, '8.0.0', '&lt;') 代码可能整段报语法错
必需扩展 extension_loaded('curl') 远程调用不可用
目录可写 is_writable(DISCUZ_ROOT.'...') 无法生成配置文件

检测项按实际依赖来列,别写一堆无关的检查——站长看到一屏红灯会以为是插件坏了。