重构 monorepo 并完善网页端订阅与首页体验

- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-08 18:17:39 +08:00
parent 5814b7bc0e
commit 88c6ce8ccc
511 changed files with 189528 additions and 22804 deletions
+201
View File
@@ -0,0 +1,201 @@
# 列表分页 API 契约
本文描述 `backend-web` 公开端(`/api/v1`)与管理端(`/api/admin`)列表接口的分页约定。实现以仓库内 PHP 控制器为准。
## 通用响应包装
所有 JSON 接口统一:
```json
{
"ok": true,
"data": { }
}
```
失败时 `ok: false`,含 `error`(机器码)与 `message`(人类可读文案)。
## 推荐分页形态(page / size / total
**新接口与前端管理列表优先使用此形态。**
### 请求 Query
| 参数 | 类型 | 默认 | 上限 | 说明 |
|------|------|------|------|------|
| `page` | int | `1` | — | 从 1 开始 |
| `size` | int | 见各接口 | 见各接口 | 每页条数 |
### 响应 `data`
| 字段 | 类型 | 说明 |
|------|------|------|
| `items` | array | 当前页记录 |
| `total` | int | 符合条件的总条数(用于算总页数) |
| `page` | int | 当前页码(回显) |
| `size` | int | 当前每页条数(回显) |
总页数:`ceil(total / size)``total === 0` 时视为 0 条、1 页。
### 示例
```http
GET /api/admin/users?page=2&size=20&q=test%40local.test
Authorization: Bearer <admin_token>
```
```json
{
"ok": true,
"data": {
"items": [ { "id": 1, "email": "..." } ],
"total": 42,
"page": 2,
"size": 20
}
}
```
---
## 管理端分页列表(`/api/admin`
需管理员 JWT`role=admin`)。默认 `size` 与上限见下表。
| 方法 | 路径 | 默认 size | size 上限 | 额外筛选 Query |
|------|------|-----------|-----------|----------------|
| GET | `/api/admin/users` | 20 | 200 | `q` 邮箱模糊 |
| GET | `/api/admin/orders` | 20 | 200 | `q` 订单号或邮箱;`status` pending/paid/cancelled/refunded`refund_status` none/pending/approved/rejected`channel` alipay/wechat`from`/`to` 日期 YYYY-MM-DD |
| GET | `/api/admin/audits` | 50 | 200 | `q` 操作/目标/管理员邮箱;`action` 精确操作码;`from`/`to` 日期 |
实现参考:
- `backend-web/src/Admin/Controllers/UsersController.php`
- `backend-web/src/Admin/Controllers/OrdersController.php`
- `backend-web/src/Admin/Controllers/AuditsController.php`
### 管理端全量列表(无分页)
以下接口一次返回全部 `items`,数据量预期较小:
| 路径 | 说明 |
|------|------|
| `GET /api/admin/plans` | 套餐配置 |
| `GET /api/admin/settings` | 系统键值 |
| `GET /api/admin/payment/status` | PEM 文件元数据 |
### 管理端固定条数嵌套列表
| 来源 | 条数 | 说明 |
|------|------|------|
| `GET /api/admin/stats``recent_orders` | 5 | 仪表盘最近订单,非翻页接口 |
| `GET /api/admin/users/{id}``recent_orders` | 5 | 用户详情内嵌 |
---
## 公开端分页列表(`/api/v1`
需用户 JWT(除 plans 列表可匿名,见各控制器)。
| 方法 | 路径 | 默认 size | size 上限 | 分页参数 | 响应 |
|------|------|-----------|-----------|----------|------|
| GET | `/api/v1/files` | 50legacy | 200 | **推荐** `page`+`size`**兼容** `limit`+`offset` | 见下文 |
| GET | `/api/v1/pay/orders` | 8 | 50 | `page`+`size` | page 形态 |
实现参考:
- `backend-web/src/Controllers/FileController.php`
- `backend-web/src/Controllers/PayController.php`
- `backend-web/src/Services/MembershipService.php``listOrders`
### `GET /api/v1/files` 双模式
**推荐(page 形态)** — 传 `page`≥1 时生效:
```http
GET /api/v1/files?page=1&size=12
```
```json
{
"ok": true,
"data": {
"items": [ { "id": 1, "name": "design.soon", "size": 1024, "version": 1 } ],
"total": 25,
"page": 1,
"size": 12
}
}
```
**兼容(offset 形态)** — 未传 `page``page=0` 时:
```http
GET /api/v1/files?limit=50&offset=0
```
```json
{
"ok": true,
"data": {
"items": [ ],
"total": 25,
"limit": 50,
"offset": 0
}
}
```
两种形态均含 `total`。新前端(首页文件列表、管理端)应使用 `page`+`size`
### `GET /api/v1/pay/orders`
会员中心订单历史,默认每页 8 条:
```http
GET /api/v1/pay/orders?page=1&size=8
```
### 公开端全量 / 非标准列表
| 路径 | 形态 |
|------|------|
| `GET /api/v1/plans` | `{ items: [...] }` 全量活跃套餐 |
| `GET /api/v1/plans/me` | 会员信息 + `recent_orders`(默认 8 条,非翻页) |
| `GET /api/v1/settings` | 扁平 key-value 对象,非 `items` 数组 |
| `GET /api/v1/soon-models` | manifest 内容 |
---
## 前端约定
### 管理后台(`frontend-web/pages/admin`
- 分页状态:`AdminState``users` / `orders` / `audits``page``size` 及筛选字段)
- 分页 UI`AdminPager` + `AdminUi.pagerHtml` / `bindPager`
- 请求:相对 admin base 的路径,如 `users?page=1&size=20`
### 门户 / 会员 / 首页
| 页面 | 接口 | 默认 size |
|------|------|-----------|
| 会员订单 | `GET /api/v1/pay/orders` | 8 |
| 首页云端文件 | `GET /api/v1/files?page=&size=` | 12 |
---
## 新增列表接口检查清单
1. 响应是否包含 `items` + `total`
2. 是否使用 `page`(从 1+ `size`,并在 `data` 中回显?
3. `size` 是否有合理默认值与上限(建议上限 200,管理端;用户订单建议 50)?
4. 筛选参数是否与分页独立(翻页时保留筛选)?
5. 是否在本文档「管理端/公开端」表中登记?
---
## 变更记录
| 日期 | 说明 |
|------|------|
| 2026-06-08 | 初版:统一 page/size/totalfiles 兼容 limit/offsetpay/orders 分页 |
+22
View File
@@ -0,0 +1,22 @@
# 支付联调
## 密钥文件(Admin → 支付密钥 或 `backend-web/storage/payment/`
| 文件 | 用途 |
|------|------|
| alipay_private_key.pem | 支付宝商户私钥 |
| alipay_public_key.pem | 支付宝公钥 |
| wechat_mch_private_key.pem | 微信商户私钥 |
| wechat_api_v3_key.pem | APIv3 密钥(32 字节) |
| wechat_platform_cert.pem | 微信平台证书 |
## local.php 必填项
`site.base_url`HTTPS 外网)、`alipay.app_id``wechat.mch_id``wechat.mch_serial_no`
## notify 路径
- `{site.base_url}/api/v1/pay/alipay/notify`
- `{site.base_url}/api/v1/pay/wechat/notify`
本地开发需 ngrok 等穿透;重复 notify 应幂等返回 success。
-83
View File
@@ -1,83 +0,0 @@
# SoonDesign 网页端功能清单
本文档列出 SoonDesign 各功能在**桌面端(Electron)**与**网页端**的实现情况,以及网页端不可用时的替代方案。
---
## 一、首页(index
| 功能 | 桌面端 | 网页端 | 替代/说明 |
|------|--------|--------|----------|
| 打开文件 | ✅ 系统对话框选文件,打开设计页 | ✅ 使用 `<input type="file">` 选文件,项目 JSON 存入 `sessionStorage`,通过 `soondesign_session:文件名` 传给设计页 | 网页端选文件后在新页打开,关闭后需重新选择 |
| 新建 1(单面) | ✅ 通过 ipcRenderer 打开设计页 | ✅ 通过 `platformBridge.openDesignPage("", 1)` 跳转 design1.web.html | 一致 |
| 新建 2(双面) | ✅ 通过 ipcRenderer 打开设计页 | ✅ 通过 `platformBridge.openDesignPage("", 2)` 跳转 design2.web.html | 一致 |
| 历史记录 | ✅ 从本地文件读取并展示 | ✅ 使用 `platformBridge.readHistory()`localStorage) | 网页端历史仅限当前域名,且为路径/名称列表,点击打开需支持 sessionKey |
| 语言切换 | ✅ 正常 | ✅ 使用 localStorage 存语言,无系统 API 时沿用本地存储 | 一致 |
| 关于 | ✅ 正常 | ✅ 可显示版本等信息(若未注入 sysAPI 则仅显示界面) | 一致 |
| 关闭 | ✅ 通过 ipcRenderer 关闭窗口 | ⚠️ 使用 `window.close()` 或隐藏;部分浏览器限制脚本关闭页签 | 替代:用户手动关闭页签 |
---
## 二、设计页(design1 / design2 通用)
### 2.1 文件操作
| 功能 | 桌面端 | 网页端 | 替代/说明 |
|------|--------|--------|----------|
| 打开项目 | ✅ `fs.readFileSync` 读 .soon 后加载画布 | ✅ `platformBridge.readJsonFile(file)` 支持路径或 `soondesign_session:xxx`,异步加载 | 从首页“打开文件”传入的 sessionKey 在设计页通过 readJsonFile 从 sessionStorage 读取 |
| 保存 | ✅ `dialog.showSaveDialog` + `fs.writeFileSync` | ✅ 使用 `platformBridge.writeFile`fileHandle 或默认文件名)+ 异步写入 | 网页端无 fileHandle 时触发下载 |
| 另存为 | ✅ 同上 | ✅ 同上,通过 showSaveDialog 选保存位置或文件名 | 一致 |
| 导出 PNG | ✅ 对话框选路径 + fs 写入 | ✅ Blob + `fileHandle.createWritable()``<a download>` + `URL.createObjectURL(blob)` 下载 | 一致 |
| 导出 PDF(仅 design2 | ✅ `dialog.showSaveDialog` + `fs.writeFile` | ✅ 使用桥 `showSaveDialog` + `writeFile(Blob)`,或 Blob 下载 | 一致 |
| 写历史(saveHistory | ✅ fs 读写本地历史文件 | ✅ `platformBridge.readHistory` / `writeHistory`localStorage | 一致 |
### 2.2 预览与打印
| 功能 | 桌面端 | 网页端 | 替代/说明 |
|------|--------|--------|----------|
| 预览 | ✅ 生成临时文件 + printJS 等 | ✅ 使用画布导出 Blob → base64 → Uint8Array → Blob URL 显示预览图 | 一致 |
| 打印 | ✅ 同上,调用打印 | ✅ 使用 `platformBridge.printPdf(blob)` 或 Blob URL 交给浏览器打印 | 一致(依赖桥提供 printPdf |
### 2.3 界面与编辑
| 功能 | 桌面端 | 网页端 | 替代/说明 |
|------|--------|--------|----------|
| 帮助 | ✅ 打开本地或在线帮助 | ✅ 新窗口打开帮助链接或 PDF | 一致 |
| 返回首页 | ✅ ipcRenderer 跳转首页 | ✅ `location.href` 跳转 index.web.html | 一致 |
| 字体 / 剪贴板 / 撤销重做 | ✅ 不依赖 Node | ✅ 同桌面端 | 一致 |
| 关闭前提示(未保存) | ✅ 通过 IPC 或 beforeunload | ✅ beforeunload 提示 | 一致 |
---
## 三、design2 额外说明
- **保存 / 另存为**design2 的 `output.js``ui.js` 已统一为:有 `platformBridge.writeFile` 时异步写入,否则使用 `fs.writeFileSync`,网页端走桥或下载。
- **导出 PDF**`design2/core.js``savePdf` 已接桥:网页端使用 `showSaveDialog` + `writeFile(blob)` 或 Blob 下载;预览/打印在无 fs 时使用 data URL,打印可走 `platformBridge.printPdf(pdfBlob)`
- **打开项目**design2 已支持 `platformBridge.readJsonFile``openFile(file, jAlready)``ui.js` 打开对话框在网页端使用 `showOpenDialog``readJsonFile(file)` 后调用 `openFile(name, j)`
---
## 四、平台桥接口(网页端 lib/platform/web.js
网页端通过 `window.platformBridge` 提供:
- `openDesignPage(file, type)`:跳转设计页(file 可为空或 sessionKey
- `openFirstPage()`:跳转首页
- `openHelp()`:新窗口打开帮助
- `runClose()`:尝试关闭当前页(受浏览器限制)
- `onClose(callback)`beforeunload 时回调(关闭前确认)
- `readJsonFile(pathOrHandle)`:支持 `soondesign_session:xxx` 从 sessionStorage 读 JSON
- `readHistory` / `writeHistory`:历史记录(localStorage
- `showOpenDialog` / `showSaveDialog`:文件选择(input 或 File System Access
- `writeFile(content, fileHandleOrFilename)`:写字符串或 Blob
- `printPdf(blob)`:打印 PDF Blob(若未实现则可不提供)
网页端 `ipcRenderer` 桩会响应:`get-sys-language``open-first-page``open-design-page``open-help-file``run-close`,并转发到上述桥方法。
---
## 五、总结
- **首页**:打开、新建、历史、语言、关于在网页端均可用;关闭为浏览器限制,无替代实现则用户手动关页。
- **设计页**:打开(含 sessionKey)、保存、另存为、导出 PNG、预览、打印、历史、帮助、返回首页、编辑相关功能在网页端可用;design2 导出 PDF 建议后续接桥或改为下载。
- **无法在网页实现的**:无;仅“关闭窗口”受浏览器限制,其余均有实现或替代方案。