[开发文档]系统插件开发文档

系统插件开发文档

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'],没有填写才读取安装清单中的版本。因此两个位置的版本号应同步维护。

入口文件只声明命名空间和类,业务代码放到对应方法中。不要在类外输出内容、处理订单或修改数据,也不要在构造方法中执行这些操作。

系统在安装校验、读取插件信息等场景下也可能加载入口文件,不能把「文件被加载」当作「用户正在使用插件」。

下文中的 inputsmenus 等片段,均放入这个 $info 数组;方法示例放入插件类中。

5. 添加页面和菜单

插件页面需要同时具备:

  1. menus 中的页面声明。
  2. 对应的页面处理方法。

仅添加一个 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处理方法示例地址
adminadmin($page, $context)https://auth.spiunet.com/Houtai/?mod=demotools
useruser($page, $context)https://auth.spiunet.com/user/?mod=demotools
developerdeveloper($page, $context)https://auth.spiunet.com/Kaifa/?mod=demotools

kaifazhedeveloper 的兼容写法,新插件统一使用 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>';
}

系统会加载对应中心的页面头部和底部。方法返回页面内容即可,不需要重复输出完整的 htmlheadbody,也不需要重复加载系统公共模板。

这些页面分别沿用后台、用户中心和开发者中心的登录检查。

5.4 页面标识与冲突

page 的规则:

  • 字母开头。
  • 只能包含字母、数字、下划线、短横线。
  • 最长 40 个字符。
  • 注册和匹配时会转换为小写,建议直接使用小写。

建议使用带有插件特点的名称:

demotools
demo_orders
demo_settings

避免使用 viewloginplugin 等系统已有页面名称。

系统会检查同一 target + page 是否重复,包括:

  • 同一个插件内部重复声明。
  • 与其他已安装插件重复,其他插件即使处于停用状态也会参与检查。

不同中心可以使用同一个 page

admin + demotools
user + demotools
developer + demotools

当前重名检查针对插件之间的注册,不代表系统会自动检查所有原生页面。尤其在用户中心,已有模板页面通常会优先加载。

6. 页面与接口的上下文

系统调用插件方法时,会传入 $context

页面方法与接口路由的上下文并不完全相同。

字段页面方法接口路由
scope当前中心,如 user路由声明的 access
plugin当前插件的数据库记录当前插件的数据库记录
config已保存的插件配置已保存的插件配置
user用户、开发者页面提供;后台页面不提供userdeveloper 路由提供对应账号;其他为 null
request不提供这个字段包含 getpostfiles

读取当前用户:

$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 请求方法数组,省略时为 GETPOST

路由名称要求字母开头,只能包含字母、数字、下划线,最长 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']
    ]
]
字段说明
typecssjs
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) 不会按插件文件夹解析。图片和字体应使用正确的站点绝对路径、完整链接或适合的内嵌资源地址。

不同首页模板不一定加载了相同的前端库。面向多个模板的插件,不要默认所有页面都有 layuijQuery

9.3 版本与缓存

资源地址中的 v 来自插件版本号,资源入口使用长期缓存。

修改 CSS 或 JavaScript 后,应同步增加:

"version": 1002

以及:

'version' => 1002

否则浏览器可能继续使用旧文件。

10. 系统钩子

钩子用于在已经接入的位置执行插件逻辑。

10.1 当前支持的钩子

钩子触发位置上下文字段
before_login用户、开发者的账号登录处理前scopeuserip
after_login对应账号登录成功后scopeuiduserip
before_register用户注册、开发者申请处理前scopeuserqqip
after_register用户注册、开发者申请记录创建成功后scopeuiduserip
before_api_request系统转发接口的请求处理前apiparamsmethodapikeyip
after_api_request上述转发接口完成处理、输出响应前前面的字段,加上 response

登录、注册钩子中的 scopeuserdeveloper。它们不是后台登录、第三方登录等所有登录方式的通用钩子。

