插件架构设计、钩子机制、数据库扩展及功能模块开发指南
新版服务器插件快速开发指南（注册式钩子）
面向插件开发者，帮你快速上手新版注册式钩子插件开发。参考脚手架：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，修改 DisplayName3. 实现业务逻辑在 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 能力钩子方法适用powerOnonPowerOnVPSpowerOffonPowerOffVPSrebootonRebootVPSvnconVncVPSreinstallonReinstallVPSresetPasswordonResetPasswordVPSchartonChartVPS其他钩子方法用途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/旧版开发手册请参考本站「服务器插件开发手册」
本文档指导开发者如何为本系统开发支付插件。目录结构所有支付插件必须存放在 public/plugins/pay/ 目录下，每个插件一个独立文件夹：public/plugins/pay/ ├── epay/ # 插件目录名（小写英文） │ ├── description.php # 插件基本信息（必需） │ ├── set.php # 后台配置定义（必需） │ ├── go.php # 发起支付（必需） │ ├── notify.php # 异步通知处理（必需） │ └── return.php # 同步回调处理（可选） ├── alipay/ │ └── ... └── wxpay/ └── ...核心流程用户下单 → 系统调用 go.php → 返回支付参数 → 用户完成支付 ↓ 支付平台 → 异步通知 notify.php → 验签成功 → 系统更新订单 ↓ 用户返回 → 同步回调 return.php → 显示支付结果文件详解1. description.php - 插件信息定义插件在后台显示的基本信息。<?php return [ "DisplayName" => "易支付", // 后台显示名称 "APIVersion" => "1.0.0", // 版本号 "HelpDoc" => "https://..." // 帮助文档链接（可选） ];返回字段说明：字段类型必填说明DisplayNamestring是插件显示名称APIVersionstring是版本号HelpDocstring否帮助文档URL2. set.php - 后台配置定义后台配置页面需要填写的字段。<?php return [ [ "name" => "商户ID", // 配置项标识（英文） "title" => "商户号", // 显示标题 "type" => "input", // 类型：input/select/textarea/switch "prompt" => "请输入商户号", // 提示文字 "value" => "" // 默认值 ], [ "name" => "密钥", "title" => "API密钥", "type" => "input", "prompt" => "请输入API密钥", "value" => "" ], [ "name" => "支付方式", "title" => "支付方式", "type" => "select", "prompt" => "选择支付方式", "value" => "alipay", "option" => [ // select类型专用 "支付宝" => "alipay", "微信" => "wxpay", "QQ" => "qqpay" ] ], [ "name" => "调试模式", "title" => "调试模式", "type" => "switch", "prompt" => "是否开启调试", "value" => "0" // 0=关，1=开 ] ];字段类型说明：类型说明input单行文本输入password密码输入（内容隐藏）textarea多行文本输入select下拉选择，需配合 optionswitch开关，value为 "0" 或 "1"number数字输入配置数据使用方式：系统在调用 go.php、notify.php、return.php 时，会将用户在后台填写的配置值通过 $params['data'] 传入。3. go.php - 发起支付函数名： go($params)作用： 接收订单信息，向支付平台发起请求，返回支付参数。入参 $params：字段类型说明order_nostring系统订单号subjectstring订单标题amountfloat订单金额（元）notify_urlstring异步通知URL（系统生成）return_urlstring同步回调URL（系统生成）client_ipstring用户IP地址dataarray/string插件配置数据返回值：// 页面跳转支付 return [ 'code' => 1, // 1=成功，0=失败 'type' => 'jump', // 固定值：jump 'data' => 'https://...' // 支付平台跳转URL ]; // API二维码支付 return [ 'code' => 1, 'type' => 'qrcode', // 固定值：qrcode 'data' => 'https://...' // 二维码图片URL或支付链接 ]; // 失败 return [ 'code' => 0, 'msg' => '错误信息' ];示例代码：<?php function go($params) { // 解析配置 $cfg = is_array($params['data']) ? $params['data'] : json_decode($params['data'], true); // 检查配置 if (empty($cfg['商户ID']) || empty($cfg['密钥'])) { return ['code' => 0, 'msg' => '请先配置支付参数']; } // 组装请求参数 $order = [ 'mch_id' => $cfg['商户ID'], 'out_trade_no' => $params['order_no'], 'total_fee' => intval($params['amount'] * 100), // 转为分 'body' => $params['subject'], 'notify_url' => $params['notify_url'], 'return_url' => $params['return_url'], 'nonce_str' => md5(uniqid()), ]; // 生成签名（按支付平台规则） $order['sign'] = sign($order, $cfg['密钥']); // 请求支付平台 $response = httpPost('https://pay.xxx.com/api', $order); $result = json_decode($response, true); if ($result['code'] == 'SUCCESS') { return [ 'code' => 1, 'type' => 'qrcode', 'data' => $result['code_url'] // 二维码链接 ]; } return ['code' => 0, 'msg' => $result['msg']]; } // 签名函数示例 function sign($data, $key) { ksort($data); $str = ''; foreach ($data as $k => $v) { if ($v !== '' && $k !== 'sign') { $str .= $k . '=' . $v . '&'; } } $str .= 'key=' . $key; return strtoupper(md5($str)); }4. notify.php - 异步通知函数名： notify($params)作用： 接收支付平台的支付结果通知，验证签名，确认支付成功。入参 $params：字段类型说明order_nostring系统订单号subjectstring订单标题amountfloat订单金额allarray支付平台POST的所有原始数据dataarray/string插件配置数据返回值：// 验签成功，系统会更新订单为已支付 return ['code' => 1, 'msg' => 'success']; // 验签失败 return ['code' => 0, 'msg' => '签名错误'];⚠️ 重要：必须返回 ['code' => 1, 'msg' => 'success'] 才算成功不要输出任何其他内容（如echo、var_dump）系统只负责验签，订单状态更新由系统统一处理示例代码：<?php function notify($params) { $cfg = is_array($params['data']) ? $params['data'] : json_decode($params['data'], true); $post = $params['all']; // 原始POST数据 // 1. 验证签名 $sign = $post['sign'] ?? ''; unset($post['sign']); $mySign = sign($post, $cfg['密钥']); if (strtoupper($sign) !== strtoupper($mySign)) { return ['code' => 0, 'msg' => '签名错误']; } // 2. 验证订单号 if ($post['out_trade_no'] !== $params['order_no']) { return ['code' => 0, 'msg' => '订单号不匹配']; } // 3. 验证金额（使用bccomp精确比较） $notifyAmount = $post['total_fee'] / 100; // 分转元 if (bccomp($notifyAmount, $params['amount'], 2) !== 0) { return ['code' => 0, 'msg' => '金额不一致']; } // 4. 验证支付状态 if ($post['status'] !== 'SUCCESS') { return ['code' => 0, 'msg' => '未支付']; } // 5. 返回成功 return ['code' => 1, 'msg' => 'success']; }5. return.php - 同步回调（可选）函数名： returns($params) （注意是 returns 不是 return）作用： 用户支付完成后，从支付平台跳转回系统的处理。入参： 同 notify.php返回值： 同 notify.php与 notify.php 的区别： notify.phpreturn.php触发服务器主动通知用户浏览器跳转可靠性高（会重试）低（可能关闭页面）更新订单是否（仅展示结果）示例代码：<?php function returns($params) { // 逻辑同notify，但只用于页面展示 // 不实际修改订单状态 $cfg = is_array($params['data']) ? $params['data'] : json_decode($params['data'], true); $get = $params['all']; // URL参数 // 验证签名、订单号、金额、状态... // 返回结果用于页面显示支付成功/失败 return ['code' => 1, 'msg' => 'success']; }系统调用流程发起支付时// 系统内部逻辑示意 $params = [ 'order_no' => '20240201123456', 'subject' => '商品购买', 'amount' => 99.99, 'notify_url' => 'https://网站/notify/epay', 'return_url' => 'https://网站/return/epay', 'client_ip' => '192.168.1.1', 'data' => ['商户ID' => 'xxx', '密钥' => 'yyy', ...] // 后台配置 ]; $result = go($params); if ($result['code'] == 1) { if ($result['type'] == 'jump') { header('Location: ' . $result['data']); } else { // 显示二维码 showQrcode($result['data']); } } else { showError($result['msg']); }异步通知时// 系统接收到支付平台通知后 $params = [ 'order_no' => '20240201123456', 'subject' => '商品购买', 'amount' => 99.99, 'all' => $_POST, // 原始POST数据 'data' => ['商户ID' => 'xxx', '密钥' => 'yyy', ...] ]; $result = notify($params); if ($result['code'] == 1) { // 系统更新订单状态为已支付 updateOrderStatus($params['order_no'], 'paid'); echo 'success'; // 响应支付平台 } else { echo $result['msg']; // 响应错误 }开发规范1. 目录命名使用小写英文字母+数字简短有意义，如 epay、alipay、wxpay、paypal2. 配置安全敏感信息（密钥等）在后台配置，不要硬编码通过 $params['data'] 获取配置3. 金额处理使用 bccomp() 进行金额比较，避免浮点误差注意单位转换（元/分）4. 签名验证严格按照支付平台文档实现签名算法空值字段通常不参与签名签名比较忽略大小写5. 错误处理所有错误返回 ['code' => 0, 'msg' => '错误描述']不要直接输出错误，通过返回值传递6. 网络请求设置合理的超时时间（建议10-30秒）处理网络异常情况快速开始：创建新插件以创建 "示例支付" 插件为例：步骤1：创建目录mkdir public/plugins/pay/demo步骤2：创建 description.php<?php return [ "DisplayName" => "示例支付", "APIVersion" => "1.0.0", "HelpDoc" => "" ];步骤3：创建 set.php<?php return [ [ "name" => "mch_id", "title" => "商户号", "type" => "input", "prompt" => "请输入商户号", "value" => "" ], [ "name" => "key", "title" => "API密钥", "type" => "input", "prompt" => "请输入API密钥", "value" => "" ] ];步骤4：创建 go.php<?php function go($params) { $cfg = is_array($params['data']) ? $params['data'] : json_decode($params['data'], true); if (empty($cfg['mch_id']) || empty($cfg['key'])) { return ['code' => 0, 'msg' => '配置不完整']; } // TODO: 根据支付平台API实现 return ['code' => 0, 'msg' => '待实现']; }步骤5：创建 notify.php<?php function notify($params) { $cfg = is_array($params['data']) ? $params['data'] : json_decode($params['data'], true); $post = $params['all']; // TODO: 验证签名、订单号、金额、状态 return ['code' => 1, 'msg' => 'success']; }步骤6：后台启用进入系统后台 → 支付设置 → 启用 "示例支付" 插件 → 填写配置调试技巧1. 记录日志在插件中写入日志便于调试：function logDebug($msg, $data = null) { file_put_contents( __DIR__ . '/debug.log', date('Y-m-d H:i:s') . ' ' . $msg . ' ' . json_encode($data) . "\n", FILE_APPEND ); } // 使用 logDebug('收到通知', $params);2. 签名调试function debugSign($data, $key) { ksort($data); $str = http_build_query($data) . '&key=' . $key; logDebug('签名字符串', $str); logDebug('签名结果', md5($str)); return md5($str); }附录：常用工具函数<?php /** * HTTP POST请求 */ function httpPost($url, $data, $timeout = 30) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($data), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'], ]); $result = curl_exec($ch); curl_close($ch); return $result; } /** * 生成签名（字典序+MD5） */ function makeSign($data, $key, $exclude = ['sign', 'sign_type']) { ksort($data); $pairs = []; foreach ($data as $k => $v) { if ($v !== '' && !in_array($k, $exclude)) { $pairs[] = $k . '=' . $v; } } return md5(implode('&', $pairs) . $key); } /** * 获取客户端IP */ function getClientIp() { $keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR']; foreach ($keys as $k) { if (!empty($_SERVER[$k])) { $ips = explode(',', $_SERVER[$k]); $ip = trim($ips[0]); if (filter_var($ip, FILTER_VALIDATE_IP)) { return $ip; } } } return '127.0.0.1'; }
目录概述目录结构插件基础结构核心函数详解配置字段类型参数说明模板开发开发示例最佳实践目录结构所有服务器插件统一存放在 public\plugins\host 目录下。完整目录结构public/ └── plugins/ └── host/ # 服务器插件根目录 ├── aliyun/ # 阿里云插件 │ ├── aliyun.php # 插件主文件 │ └── templates/ # 模板目录 │ ├── overview.html │ ├── manage.html │ └── console.html │ ├── tencentcloud/ # 腾讯云插件 │ ├── tencentcloud.php │ └── templates/ │ └── ... │ ├── huaweicloud/ # 华为云插件 │ ├── huaweicloud.php │ └── templates/ │ └── ... │ ├── aws/ # AWS插件 │ ├── aws.php │ └── templates/ │ └── ... │ └── default/ # 默认示例插件 ├── default.php └── templates/ ├── 1.html └── 2.html插件目录命名规范项目规范示例目录名小写字母+数字，与插件标识一致aliyun, tencentcloud主文件名与目录名一致aliyun.php模板目录必须命名为 templatestemplates/模板文件.html 后缀overview.html最小插件结构public\plugins\host\{插件标识}/ ├── {插件标识}.php # 必填，插件主程序 └── templates/ # 可选，模板目录 └── ... # 模板文件概述本系统采用 PHP + ThinkPHP 框架开发，服务器插件用于对接第三方服务器/云服务商 API，实现虚拟主机、云服务器等产品的自动化管理。插件命名规范插件文件名格式：{插件标识}.php插件标识只能包含英文字母、数字和下划线所有函数必须以 插件标识_ 为前缀插件基础结构在 public\plugins\host\{插件标识}\{插件标识}.php 文件中编写：<?php use think\Db; // ========== 1. 插件信息 ========== function 插件标识_MetaData() { return [ "DisplayName" => "插件显示名称", "APIVersion" => "1.0.0", "HelpDoc" => "https://文档链接" ]; } // ========== 2. 连接测试 ========== function 插件标识_TestLink($params) { return ["code" => "1", "msg" => "连接成功"]; } // ========== 3. 配置字段定义 ========== function 插件标识_AdminConfigOptions() { /* 服务器配置 */ } function 插件标识_ConfigOptions() { /* 产品配置 */ } function 插件标识_HostConfigOptions() { /* 业务配置 */ } // ========== 4. 控制面板 ========== function 插件标识_ClientArea($params) { /* 定义菜单 */ } function 插件标识_ClientAreaOutput($params, $key) { /* 页面输出 */ } // ========== 5. 生命周期函数 ========== function 插件标识_CreateAccount($params) { /* 开通 */ } function 插件标识_SuspendAccount($params) { /* 暂停 */ } function 插件标识_UnsuspendAccount($params) { /* 解除暂停 */ } function 插件标识_TerminateAccount($params) { /* 终止/删除 */ } function 插件标识_Renew($params) { /* 续费 */ } function 插件标识_ChangePackage($params) { /* 升降级 */ } // ========== 6. 自定义按钮 ========== function 插件标识_AdminButton() { /* 后台按钮 */ } function 插件标识_ClientButton() { /* 前台按钮 */ } function 插件标识_按钮标识($params) { /* 按钮执行逻辑 */ } // ========== 7. 计划任务 ========== function 插件标识_ProductCron() { /* 商品侧计划任务 */ } function 插件标识_HostCron() { /* 业务侧计划任务 */ }核心函数详解1. 插件信息 (MetaData)定义插件的基本信息，用于后台展示。function default_MetaData() { return [ "DisplayName" => "默认插件", // 插件显示名称 "APIVersion" => "1.0.1", // API版本号 "HelpDoc" => "https://www.example.com/" // 帮助文档链接 ]; }2. 连接测试 (TestLink)在后台服务器配置页面测试与第三方 API 的连接。function default_TestLink($params) { // $params 包含服务器配置信息 $server = $params['server']; // 尝试连接第三方 API // ... 连接逻辑 ... // 返回格式 return [ "code" => "1", // 1=成功, 0=失败 "msg" => "连接成功" // 提示信息 ]; }3. 配置字段3.1 服务器配置 (AdminConfigOptions)后台添加服务器时显示的配置字段。function default_AdminConfigOptions() { return [ [ "name" => "api_key", // 字段标识 "title" => "API密钥", // 显示标题 "type" => "input", // 字段类型 "prompt" => "请输入API密钥", // 提示信息 "value" => "" // 默认值 ], [ "name" => "region", "title" => "默认区域", "type" => "select", "prompt" => "选择服务器区域", "value" => "cn-hangzhou", "options" => [ 'cn-hangzhou' => '华东1（杭州）', 'cn-beijing' => '华北2（北京）', 'cn-shenzhen' => '华南1（深圳）' ] ], ]; }3.2 产品配置 (ConfigOptions)添加产品时显示的配置字段。function default_ConfigOptions() { return [ [ "name" => "os_type", "title" => "操作系统", "type" => "select", "prompt" => "选择默认操作系统", "value" => "centos8', "option" => ["centos7", "centos8", "ubuntu20", "windows2019"] ], [ "name" => "bandwidth", "title" => "带宽(Mbps)", "type" => "number", "prompt" => "请输入带宽大小", "value" => "10" ], ]; }3.3 业务配置 (HostConfigOptions)用户购买后，针对单个业务的配置字段。function default_HostConfigOptions() { return [ [ "name" => "instance_id", "title" => "实例ID", "type" => "input", "prompt" => "第三方平台的实例ID", "value" => "" ], [ "name" => "auto_backup", "title" => "自动备份", "type" => "switch", "prompt" => "是否开启自动备份", "value" => "1" // 1=开启, 0=关闭 ], ]; }4. 控制面板4.1 定义菜单 (ClientArea)定义用户控制面板中显示的菜单项。function default_ClientArea($params) { return [ "overview" => [ "name" => "概览" // 菜单显示名称 ], "manage" => [ "name" => "实例管理" ], "logs" => [ "name" => "操作日志" ], ]; }4.2 页面输出 (ClientAreaOutput)根据菜单 key 返回对应的模板和数据。function default_ClientAreaOutput($params, $key) { if ($key == "overview") { // 获取实例信息 $instance = getInstanceInfo($params); return [ "template" => "templates/overview.html", // 模板路径 "vars" => [ // 传递给模板的变量 'instance' => $instance, 'status' => $instance['status'], 'ip' => $params['host']['dedicatedip'], ] ]; } if ($key == "manage") { return [ "template" => "templates/manage.html", "vars" => [ 'host' => $params['host'], 'actions' => ['start', 'stop', 'reboot'] ] ]; } }5. 生命周期函数5.1 开通 (CreateAccount)用户支付成功后调用，用于在第三方平台创建资源。function default_CreateAccount($params) { $server = $params['server']; // 服务器信息 $product = $params['product']; // 产品信息 $host = $params['host']; // 业务信息 $server_meta = $params['server_meta']; // 服务器配置字段值 $configoptions = $params['configoptions']; // 产品配置字段值 $host_meta = $params['host_meta']; // 业务配置字段值 try { // 调用第三方 API 创建实例 $result = createInstance([ 'hostname' => $host['domain'], 'username' => $host['username'], 'password' => $host['password'], 'os' => $configoptions['os_type'], 'region' => $server_meta['region'], ]); if ($result['success']) { // 保存实例ID到业务元数据 $model = new \app\admin\model\HostMeta(); $model->ensureProductLevelField( $host['id'], 'instance_id', $result['instance_id'], '实例ID', '第三方平台实例标识' ); return ["code" => "1", "msg" => "开通成功"]; } else { return ["code" => "0", "msg" => $result['error']]; } } catch (Exception $e) { return ["code" => "0", "msg" => "开通失败：" . $e->getMessage()]; } }5.2 暂停 (SuspendAccount)暂停业务时调用。function default_SuspendAccount($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; if (empty($instance_id)) { return ["code" => "0", "msg" => "实例ID不存在"]; } // 调用 API 暂停/关机 $result = suspendInstance($instance_id); if ($result['success']) { return ["code" => "1", "msg" => "暂停成功"]; } else { return ["code" => "0", "msg" => $result['error']]; } }5.3 解除暂停 (UnsuspendAccount)恢复业务时调用。function default_UnsuspendAccount($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; // 调用 API 启动实例 $result = startInstance($instance_id); return [ "code" => $result['success'] ? "1" : "0", "msg" => $result['success'] ? "开启成功" : $result['error'] ]; }5.4 终止/删除 (TerminateAccount)删除业务时调用，应清理第三方平台资源。function default_TerminateAccount($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; if (empty($instance_id)) { return ["code" => "1", "msg" => "实例不存在，直接删除"]; } // 调用 API 删除实例 $result = deleteInstance($instance_id); return [ "code" => $result['success'] ? "1" : "0", "msg" => $result['success'] ? "删除成功" : $result['error'] ]; }5.5 续费 (Renew)业务续费时调用。function default_Renew($params) { // $params['cycle'] 续费周期：1M=1个月, 1H=1小时, 1D=1天 $cycle = $params['cycle']; $instance_id = $params['host_meta']['instance_id'] ?? ''; // 调用 API 续费 $result = renewInstance($instance_id, $cycle); return [ "code" => $result['success'] ? "1" : "0", "msg" => $result['success'] ? "续费成功" : $result['error'] ]; }5.6 升降级 (ChangePackage)修改产品配置时调用。function default_ChangePackage($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; $new_config = $params['configoptions']; // 新的配置 // 调用 API 修改配置 $result = changePackage($instance_id, $new_config); return [ "code" => $result['success'] ? "1" : "0", "msg" => $result['success'] ? "升降级成功" : $result['error'] ]; }6. 自定义按钮6.1 后台按钮 (AdminButton)function default_AdminButton() { return [ 'ResetPassword' => '重置密码', 'ReinstallOS' => '重装系统', 'VNC' => 'VNC控制台', ]; } // 重置密码按钮执行 function default_ResetPassword($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; $new_password = generatePassword(); // 生成新密码 $result = resetPassword($instance_id, $new_password); if ($result['success']) { // 更新本地密码 Db::name('host')->where('id', $params['host']['id'])->update([ 'password' => encrypt($new_password) ]); return ["code" => "1", "msg" => "重置成功，新密码：" . $new_password]; } else { return ["code" => "0", "msg" => $result['error']]; } }6.2 前台按钮 (ClientButton)function default_ClientButton() { return [ 'Reboot' => '重启服务器', 'GetVNC' => 'VNC连接', 'ReinstallOS' => '重装系统', ]; } // 重启按钮执行 function default_Reboot($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; $result = rebootInstance($instance_id); return [ "code" => $result['success'] ? "1" : "0", "msg" => $result['success'] ? "重启指令已发送" : $result['error'] ]; }6.3 后台产品侧按钮 (ProductButton)function default_ProductButton() { return [ 'ResetPassword' => '重置密码', 'ReinstallOS' => '重装系统', 'VNC' => 'VNC控制台', ]; } // 重置密码按钮执行 function default_ResetPassword($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; $new_password = generatePassword(); // 生成新密码 $result = resetPassword($instance_id, $new_password); if ($result['success']) { // 更新本地密码 Db::name('host')->where('id', $params['host']['id'])->update([ 'password' => encrypt($new_password) ]); return ["code" => "1", "msg" => "重置成功，新密码：" . $new_password]; } else { return ["code" => "0", "msg" => $result['error']]; } }7. 计划任务7.1 商品侧计划任务 (ProductCron)针对某个商品的所有业务执行。function default_ProductCron() { // 获取该商品下的所有活跃业务 $hosts = Db::name('host') ->where('product_id', $params['product']['id']) ->where('status', 'Active') ->select(); foreach ($hosts as $host) { // 同步状态、流量等信息 syncInstanceStatus($host); } return ["code" => "1", "msg" => "同步完成"]; }7.2 业务侧计划任务 (HostCron)针对单个业务执行。function default_HostCron() { $instance_id = $params['host_meta']['instance_id'] ?? ''; if (empty($instance_id)) { return ["code" => "0", "msg" => "实例ID不存在"]; } // 同步流量使用情况 $traffic = getTrafficUsage($instance_id); // 保存到业务元数据 $model = new \app\admin\model\HostMeta(); $model->ensureProductLevelField( $params['host']['id'], 'traffic_used', $traffic['used'], '已用流量', '本月已用流量(GB)' ); return ["code" => "1", "msg" => "流量同步完成"]; }配置字段类型类型说明适用场景input单行文本输入API密钥、用户名、实例ID等textarea多行文本输入备注、证书内容、多行配置number数字输入端口号、带宽、内存大小等select下拉单选区域选择、操作系统、套餐类型字段属性说明[ "name" => "字段标识", // 必填，唯一标识 "title" => "显示标题", // 必填，后台显示名称 "type" => "input", // 必填，字段类型 "prompt" => "提示信息", // 选填，输入提示 "value" => "默认值", // 选填，默认值 // select 类型特有 "options" => [ // 键值对形式（推荐） 'key1' => '显示值1', 'key2' => '显示值2' ], "option" => ["值1", "值2"] // 索引数组形式 ]参数说明$params 结构详解$params = [ // 业务信息 'host' => [ 'id' => 57, // 业务ID 'user_id' => 1, // 用户ID 'order_id' => 191, // 订单ID 'domain' => 'example.com', // 主机名/域名 'username' => 'user123', // 用户名 'password' => 'password', // 密码 'status' => 'Active', // 状态 'dedicatedip' => '192.168.1.1', // 独立IP 'port' => '22', // 端口 'assignedips' => '', // 分配的IP列表 'ns1' => '', // NS1 'ns2' => '', // NS2 'nextduedate' => '2026-02-14', // 到期时间 ], // 产品信息 'product' => [ 'id' => 15, // 产品ID 'product_name' => '云服务器', // 产品名称 'server_id' => 5, // 服务器ID 'custom_fields' => null, // 自定义字段 ], // 服务器信息（API连接信息） 'server' => [ 'id' => 5, // 服务器ID 'name' => '阿里云', // 服务器名称 'host' => 'api.aliyun.com', // API地址 'ip' => '', // IP地址 'security' => '', // 安全凭证 'port' => '', // 端口 'ssl' => '1', // 是否使用SSL 'user' => 'access_key', // 用户名/AccessKey 'password' => 'secret_key', // 密码/SecretKey ], // 服务器配置字段值（AdminConfigOptions定义） 'server_meta' => [ 'api_key' => 'xxx', 'region' => 'cn-hangzhou', ], // 产品配置字段值（ConfigOptions定义） 'configoptions' => [ 'os_type' => 'centos8', 'bandwidth' => '10', ], // 业务配置字段值（HostConfigOptions定义） 'host_meta' => [ 'instance_id' => 'i-xxx', 'auto_backup' => '1', ], ];模板开发模板文件位置模板文件存放在插件目录下的 templates 文件夹中。public\plugins\host\插件标识/ ├── 插件标识.php └── templates/ ├── overview.html # 概览页面 ├── manage.html # 管理页面 └── logs.html # 日志页面模板路径引用在 ClientAreaOutput 函数中引用模板时，使用相对路径：function aliyun_ClientAreaOutput($params, $key) { if ($key == "overview") { return [ "template" => "templates/overview.html", // 相对插件根目录 "vars" => [...] ]; } }模板语法系统使用 ThinkPHP 模板引擎。 {$vars.instance_id} {$vars.status} {if $vars.status == 'running'} 运行中 {elseif $vars.status == 'stopped'} 已停止 {else} {$vars.status} {/if} {volist name="vars.logs" id="log"} {$log.time} {$log.action} {$log.result} {/volist} {php} print_r($vars); // 或者进行数据处理 {/php}模板示例 服务器概览 实例ID {$vars.instance_id} 状态 {if $vars.status == 'running'} ● 运行中 {else/} ● 已停止 {/if} IP地址 {$vars.ip} 操作系统 {$vars.os} 开发示例完整插件示例：简易云服务器假设插件标识为 cloud，则目录结构为：public\plugins\host\cloud/ ├── cloud.php └── templates/ ├── overview.html └── console.htmlcloud.php 完整代码：<?php use think\Db; // ========== 基础信息 ========== function cloud_MetaData() { return [ "DisplayName" => "简易云服务器", "APIVersion" => "1.0.0", "HelpDoc" => "https://docs.example.com" ]; } function cloud_TestLink($params) { $server = $params['server']; // 测试 API 连接 $response = httpRequest($server['host'] . '/api/test', [ 'access_key' => $server['user'], 'secret_key' => $server['password'] ]); if ($response['code'] == 200) { return ["code" => "1", "msg" => "连接成功"]; } return ["code" => "0", "msg" => "连接失败：" . $response['msg']]; } // ========== 配置字段 ========== function cloud_AdminConfigOptions() { return [ [ "name" => "api_endpoint", "title" => "API地址", "type" => "input", "prompt" => "例如：https://api.example.com", "value" => "" ], [ "name" => "default_region", "title" => "默认区域", "type" => "select", "prompt" => "选择默认区域", "value" => "zone-a", "options" => [ 'zone-a' => 'A区', 'zone-b' => 'B区', 'zone-c' => 'C区' ] ], ]; } function cloud_ConfigOptions() { return [ [ "name" => "cpu", "title" => "CPU核心数", "type" => "select", "prompt" => "选择CPU配置", "value" => "2", "option" => ["1", "2", "4", "8"] ], [ "name" => "memory", "title" => "内存(GB)", "type" => "select", "prompt" => "选择内存配置", "value" => "4", "option" => ["1", "2", "4", "8", "16"] ], [ "name" => "disk", "title" => "磁盘(GB)", "type" => "number", "prompt" => "磁盘大小", "value" => "50" ], ]; } function cloud_HostConfigOptions() { return [ [ "name" => "instance_id", "title" => "实例ID", "type" => "input", "prompt" => "系统自动填充", "value" => "" ], ]; } // ========== 控制面板 ========== function cloud_ClientArea($params) { return [ "overview" => ["name" => "概览"], "console" => ["name" => "控制台"], ]; } function cloud_ClientAreaOutput($params, $key) { $instance_id = $params['host_meta']['instance_id'] ?? ''; if ($key == "overview") { // 获取实例详情 $detail = getInstanceDetail($params, $instance_id); return [ "template" => "templates/cloud/overview.html", "vars" => [ 'instance' => $detail, 'host' => $params['host'] ] ]; } if ($key == "console") { return [ "template" => "templates/cloud/console.html", "vars" => [ 'instance_id' => $instance_id ] ]; } } // ========== 生命周期 ========== function cloud_CreateAccount($params) { try { $result = createCloudServer($params); if ($result['success']) { // 保存实例ID $model = new \app\admin\model\HostMeta(); $model->ensureProductLevelField( $params['host']['id'], 'instance_id', $result['instance_id'], '实例ID', '云平台实例标识' ); // 更新IP地址 if (!empty($result['ip'])) { Db::name('host')->where('id', $params['host']['id'])->update([ 'dedicatedip' => $result['ip'] ]); } return ["code" => "1", "msg" => "开通成功"]; } return ["code" => "0", "msg" => $result['error']]; } catch (Exception $e) { return ["code" => "0", "msg" => "开通失败：" . $e->getMessage()]; } } function cloud_SuspendAccount($params) { return controlInstance($params, 'stop'); } function cloud_UnsuspendAccount($params) { return controlInstance($params, 'start'); } function cloud_TerminateAccount($params) { $instance_id = $params['host_meta']['instance_id'] ?? ''; if (empty($instance_id)) { return ["code" => "1", "msg" => "删除成功"]; } $result = deleteCloudServer($params, $instance_id); return [ "code" => $result['success'] ? "1" : "0", "msg" => $result['success'] ? "删除成功" : $result['error'] ]; } function cloud_Renew($params) { // 续费逻辑 return ["code" => "1", "msg" => "续费成功"]; } function cloud_ChangePackage($params) { // 升降级逻辑 return ["code" => "1", "msg" => "配置变更成功"]; } // ========== 按钮 ========== function cloud_AdminButton() { return [ 'ResetPassword' => '重置密码', 'ReinstallOS' => '重装系统', ]; } function cloud_ClientButton() { return [ 'Reboot' => '重启', 'Shutdown' => '关机', 'Start' => '开机', ]; } function cloud_Reboot($params) { return controlInstance($params, 'reboot'); } function cloud_Shutdown($params) { return controlInstance($params, 'stop'); } function cloud_Start($params) { return controlInstance($params, 'start'); } // ========== 辅助函数 ========== function controlInstance($params, $action) { $instance_id = $params['host_meta']['instance_id'] ?? ''; if (empty($instance_id)) { return ["code" => "0", "msg" => "实例ID不存在"]; } $server = $params['server']; $api_url = $params['server_meta']['api_endpoint'] ?? $server['host']; $response = httpRequest($api_url . '/api/instance/' . $action, [ 'access_key' => $server['user'], 'secret_key' => $server['password'], 'instance_id' => $instance_id ]); $action_names = [ 'start' => '开机', 'stop' => '关机', 'reboot' => '重启' ]; if ($response['code'] == 200) { return ["code" => "1", "msg" => $action_names[$action] . "成功"]; } return ["code" => "0", "msg" => $response['msg']]; } function httpRequest($url, $data) { // HTTP请求实现 // 实际开发中请使用系统的 HTTP 类或 curl }最佳实践1. 错误处理function default_CreateAccount($params) { try { // API 调用 $result = apiCall(); if (!$result) { return ["code" => "0", "msg" => "API返回空数据"]; } return ["code" => "1", "msg" => "成功"]; } catch (\Exception $e) { // 记录日志 trace("开通失败：" . $e->getMessage(), 'error'); return ["code" => "0", "msg" => "开通失败：" . $e->getMessage()]; } }2. 数据验证function default_CreateAccount($params) { // 验证必要字段 if (empty($params['server']['user'])) { return ["code" => "0", "msg" => "服务器用户名未配置"]; } if (empty($params['host']['domain'])) { return ["code" => "0", "msg" => "主机名不能为空"]; } // 继续执行... }3. 元数据操作// 保存数据到业务元数据 $model = new \app\admin\model\HostMeta(); // 简单保存 $model->ensureProductLevelField($host_id, 'key', 'value'); // 带描述的保存 $model->ensureProductLevelField( $host_id, // 业务ID 'traffic_used', // 字段名 '1024', // 字段值 '已用流量(GB)', // 中文标题 '本月已使用流量' // 描述 );4. 安全建议敏感信息（API密钥等）使用系统提供的加密存储所有 API 调用使用 HTTPS对用户输入进行过滤和验证密码等敏感字段不要明文记录在日志中5. 调试技巧// 使用系统日志 trace("调试信息", 'info'); trace("错误信息", 'error'); // 在模板中调试 {php} print_r($vars); {/php}附录返回码规范code含义1成功0失败状态常量状态说明Pending待开通Active正常Suspended已暂停Terminated已终止