Files
SoonDesign/docs/WEB-LOCAL-CACHE.md
24kycj 4ce4486b26 Web 端本地缓存、首页与会员弹窗体验优化
新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 11:05:44 +08:00

401 lines
14 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.
# Web 端本地缓存与最近文件优化(开发计划 · 细化版)
> 状态:P0P7 已实施。与 [API-PAGINATION.md](API-PAGINATION.md)「Web 虚拟 key」交叉引用。
---
## 0. 背景与约束
### 0.1 问题(已核实代码)
| 现象 | 根因 | 代码位置 |
|------|------|----------|
| `GET /templates/{id}` 2030s | PHP 二次 `json_encode` + 大响应 + 客户端双次 parse | `TemplateService::fetchPublicJson``web.js` ~268 行 |
| 重复打开仍打源站 | 无 IndexedDB | 全库无 `indexedDB` |
| 首页最近文件慢 | 每卡 `readJsonFile` 全量 | `index.js` ~381 行 |
| 保存后不显示 | Web `onCloudWriteDone` 未写 history | `design1/output.js` ~1266;仅 fs 分支 `saveHistory` |
| session 易超配额 | 大 JSON 写 session/localStorage | `cloud-files.js` `soonPutSoonSession` |
### 0.2 硬约束
- **不改** `.soon` 格式;**保留** base64。
- **不上 CDN**;靠本机 IDB + 传输路径精简 + L1 元数据。
- 保存仍 `POST/PUT /files` 传完整 `json`**仅读**本地优先。
- 配置:`frontend-web/config/local.js``backend-web/config/local.php`
### 0.3 三层架构
| 层 | 存储 | 内容 | 体积 |
|----|------|------|------|
| L1 | `localStorage` `soondesign_recent` | key/name/type/kind/fileId/thumbRef/time | <50KB |
| L2 | IndexedDB `soondesign_local` | 完整 `.soon` 对象 + thumbs | ≤20 条 blob |
| L3 | HTTP API | miss / 保存 / 可选元数据对账 | 按需 |
### 0.4 脚本加载顺序(三页统一)
`index.web.html` / `design1.web.html` / `design2.web.html`
```
config/local.js → auth → cloud-files.js → soon-local-store.js → soon-recent.js → web.js → 页面逻辑
```
---
## 1. 数据契约
### 1.1 L1`soondesign_recent`
```json
{
"version": 1,
"items": [
{
"key": "soondesign_file:12:v3",
"name": "我的卡片.soon",
"type": 1,
"kind": "cloud",
"fileId": 12,
"thumbRef": "soondesign_file:12:v3",
"time": "2026-06-08 14:30:00"
}
]
}
```
- `thumbRef`:与 L2 `thumbs` 的 cacheKey 相同(非独立字符串前缀)。
- 上限 **20**`soonRecentUpsert` 去重后 unshift,截断尾部。
- **首页以 L1 为展示主数据源**;`GET /files` 仅后台补 `name`/`version`,不默认注入未打开过的云端文件。
### 1.2 L2IndexedDB `soondesign_local` v1
| Store | keyPath | value |
|-------|---------|--------|
| `blobs` | `cacheKey` | `{ json, name, type, source, updatedAt, savedAt, bytes }` |
| `thumbs` | `cacheKey` | `{ dataUrl, savedAt }` |
**cacheKey**
| 来源 | cacheKey | 失效 |
|------|----------|------|
| 云文件 | `soondesign_file:{id}:v{ver}` | PUT 后 version+1 → 新 key;旧 blob 可 `soonLocalRemove` |
| 模板 | `soondesign_template:{id}` | 见 §1.4 |
| 会话 | `soondesign_session:...` | 迁入云 key 后 `soonLocalRenameKey` |
**LRU**`soonLocalEvictLRU(20)` 在每次 `put` 后执行;仅删 blob+thumb**不删 L1**。
### 1.3 L3:读取端点
| 场景 | 端点 | 禁止 |
|------|------|------|
| 模板首次 | `GET /api/v1/templates/{id}/file` | 门户默认不用 `GET /templates/{id}` 包装 |
| 云文件 miss | `GET /api/v1/files/{id}` | — |
| thumb 兜底 | `GET /templates/{id}/thumb`P6 `GET /files/{id}/thumb` | 禁止为 thumb 拉全量 json |
| 保存 | `POST`/`PUT /files` | 不变 |
### 1.4 模板缓存失效策略(细化)
1. 首页点模板前,`index.js` `openTemplateItem` 写入 `window._soonTemplateMeta = { id, name, type, updated_at }`(列表已有 `updated_at`)。
2. IDB `blobs``meta.updatedAt` 存该 `updated_at`
3. `readJsonFile` IDB hit 时:若 `_soonTemplateMeta.updated_at` 存在且与 blob.meta.updatedAt 不一致 → 删 blob`miss` 走网络。
4. 网络:`/templates/{id}/file` + parse`soonLocalPut` 带最新 `updatedAt`
5. 未经过首页的深链打开:无 meta 比较,信任 IDB(可接受;刷新列表后校正)。
### 1.5 共享工具 `soonExtractThumbFromJson(json)`
- 位置:`soon-local-store.js``soon-recent.js`
-`json.frontDisplayPic`;须 `data:image/` 开头。
- 超过 **102400** 字节则返回 `''`(不写 thumbs,首页用 type 默认图)。
- 打开/保存/导入成功后统一调用。
---
## 2. 核心流程
### 2.1 打开(readJsonFile
```mermaid
sequenceDiagram
participant UI as design_page
participant IDB as L2_IDB
participant L1 as L1_recent
participant API as L3_server
UI->>IDB: soonLocalGet(cacheKey)
alt hit_and_valid
IDB-->>UI: json
else miss_or_stale
UI->>API: GET file_or_files
API-->>UI: soon_bytes
UI->>IDB: soonLocalPut + PutThumb
UI->>L1: soonRecentUpsert
end
UI->>UI: openFile_loadFromJSON
```
**补充接入点**P3):`doOpenWithJson` 完成画布加载后 `soonRecentUpsert`(模板/云文件/会话均记录,kind 由 key 推断)。
### 2.2 保存(writeFile + onCloudWriteDone
**模板首次保存**`soondesign_template:*` 或空 key 时弹窗,默认文件名 `design` + `yyMMddHHmmss` + `.soon`(如 `design260610103010.soon`);保存成功后 L1 用 cloud/local 条目替换原 template 项(`soonRecentOnCloudSave` + `prevOpenKey`)。
```mermaid
sequenceDiagram
participant UI as save
participant IDB as L2
participant L1 as L1
participant API as server
UI->>IDB: soonLocalPut当前json
UI->>L1: soonRecentUpsert
alt cloud_key
UI->>API: PUT
API-->>UI: new_version
UI->>IDB: RenameKey_or_new_put
UI->>L1: upsert新key
else new_or_template
UI->>API: POST
end
```
### 2.3 首页最近文件
```mermaid
sequenceDiagram
participant Index as index
participant L1 as L1
participant IDB as thumbs
participant API as metadata_optional
Index->>L1: soonRecentList
loop each_card
Index->>IDB: soonLocalGetThumb(key)
alt no_thumb
Index->>Index: soonAsset默认图或thumb_URL
end
end
opt logged_in
Index->>API: listCloudFiles元数据
Index->>L1: 仅补name/version不增项
end
```
### 2.4 删除卡片
| 操作 | L1 | L2 | 服务器 |
|------|----|----|--------|
| 删本地项 | `soonRecentRemove` | `soonLocalRemove` | — |
| 删云文件 | `soonRecentRemove` | `soonLocalRemove` | `DELETE /files/{id}` |
| 旧 `removeLocalHistoryPath` | 改为调上述 API | 同上 | — |
---
## 3. 分阶段任务(可独立验收)
### Phase 0:契约文档 — 已完成
- [x] 本文档
- [x] `API-PAGINATION.md` 链接与 `/file` 推荐说明
---
### Phase 1IndexedDB 模块(1.5h
**新建** [`frontend-web/js/common/soon-local-store.js`](frontend-web/js/common/soon-local-store.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P1.1 | `soonLocalOpen` | `indexedDB.open('soondesign_local', 1)``onupgradeneeded``blobs`/`thumbs` |
| P1.2 | `soonLocalGet/Put` | `json` 存对象;`bytes` 估算 `JSON.stringify` 长度 |
| P1.3 | `soonLocalPutThumb/GetThumb` | thumbs store |
| P1.4 | `soonLocalRemove` | 同时删 blobs+thumbs |
| P1.5 | `soonLocalRenameKey` | 读旧→写新→删旧(session→cloud |
| P1.6 | `soonLocalEvictLRU` | 按 `savedAt` 升序删至 ≤20 |
| P1.7 | `soonExtractThumbFromJson` | §1.5 |
| P1.8 | 降级 | `indexedDB` 不可用 → 所有 get 返回 null,put 静默失败 |
| P1.9 | HTML 引入 | 三页在 `web.js` 前引入 |
**验收**
- [ ] DevTools Application → IDB 可见 stores
- [ ] put/get 往返;第 21 条触发 evict
- [ ] 隐私模式不抛未捕获异常
---
### Phase 2web.js 读写穿透(2h
**改** [`frontend-web/js/platform/web.js`](frontend-web/js/platform/web.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P2.1 | `resolveCacheKey(key)` | 模板/云/ session 统一;云用完整 `soondesign_file:id:vN` |
| P2.2 | read 模板 stale | §1.4miss 走 `GET .../templates/{id}/file` + `text()` + `JSON.parse` |
| P2.3 | read 云文件 | IDB miss → 现有 `authedFetch files/{id}`;成功后 put |
| P2.4 | read session | IDB → sessionStorage 兜底(过渡期) |
| P2.5 | read 后写 thumb | `soonExtractThumbFromJson` + `soonLocalPutThumb` |
| P2.6 | writeFile 成功 | `soonLocalPut`;云 POST/PUT 成功后若 key 变化 `soonLocalRenameKey` |
| P2.7 | write 未登录 | session key + IDB 双写;**停止** `localStorage.setItem(sessionKey, 大JSON)` |
**验收**
- [ ] 模板首次:Network 仅 `/templates/{id}/file`;二次 0 请求
- [ ] 云文件二次打开无 `GET /files/{id}`
- [ ] PUT 后 `openAs.name` 与新 version key 一致且 IDB 可命中
---
### Phase 3L1 最近列表 + 保存/打开写 L1(2h)
**新建** [`frontend-web/js/common/soon-recent.js`](frontend-web/js/common/soon-recent.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P3.1 | `soonRecentUpsert` | 按 `key` 去重;字段 §1.1`getDate()` 时间格式与现 `saveHistory` 一致 |
| P3.2 | `soonRecentList/Remove` | — |
| P3.3 | `soonRecentKindFromKey` | `cloud`/`template`/`local` |
| P3.4 | `soonRecentMigrateFromHistory` | 读 `soondesign_history``path``key`;无 thumb**仅执行一次**`soondesign_recent_migrated` 标记) |
| P3.5 | `onCloudWriteDone` | design1/2 `output.js`upsert`res.fileKey`,`res.name`, soonType |
| P3.6 | `saveHistory` | Web`soonRecentUpsert`Electron fs 路径保留 |
| P3.7 | `cloud-files` | `soonOpenSoonJsonLocally`、import 成功 upsert |
| P3.8 | `openFile` 加载成功 | design1 `doOpenWithJson` 末尾;design2 `saveInitialState` 末尾 upsert |
| P3.9 | `index` 开模板 | `openTemplateItem` 设置 `_soonTemplateMeta`(§1.4 |
**验收**
- [ ] 云端 Ctrl+S 后回首页立即见卡片
- [ ] 打开模板/云文件后 L1 有对应项
- [ ] 旧 history 用户升级后 L1 有条目(可无 thumb)
---
### Phase 4:首页 loadHistory 改造(1.5h
**改** [`frontend-web/js/index.js`](frontend-web/js/index.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P4.1 | 数据源 | `soonRecentList()` 为主;启动时 `soonRecentMigrateFromHistory` |
| P4.2 | 删除 merge 云端追加 | 移除 `mergeRecentItems` 中「云端未见过则 push」逻辑;改为 `enrichFromCloud(items, cloudMeta)` 只补 name/version |
| P4.3 | 渲染 thumb | `soonLocalGetThumb(item.key)` → fallback `soonAsset(bg_1/2)`;模板项可用 `/thumb` URL`updated_at``?v=` |
| P4.4 | 删除 readJsonFile 循环 | 移除 ~381 行;**删除** `fileExists` 灰显逻辑(改为始终可点;打开时 miss 再拉+toast |
| P4.5 | 删除 | `removeLocalHistoryPath``soonRecentRemove` + `soonLocalRemove` |
| P4.6 | 刷新 | `pageshow`/`visibilitychange``loadHistory`60s 节流 `_recentLoadedAt` |
| P4.7 | 分页 | 保留 `fileListState` 对 L1 items 分页 |
**验收**
- [ ] 首页 Network0× 全量 `files/{id}` / `templates/{id}`
- [ ] 12 卡仅 thumb 图或 0 额外请求(IDB 命中)
- [ ] 从设计页返回可见新保存项
---
### Phase 5session 迁移与清理(1h
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P5.1 | `soonPutSoonSession` | 正文 IDBsessionStorage 仅存 `{key}` 标记或短指针 |
| P5.2 | `readJsonFile` session | 优先 IDB |
| P5.3 | `soonTryConsumeCloudImport` | 成功:`soonLocalRenameKey` + `soonRecentUpsert` + 删 session |
| P5.4 | 清理 | 移除 `cloud-files`/`index` 对大 JSON 的 localStorage 写入 |
| P5.5 | `index` 删卡 | 清 sessionStorage 旧 key(保留现有 769 行逻辑并接 IDB) |
**验收**
- [ ] 本地 .soon → 编辑 → 保存 → key 为 `soondesign_file:*`
- [ ] sessionStorage 无 >1MB 字符串
---
### Phase 6:后端轻量增强(可选,1h)
| 任务 ID | 文件 | 细节 |
|---------|------|------|
| P6.1 | `TemplateService::outputFile` | `Cache-Control: public, max-age=86400, must-revalidate` |
| P6.2 | `FileController` + 路由 | `GET /api/v1/files/{id}/thumb` |
| P6.3 | `FileService` | 上传/更新时 `extractThumb` 入 DB(可复用 `TemplateService` 逻辑);`list``has_thumb` |
| P6.4 | `docs/API-PAGINATION.md` | 登记 thumb 端点 |
**验收**
- [ ] `curl -I .../templates/1/file``max-age=86400`
- [ ] 云 thumb 返回 `image/jpeg`
---
### Phase 7:全链路验收(0.5h
| # | 场景 | Network | UI |
|---|------|---------|-----|
| 1 | 首次开模板 | 1× `/templates/{id}/file` | 画布 OK |
| 2 | 再次开同模板 | 0 全量 | <1s |
| 3 | 模板改 updated_at 后再开 | 1× `/file` | 新内容 |
| 4 | 保存新文件(登录) | POST | 回首页有卡 |
| 5 | 再次开云文件 | 0 全量 | PUT 可保存 |
| 6 | 首页 12 最近 | 0 全量 json | thumb OK |
| 7 | 未登录保存 | 无 API | L1+IDB |
| 8 | 删云文件卡 | DELETE | L1+IDB 清除 |
| 9 | IDB 满 20 再开第 21 | 1× 网络 | 最旧 blob 淘汰 |
| 10 | 模板列表 | 1× `/templates` 元数据 | 不变 |
**部署**:前端 P1P5 同批;强刷 `soon-local-store.js`/`soon-recent.js`/`web.js`/`index.js`P6 可稍后。
---
## 4. 改动文件清单
| 阶段 | 文件 |
|------|------|
| P1 | `soon-local-store.js``pages/index|design1|design2.web.html` |
| P2 | `platform/web.js` |
| P3 | `soon-recent.js``design1/2/output.js``cloud-files.js``index.js`meta |
| P4 | `index.js` |
| P5 | `cloud-files.js``web.js` |
| P6 | `TemplateService.php``FileController.php``FileService.php``public/index.php``API-PAGINATION.md` |
**不改动**`.soon` schema`design*-back.js`Electron(后续可选对齐)。
---
## 5. 依赖关系
```mermaid
flowchart TD
P0[P0_done]
P1[P1_IDB]
P2[P2_web_bridge]
P3[P3_recent_L1]
P4[P4_index]
P5[P5_session]
P6[P6_backend_opt]
P7[P7_QA]
P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P7
P2 --> P6 --> P7
```
**顺序**P1 → P2 → P3 → P4 → P5 → P7P6 与 P3/P4 并行。
---
## 6. 风险与缓解
| 风险 | 缓解 |
|------|------|
| IDB 配额 | LRU 20thumb ≤100KBevict 失败 toast |
| 多设备 | 云 version 为准;409 提示刷新 |
| 模板 stale 漏检 | 首页列表带 `updated_at`;§1.4 |
| PUT 后双 version 缓存 | `onCloudWriteDone` rename + 删旧 key |
| 隐私模式 | 降级 session+网络,与现网一致 |
| 旧用户无 recent | `soonRecentMigrateFromHistory` 一次 |
---
## 7. 不在 scope
- CDN`.soon` 剥离 base64Electron IDBCRDT 离线合并。
---
**确认后**按 P1→P7 实施;每阶段勾选任务 ID 验收后再进入下一阶段。