chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
# 插件机制对比: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 等价能力)。