# 插件机制对比:Discuz! vs YwxApp > 配套文档:《插件机制对比与差距分析》(对标 FastAdmin)、《插件开发指南》、《FastAdmin 插件机制参考》。 > 本文聚焦 **Discuz! X(含应用中心/云平台)** 与本项目 **YwxApp** 的插件全生命周期对比,并给出"购买/授权闭环"落地方案。 > 编写日期:2026-07-24。 --- ## 0. 一句话结论 | | Discuz! X | YwxApp(本项目) | | --- | --- | --- | | **插件本质** | `/source/plugin//` 脚本集合,挂主程序运行 | `addon//` 是**完整独立的 ThinkPHP 多应用**(命名空间 `addon\`,自带 route/config/event/middleware) | | **扩展范式** | 模板/源码预埋 `` 点 + 同名函数类(AOP 静态织入) | ThinkPHP Event 事件监听 + `info.php['events']` 声明(运行时触发) | | **市场/分发** | 官方强中心化云平台(应用中心),一键云端推送 | 自研 `market`/`upgrade` 服务端(提交/审核/下载/校验)+ 离线包 | | **商业化** | 完整电商:购买→云端授权绑定站点→安装 | `license` 授权校验模型为主,**下单/支付/退款闭环不明显** | **总体**:你的路由/隔离/配置存储/升级自动化**优于** Discuz!;但在**钩子开箱即用性**与**商业化交易闭环**上仍有差距。注意:**购买/授权闭环其实已约 80% 实现**(见 §5),剩下的是"中心站建表缺失 + 无站点绑定 + 无退款",属补齐而非从零。 --- ## 1. Discuz! 插件全流程 ### 1.1 开发:目录 + XML 声明式 ``` /source/plugin// ← 目录名=唯一标识 ├── plugin.php / 各类脚本 ├── .class.php ← 页面嵌入脚本(普通版) ├── .inc.php ← 程序链接 / 扩展项目 ├── install.php / uninstall.php / upgrade.php / enable.php / disable.php ├── template/ table/ cron/ adv/ ... ``` - **元信息/结构全写在 `plugin.xml`**(导出 XML):含 `version / title / intro / author / modules / pluginvars(变量) / installfile / uninstallfile / upgradefile / license` 等节点。 - **模块三类**:程序链接(前台菜单挂接)、扩展项目(嵌入系统原有界面)、程序脚本(独立逻辑 / 页面嵌入)。 - **钩子(核心机制)= 页面嵌入点(AOP 静态嵌入)**: - 系统源码/模板预埋 `` 点;插件定义类 `plugin_` / `plugin__`,写**同名方法**即自动在该点输出返回值。 - 全局钩子(`common()`、`discuzcode()`、`deletethread()`…)、脚本钩子(`xxx_output()`)、自定义钩子(`CURMODULE_USERDEFINE[_output]()`)。 - 这是 DZ 插件"不改动核心源码即扩展"的根本——**模板/源码预埋点 + 同名函数**,成熟稳定。 - 所有脚本首行必须 `if(!defined('IN_DISCUZ')) exit('Access Denied');` ### 1.2 上架:官方云平台(应用中心) - 开发者在**应用中心开发者平台**上传作品,设「免费/收费」并填价格。 - 经平台审核后进入市场列表(推荐/排行/搜索)。 - 站长的站点需**绑定云平台账号**(站点唯一标识),这是后续授权校验的基础。 ### 1.3 购买(收费插件) > 官方文档原话:"如果是收费插件,需要您**购买后才可以进行安装**。" - 站长先在应用中心**付费购买**(钱给开发者)。 - 购买后云端把**授权与站点绑定**,安装时平台校验该站点是否拥有此插件的授权。 - 授权形态是**平台侧云端记录 + 站点绑定**(早期有授权码 `md5(md5(domain).license)` 模式)。 ### 1.4 安装 / 卸载 / 升级(一键云端) - **安装**:后台点「安装」→ 云端**推送文件 + 自动部署**(无 FTP/压缩包手工解压)→ 执行 `install.php` 里的 `runquery()` 建表(表名写 `cdb_` 前缀占位)→ 输出 `$finish=TRUE`。 - **启用**:安装后仍需站长**手动启用**才生效。 - **卸载**:`uninstall.php`(用 `runquery()` 删自定义表);系统**自动移除** plugin.xml 中声明的后台菜单模块。X3.1 起卸载不再支持页面跳转。 - **升级**:`upgrade.php` 可用 `$fromversion/$toversion` 判断分支。 - **开关**:X3.1+ 还有 `enable.php`/`disable.php` 脚本钩子(`runquery()` 可用)。 --- ## 2. YwxApp 插件全流程(现状) ### 2.1 开发:MultiApp 独立应用 ``` addon// ├── Addon.php # 基类钩子 install()/uninstall() 抽象 + enable/disable/upgrade 可重写 ├── info.php # 元信息(PHP 数组) ├── menu.json # 三端菜单(后台/会员/前台) ├── install.sql # 建表,安装时自动导入 ├── route/app.php # 站内路由(框架外包 前缀) ├── route/api.php # 对外 API(/api/*,经 app/api/route/route.php 加载) ├── config.php / controller/ / model/ / view/ / static/ ``` - **元信息 `info.php`**:`name/title/version/state/license/events/middleware/services`。`state=1` 才被 `AppService::loadAddonRelevant()` 加载。 - **`Addon.php` 基类**(`ywxapp/addon`):`install()`/`uninstall()` 抽象;`enable()/disable()/upgrade()` 空实现可重写,由 `AddonService` 回调。 - **钩子机制 = ThinkPHP Event 事件监听**:在 `info.php['events']` 声明 listener 类,boot 期 `loadEvent` 注册;业务点用 `event()` 触发(已埋约 30 个钩子点)。 - **菜单**:`createMenu()` 读 `menu.json` → 写 `admin_power`(后台)/ `user_rule`(前台/会员),幂等。 ### 2.2 上架 / 市场(自研服务端) - `market` 插件部署在**中心站(插件服务器)**,提供:市场列表 / 下载 / 授权校验 / **开发者提交与审核**(`POST /api/addon/submit` + `DEVELOPER_TOKEN`)。 - `upgrade` 插件部署在中心站,提供主框架在线升级(版本列表/核心包下载)。 - 客户端通过 `config ywxapp.api_url` 连接中心站。 - 表:`wxapp_appmarket_addon_list` / `wxapp_appmarket_addon_submissions` / `wxapp_appmarket_developers` / `wxapp_appmarket_addon_revenues` / `wxapp_appmarket_addon_withdrawals`。 ### 2.3 购买 / 授权 - 服务端 `addon/appappmarket/controller/api/Market.php` 已实现:**`buy()` 下单 + `notify()` 支付回调 + `completeOrder()` 签发 license + `valid()` 安装校验**。 - 接 `Yansongda\Pay`(支付宝/微信),`config/pay.php` 含 `mock_enable` 直购模式(自托管友好)。 - 客户端 `app/api/controller/v1/Addon.php` + `ywxapp/service/RemoteService.php` 远程代理;`AddonOrder`/`AddonLicense`/`AddonDownloadLog` 模型。 ### 2.4 安装 / 卸载 / 升级 - **安装**:`package/local`(离线)+ 在线下载;`install.sql` 自动导入;`createMenu()` 建菜单;`install()` 钩子可做额外初始化。 - **卸载**:`AddonService::uninstall()` → 框架统一 `deleteMenu()` 清菜单(**修复点:必须 `destroy($ids,true)` 物理删除,否则 `AdminPower`/`UserRule` 软删除残留**)+ 插件 `uninstall()` 钩子清理自建表。 - **升级**:`AddonService::onlineUpgrade()` 已实现(下载→备份→覆盖→`upgrade()` 钩子→增量 SQL)。 --- ## 3. 核心对比表 | 维度 | Discuz! | YwxApp(你) | 谁更强 | | --- | --- | --- | --- | | **插件本质** | `/source/plugin//` 脚本集合,挂主程序 | `addon//` **完整独立 TP 多应用**(自带 route/config/event) | ✅ 你:隔离更彻底 | | **元信息** | `plugin.xml`(XML 声明式) | `info.php`(PHP 数组,含 events/middleware/services) | ✅ 你:更灵活、可编程 | | **扩展/钩子机制** | **模板/源码预埋 `` + 同名函数类**(AOP 静态嵌入,极成熟) | **TP Event 事件监听** + `info.php` 声明(运行时触发,需主动埋点) | 🔶 各有所长:DZ 嵌入点开箱即用,你更现代但依赖埋点 | | **前后台菜单** | plugin.xml 声明模块,系统自动建/拆 | `menu.json` + 框架 `createMenu/deleteMenu` | ✅ 对等(你区分三端更细) | | **安装脚本** | `install.php` + `runquery()` 执行 SQL | `install.sql` 自动导入 + `install()` 钩子 | 🔶 对等,你更"约定优于配置" | | **卸载清理** | `uninstall.php` 开发者自选删表 + 系统拆菜单 | 框架统一 `deleteMenu` + `uninstall()` 钩子 | ✅ 你更一致(软删除坑已修:须物理删除) | | **升级** | `upgrade.php` 按 `$from/$to` 分支 | `onlineUpgrade()`(下载/备份/覆盖/钩子/增量 SQL) | ✅ 你:全程自动化更完整 | | **上架/市场** | 官方强中心化**云平台**(审核+分发+绑定) | 自研 `market`/`upgrade` 服务端(提交/审核/下载/校验) | 🔶 你功能对等但生态弱(无官方流量/评级) | | **购买/收费** | 应用中心**完整电商**:购买→云端授权绑定站点→安装 | `buy/notify/completeOrder/valid/refund` 已实现;中心站建表 + 站点绑定 + 退款 **已于 2026-07-24 落地** | ✅ 已对齐(可在 §6 见落地记录) | | **安装方式** | **一键云端推送**(无 FTP/压缩包) | 离线包 + 在线下载覆盖 | 🔶 你支持离线(私有化友好),DZ 纯云端 | | **授权绑定** | 站点↔云平台账号绑定(云端记录) | `valid()` 已支持**站点/域名绑定**(首次安装绑定、之后仅同域名),`appmarket_addon_licenses.domain/site_id` 落库 | ✅ 已对齐 DZ(见 §6) | --- ## 4. 关键差异与可借鉴点 1. **钩子范式不同,但你的更现代** DZ 的"`` 预埋点 + 同名函数"是**编译期/模板期静态织入**,插件作者几乎零配置即可挂到任意页面位置;你的"Event 事件监听"是**运行时触发**,更解耦、更可控,但需要框架方在业务点主动埋 `event()`。建议:把常用钩子点清单固化进文档(已做约 30 个),并保持"埋点即文档"。 2. **卸载清理:你的框架统一处理更省心,但刚踩了雷** DZ 卸载靠开发者 `uninstall.php` 自觉删表 + 系统拆菜单;你由框架 `deleteMenu()` 统一清菜单 + 插件 `uninstall()` 钩子清表,**一致性更好**。但 `AdminPower`/`UserRule` 启用软删除,普通 `delete()` 只软删→菜单残留(已修:`destroy($ids,true)` 物理删除)。**DZ 用原生 SQL 物理删,你用模型易踩软删坑**——已修复。 3. **购买/授权闭环:主体已建,补齐三项即对齐 DZ(详见 §5)** 订单表 + 支付回调 + license 签发 + 收益分成**都已实现**;真正缺的是:① 中心站 `appmall/install.sql` 漏建 `appmarket_addon_orders`/`appmarket_addon_download_logs`/`appmarket_addon_licenses`(客户端靠 `ensureTables()` 自愈,中心站不触发);② 授权只绑 `uid` 未绑站点域名(DZ 核心能力);③ 无退款/吊销。 4. **云端一键安装 vs 离线包** DZ 纯云端推送;你支持离线包(利于私有化/内网)。若要追平"一键装",可强化 `onlineUpgrade` 的在线安装路径,并把 `market` 的下载接口与客户端安装流程打通。 --- ## 5. 购买 / 授权闭环:现状盘点与落地方案 ### 5.0 已实现(实测代码,非从零) 服务端入口 `addon/appappmarket/controller/api/Market.php`,接口契约:`/api/addon/*`(由 `app/api/route/route.php` 在 api 应用阶段 include 插件 `route/api.php` 加载): | 方法 | 作用 | 状态 | | --- | --- | --- | | `lists()` | 市场列表(含 `price`) | ✅ | | `info()` | 详情 + `purchased` 标记 | ✅ | | `submit()` | 开发者提交(待审核) | ✅ | | `buy()` | 下单:写 `appmarket_addon_orders`(status=0);有商户号走 `Yansongda\Pay` 支付宝/微信(H5/web),无则 `pay.mock_enable` 直购 | ✅ | | `notify()` | 异步回调:验签→金额校验→`completeOrder()` | ✅ | | `orderStatus()` | 轮询订单状态 | ✅ | | `payResult()` | 同步落地页(return_url) | ✅ | | `completeOrder()` | 订单置已付 + 向 `appmarket_addon_licenses` 签发 `license_key` + 写 `appmarket_addon_revenues`(按 `appmarket_commission_rate` 抽成)+ 开发者 `balance` 累加 | ✅ | | `valid()` | 安装前授权校验:免费直过;付费查 `appmarket_addon_licenses`(uid+aid) 且未过期 | ✅(仅 uid 维度) | | `index()` | 下载 zip(支持签名校验) | ✅ | 客户端配套:`app/api/controller/v1/Addon.php`(`ensureTables()` 运行时自愈建表)、`ywxapp/service/RemoteService.php`(远程代理)、`AddonOrder`/`AddonLicense`/`AddonDownloadLog` 模型、`config/pay.php`(含 mock)。 ### 5.1 真实缺口(对齐 DZ 前必须补) 1. **中心站 `appmall/install.sql` 缺建表(最高优先级,急性 bug)** `Market.php` 跑在中心站,但其 `install.sql` 只建 `appmarket_addon_list / appmarket_addon_submissions / appmarket_developers / appmarket_addon_revenues / appmarket_addon_withdrawals`,**缺 `appmarket_addon_orders` / `appmarket_addon_download_logs` / `appmarket_addon_licenses`**。 客户端靠 `app/api/controller/v1/Addon.php::ensureTables()` 在"首次访问客户端 Addon 控制器"时自愈建表,但**中心站走 `Market.php`,该自愈不触发** → 全新部署的中心站调用 `buy()/notify()/下载` 会报「表不存在」。 (`appmarket_addon_licenses` 在 `public/install/install.sql` 与 `docs/appappmarket_schema.sql` 有定义,但中心站若以插件方式独立维护则仍可能缺。) 2. **无站点 / 域名绑定(DZ 核心能力缺失)** `appmarket_addon_licenses.domain` 列存在但**从未写入**;`valid()` 只按 `uid` 校验 → 同一账号可在无限站点安装。DZ 是"授权绑定到具体站点域名"。 3. **无退款 / 吊销** 无 `/api/addon/refund`;订单 `status` 仅 0/1(无 2 退款/3 关闭);license 无吊销;收益无冲正。DZ 支持退款回收授权。 4. **`docs/appappmarket_schema.sql` 与 `appmall/install.sql` 不一致** 前者缺 `appmarket_addon_orders / appmarket_addon_download_logs / appmarket_addon_revenues / appmarket_addon_withdrawals / appmarket_addon_submissions / appmarket_developers`;需统一为唯一权威 schema(供 `docs/split_install_sql.py` 校验)。 ### 5.2 落地 A:补齐中心站建表(最优先,止血) 在 `addon/appappmarket/install.sql` 追加(字段与客户端 `ensureTables()` 对齐,`status` 扩展退款/关闭态): ```sql CREATE TABLE IF NOT EXISTS `wxapp_appmarket_addon_orders` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `uid` int unsigned NOT NULL DEFAULT 0, `aid` int unsigned NOT NULL DEFAULT 0 COMMENT 'appmarket_addon_list.id', `amount` decimal(10,2) NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0=待支付 1=已支付 2=已退款 3=已关闭', `trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '商户订单号', `pay_time` int unsigned DEFAULT 0, `create_at` int unsigned DEFAULT 0, `update_at` int unsigned DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_uid` (`uid`), KEY `idx_trade` (`trade_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件购买订单表'; CREATE TABLE IF NOT EXISTS `wxapp_appmarket_addon_licenses` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `uid` int unsigned NOT NULL COMMENT '用户ID', `aid` int unsigned NOT NULL COMMENT '对应 appmarket_addon_list.id', `site_id` int unsigned NOT NULL DEFAULT 0 COMMENT '绑定站点(站点授权)', `license_key` varchar(64) NOT NULL COMMENT '授权码', `domain` varchar(255) DEFAULT NULL COMMENT '绑定域名(站点授权)', `expire_time` int unsigned DEFAULT 0 COMMENT '0 为永久', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1=有效 0=吊销', `download_count` int DEFAULT 0 COMMENT '下载次数限制', `create_at` int DEFAULT 0, `update_at` int DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `license_key` (`license_key`), UNIQUE KEY `uk_uid_aid_site` (`uid`,`aid`,`site_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件授权表'; CREATE TABLE IF NOT EXISTS `wxapp_appmarket_addon_download_logs` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `license_id` int unsigned NOT NULL DEFAULT 0, `uid` int unsigned NOT NULL DEFAULT 0, `aid` int unsigned NOT NULL DEFAULT 0, `ip` varchar(45) DEFAULT '', `create_at` int unsigned DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_license` (`license_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件下载日志表'; ``` 并在 `Market.php` 加与客户端一致的 `ensureTables()` 防御(首调用自愈),避免旧中心站漏表。 同时将 `docs/appappmarket_schema.sql` 补全为包含全部 8 张表的唯一权威 schema。 ### 5.3 落地 B:站点 / 域名授权绑定(对齐 DZ) - 新增 `wxapp_appmarket_sites`(中心站注册的客户端站点):`site_token` / `domain` / `owner_uid` / `status`。 - 客户端安装 `valid()` 时**携带 `domain` 与 `site_token`**;`Market::valid()` 改造: 1. 免费 → 放行(仍写一条 `site_id` 绑定,便于统计); 2. 付费 → 查 `appmarket_addon_licenses` 且 `site_id/domain` 匹配且 `status=1` 且未过期。 - `completeOrder()` 签发 license 时写入 `site_id`/`domain`(建议下单即采集 `domain`)。 - 达到"一个授权码只在一个站点生效",与 DZ「站点↔授权绑定」一致。 ### 5.4 落地 C:退款 / 吊销 - 新增 `POST /api/addon/refund {order_sn, rtoken}`: - 订单 `status=2`;对应 license `status=0`(吊销); - 收益冲正:`appmarket_addon_revenues` 标记退款,开发者 `balance` 扣回 `income`(或转 `frozen_balance` 待审); - 幂等:已退款直接返回成功。 - `valid()` 对 `status=0` 的 license 返回"授权已吊销"。 ### 5.5 与 DZ 对齐对照(补齐后) | DZ 能力 | 你现状 | 补齐后 | | --- | --- | --- | | 下单 | `buy()` ✅ | ✅ | | 支付(支付宝/微信) | `notify()` + Yansongda ✅ | ✅ | | 授权绑定站点域名 | ❌(仅 uid) | ✅(site_id/domain) | | 安装前校验 | `valid()` ✅ | ✅(含域名) | | 退款 | ❌ | ✅ | | 开发者分成 | `revenues`+`balance` ✅ | ✅ | | 一键云端安装 | 离线包 + 在线下载 | 在线下载 ✅ | ### 5.6 实施顺序 1. **补 `appmall/install.sql` 三表 + `Market.php::ensureTables()`**(止血,最高优先)。 2. 统一 `docs/appappmarket_schema.sql` 为权威 8 表。 3. 站点绑定(`wxapp_appmarket_sites` + `valid`/`completeOrder` 改造)。 4. 退款 / 吊销端点。 5. 前端:下单页 + 安装前域名校验接入 `AddonService::install()`。 > 完成后,你的商业化能力将与 DZ 应用中心对齐(购买→授权绑定站点→安装校验→退款吊销),同时保留离线/私有化优势。 --- ## 6. 落地记录(2026-07-24,已实施) 上述 §5.2–§5.4 方案已综合落地,改动文件清单: | 文件 | 改动 | | --- | --- | | `addon/appappmarket/install.sql` | **A**:补齐 `wxapp_appmarket_addon_licenses`(含 `site_id`/`status`)、`wxapp_appmarket_addon_orders`(含 `status` 0/1/2 退款态、`site_id`)、`wxapp_appmarket_addon_download_logs`、`wxapp_appmarket_sites` 四表 | | `docs/appappmarket_schema.sql` | **A**:重写为权威 9 表 schema(与 install.sql、客户端 ensureTables 对齐),收益账本加 `status=2 退款冲正` | | `addon/appappmarket/controller/api/Market.php` | **A/B/C**:新增 `ensureTables()` 自愈并接入 `buy/notify/valid/refund`;`valid()` 加**站点/域名绑定**(首次安装绑定、之后仅同域名、已吊销拒绝);新增 `refund()`(订单置 2 + license 吊销 + 收益冲正,幂等) | | `addon/appappmarket/route/api.php` | **C**:注册 `POST /api/addon/refund` | | `app/api/controller/v1/Addon.php` | **B/C**:`ensureTables()` 防御补列 `appmarket_addon_licenses.status/site_id`;`download()` 加授权吊销校验;新增 `refund()` 本地退款/吊销 + `remoteRefund()` 代理 | | `ywxapp/service/RemoteService.php` | **C**:新增 `refund()` 代理中心站 | | `ywxapp/service/AddonService.php` | **B**:`prepareInstallParams()` 注入当前站点 `domain`,供中心站 `valid()` 做绑定判定 | ### 6.1 落地后行为 - **购买闭环**:新装中心站不再报「表不存在」(install.sql + ensureTables 双保险)。 - **站点绑定(对齐 DZ)**:用户购买后首次安装某付费插件时,中心站把 `appmarket_addon_licenses.domain` 绑定到当前站点域名;之后换站点安装同一授权被拒。免费插件不受影响。 - **退款**:`POST /api/v1/addon/refund`(本地或远程)将订单置 `status=2`、授权吊销(`status=0` 且 `expire_time=now`,`download/valid` 双重拦截)、未结算收益扣回开发者余额,幂等。 - **兼容旧库**:客户端 `ensureTables()` 用 `ensureColumn` 给已存在的 `appmarket_addon_licenses` 补 `status`/`site_id` 列;服务端 `ensureTables()` 在表缺失时自愈,均不破坏旧结构。 ### 6.2 远程下载严格校验(2026-07-24 落地) - 中心站 `Market::index()` 下载端点已加**纵深防御**:请求带 `uid` 时校验该用户已购买、授权未吊销、未过期、且绑定域名匹配(免费插件与不带 `uid` 的 sign 模式维持原白名单,向后兼容)。 - 客户端 `Addon.php::remoteDownload` 经 `RemoteService::downloadBinary($name,$version,$uid,$domain)` 透传当前用户与站点域名(注入 `$request->domain()`)。 - 注:客户端 `remoteDownload` 原本已用中心站 `info()` 的 `purchased` 做购买校验,此次为中心站侧再加一道授权校验。 ### 6.3 后台商店页退款按钮 + 安装前域名提示(2026-07-24 落地) - 新增后台 `addon::refund(name)`:按插件名退本地最新「已支付」订单,吊销授权 + 订单置 `status=2` + 收益冲正(与会员端 `/api/v1/addon/refund` 逻辑一致)。**仅本地模式**(`appmarket_remote_enable=false`,默认);远程模式订单在中心站、属会员 uid,后台无会员信息,返回提示「请于会员中心申请退款」。 - `public/static/backend/modules/appmall.js`:已安装且付费插件卡片加「退款」按钮;付费插件安装前 `layer.confirm` 弹出域名授权提示(显示当前 `location.host`)。 - 会员端退款接口 `/api/v1/addon/refund`(上一轮落地)保留,供会员中心/H5 前端使用。 ### 6.4 插件前台「我的插件」页 + 远程退款闭环(2026-07-24 落地) - 新增中心站 `Market::my()`(`GET /api/addon/my`,`authRemote` 保护):列出指定 `uid` 的已购付费插件(appmarket_addon_licenses 联 appmarket_addon_list + 最近订单),返回 `license_status`/`expire_time`/`domain`/`order_id`/`trade_no`/`refundable`。路由 `addon/appappmarket/route/api.php` 注册 `addon/my`。 - `ywxapp/service/RemoteService.php` 新增 `my($uid)` 代理 `/api/addon/my`。 - 客户端 `app/api/controller/v1/Addon.php` 新增 `mine()`(本地读本地库 / 远程代理 `remoteMine()`),与 `refund()` 同一控制器,复用登录态。 - **应用商店 / 我的插件 = 核心后台「插件管理」(非 market 插件)**:用户明确——**客户机不安装 market 插件**,其所需的「应用商店(浏览中心站市场并一键安装)+ 我的插件(本机已装管理)」直接写在核心 `app\backend\controller\addon.php`:`market()`(服务端经 `RemoteService::lists()` 渲染,`/backend/addon/market`)+ `my()`(列本机已装,`/backend/addon/my`);视图 `app/backend/view/addon/{market,my}.html`;`app/backend/view/addon/index.html` 已挂「应用市场 / 我的插件」按钮。market 插件因此**只保留中心站运营职能**(`menu.json` 的 `store` 组已删除,仅剩 `market`(应用市场管理)/`revenue`(收益与结算) 两个 `centerOnly` 组)。早期 store 曾误放 `controller/` 走公开前台 `/appmall/store`、后又放 market 插件 `controller/backend/Store.php`,现已统一迁入核心后台。 - 远程模式退款闭环:付费插件购买/退款仍由会员中心 `/api/v1/addon/*`(含 `mine`/`refund`,本地/远程自动切换)承载;后台「我的插件」页(核心 addon::my)展示本机已装插件,完整升级/启停/卸载见「插件管理」列表。 - **market 插件菜单挂载到「插件管理」**:`menu.json` 顶层 `"superior": "addon:index"`(核心「插件管理」菜单 id=8,见 `public/install/install.sql:158`),使 market 插件后台菜单整体挂到「插件管理」下;仅 `market`/`revenue` 两个 `centerOnly` 组(应用市场管理 / 收益与结算)。改完 `menu.json` 须重导菜单:`php think addon:manage appmall -a refresh-menu`。 - **客户机隐藏中心运营菜单**:`menu.json` 给 `应用市场管理`(`market`) 与 `收益与结算`(`revenue`) 两组加 `"centerOnly": true`。`AddonService::saveMenus()` 递归里新增过滤——当 `config('ywxapp.appmarket_remote_enable')=true`(本机作为远程客户端连接中心站,即客户机)时,跳过 `centerOnly` 项及其子树;中心站(`appmarket_remote_enable=false`,默认) 正常显示全部。该过滤在 `createMenu()` 内,**install/enable/refresh-menu 三入口共用**,故重导菜单 `php think addon:manage appmall -a refresh-menu` 即可生效(客户机须先设 `appmarket_remote_enable=true`)。另 `getOrCreateAddonRootMenuId()` 在客户机下把根菜单标题里的「(服务端)」字样剥离(`插件应用市场(服务端)`→`插件应用市场`),避免误导;角色切换后重导菜单即按新环境重建。(注:客户机不安装 market 插件,上述菜单/路由均不加载;拦截仅作「万一安装」兜底。) - **前台开发者中心同样仅中心站**:独立前台应用 `controller/developer/Developer.php`(路由 `/appmall/developer/*`,注册/令牌/收益/提现)依赖 `appmarket_developers`/`appmarket_addon_revenues`/`appmarket_addon_withdrawals` 运营表,客户机不持有,故**双层拦截**:① 路由层 `route/app.php` 的 `Route::group('developer')` 内 `if (Config::get('ywxapp.appmarket_remote_enable', false)) return;` 使客户机整组不注册(访问 404);② 控制器 `initialize()` 运行时兜底 `if (config('ywxapp.appmarket_remote_enable', false)) abort(404);`(防路由缓存陈旧/直连)。客户机若开过路由缓存须 `php think route:clear`。全项目无指向 `/appmall/developer` 的入口链接,不会出现死链。 - **服务中心对外 API 仅中心站(关键)**:`addon/appappmarket/route/api.php` 提供 `/api/addon/*`(lists/valid/submit/info/buy/orderStatus/notify/payResult/refund/my)与 `/api/index`(下载 zip),均操作中心站运营表,且 `/api/addon/notify` 接收支付平台回调写订单。但 `app/api/route/route.php` 对所有 `state=1` 插件的 `route/api.php` 无条件 include → **客户机也会注册这套 API**,可能写出脏数据/越权下载。已在 `route/api.php` 顶部 `if ((bool) Config::get('ywxapp.appmarket_remote_enable', false)) return;` 整文件跳过注册(客户机访问 404)。路由缓存陈旧时须 `php think route:clear`。注:`Market` 控制器无统一基类、仅经路由可达,故路由层拦截即足够(无需控制器级兜底)。 ### 6.5 待办(非阻断,可选) - 可选的 `site_token` 上报与 `wxapp_appmarket_sites` 落库逻辑(当前仅用 `domain` 字符串绑定,已满足 DZ 等价能力)。