# Ywxapp 插件应用开发指南 > 适用框架:Ywxapp(基于 ThinkPHP 8 的多应用框架) > 本文以 `mqttbroker` 插件为实例,说明从脚手架到打包上线的完整流程。 --- ## 1. 概述 Ywxapp 的「插件」本质上就是一个 **ThinkPHP 多应用(Multi-App)**: - 插件源码目录:`addon//` - 插件命名空间:`addon\\...` - 访问 URL 前缀:`//...`(例如 `/mqttbroker/backend/index`) 核心支撑类: | 类 / 文件 | 职责 | | --- | --- | | `ywxapp\addon` | 插件基类,定义 `install()` / `uninstall()` 抽象钩子 | | `ywxapp\service\AddonService` | 安装 / 卸载 / 打包 / 开发者模式 | | `ywxapp\service\AppService` | 框架 boot 时扫描 `addon/`,加载已启用插件的事件、中间件、服务 | | `config/addon.php` | 插件机制全局配置(路径、优先级、白名单等) | 一个插件 = 一个自带路由、控制器、模型、视图、配置、命令、静态资源的独立应用。 --- ## 2. 插件目录结构 以 `mqttbroker` 为例: ``` addon/mqttbroker/ ├── Addon.php # 安装/卸载钩子(必须) ├── info.php # 插件元信息(必须) ├── menu.json # 后台/会员/前台菜单定义 ├── install.sql # 建表/种子 SQL(安装时自动导入) ├── common.php # 应用公共文件(随应用自动加载,可选) ├── config.php # 设置表单字段定义(可选,仅当插wophp 文件, # 统一在 info.php 的 events / middleware / services 数组里声明(详见 3.1)。 # - config.php 非必须:无可配置项的插件直接不建即可。 ├── controller/ # 控制器:admin/、api/ ... ├── model/ # 模型 ├── service/ # 业务服务类 ├── protocol/ # 自定义协议(如 MQTT) ├── view/ # 视图模板(backend/admin/*.html) ├── route/ │ └── app.php # 插件路由 └── static/ # 静态资源(打包时自动迁出到 public/static/addon/mqttbroker/) ``` --- ## 3. 核心文件详解 ### 3.1 `info.php`(元信息,必须) ```php return [ 'name' => 'mqttbroker', // 小写字母,唯一标识,须与目录名一致 'title' => 'MQTT代理', 'intro' => '...', 'author' => '', 'website' => '', 'version' => '2.1.0', // 语义化版本 x.y.z 'state' => 1, // 0=禁用 1=启用(由框架读取控制加载) 'url' => '/mqttbroker/admin', 'license' => '', // 付费插件可声明 license=>true,配合 addon_license_check 'config' => [], // boot 时已合并进容器(见 3.5) 'events' => [], // 随应用加载:app()->loadEvent(...) 'middleware' => [], // 随应用加载:app()->middleware->import(...) 'services' => [], // 随应用加载:app()->bind(...) ]; ``` - 框架 `AppService::loadAddonRelevant()` 在 boot 期扫描 `addon/`,仅对 `state=1` 的插件加载其 `events/middleware/services`。 - 安装流程会写入 `install_time`,并管理 `state`。 > **事件 / 中间件 / 服务在哪里声明?** 统一在 `info.php` 对应数组里填写(见下方示例)。虽然 ThinkPHP 的 MultiApp 在分发到该插件应用时也会自动加载 `event.php` / `middleware.php` / `provider.php`,但插件体系的**推荐入口是 `info.php`**:它在 boot 期由 `AppService::loadAddonRelevant()` 读取,对所有启用插件生效,且不依赖当前请求是否命中该插件(时机早、行为一致)。因此插件**无需创建** `event.php` / `middleware.php` 文件。 > - `events`:事件监听映射,如 `['MqttClientConnect' => \addon\mqttbroker\listener\Connect::class]`(等价于 `Event::listen`)。 > - `middleware`:要注册进应用中间件栈的类,如 `[\addon\mqttbroker\middleware\MqttAuth::class]`。 > - `services`:服务容器绑定(TP 服务类),如 `['mqttBroker' => \addon\mqttbroker\service\Broker::class]`。 > > 没有要注册的就保留空数组 `[]`(你的 `mqttbroker` 当前即如此)。这些数组仅在 `state=1`(启用)时才会被加载。 ### 3.2 `Addon.php`(钩子,必须实现 install/uninstall) ```php ⚠️ **重要事实**:`enable/disable` 在 `ywxapp\addon` 中**不是**抽象方法;框架 `AddonService::enable()/disable()` 仅切换 `info.php` 的 `state` 值,**不会**回调插件的 `enable()/disable()` 钩子。因此不要把"启用/禁用时需要执行的逻辑"写在钩子里,菜单显隐完全由 `state` 决定。 ### 3.3 `install.sql`(建表,自动导入) 安装时由 `AddonService::importsql()` 自动执行,规则如下: - **白名单限制**:仅允许 `CREATE TABLE` 与 `INSERT` 语句;`SET NAMES` / `SET FOREIGN_KEY_CHECKS` 等会被跳过(仅记日志)。 - **表命名**:`wxapp_<插件名>_*`,与核心表前缀保持一致。 - **前缀替换**:SQL 中的 `__PREFIX__` 会被替换为数据库实际前缀(若直接写死 `wxapp_` 也可,但需与数据库配置一致)。 - **建议全部用 `CREATE TABLE IF NOT EXISTS`**,便于重跑。 - **破坏性语句(DROP / ALTER / DELETE)必须放进 `Addon::uninstall()`,绝不能写进 install.sql**(白名单会拒绝执行)。 示例: ```sql CREATE TABLE IF NOT EXISTS `wxapp_mqttbroker_connection` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `client_id` varchar(191) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `idx_client_id` (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MQTT客户端连接表'; ``` ### 3.4 `menu.json`(菜单,自动注入) ```json { "backend": [ { "name": "mqttbroker", "title": "MQTT代理", "icon": "fa fa-sitemap", "type": 1, "sort": 60, "status": 1, "child": [ { "name": "mqttbroker/index", "title": "概览", "icon": "fa fa-dashboard", "type": 2, "sort": 1, "route": "/mqttbroker/backend/index" }, { "name": "mqttbroker/stats", "title": "实时监控", "type": 2, "sort": 8, "route": "/mqttbroker/backend/stats" } ] } ], "member": [], "frontend": [] } ``` 字段说明: - 顶层三类:`backend`(后台)、`member`(会员中心)、`frontend`(前台)。 - 每项:`name`(权限标识,首个菜单须与插件标识同名)、`title`、`icon`、`type`(1=菜单 2=按钮)、`sort`、`route`(实际访问 URL)、`child`(子项)。 - 框架会把 `name` 处理为 `::`,写入 `wxapp_admin_power`(后台)或 `wxapp_user_rule`(会员/前台)。 - 安装时 `createMenu()` 注入;卸载/重装时先清理旧菜单再重建(保证幂等)。 ### 3.5 `config.php`(后台设置表单,**可选**) > **重要:`config.php` 不是必须文件,框架也不会自动读取/渲染它。** 它只是一份「设置表单字段定义」,是否生效完全取决于插件**自己有没有实现一个后台设置页**去 `include` 它。 > - 若插件**没有可配置项**(如 `wxchat`),就**不要创建** `config.php`(早期脚手架生成的 `['status'=>true]` 属于无效占位,已清理)。 > - 若插件**需要后台可配置**(如 `mqttbroker`),则用「字段描述符数组」格式编写,并在插件内自建设置页读取它(见下方"如何真正用起来")。 ```php return [ [ 'name' => 'port', 'title' => '监听端口', 'type' => 'number', 'value' => '1883', 'tip' => 'Broker 监听的 TCP 端口', ], [ 'name' => 'ssl_enabled', 'title' => '启用TLS', 'type' => 'select', 'options' => ['0' => '关闭', '1' => '开启'], 'value' => '0', ], // ... ]; ``` **字段格式**:每项为一个描述符 `['name'=>标识, 'title'=>显示名, 'type'=>控件类型, 'value'=>默认值, 'tip'=>提示, 'options'=>下拉项]`。`type` 常用 `string / number / select / textarea`。 **如何真正用起来(以 `mqttbroker` 为例)**:`config.php` 是「设置表单字段定义」,框架统一把配置以**独立项**写入数据库表 `wxapp_addon_config`(`addon`+`name` 唯一,每行一个配置项),并配合缓存(键 `addon_config_`,默认 1 小时)。读取/保存统一走 `AddonService::config()`: 1. **后台通用配置页 + 列表快捷入口**:后台「插件管理」列表页(`app/backend/view/backend/addon/index.html`)每行带「配置」按钮(仅对含 `config.php` 的插件显示),直达通用页(`app/backend/controller/addon.php::setting`)。通用页自动按 `config.php` 渲染表单、AJAX 保存写库并清缓存。**无需每个插件自建页面**,没有 `config.php` 的插件提示「无可配置项」。 2. 插件也可自建设置页(如 mqttbroker 的 `controller/backend/MqttBroker.php::setting/saveSetting`),只需调用: ```php $def = include ADDON_PATH . 'mqttbroker' . DIRECTORY_SEPARATOR . 'config.php'; $saved = \ywxapp\service\AddonService::config('mqttbroker'); // 读数据库配置(带缓存) // 保存:\ywxapp\service\AddonService::config('mqttbroker', input('post.')); ``` 3. 运行期读取:业务代码用 `AddonService::config($name)`、基类 `getConfig()` 或助手 `get_addon_config($name)` 取值。建议业务默认值在代码里用 `array_merge($defaults, $saved)` 叠加,保证未配置时使用默认值(`config.php` 的 `value` 仅作表单初始显示)。 **三条容易混淆的配置入口,务必区分**: | 入口 | 存储位置 | 加载时机 | 用途 | | --- | --- | --- | --- | | `config.php` + 数据库 | `wxapp_addon_config`(独立项,每行一个配置) | 后台通用配置页或插件自建设置页调用 `AddonService::config()` | 管理员可视化配置(端口、开关等),带缓存 | | `info.php` 的 `config`/`events`/`middleware`/`services` | `info.php` | boot 期由 `AppService` 加载 | 事件/中间件/服务注册 | | `config/` 目录下的 `*.php` | 各配置文件 | MultiApp 分发到该插件时加载 | 标准 TP 应用级配置 | > 另:框架基类 `ywxapp\addon::getConfig()` 也能读 `config.php`(同样合并 `.addonrc`),但仅当你的类继承自它时可用;`mqttbroker` 的做法是直接 `include` + `AddonService::config()`,二者等效,按需选一即可。 ### 3.6 `route/app.php`(路由) ```php ` 即应用 ``,URL 前缀为 `//`。 - 控制器引用格式:`layer/Controller/action` → 对应文件 `controller//.php`(如 `backend/MqttBroker/index` → `controller/backend/MqttBroker.php::index()`)。 - 同目录下的 `common.php` 会随应用自动加载。 - ⚠️ 插件的事件监听、中间件、服务容器绑定**推荐通过 `info.php` 的 `events` / `middleware` / `services` 数组声明**(boot 期由 `AppService::loadAddonRelevant()` 读取,见 3.1),而**不要**依赖 `event.php` / `middleware.php` 文件——后者仅由 MultiApp 在请求命中该插件应用时才分发加载,时机较晚且与插件统一加载机制不一致。所以插件**无需创建**这两个文件;要注册就写进 `info.php`,不需要则留空数组 `[]`。 ### 3.7 控制器 / 模型 / 视图 / 服务 - 命名空间: - 后台控制器:`addon\\controller\admin`,继承 `ywxapp\controller\AddonBackend` - API 控制器:`addon\\controller\api`,继承 `ywxapp\controller\ApiController` - 模型:`addon\\model\Xxx` - 服务:`addon\\service\Xxx` - 鉴权开关(开发期可放开): ```php protected $noNeedLogin = ['*']; protected $noNeedVerify = ['*']; ``` - 视图:后台模板放 `view/backend/admin/*.html`,用 `$this->fetch('admin/index')` 渲染(路径前缀由框架视图配置决定)。 ### 3.8 `command/`(命令行 / 常驻进程) ```php // config/console.php 注册 "mqttbroker:start" => \addon\mqttbroker\command\MqttBroker::class, ``` - 命令行放在 `command/`,在 `config/console.php` 注册。 - 长驻进程(如 Broker)基于 Workerman:命令内 `Worker::runAll()`。 - Windows 与 Linux 行为差异:单 PHP 进程无法多 `Worker` 实例,需用 `DIRECTORY_SEPARATOR === '\\'` 做平台降级(如 WS 监听仅 Linux 实网生效)。 --- ## 4. 插件生命周期 ``` 框架启动 (AppService::boot) └─ loadAddonRelevant(): 扫描 addon/,对 state=1 的插件加载 events/middleware/services └─ 加载 ywxapp/helper.php(提供 addon_url / hook / get_addon_* 等全局函数) 安装 (addon:manage -a develop 或 后台离线/在线安装) └─ executeInstall(): 写 info(state=0) → install 钩子 → 注入菜单 → importsql (离线/在线安装默认 state=0,需手动启用;addon:manage -a develop 直接置 state=1) 启用 / 禁用 (后台 toggle) └─ enable()/disable() 钩子(可重写,返回 false 阻断)→ 再修改 info.php 的 state 默认实现会联动 Menu::enable/disable 同步菜单可见性(见 Addon.php 生成模板) 升级 (后台升级) └─ upgrade($currentVersion) 钩子(可重写,用于数据/配置迁移)→ 清理配置缓存 卸载 (后台卸载) └─ uninstall() 钩子(DROP 表 / 清菜单)→ 删除插件目录 ``` > 生命周期钩子统一由 `AddonService::callAddonHook($method)` 调用插件主类 > `addon\\Addon` 的同名方法(`install`/`uninstall`/`enable`/`disable`/`upgrade`)。 > 基类 `ywxapp\addon` 已为 `enable`/`disable`/`upgrade` 提供默认空实现,**只有 `install`/`uninstall` 仍是抽象方法,必须实现**。 --- ## 4.1 钩子体系(可插拔扩展点) 直接复用 ThinkPHP 自带的事件机制,**无需框架额外封装**: - 业务侧在关键节点用 ThinkPHP 内置全局助手 `event('事件名', $params)` 触发(即 `Event::trigger` 的全局封装,无需额外封装)。 - 插件侧在 `info.php['events']['listen']` 中声明监听器即可挂接,框架 boot 期 `loadEvent` 会自动注册,无需改框架源码。 ```php // addon//info.php 'events' => [ 'listen' => [ 'user_login_after' => [\addon\xxx\listener\UserLogin::class], ], ], ``` ```php // addon//listener/UserLogin.php namespace addon\xxx\listener; class UserLogin { public function handle($user) { // $user 为登录成功的会员模型实例 } } ``` 业务侧在关键节点预埋触发点(完整清单): ### 会员 / 认证(`ywxapp/library/User.php`) | 事件名 | 触发时机 | 参数 | 代码位置 | | --- | --- | --- | --- | | `user_register_after` | 会员注册成功 | 会员模型实例 `$user` | `User::register()` 事务提交前 | | `user_login_after` | 会员/API 登录成功 | 会员模型实例 `$user` | `User::login()` 末尾 | | `user_logout_after` | 会员登出成功 | 会员模型实例 `$user` | `User::logout()` 末尾 | | `user_changepwd_after` | 会员修改密码成功 | 会员模型实例 `$user` | `User::changepwd()` 事务提交前 | > 监听器 `handle($user)` 收到的 `$user` 均为 `ywxapp\model\User` 实例。 ### 内容 / 文章(`app/api/controller/v1/Article.php`) | 事件名 | 触发时机 | 参数 | 代码位置 | | --- | --- | --- | --- | | `article_create_after` | 文章创建成功 | 文章模型实例 `$article` | `Article::create()` 成功后 | | `article_update_after` | 文章更新成功 | 文章模型实例 `$article` | `Article::update()` 保存后 | | `article_delete_after` | 文章删除成功 | 文章模型实例 `$article` | `Article::delete()` 删除后 | ### MQTT 消息(`addon/mqttbroker/service/Broker.php`) | 事件名 | 触发时机 | 参数(数组) | 代码位置 | | --- | --- | --- | --- | | `mqtt_message_published` | 客户端发布消息被 Broker 接受并分发 | `['topic'=>string, 'payload'=>string, 'qos'=>int, 'retain'=>bool, 'clientId'=>string]` | `Broker::handlePublish()`(qos0/1)与 `Broker::handlePubRel()`(qos2)| > 说明:`$SYS/` 开头的内部指标主题**不会**触发该事件(避免每秒指标造成噪声); > qos2 消息在 PUBREL 确认后才真正分发,故事件在 `handlePubRel` 中触发,保证每一条消息仅触发一次。 ### 全局异常(`ywxapp/handler/ExceptionHandle.php`) | 事件名 | 触发时机 | 参数 | 代码位置 | | --- | --- | --- | --- | | `app_exception` | 应用抛出异常 | `['exception'=>\Throwable, 'ignoreReport'=>bool]` | `ExceptionHandle::report()` | | `app_exception_report` | 异常进入上报流程 | 同上 | `ExceptionHandle::report()` | ### 插件市场 / 订单(`app/api/controller/v1/Addon.php`) | 事件名 | 触发时机 | 参数(数组) | 代码位置 | | --- | --- | --- | --- | | `order_paid` | 插件购买订单支付完成(已签发授权 + 写入开发者分红账本) | `['order'=>订单模型, 'aid'=>int 插件id, 'uid'=>int 用户id]` | `Addon::completeOrder()` 末尾 | > 插件可监听此钩子实现"发货 / 开通权益 / 发送通知"等购买后联动。 ### 插件生命周期(`ywxapp/service/AddonService.php`) | 事件名 | 触发时机 | 参数(数组) | 代码位置 | | --- | --- | --- | --- | | `addon_install_after` | 插件安装成功(已写 info 状态、注入菜单、导入 SQL) | `['name'=>string 插件标识, 'info'=>array 插件信息]` | `AddonService::executeInstall()` 提交后 | | `addon_uninstall_after` | 插件卸载完成(目录与数据表已清理) | `['name'=>string 插件标识]` | `AddonService::uninstall()` 末尾 | | `addon_enable_after` | 插件被启用 | `['name'=>string 插件标识]` | `AddonService::enable()` 末尾 | | `addon_disable_after` | 插件被禁用 | `['name'=>string 插件标识]` | `AddonService::disable()` 末尾 | > 注意:与插件主类自身的 `install()/uninstall()/enable()/disable()` 生命周期方法(由 `callAddonHook` 调用)不同, > 上述 `*_after` 是**全局事件**——任意插件均可监听以对"其它插件"的状态变化做出联动。 > 新增业务钩子点只需在对应位置调用 `event('your_event', $data)` 即可, > 插件侧用同名事件名在 `info.php['events']['listen']` 中监听,无需任何注册代码。 --- ## 4.2 辅助函数层(`ywxapp/helper.php`) 由 `AppService::register()` 在框架启动时自动加载,全局可用: | 函数 | 作用 | | --- | --- | | `addon_url($name, $url='', $vars=[])` | 生成插件访问地址 `//`(修复了原先未定义的致命 bug) | | `get_addon_instance($name)` | 获取插件主类实例 | | `get_addon_info($name, $force=false)` | 读取插件元信息(优先缓存) | | `get_addon_config($name, $force=false)` | 读取插件配置(优先缓存) | | `get_appmarket_addon_list($onlyEnabled=false)` | 扫描插件目录,返回元信息列表 | > 事件触发统一使用 ThinkPHP 自带的 `event('事件名', $params)` 助手,无需框架额外封装。 --- ## 5. 开发者模式(本地边改边测)⭐ ### 痛点 当你的插件源码**直接放在 `addon//`** 本地改代码时,框架的「压缩包安装」流程要求目标目录**不存在**才允许安装(`validateAddonInfo` 会抛 "Addon already exists")。结果是:目录在、代码能跑,但 `install.sql`(建表)和 `menu.json`(菜单)**从未执行过**,卡在中间态,无法测试。 ### 解决方案:`addon:manage -a develop` 命令 ```bash php think addon:manage mqttbroker -a develop ``` 它在**原地**执行标准安装步骤,**不需要先打包 zip**: 1. 置 `info.php` 的 `state = 1`(直接启用) 2. 调用 `install` 钩子 3. 清理旧菜单 → 重新注入 `menu.json` 4. 执行 `importsql()`(建表) **幂等、可反复执行**:改了 `install.sql` 或 `menu.json` 后,再跑一次即可生效(`CREATE TABLE IF NOT EXISTS` + 菜单先清后建,不会报错)。 > 开发环境若需跳过远程授权校验:设置 `app_debug=1` 且 `ywxapp.unknownsources=1`(`AddonService::valid()` 会直接放行)。 --- ## 6. 打包与发布上线 ### 本地打包 通过**后台「插件管理 → 打包」按钮**(调用 `AddonService::package()`),生成: ``` addon/temp/mqttbroker-2.1.0.zip ``` 打包内容 = 整个插件目录 + `public/static/addon//`(若存在)。注意:打包时 `static/` 会被迁出到 `public/static/addon//` 并删除插件目录内的 `static` 副本。 ### 上传到服务器 1. 服务器后台「插件管理 → 离线安装」,上传该 zip。 2. 服务器侧 `local()` 流程:校验 → 解压到 `addon//` → `install` 钩子 → 注入菜单 → `importsql()`。 3. 安装后默认 `state=0`(禁用),在后台**启用**插件即可。 ### 注意事项 - 离线安装要求服务器上 `addon/` **不存在**;重装需先卸载。 - `install.sql` 白名单限制同上(仅 `CREATE TABLE` / `INSERT`)。 - 静态资源访问路径:`/static/addon//...`。 --- ## 7. 完整开发流程(实例) ```bash # 1. 脚手架(或手写目录结构) php think addon:make mqttbroker -a build # 2. 编写 info.php / Addon.php / install.sql / menu.json / config.php / route/app.php / 控制器 / 模型 # 3. 开发者模式:建表 + 注入菜单 + 启用(只需一次,之后改 SQL/菜单再跑) php think addon:manage mqttbroker -a develop # 4. 启动服务 / 调试 php think mqttbroker:start # 5. 迭代:改业务代码即时生效; # 改了 install.sql 或 menu.json → 再跑一次 addon:manage -a develop # 6. 打包 # 后台「插件管理 → 打包」→ 下载 mqttbroker-2.1.0.zip # 7. 部署:服务器后台离线安装 → 启用 ``` --- ## 8. 常用命令速查 | 命令 | 作用 | | --- | --- | | `php think addon:manage -a develop` | 开发者模式安装(建表+菜单+启用,免打包,可重跑) | | `php think addon:make -a build` | 生成插件脚手架(统一命令 `addon:make`,`-a` 指定动作) | | `php think addon:make -a clear` | 清理插件缓存/临时文件 | | `php think addon:make -a controller --name=<类>` | 生成控制器(另有 `--api`/`--plain`) | | `php think addon:make -a model --name=<类>` | 生成模型 | | `php think addon:make -a --name=<类>` | 其余生成器:`middleware`/`validate`/`event`/`listener`/`service`/`subscribe` | | (后台按钮)打包 | 生成 `-.zip` | | `php think mqttbroker:start` | 示例插件:启动 Broker(具体命令名随插件自定义) | --- ## 9. 常见坑 1. **install.sql 只允许 CREATE TABLE / INSERT**;DROP / ALTER / DELETE 必须放 `Addon::uninstall()`。 2. **enable/disable 不触发插件钩子**,仅切换 `info.php` 的 `state`。 3. **表前缀**:install.sql 用 `__PREFIX__` 或固定 `wxapp_`,需与数据库配置一致。 4. **路由控制器引用格式**:`layer/Controller/action` 对应 `controller//.php`。 5. **卸载会 DROP 表且删除目录**,生产环境操作前务必备份。 6. **本地改了 install.sql 没生效**:忘了重跑 `addon:manage -a develop`(它幂等,放心跑)。 7. **Windows 下多 Worker / WebSocket 仅 Linux 实网可验**:用 `DIRECTORY_SEPARATOR === '\\'` 做平台降级。 8. **事件/中间件/服务不要写进 `event.php` / `middleware.php` 文件**:插件体系通过 `info.php` 的 `events` / `middleware` / `services` 数组声明(boot 期由 `AppService` 加载),插件目录里**无需创建**这两个文件;命令行则放进 `command/` 并在 `config/console.php` 注册(不能省略)。 9. **`config.php` 是可选的,框架不会自动读它**:只有插件自建后台设置页去 `include` 它才生效(见 3.5)。没有可配置项就别建(`['status'=>true]` 这类占位无意义,会误导)。默认值别忘了与运行期代码里的默认值保持一致。