Files
YwxAppThink/docs/插件机制对比_Discuz.md

320 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件机制对比:Discuz! vs YwxApp
> 配套文档:《插件机制对比与差距分析》(对标 FastAdmin)、《插件开发指南》、《FastAdmin 插件机制参考》。
> 本文聚焦 **Discuz! X(含应用中心/云平台)** 与本项目 **YwxApp** 的插件全生命周期对比,并给出"购买/授权闭环"落地方案。
> 编写日期:2026-07-24。
---
## 0. 一句话结论
| | Discuz! X | YwxApp(本项目) |
| --- | --- | --- |
| **插件本质** | `/source/plugin/<identifier>/` 脚本集合,挂主程序运行 | `addon/<name>/` 是**完整独立的 ThinkPHP 多应用**(命名空间 `addon\<name>`,自带 route/config/event/middleware |
| **扩展范式** | 模板/源码预埋 `<hook>` 点 + 同名函数类(AOP 静态织入) | ThinkPHP Event 事件监听 + `info.php['events']` 声明(运行时触发) |
| **市场/分发** | 官方强中心化云平台(应用中心),一键云端推送 | 自研 `market`/`upgrade` 服务端(提交/审核/下载/校验)+ 离线包 |
| **商业化** | 完整电商:购买→云端授权绑定站点→安装 | `license` 授权校验模型为主,**下单/支付/退款闭环不明显** |
**总体**:你的路由/隔离/配置存储/升级自动化**优于** Discuz!;但在**钩子开箱即用性**与**商业化交易闭环**上仍有差距。注意:**购买/授权闭环其实已约 80% 实现**(见 §5),剩下的是"中心站建表缺失 + 无站点绑定 + 无退款",属补齐而非从零。
---
## 1. Discuz! 插件全流程
### 1.1 开发:目录 + XML 声明式
```
/source/plugin/<identifier>/ ← 目录名=唯一标识
├── plugin.php / 各类脚本
├── <module>.class.php ← 页面嵌入脚本(普通版)
├── <module>.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 静态嵌入)**:
- 系统源码/模板预埋 `<hook>` 点;插件定义类 `plugin_<identifier>` / `plugin_<identifier>_<CURSCRIPT>`,写**同名方法**即自动在该点输出返回值。
- 全局钩子(`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/<name>/
├── Addon.php # 基类钩子 install()/uninstall() 抽象 + enable/disable/upgrade 可重写
├── info.php # 元信息(PHP 数组)
├── menu.json # 三端菜单(后台/会员/前台)
├── install.sql # 建表,安装时自动导入
├── route/app.php # 站内路由(框架外包 <plugin> 前缀)
├── 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/<id>/` 脚本集合,挂主程序 | `addon/<name>/` **完整独立 TP 多应用**(自带 route/config/event | ✅ 你:隔离更彻底 |
| **元信息** | `plugin.xml`XML 声明式) | `info.php`PHP 数组,含 events/middleware/services | ✅ 你:更灵活、可编程 |
| **扩展/钩子机制** | **模板/源码预埋 `<hook>` + 同名函数类**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 的"`<hook>` 预埋点 + 同名函数"是**编译期/模板期静态织入**,插件作者几乎零配置即可挂到任意页面位置;你的"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 等价能力)。