library_books 思博业务

新版服务器插件快速开发指南(注册式钩子)

面向插件开发者,帮你快速上手新版注册式钩子插件开发。

参考脚手架:public/plugins/host/example/(完整示例,可直接复制使用)


快速开始(5 分钟完成)

1. 复制脚手架

cd public/plugins/host/
cp -r example myplugin          # 复制示例目录
mv myplugin/example.php myplugin/myplugin.php   # 重命名入口文件

2. 改 3 个地方

文件修改内容
myplugin.php第 109 行 'example''myplugin'
myplugin.phpnew ExamplePlugin()new MyPlugin()
src/ExamplePlugin.php类名改为 MyPlugin,修改 DisplayName

3. 实现业务逻辑

src/MyPlugin.php 中填写你的 API 调用代码。

4. 后台绑定

系统后台「服务器」→「添加服务器」→ 选择你的插件即可使用。


目录结构

public/plugins/host/myplugin/
├── myplugin.php                # 入口文件:只做钩子注册
├── src/
│   └── MyPlugin.php            # 业务类:实现所有钩子方法
├── templates/                  # 前台模板
│   ├── overview.html
│   └── manage.html
└── assets/                     # 静态资源
    ├── css/style.css
    └── js/main.js

命名约束:插件标识(文件夹名 + 入口文件名)只允许小写字母、数字、下划线,以字母开头,最长 32 字符。


入口文件(只注册,不写业务)

myplugin.php 只做三件事

<?php
// 1. 加载业务类
require_once __DIR__ . '/src/MyPlugin.php';

// 2. 注册能力钩子
use app\common\hook\Registry;

$plugin = new MyPlugin();
Registry::onMany('host', 'myplugin', [
    'metaData'   => [$plugin, 'onMetaData'],
    'testLink'   => [$plugin, 'onTestLink'],
    'create'     => [$plugin, 'onCreate'],
    'terminate'  => [$plugin, 'onTerminate'],
    // ... 其他钩子
]);

// 3. 注册事件监听(可选)
use think\Hook;
use app\common\hook\EventRegistry;

Hook::add(EventRegistry::HOST_AFTER_CREATE, function($params) {
    // 开通后做点啥
});
⚠️ 入口文件被加载时立即执行,不要放业务逻辑(不要连 DB、调 API、echo 内容)。

必须实现的钩子

插件元数据

public function onMetaData(): array
{
    return [
        'DisplayName' => '我的插件',
        'APIVersion'  => '1.0.0',
        'HelpDoc'     => 'https://...',
    ];
}

连接测试

public function onTestLink(array $params): array
{
    $server = $params['server'];
    return ['code' => 1, 'msg' => '连接成功'];
}

核心生命周期

钩子方法说明
createonCreate开通账号(支付后自动调用)
terminateonTerminate删除/终止
suspendonSuspend暂停
unsuspendonUnsuspend恢复
renewonRenew续费

所有方法接收 array $params,返回 ['code' => 1/0, 'msg' => '...']

$params 包含:

$params = [
    'host'        => [...],   // 主机信息(id, domain, username, password, status...)
    'product'     => [...],   // 商品信息(id, product_name, server_id...)
    'server'      => [...],   // 服务器信息(host, ip, port, user, password...)
    'server_meta' => [...],   // 服务器自定义配置
    'configoptions' => [...], // 商品选配字段
    'host_meta'   => [...],   // 主机元数据
    'addons'      => [...],   // 订单选配数据
];

可选能力钩子

前台交互

钩子方法用途
clientAreaonClientArea定义前台详情页的标签页
clientAreaOutputonClientAreaOutput输出标签页内容
clientButtononClientButton定义前台操作按钮
clientButtonActiononClientButtonAction执行按钮动作
handleActiononHandleAction前台表单提交统一入口

标签页示例:

public function onClientArea(array $params): array
{
    return [
        'overview' => ['name' => '概览', 'icon' => 'fa-dashboard'],
        'manage'   => ['name' => '管理', 'icon' => 'fa-cog'],
    ];
}

public function onClientAreaOutput(array $params, string $key): array
{
    if ($key === 'overview') {
        return [
            'template' => 'templates/overview.html',
            'vars'     => ['hostId' => $params['host']['id']],
        ];
    }
}

后台交互

钩子方法用途
adminButtononAdminButton后台操作按钮
adminButtonActiononAdminButtonAction执行后台按钮动作
productButtononProductButton产品管理页按钮
productButtonActiononProductButtonAction执行产品按钮动作

VPS 能力

钩子方法适用
powerOnonPowerOnVPS
powerOffonPowerOffVPS
rebootonRebootVPS
vnconVncVPS
reinstallonReinstallVPS
resetPasswordonResetPasswordVPS
chartonChartVPS

其他

钩子方法用途
statusonStatus状态查询
synconSync同步数据
changePackageonChangePackage升降级
crononCron主机定时任务
productCrononProductCron商品定时任务

配置项

钩子方法显示位置
adminConfigOptionsonAdminConfigOptions后台服务器编辑页
configOptionsonConfigOptions后台商品编辑页
hostConfigOptionsonHostConfigOptions自动插入产品元数据

配置项字段格式:

[
    'name'    => 'field_name',
    'title'   => '显示名称',
    'type'    => 'input',     // input/select/checkbox/textarea/number/password/switch
    'value'   => '默认值',
    'prompt'  => '提示说明',
    'options' => ['a' => '选项A', 'b' => '选项B'],  // select 用
]

生命周期事件监听

能力钩子是"系统叫你做事",事件钩子是"系统通知你事情发生了"。

