思博业务系统二次开发、API 接口、数据库结构及扩展开发文档
概述实名认证系统采用插件化架构，每个认证通道（如阿里云二要素、三要素、人脸识别等）作为一个独立的插件，放置在 public/plugins/certification/ 目录下。插件通过实现统一的接口与系统交互，支持多通道并行、同步/异步模式、第三方回调等特性。目录结构public/plugins/certification/{plugin_name}/ ├── {plugin_name}.php # 插件主文件（类定义） └── manifest.json # 插件自描述文件示例public/plugins/certification/ ├── manual/ │ ├── manual.php │ └── manifest.json ├── alitwo/ │ ├── alitwo.php │ └── manifest.json ├── edgeone/ │ ├── edgeone.php │ └── manifest.json └── threehc/ ├── threehc.php └── manifest.json插件接口所有插件必须实现 app\common\library\certification\CertificationPluginInterface 接口。接口定义interface CertificationPluginInterface { /** 插件标识（目录名） */ public static function getName(): string; /** 提交认证 */ public function submit(SubmitParams $params): SubmitResult; /** 查询认证结果 */ public function query(QueryParams $params): QueryResult; /** 处理第三方回调 */ public function handleCallback(array $request): CallbackResult; /** 获取插件自描述信息 */ public static function getManifest(): array; }参数类说明SubmitParams（提交入参）属性类型说明realNamestring真实姓名idCardstring身份证号mobilestring用户手机号userIdint用户 IDextraarray插件自定义字段（银行卡号、视频等）configarray插件自定义配置（custom_config 解码后）timeoutint超时时间，秒SubmitResult（提交出参）属性类型说明successbool是否成功messagestring提示信息modestring'sync' \'async' — 决定写入状态的方式statusstring'passed' \'rejected' \'pending'tokenstring业务防重号typestring'none' \'qrcode' \'redirect' \'manual'datamixed前端需要的数据（URL、二维码等）extraarray需要存入 DB extra 字段的自定义数据codeintErrorCodeQueryParams（查询入参）属性类型说明verifySnstring业务防重号statusstring当前数据库状态namestring真实姓名idCardNostring身份证号extraarray提交时存的 extra 数据configarray插件自定义配置QueryResult（查询出参）属性类型说明successbool是否成功messagestring提示信息statusstring'passed' \'rejected' \'pending'rejectReasonstring驳回原因extraarray需要更新的 extra 数据codeintErrorCodeCallbackResult（回调出参）属性类型说明successbool是否成功messagestring提示信息statusstring'passed' \'rejected'rejectReasonstring驳回原因tokenstring用于匹配 Idv 记录codeintErrorCode错误码class ErrorCode { const SUCCESS = 1; // 成功 const VALIDATE = 0; // 参数校验失败（前端可重新提交） const REJECTED = -1; // 认证被拒绝 const UPSTREAM = -2; // 上游接口错误（可重试） const SYSTEM = -3; // 系统异常 }mode 字段说明mode 字段决定控制器如何写入认证状态：mode含义控制器行为sync同步验证直接写入最终状态（passed/rejected）async异步验证先写入 pending，等待回调或轮询更新典型场景：同步：三要素核验、四要素核验、人脸比对、活体检测 — 调用上游 API 立即返回结果异步：扫码认证、H5 跳转认证、人工审核 — 需要用户完成交互后才有结果type 字段说明type 字段控制前端行为：type前端行为none直接刷新页面qrcode显示二维码弹窗，用户扫码后轮询redirect跳转到第三方 URLmanual提示等待人工审核插件开发步骤1. 创建目录和文件public/plugins/certification/myalitwo/ ├── myalitwo.php └── manifest.json2. 编写插件类<?php namespace plugins\certification\myalitwo; use app\common\library\certification\CertificationPluginInterface; use app\common\library\certification\SubmitParams; use app\common\library\certification\SubmitResult; use app\common\library\certification\QueryParams; use app\common\library\certification\QueryResult; use app\common\library\certification\CallbackResult; use app\common\library\certification\ErrorCode; class MyalitwoPlugin implements CertificationPluginInterface { private array $config; public function __construct(array $config = []) { $this->config = $config; } public static function getName(): string { return 'myalitwo'; } public static function getManifest(): array { $file = __DIR__ . '/manifest.json'; return is_file($file) ? json_decode(file_get_contents($file), true) : []; } public function submit(SubmitParams $params): SubmitResult { $appCode = trim($this->config['app_code'] ?? ''); if (!$appCode) { return SubmitResult::error('请先配置 AppCode', ErrorCode::SYSTEM); } $realName = $params->realName; $idCard = $params->idCard; if (!$realName) { return SubmitResult::error('请输入真实姓名'); } if (!$idCard) { return SubmitResult::error('请输入身份证号码'); } $result = $this->apiRequest($idCard, $realName, $appCode); $passed = ($result['status'] === '01'); return SubmitResult::success($passed ? '实名认证通过' : '信息核验不匹配', [ 'mode' => 'sync', 'status' => $passed ? 'passed' : 'rejected', 'token' => $result['traceId'] ?? uniqid('ALI_'), 'type' => 'none', 'extra' => ['trace_id' => $result['traceId'] ?? ''], ]); } public function query(QueryParams $params): QueryResult { if ($params->status === 'passed') { return QueryResult::success('实名认证已通过', 'passed'); } if ($params->status === 'rejected') { return QueryResult::error('实名认证未通过', ErrorCode::REJECTED, ['status' => 'rejected']); } return QueryResult::error('认证处理中', ErrorCode::VALIDATE, ['status' => 'pending']); } public function handleCallback(array $request): CallbackResult { return CallbackResult::error('不支持回调', ErrorCode::SYSTEM); } private function apiRequest(string $idCard, string $name, string $appCode): array { // 调用上游 API 的 HTTP 请求 return []; } }3. 编写 manifest.json{ "name": "myalitwo", "display_name": "我的阿里云二要素", "version": "1.0.0", "mode": "sync", "callback": { "required": false }, "required_fields": ["real_name", "id_card"], "optional_fields": [], "extra_fields": [], "config": { "app_code": { "title": "AppCode", "type": "text", "value": "", "tip": "阿里云云市场购买的 AppCode" } } }4. 后台添加通道在后台「实名认证 → 插件管理」中添加新通道，选择 myalitwo 插件，填入配置即可启用。manifest.json 字段说明{ "name": "插件标识（目录名，与插件类 getName() 一致）", "display_name": "前台显示名称", "version": "版本号", "mode": "sync | async（默认模式）", "callback": { "required": true | false, "method": "POST | GET", "content_type": "application/json", "params": ["token", "channel"] }, "required_fields": ["real_name", "id_card"], "optional_fields": ["front_image", "back_image", "handheld_image"], "extra_fields": { "mobile": { "title": "手机号", "type": "text", "required": true, "show_when": {"verify_type": ["mobile_three", "bankcard_four"]}, "tip": "用于三要素/四要素验证" } }, "config": { "secret_id": { "title": "SecretId", "type": "text", "value": "", "tip": "API 密钥 ID" } } }extra_fields.show_when 说明show_when 用于条件显示额外字段，根据 config 中其他配置项的值来决定是否显示该字段。当 config.verify_type 的值为 mobile_three 或 bankcard_four 时，显示手机号字段。日志调试插件日志写入 public/plugins/certification/{plugin_name}/logs/ 目录下，按日期分文件。开启 app_debug=true 时自动写入。use app\common\service\PluginLogger; // 写入调试日志（仅在 app_debug=true 时输出） PluginLogger::write('myalitwo', '请求URL: ' . $url, 'debug'); // 强制写入（忽略调试模式开关） PluginLogger::force('myalitwo', 'HTTP状态码: ' . $httpCode);完整插件示例参考现有插件：插件目录特点人工审核manual异步、人工审核、支持图片上传阿里云二要素alitwo同步、姓名+身份证号核验三要素/四要素threehc同步、银行卡核验、支持多要素易付云edgeone混合模式、支持多种认证方式数据库表结构dd_idv_plugins（认证通道配置表）字段类型说明idint通道编号pluginvarchar(50)插件目录名vendorvarchar(50)厂商名称prioritytinyint优先级，越小越优先weighttinyint负载权重 0-100timeoutsmallint超时时间（秒）free_times_totalint免费次数fee_after_freeint超出后每次费用（分）daily_limitint每日最大调用次数，0 不限statusenumdisabled/enabledcustom_configtext插件自定义配置（JSON）dd_user_idv（用户认证快照）字段类型说明idbigint主键user_idbigint用户 IDchannelvarchar(20)认证通道（插件 ID）statusenumpending/passed/rejected/expirednamevarchar(64)真实姓名id_card_novarchar(20)身份证号verify_snvarchar(64)业务防重号reject_reasonvarchar(255)驳回原因extratext扩展字段（JSON）dd_user_idv_logs（认证流水）字段类型说明idbigint主键user_idbigint用户 IDchannelvarchar(20)认证通道statusenum审核状态namevarchar(64)真实姓名id_card_novarchar(20)身份证号reject_reasonvarchar(255)驳回原因ipvarchar(45)提交 IPuatext提交 UAcreated_atdatetime提交时间请求流程用户提交认证信息 │ ▼ Realname::submit() │ ├─ ChannelSelector::selectChannel() → 选择可用通道 ├─ 检查日限额、免费次数、扣费 ├─ $plugin->submit($params) → 调用插件 │ ├─ mode=sync → 写入最终状态（passed/rejected） └─ mode=async → 写入 pending，等待回调/轮询 用户查询认证结果 │ ▼ Realname::query() │ ├─ 已 passed → 直接返回 ├─ 已 rejected → 直接返回（含驳回原因） └─ 其他 → 调用插件 query() 查询上游 第三方服务完成认证 │ ▼ Realname::callback() │ ├─ $plugin->handleCallback() → 解析结果 ├─ 更新 Idv 状态 └─ 更新 user.verification.idv
新版服务器插件快速开发指南（注册式钩子）
面向插件开发者，帮你快速上手新版注册式钩子插件开发。参考脚手架：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已终止
目录结构public/template/clientarea/{模板标识}/ ├── {模板标识}.php # 模板配置文件 ├── clientarea.html # 仪表盘/首页 ├── login.html # 登录页面 ├── register.html # 注册页面 ├── hostlist.html # 产品列表 ├── host.html # 产品详情 ├── orders.html # 订单列表 ├── order.html # 订单详情 ├── addfunds.html # 充值页面 ├── balanceLog.html # 余额日志 ├── profile.html # 个人资料 ├── realname.html # 实名认证 ├── verified.html # 实名认证检测 ├── ticketlist.html # 工单列表 ├── ticket.html # 工单详情 ├── submitaticket.html # 提交工单 ├── common/ # 公共组件 │ ├── header.html # 页面头部 │ ├── footer.html # 页面底部 │ ├── meta.html # Meta 标签 │ ├── script.html # 脚本引用 │ ├── sidenav.html # 侧边导航 │ └── captcha.html # 验证码组件 └── assets/ # 静态资源（可选） ├── css/ ├── js/ └── images/路由规范路由对应模板说明/clientareaclientarea.html仪表盘/loginlogin.html登录/registerregister.html注册/clientarea/hostlisthostlist.html产品列表/clientarea/host/{id}host.html产品详情/clientarea/ordersorders.html订单列表/clientarea/order/{id}order.html订单详情/clientarea/addfundsaddfunds.html充值/clientarea/balanceLogbalanceLog.html余额日志/clientarea/profileprofile.html个人资料/clientarea/realnamerealname.html实名认证/clientarea/verifiedverified.html实名认证检测/clientarea/ticketlistticketlist.html工单列表/clientarea/ticket/{id}ticket.html工单详情/clientarea/submitaticketsubmitaticket.html提交工单/clientarea/page/{name}{name}.html自定义页面配置文件<?php function {模板标识}_MetaData() { return [ "DisplayName" => "用户中心模板", "APIVersion" => "1.0.0", "HelpDoc" => "http://example.com" ]; } function {模板标识}_ConfigOptions() { return [ [ 'key' => 'site_title', 'title' => '网站标题', 'type' => 'input', 'default' => '', 'options' => [], 'tip' => '显示在浏览器标签页', ], [ 'key' => 'footer', 'title' => '页脚内容', 'type' => 'textarea', 'default' => '', 'options' => [], 'tip' => '支持HTML', ], [ 'key' => 'theme', 'title' => '主题风格', 'type' => 'select', 'default' => 'light', 'options' => [ 'light' => '浅色', 'dark' => '深色', ], 'tip' => '选择配色方案', ], ]; }配置访问：{$config.clientarea_meta.xxx}字段类型：input（单行）、textarea（多行）、select（下拉）全局变量变量说明$site.name站点名称$site.favicon站点图标$site.version站点版本$config系统配置$user.id用户ID$user.nickname昵称$user.email邮箱$user.mobile手机号$user.avatar头像URL$user.money余额$Request.tokenCSRF Token页面变量速查clientarea.html（仪表盘）变量说明$runningServices运行中服务数$accountBalance账户余额$pendingTickets待处理工单数$weekConsumption近7天消费$securityStatus安全状态$recentServices最近服务列表$recentOrders最近订单列表hostlist.html（产品列表）变量说明$hosts产品列表（分页）$p.id产品ID$p.domain主机名/域名$p.status状态$p.product.product_name产品名称$p.priceMatrix续费周期价格$page当前页码$lastPage总页数host.html（产品详情）变量说明$host.id产品ID$host.domain主机名$host.status状态$host.nextduedate到期时间$host.product.product_name产品名称$host.product.billing_type计费类型$moduleButtons模块操作按钮$priceMatrix续费价格orders.html（订单列表）变量说明$orders订单列表$order.ordernum订单号$order.amount金额$order.status状态$order.type类型$order.product.product_name产品名称$page当前页码$lastPage总页数order.html（订单详情）变量说明$order.id订单ID$order.ordernum订单号$order.amount金额$order.status状态$order.create_time创建时间$order.pay_time支付时间$order.payment支付方式$order.product.product_name产品名称$pay_channel支付渠道列表ticketlist.html（工单列表）变量说明$tickets工单列表$ticket.id工单ID$ticket.tid工单编号$ticket.title标题$ticket.user_side_status用户侧状态$ticket.priority优先级$ticket.category.name部门名称$page当前页码$lastPage总页数ticket.html（工单详情）变量说明$ticket.id工单ID$ticket.tid工单编号$ticket.title标题$ticket.content内容$ticket.user_side_status用户侧状态$ticket.priority优先级$replies回复列表$reply.content回复内容$reply.is_staff是否为客服回复$reply.create_time回复时间profile.html（个人资料）变量说明$user.id用户ID$user.nickname昵称$user.email邮箱$user.mobile手机号$user.avatar头像URL$user.money余额$user.group.name用户组名称$user.create_time注册时间$securityStatus安全状态$countries国家/地区列表realname.html（实名认证）变量说明$realNameStatus认证状态$idv.name真实姓名$idv.id_card_no证件号码（脱敏）$idv.reject_reason拒绝原因$used已使用认证次数$plugin.free_times_total免费认证次数$plugin.fee_after_free超出免费次数后的费用addfunds.html（充值）变量说明$user.money当前余额$paymentMethods可用支付方式$minAmount最小充值金额$maxAmount最大充值金额$rechargeBonuses充值赠送规则balanceLog.html（余额日志）变量说明$user.money当前余额$increase较上月增加金额$logs余额变动记录$log.type类型$log.amount变动金额$log.before变动前余额$log.after变动后余额$log.remark备注模板语法 {$variable} {$variable|default='默认值'} {if $condition} ... {elseif $other} ... {else} ... {/if} {switch name="$variable"} {case value="a"}...{/case} {default}...{/default} {/switch} {foreach $list as $item} {$item.name} {/foreach} {volist name="list" id="item" key="k"} {$k} - {$item.name} {/volist} {include file="clientarea/{模板标识}/common/header" /} {php} $var = 'value'; {/php} {$var|htmlspecialchars} {$time|date='Y-m-d'}页面结构普通页面{php} $title='页面标题'; {/php} {include file="clientarea/{模板标识}/common/header" /} {include file="clientarea/{模板标识}/common/footer" /}登录/注册页面（独立页面） {$site.name|default='系统'} - 登录 {:token()} 登录 注册页面特殊标记： 常用 API 接口订单相关接口方法参数说明/api/order/channelsGET-支付方式列表/api/order/createRechargeOrderPOSTamount, pay_id创建充值订单/api/order/createRenewOrderPOSThost_id, period_key创建续费订单/api/order/payPOSTordernum, payment订单支付/api/order/cancelPOSTordernum取消订单主机/产品相关接口方法参数说明/api/host/controlPOSThost_id, action主机控制（boot/reboot/shutdown）/api/host/notesPOSThost_id, notes保存主机备注用户相关接口方法参数说明/api/user/profilePOSTnickname, bio 等更新用户资料/api/user/changepwdPOSToldpassword, newpassword修改密码/api/user/avatarPOSTavatar更新头像/index/user/loginPOSTaccount, password, captcha用户登录/index/user/registerPOSTusername, email, password 等用户注册/index/user/logoutPOST-退出登录工单相关接口方法参数说明/index/clientarea/submitaticketPOSTtitle, department, priority, description 等提交工单/clientarea/replyticketPOSTticket_id, content回复工单/clientarea/closeticketPOSTticket_id关闭工单附件/上传接口方法参数说明/index/attachment/uploadPOSTfile (multipart/form-data)文件上传调试{php} print_r($user); var_dump($order); {/php}附录Bootstrap Icons 常用图标图标用途bi bi-speedometer2仪表盘bi bi-server服务器/产品bi bi-wallet2财务/余额bi bi-gear设置bi bi-headset工单/客服bi bi-person用户bi bi-cart-plus购买bi bi-check-circle成功/完成bi bi-inbox空状态颜色参考颜色类名用途主色primary / #0d6efd主要操作、链接成功success / #198754成功状态、运行中警告warning / #ffc107警告状态、待处理危险danger / #dc3545错误、已暂停信息info / #0dcaf0提示信息
目录结构public/template/cart/{模板标识}/ ├── {模板标识}.php # 模板配置文件（必需） ├── cart.html # 商品列表页（必需） ├── product.html # 商品详情页（必需） ├── header.html # 公共头部（可选） ├── footer.html # 公共底部（可选） ├── xxx.html # 自定义页面（可选） └── assets/ # 静态资源（可选） ├── css/ ├── js/ └── images/核心文件说明1. {模板标识}.php<?php function {模板标识}_MetaData() { return [ "DisplayName" => "模板显示名称", "APIVersion" => "1.0.0", "HelpDoc" => "http://example.com" ]; } function {模板标识}_ConfigOptions() { return [ // 单行文本 [ 'key' => 'nav_title', 'title' => '导航标题', 'type' => 'input', 'default' => '', 'options' => [], 'tip' => '显示在导航栏的标题', ], // 下拉选择 [ 'key' => 'theme_color', 'title' => '主题颜色', 'type' => 'select', 'default' => 'blue', 'options' => [ 'blue' => '蓝色主题', 'red' => '红色主题', 'dark' => '深色主题', ], 'tip' => '选择站点配色风格', ], ]; } function {模板标识}_CartArea() { return [ // 单行文本 [ 'key' => 'product_slogan', 'title' => '产品标语', 'type' => 'input', 'default' => '', 'options' => [], 'tip' => '显示在商品名称下方的简短标语', ], // 下拉选择 [ 'key' => 'recommend_level', 'title' => '推荐等级', 'type' => 'select', 'default' => 'normal', 'options' => [ 'normal' => '普通', 'hot' => '热销', 'new' => '新品', ], 'tip' => '商品标签展示级别', ], ]; }配置访问：{$config.cart.xxx} - ConfigOptions 配置{$meta.xxx} - CartArea 扩展字段字段类型：类型说明input单行文本textarea多行文本select下拉选择2. 页面文件cart.html - 商品列表页路由：/cart可用变量：变量类型说明$siteArray站点信息$configArray模板配置$userArray当前用户$productsObject商品分页数据$topListArray商品分类树$groupIdInt当前分类ID$keywordString搜索关键词$sortString排序方式商品数据结构：$p = [ 'id' => 1, 'product_name' => '商品名称', 'product_desc' => '商品描述', 'billing_type' => 'cycle', // free/oneoff/cycle 'price_matrix' => [ 'first_original' => 100.00, 'first_discount' => 80.00, 'first_cycle_txt'=> '月', ], ];排序参数：值说明weigh综合排序（默认）new最新上架price_asc价格从低到高price_desc价格从高到低product.html - 商品详情页路由：/cart/product/{id}可用变量：变量类型说明$productArray商品详细信息$priceArray价格配置信息$metaArray商品扩展字段值$metaListArray商品配置项列表价格结构：$price = [ 'type' => 'cycle', // free/oneoff/cycle 'oneoff_original' => 100.00, 'oneoff_discount' => '80.00', 'cycles' => [ '1M' => 10.00, '3M' => 27.00, '6M' => 50.00, '1Y' => 90.00, ], 'cycles_discount' => [ '1M' => '', '3M' => '25.00', '6M' => '', '1Y' => '80.00', ], ];配置项结构：$metaList = [ [ 'meta_key' => 'os', 'title' => '操作系统', 'value_type' => 'select', // select/switch/text/number 'required' => 1, 'option_list' => ['CentOS', 'Ubuntu', 'Windows'], 'meta_value' => '', 'prompt' => '请选择操作系统', ], ];创建订单 API：fetch('/api/order/createProductOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: '商品ID', period_key: '周期标识（如：1M, 3M, oneoff, free）', meta: { 'os': 'CentOS', 'hostname': 'myserver' } }) }) .then(res => res.json()) .then(json => { if (json.code === 1) { location.href = json.data.jump_url; } else { console.log(json.msg); } });响应说明：code === 1：订单创建成功，data.jump_url 为跳转地址msg 包含 "手机认证"/"绑定手机"：需要绑定手机msg 包含 "实名认证"：需要实名认证4. 辅助文件header.html / footer.html引用：{include file='cart/{模板标识}/header' /}可用变量：$_h, $site, $config, $user, $topList自定义页面路由：/cart/page/{xxx} → xxx.html5. 开发规范文件命名必需：{模板标识}.php, cart.html, product.html小写字母，- 或 _ 分隔页面结构{php} $_h['title'] = '页面标题'; {/php} {include file='cart/{标识}/header' /} {include file="cart/{标识}/footer" /}资源引用路径：/template/cart/{标识}/assets/...CSS 放 header，JS 放 footer6. 模板语法 {$var} {if $condition}...{else}...{/if} {volist name="list" id="item"}...{/volist} {include file='cart/{标识}/header' /} {$var|htmlspecialchars|date='Y-m-d'}7. 调试{php}var_dump($product);{/php}
概述本文档定义了在 public/template/web/{模板名}/ 目录下开发首页及相关页面的规范。目录结构规范public/template/web/{模板标识名}/ ├── {模板标识名}.php # 模板配置文件（必须） ├── index.html # 首页模板（必须） ├── header.html # 页头组件（可选） ├── footer.html # 页脚组件（可选） ├── announcement.html # 公告列表页（必须 ├── announcement_detail.html # 公告详情页（必须） ├── xxx.html # 自定义页面（可选） ├── template.png # 模板预览图（建议 400x300） └── assets/ # 静态资源（可选，按需添加） ├── css/ ├── js/ ├── images/ └── fonts/注意：assets 目录不是必须的。如果使用外部 CDN 或系统自带资源，可以不创建。配置文件详解模板配置文件 {模板名}.php 必须包含以下三个函数：1. {模板名}_MetaData() - 模板元数据（必须）<?php function {模板名}_MetaData() { return [ "DisplayName" => "简洁首页", // 后台显示的名称 "APIVersion" => "1.0.0", // API版本号 "HelpDoc" => "http://www.sib.cc/" // 帮助文档链接（可选） ]; }2. {模板名}_ConfigOptions() - 首页配置字段（必须）定义首页模板后台可配置的字段，通过 $config.web_meta.xxx 访问。function {模板名}_ConfigOptions() { return [ // 单行文本 - 标题类 [ 'key' => 'hero_title', 'title' => '首页主标题', 'type' => 'input', 'default' => '', 'options' => [], 'tip' => '显示在首页Banner区域的主标题', ], // 单行文本 - 联系方式 [ 'key' => 'contact_email', 'title' => '联系邮箱', 'type' => 'input', 'default' => '', 'options' => [], 'tip' => '显示在联系我们页面的邮箱地址', ], // 多行文本 - 富文本内容 [ 'key' => 'welcome_content', 'title' => '欢迎语内容', 'type' => 'textarea', 'default' => '', 'options' => [], 'tip' => '支持HTML标签，显示在首页欢迎区域', ], // 多行文本 - 页脚代码 [ 'key' => 'footer_html', 'title' => '页脚自定义代码', 'type' => 'textarea', 'default' => '', 'options' => [], 'tip' => '支持HTML/JS代码，显示在页面底部', ], // 下拉选择 - 主题风格 [ 'key' => 'theme_style', 'title' => '主题风格', 'type' => 'select', 'default' => 'default', 'options' => [ 'default' => '默认风格', 'dark' => '深色模式', 'light' => '浅色模式', ], 'tip' => '选择站点整体配色方案', ], // 下拉选择 - 布局模式 [ 'key' => 'layout_mode', 'title' => '首页布局', 'type' => 'select', 'default' => 'standard', 'options' => [ 'standard' => '标准布局', 'full' => '全屏布局', 'compact' => '紧凑布局', ], 'tip' => '选择首页的整体布局风格', ], ]; }3. {模板名}_ArticleArea() - 文章配置字段（可选）定义文章详情页的自定义字段，通过 $config.article_meta.xxx 访问。function {模板名}_ArticleArea() { return [ [ 'key' => 'article_sidebar', 'title' => '文章侧边栏', 'type' => 'textarea', 'default' => ' 联系客服 ', 'tip' => '支持HTML', ], ]; }全局可用变量（所有页面共用）1. 站点信息 $site变量类型示例值$site['name']string"思博云"$site['title']string"思博云 - 云主机销售系统"$site['favicon']string"/favicon.ico"$site['description']string"专业云主机服务提供商"$site['keywords']string"云主机,VPS,服务器"2. 自定义配置配置来源访问方式说明ConfigOptions()$config.web_meta.xxx首页配置CartArea()$config.cart_meta.xxx购物车配置ArticleArea()$config.article_meta.xxx文章页配置3. 商品分类 $cates所有页面都可用的商品分类数组，用于展示分类导航。示例数据：$cates = [ [ 'id' => 1, 'name' => '云服务器', 'pid' => 0, // 0表示顶级分类 ], [ 'id' => 2, 'name' => '高防IP', 'pid' => 0, ], [ 'id' => 3, 'name' => '香港节点', 'pid' => 1, // 父分类ID为1（云服务器） ], [ 'id' => 4, 'name' => '美国节点', 'pid' => 1, ], ];页面详解1. index.html - 首页路由： /1.1 页面基础 SEO 变量变量名类型说明{$page_title}string页面标题，覆盖默认 $site.title{$page_keywords}stringSEO 关键词{$page_description}stringSEO 描述模板中定义：{php} $page_title = $site['title']; $page_keywords = $site['keywords']; $page_description = $site['description']; {/php}1.2 自定义配置变量从 $config 中读取的后台配置，后台「系统配置 → 自定义配置」中维护：配置来源访问方式说明ConfigOptions(){$config.web_meta.xxx}首页配置ArticleArea(){$config.article_meta.xxx}文章页配置1.3 动态内容数据公告列表（Announcements）变量名类型说明{$announcements}array首页展示的最新公告列表（通常取 3-6 条）数组元素结构：字段名类型说明{$a.id}int公告 ID{$a.title}string公告标题{$a.content}string公告内容（HTML 或纯文本）{$a.publish_time}string发布时间（格式：YYYY-MM-DD HH:ii:ss）{$a.views}int浏览次数{$a.category.name}string公告分类名称示例数据：$announcements = [ [ 'id' => 1, 'title' => '系统维护通知', 'content' => ' 将于今晚进行系统维护... ', 'publish_time' => '2025-01-15 10:00:00', 'views' => 128, 'category' => ['name' => '系统公告'], ], [ 'id' => 2, 'title' => '新年优惠活动', 'content' => ' 春节期间全场8折... ', 'publish_time' => '2025-01-10 09:00:00', 'views' => 256, 'category' => ['name' => '活动优惠'], ], ];模板中使用（PHP变量方式）：{foreach $announcements as $a} {$a.category.name} {$a.title} {:mb_substr(strip_tags($a['content']),0,40)}... {:date('Y-m-d', strtotime($a['publish_time']))} 浏览：{$a.views} 查看详情 {/foreach}或通过 JS API 获取： 1.4 全局辅助变量变量名类型说明{$user}object当前登录用户信息，未登录为 null{$user.username}string用户名{$user.email}string用户邮箱使用逻辑：{if $user} 访问控制台 欢迎，{$user.username} {else} 免费试用 立即登录 {/if}2. announcement.html - 公告列表页路由： /announcementURL 参数：?cate={id} - 按分类筛选?page={n} - 分页，默认第1页专属变量：变量类型示例数据$listarray文章列表数组$categoryarray公告分类列表$cateMaparray分类ID映射表$currentCateIdstring/int'all' 或 1$pageint1$lastPageint5示例数据：// $list - 文章列表 $list = [ [ 'id' => 1, 'title' => '系统维护通知', 'content' => ' 将于今晚进行系统维护... ', 'publish_time' => '2025-01-15 10:00:00', 'views' => 128, ], [ 'id' => 2, 'title' => '新年优惠活动', 'content' => ' 春节期间全场8折... ', 'publish_time' => '2025-01-10 09:00:00', 'views' => 256, ], ]; // $category - 公告分类 $category = [ ['id' => 1, 'name' => '系统公告', 'nickname' => '', 'description' => '', 'keywords' => ''], ['id' => 2, 'name' => '活动优惠', 'nickname' => '优惠活动', 'description' => '最新优惠活动', 'keywords' => '优惠,活动'], ]; // $cateMap - 分类映射 $cateMap = [ 1 => ['id' => 1, 'name' => '系统公告', ...], 2 => ['id' => 2, 'name' => '活动优惠', ...], ];3. announcement_detail.html - 公告详情页路由： /announcementdetail/{id}专属变量：变量类型示例数据$articlearray当前文章详情$cateNamestring'系统公告'$prevarray/null上一篇或null$nextarray/null下一篇或null示例数据：// $article - 当前文章 $article = [ 'id' => 1, 'title' => '系统维护通知', 'content' => ' 尊敬的用户： 将于今晚进行系统维护... ', 'publish_time' => '2025-01-15 10:00:00', 'views' => 128, 'tags' => '维护,通知,系统', ]; // $cateName - 分类名称 $cateName = '系统公告'; // $prev - 上一篇（可能为null） $prev = [ 'id' => 0, // 如果是第一篇，id可能为0 'title' => '无', ]; // 或 $prev = null; // 没有上一篇 // $next - 下一篇（可能为null） $next = [ 'id' => 2, 'title' => '新年优惠活动', ]; // 或 $next = null; // 没有下一篇4. 自定义页面 xxx.html路由规则： /page/{文件名}例如创建 about.html，访问地址为 /page/about可用变量：所有全局变量（$site, $config.web_meta, $cates）自定义页面无专属变量系统页面路由汇总路由对应文件说明/index.html首页/cart系统内置商品列表/购物车/cart?group={id}系统内置按分类筛选商品/announcementannouncement.html公告列表/announcement?cate={id}announcement.html按分类筛选公告/announcementdetail/{id}announcement_detail.html公告详情/page/{name}{name}.html自定义页面模板语法1. PHP 代码块{php} $page_title = $site['title']; {/php}2. 变量输出{$variable} {$variable|default='默认值'}3. 条件判断{if condition="$user"} 欢迎，{$user.username} {else /} 登录 {/if}4. 循环{foreach $announcements as $a} {$a.title} {/foreach}5. 包含文件{include file='web/{模板名}/header' /} {include file='web/{模板名}/footer' /}静态资源引用规范使用本地 assets（可选） 使用外部 CDN 开发 checklist[ ] 创建模板目录 public/template/web/{模板名}/[ ] 创建配置文件 {模板名}.php[ ] 实现 MetaData() 函数（必须）[ ] 实现 ConfigOptions() 函数（必须）[ ] 实现 CartArea() 函数（可选）[ ] 实现 ArticleArea() 函数（可选）[ ] 函数命名与目录名完全一致（区分大小写）[ ] 创建 index.html 首页模板[ ] 创建 announcement.html 公告列表页[ ] 创建 announcement_detail.html 公告详情页[ ] （可选）创建自定义页面如 about.html[ ] 添加 template.png 预览图（建议 400x300）[ ] 测试所有配置字段在后台正常显示[ ] 测试各页面前端正常渲染注意事项函数命名必须与目录名一致，包括大小写三个配置函数：MetaData() 和 ConfigOptions() 必须实现，CartArea() 可选ConfigOptions 中的 key 要唯一，建议使用前缀避免冲突assets 目录可选，不需要时可以不创建template.png 建议尺寸 400x300，用于后台预览路径使用绝对路径，以 /template/web/ 开头所有页面都可用 $cates 展示商品分类导航
$site 全局配置变量作用域：全站模板可用（首页模板、购物车模板、会员中心模板） 语法：{$site.字段名} 或 {$site['字段名']} 数据来源：后台「系统配置」各分组设定值站点基础信息变量名类型说明示例输出{$site.name}string站点名称思博业务系统{$site.domain}string站点根地址http://sib.cc{$site.favicon}string站点图标路径/uploads/xxx.png{$site.beian}stringICP备案号京ICP备xxx号{$site.cdnurl}stringCDN域名https://cdn.xxx.com{$site.timezone}string时区Asia/Shanghai{$site.version}int版本时间戳1769933723SEO 与页面元数据变量名类型数据来源说明{$site.title}string后台 → 系统配置 → Seo设置 → 标题站点默认标题，通常用于首页 标签{$site.keywords}string后台 → 系统配置 → Seo设置 → 关键词默认 SEO 关键词，多个关键词用英文逗号分隔{$site.description}string后台 → 系统配置 → Seo设置 → 描述默认站点描述，建议控制在 80-120 字符会员与认证配置变量名类型数据来源可选值说明{$site.user_register_captcha}string后台 → 系统配置 → 会员配置 → 注册验证方式email / mobile / none新用户注册时的验证方式{$site.global_idv}string后台 → 系统配置 → 会员配置 → 强制实名认证0 / 1是否全局强制要求实名认证{$site.global_phone}string后台 → 系统配置 → 会员配置 → 强制手机绑定0 / 1是否全局强制要求绑定手机号IDC 业务核心配置变量名类型单位数据来源业务含义{$site.renew_before}string天后台 → 常规配置 → 计划任务 → 计划任务设置到期前 X 天开始发送续费提醒通知{$site.pause_before}string天后台 → 常规配置 → 计划任务 → 计划任务设置到期后 X 天自动暂停服务（置为暂停状态）{$site.keep_days}string天后台 → 常规配置 → 计划任务 → 计划任务设置暂停后保留数据的宽限期，超期可删除{$site.purge_after}string天后台 → 常规配置 → 计划任务 → 计划任务设置产品删除后等待 X 天再从物理上清除数据$user 用户变量使用前提：仅在用户登录状态下有效，未登录时为 null。使用前必须先判断 {if $user}。基础信息变量名类型示例值说明{$user.id}int1用户ID{$user.username}string"admin"用户名（登录账号）{$user.nickname}string"帅哥1是"用户昵称{$user.email}string"king@sib.cc"邮箱地址{$user.mobile}string"17699122121"手机号{$user.avatar}string"http://.../avatar.jpg"头像URL账户状态变量名类型示例值说明{$user.status}string"normal"账户状态：normal=正常，disabled=禁用{$user.level}int0用户等级{$user.group_id}int1用户组ID{$user.agent_id}int1代理商ID财务信息变量名类型示例值说明{$user.money}string"574.01"账户余额{$user.score}int0积分个人资料变量名类型示例值说明{$user.gender}int1性别：0=未知，1=男，2=女{$user.birthday}string"2017-04-04"生日{$user.bio}string"7777s"个人简介登录记录变量名类型示例值说明{$user.logintime}int1769750628最后登录时间（时间戳）{$user.loginip}string"127.0.0.1"最后登录IP{$user.prevtime}int1769398867上次登录时间（时间戳）{$user.successions}int1连续登录天数{$user.maxsuccessions}int2最大连续登录天数注册信息变量名类型示例值说明{$user.jointime}int1491635035注册时间（时间戳）{$user.joinip}string"127.0.0.1"注册IP{$user.createtime}int0创建时间{$user.updatetime}int1769865594更新时间验证状态变量名类型示例值说明{$user.verification}string{"email":1,"mobile":0,"idv":1}验证状态JSON字符串注意事项$site 变量注意事项时区一致性：{$site.timezone} 影响所有时间格式化输出，确保与服务器时区配置一致CDN 路径拼接：使用 {$site.cdnurl} 拼接静态资源路径时，注意去除重复斜杠，建议写法：{$site.cdnurl}/static/xxx版本戳缓存：{$site.version} 用于强制刷新浏览器缓存，更新后自动变化，可用于 CSS/JS 文件后缀 ?v={$site.version}生命周期配置单位：所有 IDC 配置项（renew_before、pause_before 等）虽为字符串类型，但业务逻辑中按整数天数处理，确保填写纯数字$user 变量注意事项必须先判断是否存在：使用前必须用 {if $user} 判断，直接访问未登录用户变量会导致模板报错或空值异常时间字段为时间戳：logintime、jointime、prevtime 等时间字段为 Unix 时间戳（秒级），显示时需用 {:date('Y-m-d', $user.xxx)} 格式化money 为字符串类型：余额字段为字符串类型展示，进行数值计算时建议先转为 float 类型避免精度问题verification 需 JSON 解析：验证状态字段为 JSON 字符串，模板中可直接用对象语法访问（如 $user.verification.email），但需确保数据格式合法敏感字段已过滤：password、salt、token 等敏感字段已在控制器层过滤，不会传递到前端模板，即使强制访问也为空值状态值常量定义：status 字段除了 normal 和 disabled，还可能存在 pending（待审核）等自定义状态，判断时建议使用 {if $user.status != 'normal'} 而非仅判断 disabled