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 '<ul>';
foreach($errors as $err) {
echo '<li>'.$err.'</li>';
}
echo '</ul>';
exit;
}
这段放在 source/plugin/myplugin/check.php。可抄的检测项大致就这几类:
| 检测项 | 判断方式 | 不通过的后果 |
|---|---|---|
| PHP 版本 | version_compare(PHP_VERSION, '8.0.0', '<') |
代码可能整段报语法错 |
| 必需扩展 | extension_loaded('curl') |
远程调用不可用 |
| 目录可写 | is_writable(DISCUZ_ROOT.'...') |
无法生成配置文件 |
检测项按实际依赖来列,别写一堆无关的检查——站长看到一屏红灯会以为是插件坏了。