开发者的 after_register 表示申请记录已创建,不代表已经审核通过。

两个 API 钩子当前接入的是 api.phpact=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_requestparams,且必须保持为数组
after_api_requestresponse,应使用字符串
登录、注册钩子当前不会把修改后的账号、QQ 等上下文字段回写到主流程

before_api_request 中的 apimethodapikeyip 可以读取,但修改它们不会改变对应业务变量。

修改 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);
}

钩子上下文不会自动附带页面方法中的 configplugin 等字段。需要配置时自行读取:

$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);

对应关系:

写法表名参数
手写 SQLpre_demo
insert()update()delete() 等封装方法demo

11.2 查询与写入

以下示例假设插件已有 pre_demo 表,包含 iduidcontentdate 字段,并在 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

安装步骤:

  1. 检查标识、入口文件、命名空间和类名是否一致。
  2. 检查版本号及基本信息。
  3. 将插件打包成 ZIP。
  4. 后台进入「插件管理」,点击「上传插件」。
  5. 安装成功后打开配置,填写并保存。
  6. 启用插件。
  7. 刷新相应中心的主框架,查看新增菜单。

插件页面和业务接口需要插件处于启用状态。没有新增菜单的插件,也可能正常工作,例如只注入前端资源的插件。

13. 更新、停用与卸载

13.1 更新

当前上传入口不支持覆盖安装。上传相同标识的插件,会提示已经安装;仅增加版本号也不会改变这一点。

需要保留配置更新时,可由站长:

  1. 备份插件目录、配置及相关业务数据。
  2. 停用插件。
  3. 按作者说明,将新版文件更新到原插件目录。
  4. 如有数据库变更,执行对应升级 SQL。
  5. 检查配置后重新启用。

文件更新后,运行时读取的是新版入口内容;后台列表中保存的名称、版本等记录不会因此自动同步。因此当前不能把手动覆盖文件视为完整的后台升级流程。

通过卸载再安装更新,会丢失原插件配置,应提前备份。

13.2 停用

停用后,插件注册的业务页面、接口、钩子和自动资源加载不再正常提供服务。

入口文件仍可能被管理页面加载以读取信息,因此业务操作始终应放在明确的处理方法中。

13.3 卸载

卸载前需要先停用。

系统卸载会删除:

  • 插件目录及其中的文件。
  • 插件安装记录和已保存配置。

插件自建表及其中的数据不会自动删除,需要按插件作者的说明另行处理。

当前没有自动调用的 install()upgrade()uninstall() 生命周期。仅声明这些方法,系统不会自动执行。

14. 常见问题

问题检查位置
上传提示入口信息不正确标识、文件名、命名空间、类名、type_id$info['name'] 是否一致
插件安装后没有菜单是否启用、是否声明 menusidtitle 是否填写、是否刷新主框架
页面方法写了但打不开是否注册相同 target + page,方法中的 $page 是否匹配
用户中心打开了原生页面插件 page 是否与系统已有页面重名
配置默认值没有生效是否保存过配置,代码是否提供运行时默认值
修改开关后没有效果是否在业务方法中读取并判断配置
插件接口返回 403对应中心是否登录;非公开、非 GET 请求是否携带正确来源
插件接口返回 404插件是否启用,nameaction、处理方法是否存在
插件接口返回 405请求方法是否在 request 声明中
插件接口返回 500 / Plugin Error检查方法运行错误及 PHP 错误日志,必要时自行记录插件异常
JSON 请求参数读取不到是否错误地从 request.post 读取 JSON 请求体
钩子没有触发是否进入已接入钩子的业务流程,声明和方法名是否一致
钩子修改后没有效果修改字段是否属于主流程实际采用的字段
CSS、JS 更新后还是旧效果是否同步递增插件版本号(可清理缓存)
资源没有加载scopepage、资源路径、页面结构及插件状态是否正确
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

分别使用对应中心的账号登录后,打开「示例功能」,点击「检查插件状态」,页面会显示接口返回的结果。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容