维度能力钩子事件钩子
关系一对一一对多广播
返回值必须返回,影响主流程不依赖返回值
用途实现核心能力做附属操作(发邮件、记日志、同步)

可用事件:

use think\Hook;
use app\common\hook\EventRegistry;

// Host 生命周期
Hook::add(EventRegistry::HOST_BEFORE_CREATE,    ...);  // 开通前
Hook::add(EventRegistry::HOST_AFTER_CREATE,     ...);  // 开通后
Hook::add(EventRegistry::HOST_BEFORE_TERMINATE, ...);  // 删除前
Hook::add(EventRegistry::HOST_AFTER_TERMINATE,  ...);  // 删除后
Hook::add(EventRegistry::HOST_BEFORE_SUSPEND,   ...);  // 暂停前
Hook::add(EventRegistry::HOST_AFTER_SUSPEND,    ...);  // 暂停后
Hook::add(EventRegistry::HOST_BEFORE_UNSUSPEND, ...);  // 恢复前
Hook::add(EventRegistry::HOST_AFTER_UNSUSPEND,  ...);  // 恢复后
Hook::add(EventRegistry::HOST_BEFORE_RENEW,     ...);  // 续费前
Hook::add(EventRegistry::HOST_AFTER_RENEW,      ...);  // 续费后
Hook::add(EventRegistry::HOST_STATUS_CHANGED,   ...);  // 状态变更

// 订单
Hook::add(EventRegistry::ORDER_BEFORE_PAY,    ...);
Hook::add(EventRegistry::ORDER_AFTER_PAY,     ...);
Hook::add(EventRegistry::ORDER_BEFORE_CANCEL, ...);
Hook::add(EventRegistry::ORDER_AFTER_CANCEL,  ...);
Hook::add(EventRegistry::ORDER_BEFORE_REFUND, ...);
Hook::add(EventRegistry::ORDER_AFTER_REFUND,  ...);

// 用户
Hook::add(EventRegistry::USER_AFTER_REGISTER, ...);
Hook::add(EventRegistry::USER_AFTER_LOGIN,    ...);
Hook::add(EventRegistry::USER_AFTER_LOGOUT,   ...);

// 工单
Hook::add(EventRegistry::TICKET_AFTER_CREATE, ...);
Hook::add(EventRegistry::TICKET_AFTER_REPLY,  ...);
Hook::add(EventRegistry::TICKET_AFTER_CLOSE,  ...);

监听示例:

Hook::add(EventRegistry::HOST_AFTER_CREATE, function($params) {
    $host   = $params['host'];
    $result = $params['result'];

    if ($result['code'] == 1) {
        // 发送欢迎邮件
        \app\common\service\BillNoticeService::sendGroup([
            'user' => ['to' => $host['user_id'], 'tpl' => 'host_open_success_user'],
        ]);
    }
});

返回值规范

// 成功
['code' => 1, 'msg' => '操作成功']

// 失败
['code' => 0, 'msg' => '失败原因']

// 成功 + 数据
['code' => 1, 'msg' => '查询成功', 'data' => [...]]

特殊钩子的返回值:

钩子返回值格式
metaData['DisplayName'=>'名称', 'APIVersion'=>'1.0', 'HelpDoc'=>'url']
clientArea['tabKey' => ['name'=>'标签名', 'icon'=>'fa-xxx']]
clientAreaOutput['template'=>'路径', 'vars'=>[]]['html'=>'...']
clientButton['buttonKey' => '按钮名称']
vnc['code'=>1, 'data'=>['url'=>'vnc链接']]
status['code'=>1, 'data'=>['status'=>'Active', 'ip'=>'...']]
chart['code'=>1, 'data'=>['labels'=>[], 'values'=>[]]]

最佳实践

入口文件只注册,不写业务

❌ 错误:

// myplugin.php 中直接调 API
$client = new GuzzleHttp\Client();
$client->post(...);

✅ 正确:

// myplugin.php 只注册
Registry::onMany('host', 'myplugin', ['create' => [$plugin, 'onCreate']]);
// 业务逻辑全在 src/MyPlugin.php 的方法里

异常自己捕获

public function onCreate(array $params): array
{
    try {
        return ['code' => 1, 'msg' => '成功'];
    } catch (\Throwable $e) {
        return ['code' => 0, 'msg' => '失败:' . $e->getMessage()];
    }
}

不实现的钩子不要注册

只注册你实现了的钩子。Registry::onMany() 中不存在的方法不会被调用。

GuzzleHttp 做网络请求

$client = new \GuzzleHttp\Client(['timeout' => 15]);
$response = $client->post('https://api.example.com/create', ['json' => $data]);

调试技巧

// 查看插件注册了哪些钩子
use app\common\hook\host\HostPluginHook;
$hooks = HostPluginHook::hooks('myplugin');
dump($hooks);

// 手动触发钩子测试
$params = (new \app\common\controller\Plugins)->build_host_plugin_param(57);
$result = HostPluginHook::create('myplugin', $params);
dump($result);

系统日志位置:runtime/log/,搜索 Hook 关键词。


常见问题

Q:新插件和旧插件能共存吗?
可以。A 产品用新插件,B 产品用旧插件,互不影响。

Q:插件方法里能用 ThinkPHP 的功能吗?
可以。Db::name()model()cache() 等都能正常使用。

Q:插件超时怎么办?
设置合理的超时时间(5-30s),内部用 try/catch 捕获。

Q:只注册了一部分钩子,系统会报错吗?
不会。没注册的钩子系统会忽略,只是对应功能不可用。


参考

  • 完整示例插件:public/plugins/host/example/
  • 旧版开发手册请参考本站「服务器插件开发手册」

chat_bubble_outline 评论 0

edit 发表评论

目录