系统插件开发文档
1. 插件用途
系统插件用于扩展站点功能
开发完将插件打包为 ZIP 后,在后台「插件管理」上传安装。,配置完成后再启用。
系统插件类型固定为:
"type_id": 2
开发环境使用 PHP 8.1。通过后台上传插件,需要服务器启用 ZipArchive 扩展。
2. 插件目录
以 DemoPlugin 为例:
DemoPlugin/
├── plugin.json
├── DemoPlugin_plugin.php
└── assets/
├── demo.css
└── demo.js
安装后,插件存放在:
includes/plugins/DemoPlugin/
命名要求:
DemoPlugin
OrderNotice
UserTools
3. plugin.json
{
"name": "DemoPlugin",
"type_id": 2,
"version": 1001,
"entry": "DemoPlugin_plugin.php"
}
| 字段 | 说明 |
|---|---|
name | 必填,插件唯一标识 |
type_id | 必填,系统插件填写 2 |
version | 建议填写正整数,并与入口文件中的版本号保持一致 |
entry | 入口文件名,省略时按 name_plugin.php 查找 |
plugin.json 必须使用标准 JSON,不能写注释或末尾多余的逗号。
插件的菜单、配置项、路由、钩子和资源声明,写在入口类的 $info 中。只在 plugin.json 中添加这些字段,不会自动生效。
4. 插件入口与基本信息
DemoPlugin_plugin.php:
<?php
namespace plugins\DemoPlugin;
class DemoPlugin_plugin
{
public static $info = [
'type_id' => 2,
'name' => 'DemoPlugin',
'plugname' => '示例插件',
'showname' => '演示系统插件的基本用法',
'author' => 'SpiuNet',
'version' => 1001,
'link' => 'https://auth.spiunet.com',
'inputs' => [],
'menus' => [],
'hooks' => [],
'assets' => [],
'routes' => []
];
}
| 字段 | 说明 |
|---|---|
type_id | 系统插件填写 2,与安装清单一致 |
name | 插件标识,与安装清单一致 |
plugname | 后台显示的插件名称,不能为空,最多 30 个字符 |
showname | 插件描述,最多 60 个字符 |
author | 作者名称,最多 60 个字符 |
version | 正整数版本号 |
link | 作者官网,可留空;填写时必须是有效的 HTTP 或 HTTPS 地址 |
inputs | 后台配置项 |
menus | 插件页面及菜单 |
hooks | 系统钩子 |
assets | 前端 CSS、JavaScript |
routes | 插件接口路由 |
安装时优先采用 $info['version'],没有填写才读取安装清单中的版本。因此两个位置的版本号应同步维护。
入口文件只声明命名空间和类,业务代码放到对应方法中。不要在类外输出内容、处理订单或修改数据,也不要在构造方法中执行这些操作。
系统在安装校验、读取插件信息等场景下也可能加载入口文件,不能把「文件被加载」当作「用户正在使用插件」。
下文中的 inputs、menus 等片段,均放入这个 $info 数组;方法示例放入插件类中。
5. 添加页面和菜单
插件页面需要同时具备:
menus中的页面声明。- 对应的页面处理方法。
仅添加一个 user() 方法,不会自动生成用户中心页面。
5.1 声明菜单
下面在三个中心分别添加一个 demotools 页面:
'menus' => [
[
'id' => 'demoadmin',
'title' => '示例功能',
'target' => 'admin',
'page' => 'demotools',
'icon' => 'layui-icon layui-icon-app'
],
[
'id' => 'demouser',
'title' => '示例功能',
'target' => 'user',
'page' => 'demotools',
'icon' => 'layui-icon layui-icon-app'
],
[
'id' => 'demodeveloper',
'title' => '示例功能',
'target' => 'developer',
'page' => 'demotools',
'icon' => 'layui-icon layui-icon-app'
]
]
| 字段 | 说明 |
|---|---|
id | 菜单标识,建议在插件内保持唯一,使用字母、数字、下划线或短横线 |
title | 菜单名称,不能为空 |
target | 页面所在中心 |
page | 页面标识,对应访问地址中的 mod |
icon | 菜单图标,可使用系统现有的 Layui 图标类 |
href | 可选,自定义站内跳转地址;通常不需要填写 |
不填写 href 时,系统根据 page 自动生成链接。
href 只改变菜单点击后的去向,不会代替 page 注册,也不会取消页面重名检查。
5.2 页面所在中心
target | 处理方法 | 示例地址 |
|---|---|---|
admin | admin($page, $context) | https://auth.spiunet.com/Houtai/?mod=demotools |
user | user($page, $context) | https://auth.spiunet.com/user/?mod=demotools |
developer | developer($page, $context) | https://auth.spiunet.com/Kaifa/?mod=demotools |
kaifazhe 是 developer 的兼容写法,新插件统一使用 developer。
当前首页没有接入上述插件页面分发。不能通过声明 target=index 自动添加 /?mod=demotools 页面。首页的前端扩展使用后文的 assets 资源声明。
5.3 页面方法
后台页面:
public function admin($page, $context)
{
if ($page !== 'demotools') {
return '<div class="layui-card"><div class="layui-card-body">页面不存在</div></div>';
}
return '<div class="layui-card"><div class="layui-card-body">后台插件页面</div></div>';
}
用户中心页面:
public function user($page, $context)
{
if ($page !== 'demotools') {
return '<div class="layui-card"><div class="layui-card-body">页面不存在</div></div>';
}
$username = htmlspecialchars(
(string)($context['user']['user'] ?? ''),
ENT_QUOTES,
'UTF-8'
);
return '<div class="layui-card"><div class="layui-card-body">您好,'.$username.'</div></div>';
}
开发者中心页面:
public function developer($page, $context)
{
if ($page !== 'demotools') {
return '<div class="layui-card"><div class="layui-card-body">页面不存在</div></div>';
}
$username = htmlspecialchars(
(string)($context['user']['user'] ?? ''),
ENT_QUOTES,
'UTF-8'
);
return '<div class="layui-card"><div class="layui-card-body">开发者:'.$username.'</div></div>';
}
系统会加载对应中心的页面头部和底部。方法返回页面内容即可,不需要重复输出完整的 html、head、body,也不需要重复加载系统公共模板。
这些页面分别沿用后台、用户中心和开发者中心的登录检查。
5.4 页面标识与冲突
page 的规则:
- 字母开头。
- 只能包含字母、数字、下划线、短横线。
- 最长 40 个字符。
- 注册和匹配时会转换为小写,建议直接使用小写。
建议使用带有插件特点的名称:
demotools
demo_orders
demo_settings
避免使用 view、login、plugin 等系统已有页面名称。
系统会检查同一 target + page 是否重复,包括:
- 同一个插件内部重复声明。
- 与其他已安装插件重复,其他插件即使处于停用状态也会参与检查。
不同中心可以使用同一个 page:
admin + demotools
user + demotools
developer + demotools
当前重名检查针对插件之间的注册,不代表系统会自动检查所有原生页面。尤其在用户中心,已有模板页面通常会优先加载。
6. 页面与接口的上下文
系统调用插件方法时,会传入 $context。
页面方法与接口路由的上下文并不完全相同。
| 字段 | 页面方法 | 接口路由 |
|---|---|---|
scope | 当前中心,如 user | 路由声明的 access |
plugin | 当前插件的数据库记录 | 当前插件的数据库记录 |
config | 已保存的插件配置 | 已保存的插件配置 |
user | 用户、开发者页面提供;后台页面不提供 | user、developer 路由提供对应账号;其他为 null |
request | 不提供这个字段 | 包含 get、post、files |
读取当前用户:
$uid = (int)($context['user']['uid'] ?? 0);
$username = (string)($context['user']['user'] ?? '');
开发者页面使用相同写法,但其中的数据来自当前开发者账号。
不要把整个用户记录直接返回给浏览器,只返回业务需要的字段。
接口路由中的 scope 不是自动识别出的登录身份。例如,路由声明 access=public,它的 scope 就是 public,即使访问者已登录,$context['user'] 仍然是 null。
7. 后台配置项
通过 $info['inputs'] 声明配置项,系统会在插件管理中生成配置表单。
7.1 支持的类型
| 类型 | 用途 | 保存后的值 |
|---|---|---|
input | 普通输入框 | 字符串 |
password | 密码输入框 | 字符串 |
number | 数字输入框 | 字符串,使用时自行转换 |
textarea | 多行输入框 | 字符串 |
select | 下拉选择 | 选项值字符串 |
checkbox | 多选框 | 字符串数组 |
switch | 滑动开关 | '1' 或 '0' |
配置键必须字母开头,只能包含字母、数字、下划线,长度为 1~32 位。
不要使用 id 作为配置键,它与配置表单中的插件记录 ID 重名。
7.2 配置示例
'inputs' => [
'api_url' => [
'name' => '接口地址',
'type' => 'input',
'default' => 'https://auth.spiunet.com',
'note' => '请输入接口地址',
'required' => true
],
'api_key' => [
'name' => '接口密钥',
'type' => 'password',
'default' => ''
],
'limit' => [
'name' => '数量限制',
'type' => 'number',
'default' => '10'
],
'message' => [
'name' => '页面提示',
'type' => 'textarea',
'default' => '欢迎使用',
'note' => '请输入页面提示内容'
],
'method' => [
'name' => '请求方式',
'type' => 'select',
'default' => 'GET',
'options' => [
'GET' => 'GET 请求',
'POST' => 'POST 请求'
]
],
'types' => [
'name' => '适用账号',
'type' => 'checkbox',
'default' => ['user'],
'options' => [
'user' => '用户',
'developer' => '开发者'
]
],
'enabled' => [
'name' => '启用功能',
'type' => 'switch',
'default' => '1'
]
]
| 配置属性 | 说明 |
|---|---|
name | 表单显示名称 |
type | 配置类型,省略时按普通输入框处理 |
default | 配置表单的默认显示值 |
note | 输入提示,输入框、多行文本等使用 |
required | 保存时检查是否为空 |
options | 下拉框、多选框的选项,键是保存值,值是显示名称 |
number 只提供数字输入框,不代表系统已经完成业务范围校验。插件使用数量、金额等配置时,仍需检查数值是否合法。
7.3 读取配置
页面和接口方法中:
$config = $context['config'] ?? [];
$enabled = (string)($config['enabled'] ?? '1');
$limit = (int)($config['limit'] ?? 10);
$message = (string)($config['message'] ?? '欢迎使用');
$types = $config['types'] ?? ['user'];
其他位置,例如钩子方法中:
$config = \lib\Plugin::getSettings(self::$info['name']);
$enabled = (string)($config['enabled'] ?? '1');
default 不会自动合并进运行时配置。插件刚安装时,保存的配置为空,因此应在代码中提供相同的默认值,或者要求站长先保存配置再使用。
不要使用 empty() 判断开关是否需要默认值,否则已保存的 '0' 容易被当作未配置。
后台插件列表的「启用状态」与插件自定义的 enabled 配置是两回事。自定义开关是否生效,需要插件业务代码自行判断。
8. 插件接口路由
插件接口统一通过以下入口访问:
https://auth.spiunet.com/plugin.php?name=DemoPlugin&action=status
其中:
name是插件标识。action是$info['routes']中的路由名称。
8.1 声明路由
'routes' => [
'status' => [
'method' => 'status',
'access' => 'public',
'request' => ['GET']
],
'profile' => [
'method' => 'profile',
'access' => 'user',
'request' => ['GET']
],
'preview' => [
'method' => 'preview',
'access' => 'user',
'request' => ['POST']
]
]
| 字段 | 说明 |
|---|---|
| 路由名称 | 对应 URL 中的 action |
method | 插件类中要执行的公共方法 |
access | 登录要求,省略时为 public |
request | 允许的 HTTP 请求方法数组,省略时为 GET、POST |
路由名称要求字母开头,只能包含字母、数字、下划线,最长 40 位。路由名称应按声明时的大小写访问。
asset 是系统资源入口使用的名称,不要将它注册为业务路由。
路由处理方法应使用 public,方法名只能包含字母、数字、下划线,以字母或下划线开头,最长 40 位。
8.2 登录要求
access | 访问要求 |
|---|---|
public | 无需登录 |
user | 需要用户中心登录状态 |
developer | 需要开发者中心登录状态 |
admin | 需要后台登录状态 |
public 仅表示无需账号登录,插件仍通过系统公共入口运行。
登录检查只确定访问者是否登录。查询或修改用户数据时,还需要按当前用户 ID 限定数据范围。
8.3 GET 接口
public function status($context)
{
return [
'code' => 0,
'msg' => '插件运行正常',
'data' => [
'version' => self::$info['version']
]
];
}
访问:
https://auth.spiunet.com/plugin.php?name=DemoPlugin&action=status
返回:
{
"code": 0,
"msg": "插件运行正常",
"data": {
"version": 1001
}
}
方法返回数组时,系统自动编码为 JSON。
本文用 code=0 表示业务成功,这属于示例约定,系统不会强制插件采用某一种业务返回结构。
读取 URL 参数:
$get = $context['request']['get'];
$idValue = $get['id'] ?? '';
$id = is_scalar($idValue) ? (int)$idValue : 0;
8.4 获取当前用户
public function profile($context)
{
$user = $context['user'];
return [
'code' => 0,
'data' => [
'uid' => (int)$user['uid'],
'username' => (string)$user['user']
]
];
}
这个方法对应前面声明的 access=user 路由。
访问:
https://auth.spiunet.com/plugin.php?name=DemoPlugin&action=profile
8.5 POST 接口
下面的示例读取文本并返回预览结果,不修改数据库。
public function preview($context)
{
$post = $context['request']['post'];
$value = $post['text'] ?? null;
if (!is_string($value) || trim($value) === '') {
return [
'code' => 1,
'msg' => '请输入内容'
];
}
return [
'code' => 0,
'msg' => '预览成功',
'data' => [
'text' => trim($value)
]
];
}
在同一站点、已登录的用户页面中调用:
async function previewText(text) {
const response = await fetch(
'/plugin.php?name=DemoPlugin&action=preview',
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({ text })
}
);
if (!response.ok) {
throw new Error('请求失败,HTTP ' + response.status);
}
return response.json();
}
非 public 路由使用非 GET 请求时,系统会检查 Referer 的主机名是否与当前请求主机一致。
因此,使用外部调试工具时,仅填写接口地址通常不够,还需要相应的登录 Cookie 和符合要求的请求来源。页面设置了不发送 Referer 的策略,也可能导致请求返回 403。
这项来源检查不代替插件自己的业务校验。保存配置、扣费、删除记录等写入操作,应使用 POST,并在插件中验证自己的会话令牌及数据归属。
8.6 表单、文件与 JSON
系统提供:
$get = $context['request']['get'];
$post = $context['request']['post'];
$files = $context['request']['files'];
它们分别对应 PHP 的 $_GET、$_POST、$_FILES。
application/json 请求体不会自动进入 post,需要自行读取:
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body)) {
return [
'code' => 1,
'msg' => '请求内容格式不正确'
];
}
读取上传文件后,插件需要自行检查上传错误、大小和实际文件类型。
8.7 返回文本或 HTML
返回字符串时,系统会直接输出:
public function text($context)
{
header('Content-Type: text/plain; charset=utf-8');
return '请求成功';
}
也可以返回 HTML,但通过 plugin.php 输出的页面不会自动套用后台或用户中心模板,也不会自动注入插件资源。需要模板框架的页面应使用前面的 menus 页面方式。
9. 前端资源
通过 $info['assets'] 声明 CSS 和 JavaScript。
'assets' => [
[
'type' => 'css',
'file' => 'assets/demo.css',
'scope' => ['user'],
'page' => ['demotools']
],
[
'type' => 'js',
'file' => 'assets/demo.js',
'scope' => ['user'],
'page' => ['demotools']
]
]
| 字段 | 说明 |
|---|---|
type | css 或 js |
file | 相对插件目录的资源路径 |
scope | 页面范围 |
page | 该范围内的页面名称 |
资源文件必须真实存在,路径不能包含 ..。建议文件名只使用字母、数字、下划线、短横线和点,扩展名使用小写。
9.1 页面范围
scope | 范围 |
|---|---|
index | 站点前台,包括首页、接口文档等页面 |
user | 用户中心 |
developer | 开发者中心 |
admin | 后台 |
* | 所有支持的范围 |
只在接口文档页面加载:
'scope' => ['index'],
'page' => ['doc']
在用户中心和开发者中心所有页面加载:
'scope' => ['user', 'developer'],
'page' => ['*']
scope=index 表示站点前台范围,不是只表示首页。
page 通常对应 mod 参数;直接访问 PHP 页面时,对应文件名,例如 view.php 对应 view。
9.2 加载方式
系统自动将 CSS 插入页面头部,将 JavaScript 插入页面底部,不需要修改业务模板添加引用。
自动注入适用于正常的 GET HTML 页面。页面需要包含 </body>,CSS 注入还需要 </head>。JSON 接口、plugin.php 路由等不会自动注入。
资源通过统一地址访问,例如:
https://auth.spiunet.com/plugin.php?name=DemoPlugin&action=asset&file=assets%2Fdemo.css&v=1001
资源入口只提供插件已经声明的 CSS、JavaScript,不提供图片和字体代理。
CSS 通过 plugin.php 加载后,url(images/icon.png) 不会按插件文件夹解析。图片和字体应使用正确的站点绝对路径、完整链接或适合的内嵌资源地址。
不同首页模板不一定加载了相同的前端库。面向多个模板的插件,不要默认所有页面都有 layui 或 jQuery。
9.3 版本与缓存
资源地址中的 v 来自插件版本号,资源入口使用长期缓存。
修改 CSS 或 JavaScript 后,应同步增加:
"version": 1002
以及:
'version' => 1002
否则浏览器可能继续使用旧文件。
10. 系统钩子
钩子用于在已经接入的位置执行插件逻辑。
10.1 当前支持的钩子
| 钩子 | 触发位置 | 上下文字段 |
|---|---|---|
before_login | 用户、开发者的账号登录处理前 | scope、user、ip |
after_login | 对应账号登录成功后 | scope、uid、user、ip |
before_register | 用户注册、开发者申请处理前 | scope、user、qq、ip |
after_register | 用户注册、开发者申请记录创建成功后 | scope、uid、user、ip |
before_api_request | 系统转发接口的请求处理前 | api、params、method、apikey、ip |
after_api_request | 上述转发接口完成处理、输出响应前 | 前面的字段,加上 response |
登录、注册钩子中的 scope 为 user 或 developer。它们不是后台登录、第三方登录等所有登录方式的通用钩子。
开发者的 after_register 表示申请记录已创建,不代表已经审核通过。
两个 API 钩子当前接入的是 api.php 中 act=Api_send 的转发接口流程,不覆盖全部本地接口、在线调试或所有网站请求。流程提前退出时,后置钩子也不会执行。
10.2 声明钩子
'hooks' => [
'before_api_request' => 'beforeApiRequest',
'after_api_request' => 'afterApiRequest',
'after_login' => 'afterLogin'
]
处理方法接收引用参数:
public function beforeApiRequest(&$context)
{
if ((int)($context['api']['id'] ?? 0) !== 3) {
return;
}
$context['params']['source'] = 'DemoPlugin';
}
示例只处理 ID 为 3 的转发接口,实际使用时替换为需要处理的接口。
10.3 哪些修改会生效
| 钩子 | 会被主流程采用的修改 |
|---|---|
before_api_request | params,且必须保持为数组 |
after_api_request | response,应使用字符串 |
| 登录、注册钩子 | 当前不会把修改后的账号、QQ 等上下文字段回写到主流程 |
before_api_request 中的 api、method、apikey、ip 可以读取,但修改它们不会改变对应业务变量。
修改 JSON 响应:
public function afterApiRequest(&$context)
{
if ((int)($context['api']['id'] ?? 0) !== 3) {
return;
}
$response = $context['response'] ?? null;
if (!is_string($response)) {
return;
}
$data = json_decode($response, true);
if (!is_array($data)) {
return;
}
$data['source'] = 'DemoPlugin';
$json = json_encode(
$data,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if ($json !== false) {
$context['response'] = $json;
}
}
非 JSON 响应不要按这个方式处理。
后置钩子修改的是最终输出内容,此时主流程可能已经完成计费、统计等操作。修改响应不等于撤销这些业务操作。
10.4 登录钩子
public function afterLogin(&$context)
{
if (($context['scope'] ?? '') !== 'user') {
return;
}
$uid = (int)($context['uid'] ?? 0);
$username = (string)($context['user'] ?? '');
error_log('[DemoPlugin] user login: '.$uid);
}
钩子上下文不会自动附带页面方法中的 config、plugin 等字段。需要配置时自行读取:
$config = \lib\Plugin::getSettings(self::$info['name']);
10.5 执行规则
多个插件的钩子按插件记录 ID 升序执行。同一次调用中,后面的插件能看到前面插件对上下文的修改。
系统会分别创建插件实例,不要依赖某个对象属性在前置、后置钩子之间保留。
当前钩子没有统一的「返回 false 阻止操作」约定:
- 返回值不会自动改变主业务结果。
- 钩子方法中直接
echo的内容会被丢弃。 - 抛出异常不会自动形成业务拒绝,系统会捕获钩子异常。
需要修改响应时使用规定的上下文字段,需要排查异常时自行记录必要的错误信息。
11. 数据库使用
插件可以使用系统数据库对象:
global $DB;
11.1 表名前缀
原生 SQL 使用 pre_ 作为前缀占位符,系统会替换为当前站点的实际前缀:
$row = $DB->getRow(
"SELECT * FROM pre_demo WHERE id=:id LIMIT 1",
[':id' => $id]
);
使用封装方法时,不要重复传入 pre_:
$DB->insert('demo', $data);
$DB->update('demo', $data, $where);
$DB->delete('demo', $where);
对应关系:
| 写法 | 表名参数 |
|---|---|
| 手写 SQL | pre_demo |
insert()、update()、delete() 等封装方法 | demo |
11.2 查询与写入
以下示例假设插件已有 pre_demo 表,包含 id、uid、content、date 字段,并在 access=user 的处理方法中使用。
读取当前用户的一条记录:
global $DB;
$uid = (int)$context['user']['uid'];
$row = $DB->getRow(
"SELECT * FROM pre_demo WHERE id=:id AND uid=:uid LIMIT 1",
[
':id' => $id,
':uid' => $uid
]
);
读取列表:
$list = $DB->getAll(
"SELECT id,content,date FROM pre_demo WHERE uid=:uid ORDER BY id DESC LIMIT 20",
[':uid' => $uid]
);
统计数量:
$count = $DB->getColumn(
"SELECT COUNT(*) FROM pre_demo WHERE uid=:uid",
[':uid' => $uid]
);
新增记录:
$id = $DB->insert('demo', [
'uid' => $uid,
'content' => $content,
'date' => date('Y-m-d H:i:s')
]);
if ($id === false) {
return ['code' => 1, 'msg' => '保存失败'];
}
修改记录:
$result = $DB->update(
'demo',
['content' => $content],
[
'id' => $id,
'uid' => $uid
]
);
if ($result === false) {
return ['code' => 1, 'msg' => '保存失败'];
}
删除记录:
$result = $DB->delete('demo', [
'id' => $id,
'uid' => $uid
]);
if ($result === false) {
return ['code' => 1, 'msg' => '删除失败'];
}
$id、$content 等请求数据应先完成类型、内容和业务校验。用户归属使用当前登录账号的 UID,不使用客户端自行提交的 UID。
11.3 返回值与事务
| 方法 | 用途 |
|---|---|
getRow() | 查询一条记录,无结果或失败时可能返回 false |
getAll() | 查询多条记录,无记录时为空数组,失败时返回 false |
getColumn() | 查询一个字段值 |
insert() | 插入记录,成功返回新记录 ID,失败返回 false |
update() | 更新记录 |
delete() | 删除记录 |
exec() | 执行 SQL |
query() | 成功时返回 PDOStatement |
beginTransaction() | 开启事务 |
commit() | 提交事务 |
rollBack() | 回滚事务 |
error() | 获取数据库错误信息 |
判断执行失败使用 === false。
update()、delete() 以及带绑定参数的 exec(),不能直接当作受影响行数使用。执行成功也不一定代表存在符合条件的记录。
需要判断影响行数时:
$stmt = $DB->query(
"DELETE FROM pre_demo WHERE id=:id AND uid=:uid",
[
':id' => $id,
':uid' => $uid
]
);
if ($stmt === false) {
return ['code' => 1, 'msg' => '删除失败'];
}
$affected = $stmt->rowCount();
数据库对象默认不会把所有执行失败都转成异常。使用事务时,也要检查每一步的返回值,失败后主动回滚。
11.4 新增表或字段
插件需要新表、新字段时,提供独立的安装或升级 SQL,由站长执行。
例如可以随插件提供:
sql/
├── install.sql
└── update.sql
系统不会自动执行这些文件,也不会因为文件名叫 install.sql 就自动安装表结构。
交付说明应写清楚适用版本、表名前缀替换方法和执行顺序。不要在页面访问、配置读取或普通业务请求中自动建表、修改表结构。
12. 打包与安装
推荐将整个插件目录打包:
DemoPlugin.zip
└── DemoPlugin/
├── plugin.json
├── DemoPlugin_plugin.php
└── assets/
├── demo.css
└── demo.js
系统也支持文件直接位于 ZIP 根目录:
DemoPlugin.zip
├── plugin.json
├── DemoPlugin_plugin.php
└── assets/
├── demo.css
└── demo.js
两种方式任选一种,不要在同一个包里混用,不要包含多个插件。
安装包要求:
| 项目 | 限制 |
|---|---|
| 格式 | ZIP |
| 压缩包大小 | 最大 10MB |
| 解压后总大小 | 最大 30MB |
| ZIP 条目数量 | 2~500 个,包含目录条目 |
| 清单 | 根目录或一层插件目录下的一个 plugin.json |
| 路径 | 不能包含绝对路径、目录穿越、重复路径或链接文件 |
安装包不允许包含:
.htaccess
.user.ini
web.config
.phar
.phtml
.cgi
.pl
.exe
.dll
.so
安装步骤:
- 检查标识、入口文件、命名空间和类名是否一致。
- 检查版本号及基本信息。
- 将插件打包成 ZIP。
- 后台进入「插件管理」,点击「上传插件」。
- 安装成功后打开配置,填写并保存。
- 启用插件。
- 刷新相应中心的主框架,查看新增菜单。
插件页面和业务接口需要插件处于启用状态。没有新增菜单的插件,也可能正常工作,例如只注入前端资源的插件。
13. 更新、停用与卸载
13.1 更新
当前上传入口不支持覆盖安装。上传相同标识的插件,会提示已经安装;仅增加版本号也不会改变这一点。
需要保留配置更新时,可由站长:
- 备份插件目录、配置及相关业务数据。
- 停用插件。
- 按作者说明,将新版文件更新到原插件目录。
- 如有数据库变更,执行对应升级 SQL。
- 检查配置后重新启用。
文件更新后,运行时读取的是新版入口内容;后台列表中保存的名称、版本等记录不会因此自动同步。因此当前不能把手动覆盖文件视为完整的后台升级流程。
通过卸载再安装更新,会丢失原插件配置,应提前备份。
13.2 停用
停用后,插件注册的业务页面、接口、钩子和自动资源加载不再正常提供服务。
入口文件仍可能被管理页面加载以读取信息,因此业务操作始终应放在明确的处理方法中。
13.3 卸载
卸载前需要先停用。
系统卸载会删除:
- 插件目录及其中的文件。
- 插件安装记录和已保存配置。
插件自建表及其中的数据不会自动删除,需要按插件作者的说明另行处理。
当前没有自动调用的 install()、upgrade()、uninstall() 生命周期。仅声明这些方法,系统不会自动执行。
14. 常见问题
| 问题 | 检查位置 |
|---|---|
| 上传提示入口信息不正确 | 标识、文件名、命名空间、类名、type_id、$info['name'] 是否一致 |
| 插件安装后没有菜单 | 是否启用、是否声明 menus、id 和 title 是否填写、是否刷新主框架 |
| 页面方法写了但打不开 | 是否注册相同 target + page,方法中的 $page 是否匹配 |
| 用户中心打开了原生页面 | 插件 page 是否与系统已有页面重名 |
| 配置默认值没有生效 | 是否保存过配置,代码是否提供运行时默认值 |
| 修改开关后没有效果 | 是否在业务方法中读取并判断配置 |
| 插件接口返回 403 | 对应中心是否登录;非公开、非 GET 请求是否携带正确来源 |
| 插件接口返回 404 | 插件是否启用,name、action、处理方法是否存在 |
| 插件接口返回 405 | 请求方法是否在 request 声明中 |
插件接口返回 500 / Plugin Error | 检查方法运行错误及 PHP 错误日志,必要时自行记录插件异常 |
| JSON 请求参数读取不到 | 是否错误地从 request.post 读取 JSON 请求体 |
| 钩子没有触发 | 是否进入已接入钩子的业务流程,声明和方法名是否一致 |
| 钩子修改后没有效果 | 修改字段是否属于主流程实际采用的字段 |
| CSS、JS 更新后还是旧效果 | 是否同步递增插件版本号(可清理缓存) |
| 资源没有加载 | scope、page、资源路径、页面结构及插件状态是否正确 |
| CSS 中的图片打不开 | 相对路径是否因资源代理地址而解析到了错误位置 |
前台提示 layui is not defined | 当前模板是否加载该库,不要假定所有模板使用相同资源 |
| 新版本 ZIP 无法再次上传 | 当前不支持同名插件覆盖安装 |
15. 完整示例
下面的插件会在后台、用户中心和开发者中心各添加一个「示例功能」页面。
页面提示内容可以在后台修改,页面上的按钮会调用插件的 status 接口。示例不需要新建数据表。
15.1 目录结构
DemoPlugin/
├── plugin.json
├── DemoPlugin_plugin.php
└── assets/
├── demo.css
└── demo.js
15.2 plugin.json
{
"name": "DemoPlugin",
"type_id": 2,
"version": 1001,
"entry": "DemoPlugin_plugin.php"
}
15.3 DemoPlugin_plugin.php
<?php
namespace plugins\DemoPlugin;
class DemoPlugin_plugin
{
public static $info = [
'type_id' => 2,
'name' => 'DemoPlugin',
'plugname' => '示例插件',
'showname' => '演示插件页面、配置和接口调用',
'author' => 'SpiuNet',
'version' => 1001,
'link' => 'https://auth.spiunet.com',
'inputs' => [
'message' => [
'name' => '页面提示',
'type' => 'textarea',
'default' => '欢迎使用示例插件',
'required' => true
],
'enabled' => [
'name' => '启用功能',
'type' => 'switch',
'default' => '1'
]
],
'menus' => [
[
'id' => 'demoadmin',
'title' => '示例功能',
'target' => 'admin',
'page' => 'demotools',
'icon' => 'layui-icon layui-icon-app'
],
[
'id' => 'demouser',
'title' => '示例功能',
'target' => 'user',
'page' => 'demotools',
'icon' => 'layui-icon layui-icon-app'
],
[
'id' => 'demodeveloper',
'title' => '示例功能',
'target' => 'developer',
'page' => 'demotools',
'icon' => 'layui-icon layui-icon-app'
]
],
'hooks' => [],
'assets' => [
[
'type' => 'css',
'file' => 'assets/demo.css',
'scope' => ['admin', 'user', 'developer'],
'page' => ['demotools']
],
[
'type' => 'js',
'file' => 'assets/demo.js',
'scope' => ['admin', 'user', 'developer'],
'page' => ['demotools']
]
],
'routes' => [
'status' => [
'method' => 'status',
'access' => 'public',
'request' => ['GET']
]
]
];
public function admin($page, $context)
{
return $this->renderPage($page, $context);
}
public function user($page, $context)
{
return $this->renderPage($page, $context);
}
public function developer($page, $context)
{
return $this->renderPage($page, $context);
}
public function status($context)
{
$config = $context['config'] ?? [];
if ((string)($config['enabled'] ?? '1') !== '1') {
return ['code' => 1, 'msg' => '功能已关闭'];
}
return [
'code' => 0,
'msg' => '插件运行正常',
'data' => [
'name' => self::$info['plugname'],
'version' => self::$info['version']
]
];
}
private function renderPage($page, $context)
{
if ($page !== 'demotools') {
return '<div class="layui-card"><div class="layui-card-body">页面不存在</div></div>';
}
$config = $context['config'] ?? [];
if ((string)($config['enabled'] ?? '1') !== '1') {
return '<div class="layui-card"><div class="layui-card-body">功能已关闭</div></div>';
}
$message = htmlspecialchars(
(string)($config['message'] ?? '欢迎使用示例插件'),
ENT_QUOTES,
'UTF-8'
);
return <<<HTML
<div class="layui-card demo-plugin">
<div class="layui-card-header">示例插件</div>
<div class="layui-card-body">
<p class="demo-message">{$message}</p>
<button type="button" class="pear-btn pear-btn-primary demo-status">
检查插件状态
</button>
<p class="demo-result" aria-live="polite"></p>
</div>
</div>
HTML;
}
}
15.4 assets/demo.css
.demo-plugin .demo-message {
margin-bottom: 16px;
line-height: 1.8;
white-space: pre-wrap;
}
.demo-plugin .demo-result {
margin-top: 12px;
min-height: 22px;
line-height: 1.6;
color: #606266;
}
.demo-plugin .demo-status:disabled {
cursor: wait;
opacity: 0.65;
}
15.5 assets/demo.js
(() => {
document.querySelectorAll('.demo-plugin').forEach((box) => {
const button = box.querySelector('.demo-status');
const result = box.querySelector('.demo-result');
if (!button || !result) {
return;
}
button.addEventListener('click', async () => {
button.disabled = true;
result.textContent = '请求中……';
try {
const response = await fetch(
'/plugin.php?name=DemoPlugin&action=status',
{ credentials: 'same-origin' }
);
if (!response.ok) {
throw new Error('请求失败,HTTP ' + response.status);
}
const data = await response.json();
result.textContent = data.msg || '请求完成';
} catch (error) {
result.textContent = '请求失败,请稍后重试';
} finally {
button.disabled = false;
}
});
});
})();
15.6 安装后访问
将以上四个文件按目录结构打包,上传安装、保存配置并启用。
后台页面:
https://auth.spiunet.com/Houtai/?mod=demotools
用户中心页面:
https://auth.spiunet.com/user/?mod=demotools
开发者中心页面:
https://auth.spiunet.com/Kaifa/?mod=demotools
状态接口:
https://auth.spiunet.com/plugin.php?name=DemoPlugin&action=status
分别使用对应中心的账号登录后,打开「示例功能」,点击「检查插件状态」,页面会显示接口返回的结果。
![[开发文档]系统插件开发文档-Spiu-Cloud](https://dev.spiunet.com/wp-content/uploads/2026/08/3-300x210.png)


![[20260831 V1.0.0 1002]更新日志-Spiu-Cloud](https://dev.spiunet.com/wp-content/uploads/2026/08/4-300x211.png)


暂无评论内容