693 lines
48 KiB
Markdown
693 lines
48 KiB
Markdown
# 缓存清理功能设计(2026-07-28)
|
||
|
||
> 配套前置分析:本次会话中已扫描 `frontend/` 全量代码,识别出 4 大类存储(uni 本地持久化、内存缓存、临时文件、沙盒文件系统)。本文把"统一清理入口"这一需求转化为**可执行的 MVP 实施方案**。
|
||
|
||
---
|
||
|
||
## 一、方案概述(必读)
|
||
|
||
### 要解决的问题
|
||
|
||
**业务问题**
|
||
- 用户遇到"App 占用过大"、"切换账号后旧账号残留"、"创作草稿一直清不掉"、"活动进度缓存过期"等场景时,**没有统一的清理入口**。
|
||
- 当前只能在登出时被动清一部分(`store/modules/user.js#CLEAR_AUTH`),普通用户无法主动触发。
|
||
- 多个 utils 各自实现清理逻辑(`avatarCache.js`、`likeHelper.js`、`preloadApi/storage.js`、`ioPath.js`),但**没有一个聚合的 UI 入口**告知用户"我占了多少、可以清哪些"。
|
||
|
||
**技术问题**
|
||
- 清理逻辑分散在 7+ 个文件,UI 层如果要"一键清理"必须串联调用 7 个函数,缺乏统一抽象。
|
||
- 新增缓存类型时需要改 UI(如未来加 `eventCache`),违反"加 handler 不动 UI"的原则。
|
||
- 没有 size 计算的统一口径:部分用 `JSON.stringify(v).length`,部分用 `uni.getStorageInfoSync().currentSize`,用户看到的"总占用"无来源解释。
|
||
|
||
### 整体实现路径(MVP 阶段,1 周可落地)
|
||
|
||
| Milestone | 主题 | 目标 | 工作量 |
|
||
|-----------|------|------|--------|
|
||
| **M0**(前置迁移) | 草稿 key 改造 | `utils/draftStorage.js` 封装 + 7 个文件读写改造(§11) | 1.5 天 |
|
||
| **M1** | 核心封装层 | `cacheManager.js` + 黑名单 + 大小格式化 + `getCacheInfo` / `getCategoryBreakdown` / `cleanCategory` / `cleanCategoryGroup` 框架 | 0.5 天 |
|
||
| **M2** | 6 个 handler | preload / draft / progress / guide / sandbox-tmp / others + 内存层清理接入 | 1 天 |
|
||
| **M3** | UI 页面(列表 + 详情) | `cache-cleanup.vue` + `cache-cleanup-detail.vue` + ConfirmModal + profile.vue 入口 + pages.json | 1.5 天 |
|
||
| **M4** | 测试 & 回归 | 单元测试 + 7 个手工场景(含跨账号分组清理)+ CLAUDE.md 自检 | 0.5 天 |
|
||
|
||
合计:约 **5 人天**(含 M0 前置迁移 + 详情页)。
|
||
|
||
### 关键决策
|
||
|
||
1. **统一封装层模式(方案 A)**——新建 `frontend/utils/cacheManager.js`,UI 只调 manager,**永远不直接**调 `uni.removeStorageSync` 等底层 API。这与项目里 `frontend/utils/preloadApi/core.js` 已采用的封装模式一致。
|
||
2. **必须有二级详情页(用户决策)**——MVP 阶段**没有一键清理按钮**,每类缓存必须进详情页才能清理。理由:
|
||
- 草稿(draft)和引导(guide)天然有"我的 / 其他用户的"分组(一台设备多账号切换常见),一键清理会把前任账号数据也清掉,体验突兀
|
||
- 用户进入详情页可以看到每个分组的占用与项数,**明确知道清的是什么**
|
||
- 清理"其他用户的草稿"需要单独更强的警告(避免误操作),而清理"自己的草稿"只需要普通警告
|
||
- 其他分类(preload / progress / sandbox-tmp / others)详情页可简化(一组 + 单按钮)
|
||
3. **永远不清的 key 黑名单**——登录态(`access_token`/`user`/`star_id`/`login_mobile`/`cid`/`deviceFp`/`pending_scan_url`/`gallery_owner_id`)、设备态(`needs_welcome`/`has_seen_welcome`/`is_new_user`/`daily_login_completed_*`)、用户行为(`liked_assets_exhibition`)、外部资源(`avatar_file_*` 元数据)、注册中状态(`temp_register_*` 含明文密码)**五类** key 由 manager 内置黑名单拦截,**任何清理路径都不会触碰**。详见 §3.4.1。
|
||
4. **范围不包含头像/图片缓存**——`avatarCache.js` 的 `avatar_file_*` storage key 与 `uni.saveFile` 的 `savedFilePath` 均纳入黑名单(§3.4.1)。原因:用户误清会导致头像全部重新下载,体验差;且与本设计目标(清理临时性业务缓存)不符。**MVP 不纳入清理,仅作黑名单保护**。
|
||
5. **范围包含创作草稿(带警告)**——草稿 key 改造(§11 迁移项)后形式为 `*_${currentUid}`。**所有**草稿 key(无论当前 uid / 其他 uid / legacy 无后缀)**统一归 `draft` handler**,通过 `computeBreakdown()` 按 uid 分组展示;强警告只在清理"其他用户/legacy"组时触发。UI ConfirmModal 在清理自己草稿时弹普通警告(`handler.warning = true`),清理他人时弹强警告(`handler.strongWarning = true`)。**不**走 `others` handler 兜底,避免双重计入 totalBytes 且强警告无法生效。
|
||
6. **preload handler 清所有用户**——`preload:${oldUid}:*` 是跨账号的真正垃圾。spec 的 `preload.clean()` 通过遍历所有 `preload:` 前缀 key 一次性删,不依赖 currentUid。
|
||
7. **cleanAll 同步调 `invalidateAll()`**——`preloadApi/core.js` 的 `memoryMap` 是进程级共享,切账号不清会残留老用户数据。`cacheManager.cleanAll()` 在 storage/sandbox 清理完成后调 `core.invalidateAll()` 清空整个内存层(含 `inFlightMap`)。
|
||
8. **草稿 key 改造为 uid 绑定(新增迁移项)**——将 `castlove_form_data` 等草稿 key 改为 `castlove_form_data_${currentUid}` 等 uid 后缀形式(详见 §11 迁移项),账号切换后看不到对方草稿(隐私 + 体验)。**所有**草稿 key(无论 currentUid / 其他 uid / legacy 无后缀)**统一归 `draft` handler**,通过 `computeBreakdown()` 按 uid 分组展示;强警告只在清理"其他用户/legacy"组时触发。**不**走 `others` 兜底(避免双重计入 totalBytes 且强警告无法生效)。
|
||
|
||
### 核心架构图(TL;DR)
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ pages/profile/cache-cleanup.vue │ ← 列表页(无清理按钮,只跳转)
|
||
│ - 顶部:大字 + 百分比 + 进度条 + 3 chip │
|
||
│ - 缓存分类(每行 → 进详情) │
|
||
│ - "其他" section(不可点击) │
|
||
└──────────────┬───────────────────────────┘
|
||
│ uni.navigateTo
|
||
┌──────────────▼───────────────────────────┐
|
||
│ pages/profile/cache-cleanup-detail.vue │ ← 详情页(按 id 分发)
|
||
│ - 简单页:单按钮 │
|
||
│ - 分组页:按 uid 分组卡片 + 每组单按钮 │
|
||
└──────────────┬───────────────────────────┘
|
||
│ 调用
|
||
┌──────────────▼───────────────────────────┐
|
||
│ utils/cacheManager.js │ ← 统一封装层(核心)
|
||
│ - registerCategory(handler) │
|
||
│ - getCacheInfo() │
|
||
│ - getCategoryBreakdown(id) │
|
||
│ - cleanCategory(id) / cleanCategoryGroup(id, {uid}) │
|
||
│ - cleanAll() — 编程式入口,UI 不调用 │
|
||
│ - PROTECTED_KEYS(内置黑名单) │
|
||
└──────────────┬───────────────────────┘
|
||
│ 委托
|
||
┌───────────┼─────────────┬─────────────────────┐
|
||
▼ ▼ ▼ ▼
|
||
preload draftHandler sandboxTmp 各种 handler
|
||
Handler (含警告) Handler (基于 (每个独立模块)
|
||
(基于 ioPath.js)
|
||
preloadApi)
|
||
```
|
||
|
||
---
|
||
|
||
## 二、文档说明
|
||
|
||
- **适用范围**:`frontend/` 全部业务代码(不含 `uni_modules/` 第三方模块);uni-app app-plus 端为主,H5/小程序兼容性按现有项目规范。
|
||
- **工作量估算**:5 人天(MVP,包含列表页 + 详情页 + 跨账号分组清理)。
|
||
- **前置版本**:基线 commit `ea39ee1`(feat:修改图片尺寸和uni配置)。
|
||
- **目标读者**:前端工程师、测试、产品。
|
||
- **后续优化(不在 MVP)**:
|
||
- ~~二级详情页~~(MVP 包含)
|
||
- 头像/图片缓存清理(需评估用户体验)
|
||
- 清理历史记录(用户看到"上次清理于 X 分钟前")
|
||
- 自动清理策略(按 LRU 自动触发)
|
||
|
||
---
|
||
|
||
## 三、Handler 接口契约
|
||
|
||
### 3.1 类型定义
|
||
|
||
```js
|
||
/**
|
||
* @typedef {Object} CacheCategoryHandler
|
||
* @property {string} id // 唯一 ID,如 'preload' / 'draft' / 'sandbox-tmp'
|
||
* @property {string} label // UI 展示名(中文)
|
||
* @property {string} description // 副标题/说明(可选)
|
||
* @property {() => Promise<{sizeBytes: number, keyCount: number}>} computeSize
|
||
* // 列表页汇总展示用
|
||
* @property {() => Promise<Array<GroupInfo>>} [computeBreakdown]
|
||
* // 详情页按 uid 分组(draft/guide 用,其他可选)
|
||
* @property {() => Promise<{freedBytes: number, keyCount: number}>} [clean]
|
||
* // 简单页清理(无分组):清理整类
|
||
* @property {(params: {uid: string|null}) => Promise<{freedBytes: number, keyCount: number}>} [cleanGroup]
|
||
* // 分组页清理:按 uid 维度清理,uid=null 表示"其他用户"
|
||
* @property {boolean} [warning] // 是否需要"草稿会丢失"警告
|
||
* @property {string} [warningText] // 警告文案(如 draft: "将清空你未提交的创作草稿,是否继续?")
|
||
* @property {boolean} [strongWarning] // 是否需要强警告(用于清理他人数据)
|
||
* @property {string} [strongWarningText] // 强警告文案(如 draft-other: "将清空 uid 10002 的草稿,对方下次登录不会看到,是否继续?")
|
||
*/
|
||
|
||
/**
|
||
* @typedef {Object} GroupInfo
|
||
* @property {string} uid // 'self' = currentUid,'others' = 其他用户聚合,'__legacy__' = 老无 uid 后缀 key,其他 = 具体 uid
|
||
* @property {string} displayUid // UI 展示的 uid 字符串(如 "10001")
|
||
* @property {boolean} isCurrent // 是否当前用户
|
||
* @property {number} sizeBytes
|
||
* @property {number} keyCount
|
||
* @property {boolean} [canClean] // 该分组是否可清理(handler 决定,UI 据此 enable/disable 按钮)
|
||
* @property {string} [disabledReason] // 不可清理的原因('empty' / 'logged-out' / 'guide-current' / 'guide-global' / 'draft-legacy')
|
||
*/
|
||
```
|
||
|
||
**Handler 接口选择规则**:
|
||
- **简单 handler**(preload / progress / sandbox-tmp / others):实现 `computeSize` + `clean`,不实现 `computeBreakdown`/`cleanGroup`
|
||
- **分组 handler**(draft / guide):实现 `computeSize` + `computeBreakdown` + `cleanGroup`;不实现 `clean`(分组页不提供"一键清整类"按钮,避免误清他人数据)
|
||
|
||
### 3.2 内置 Handler 列表
|
||
|
||
| Handler ID | label | description | warning | 说明 |
|
||
|----------|------|-------------|---------|------|
|
||
| `preload` | "预加载数据缓存" | "列表页提前拉取的数据" | false | `preloadApi/storage.js` 的所有 `preload:*` key |
|
||
| `draft` | "创作中的草稿" | "未提交的创作表单/生成结果" | **true** | 草稿 key 改造(§11)后:`*_${currentUid}` 后缀形式。**实现方式**:handler 内部维护一个 7 个 base key 的静态数组(`['castlove_form_data', 'CASTLOVE_FORM_KEY', 'temp_nft_data', 'GENERATED_IMAGES_KEY', 'GENERATION_RESULT_META_KEY', 'LENTICULAR_STUDIO_STORAGE_KEY', 'CRAFT_SELECTED_IMAGE_KEY']`),运行时取 `currentUid` 拼成 `baseKey_${currentUid}`,遍历 `uni.getStorageInfoSync` 找匹配项。新 key 形式:`castlove_form_data_${currentUid}` / `CASTLOVE_FORM_KEY_${currentUid}` / ...(同上) |
|
||
| `progress` | "活动进度缓存" | "支持活动页断网浏览" | false | 全部 `progress_${activityId}` key(实现方式:遍历 `uni.getStorageInfoSync` 中所有以 `progress_` 开头的 key 全部删除) |
|
||
| `guide` | "引导记录" | "新手引导完成标记" | false | `guide_*` 系列(仅清非当前用户/会话残留;详见 §3.4) |
|
||
| `sandbox-tmp` | "临时文件" | "上传/分享过程产生的临时文件" | false | `clearAllSandboxTmpFiles`(保留 `preload/share/image` 白名单) |
|
||
| `others` | "其他业务缓存" | "未归类的少量业务数据" | false | 兜底分类:未匹配上述任一规则**且不在黑名单**的 key 汇总。**不**包含草稿 keys(任何 uid 后缀 + legacy),那些全部归 `draft` handler。包含需保护的工作流关键 key(`generation_flow_payload`、`__package_info__` 等)的**白名单排除**——这些 key 即使不在黑名单也不归 others 清理(避免破坏进行中的生成流程 / 升级包)。 |
|
||
|
||
### 3.3 cacheManager 公共 API
|
||
|
||
```js
|
||
// 注册(通常在 manager 模块底部 import 时一次性注册)
|
||
cacheManager.registerCategory(handler)
|
||
|
||
// 列表页读取(汇总 + 存储配额 + 其他 section 数据)
|
||
const info = await cacheManager.getCacheInfo()
|
||
// → {
|
||
// totalBytes, // 可清理总量(=所有 category.sizeBytes 之和,不含黑名单)
|
||
// appUsedBytes, // 软件占用(WeChat 口径,包含黑名单)= uni.getStorageInfoSync().currentSize*1024 + 沙盒文件
|
||
// // 注:currentSize 是 SQLite 全量(含黑名单),这是有意的 —— 表达"app 真实占用"而非"可清理"
|
||
// quotaTotalBytes, // 配额总量 = uni.getStorageInfoSync().limitSize*1024
|
||
// quotaAvailableBytes, // 配额可用 = quotaTotalBytes - appUsedBytes
|
||
// usagePercent, // 百分比(0-100,1 位小数)= appUsedBytes / quotaTotalBytes * 100
|
||
// deviceTotalBytes, // 设备总存储 = plus.io.getStorageInfo().totalSize * 1024(HTML5+ API,MVP 已支持)
|
||
// deviceFreeBytes, // 设备可用存储 = plus.io.getStorageInfo().availableSize * 1024
|
||
// deviceUsagePercent, // 设备占比(0-100,2 位小数)= appUsedBytes / deviceTotalBytes * 100
|
||
// othersBytes, // "其他" section 大小 = 所有黑名单 keys + 不可清理文件大小
|
||
// categories: [{id, label, description, sizeBytes, keyCount, warning}, ...]
|
||
// }
|
||
|
||
// 详情页读取(分组详情;简单 handler 返回 null)
|
||
const groups = await cacheManager.getCategoryBreakdown(id)
|
||
// → Array<GroupInfo> | null
|
||
|
||
// 简单页清理(无分组维度)
|
||
await cacheManager.cleanCategory(id)
|
||
// → { freedBytes, keyCount }
|
||
|
||
// 分组页清理(按 uid 维度,uid=null 表示其他用户聚合)
|
||
await cacheManager.cleanCategoryGroup(id, { uid: '10001' })
|
||
// → { freedBytes, keyCount }
|
||
|
||
// 编程式一键清理(UI 不调用;供登出流程/测试用,按 handler 顺序依次执行)
|
||
await cacheManager.cleanAll()
|
||
// → { freedBytes, perCategory: [{id, freedBytes, keyCount, error?}, ...] }
|
||
```
|
||
|
||
### 3.4 关键设计决策
|
||
|
||
#### 3.4.1 永远不清的 key(黑名单,由 manager 内置)
|
||
|
||
**精确匹配(全等字符串)**:
|
||
- `access_token` / `user` / `star_id` / `login_mobile` / `cid` / `deviceFp` / `pending_scan_url` / `gallery_owner_id` / `needs_welcome` / `has_seen_welcome` / `is_new_user`
|
||
|
||
**前缀匹配**:
|
||
- `daily_login_completed_` —— 每日登录打卡
|
||
- `avatar_file_` —— 头像文件元数据(`avatarCache.js`,与 `uni.saveFile` 配对;MVP 不清理 `uni.saveFile` 的 savedFilePath,由 OS 缓存淘汰负责)
|
||
- `liked_assets_exhibition` —— 用户点赞记录(点赞是用户行为,不是临时缓存)
|
||
- `temp_register_` —— 注册中状态(**含 `temp_register_password` 明文密码**)。原因:用户注册途中点清理会导致注册流程不可恢复;密码明文也不应在 cache 层有任何存活窗口风险。
|
||
|
||
不论怎么点这些 key **绝对不会被清理**,保证:
|
||
1. 不会误踢登录(token/user/star_id/cid)
|
||
2. 不会误清点赞(用户主动行为,有业务价值)
|
||
3. 不会误清头像文件(避免全部重新下载,**这是 MVP 范围外的关键回归点**)
|
||
4. 不会误清注册中状态导致注册流程断掉 + 避免密码明文暴露风险
|
||
|
||
#### 3.4.2 引导记录(guide handler)保留规则
|
||
|
||
- **当前用户判定**:handler 启动时 `uni.getStorageSync('user')`,parse 出 `uid`。**取不到(未登录)则视为无当前用户**。
|
||
- **保留规则**(**登录态与未登录态一致**):
|
||
- `guide_*_${currentUid}_*`(当前用户的引导进度)→ 全部保留
|
||
- 无 userId 段的 `guide_*`(`guide_shown_${configKey}`、`guide_debug_mode`、`guide_first_show`)→ **全部保留**(与登录态无关)
|
||
- 其他用户的 `guide_done_${otherUid}_*` / `guide_step_${otherUid}_*` / `guide_rewards_claimed_${otherUid}` / `guide_completed_steps_${otherUid}_*` → 仅登录态可清理(**未登录态禁止清理**,按钮置灰 + tooltip"请先登录")
|
||
- **未登录态**:currentUid = null,全部 `guide_*_${anyUid}_*` 都视作"其他用户"(按"其他用户的引导记录"清理规则处理,但**未登录时不可清理**);无 userId 段 `guide_*` 仍保留。
|
||
- **owner 段识别**:支持数字 uid(`guide_done_10001_*`)和 `default`(`guide_done_default_*`,guideConfig.js 在未登录场景下使用)两种。
|
||
- 实现方式:handler 拿到 currentUid 后,分类处理 —— 含 currentUid 段的跳过;无 userId 段但属于 `guide_shown_*`/`guide_debug_mode`/`guide_first_show` 的跳过;其余 `guide_*` 仅在登录态下可清理。
|
||
|
||
#### 3.4.3 分类分区规则(避免 others 误吞)
|
||
|
||
每个 key 的归属判定 **必须按以下优先级**:
|
||
1. 黑名单前缀(§3.4.1)→ **永不删**(不计入 totalBytes,不计入"其他"section,仅用于计算 `othersBytes`)
|
||
2. `preload:` 前缀 → `preload` handler
|
||
3. 创作草稿白名单(§3.2 draft) → `draft` handler
|
||
4. `progress_` 前缀 → `progress` handler
|
||
5. `guide_` 前缀 → `guide` handler(含 §3.4.2 的保留判断)
|
||
6. 其余非黑名单 key → `others` handler
|
||
|
||
**关键**:§5.1 的 `getCacheInfo()` 必须先过滤黑名单 key,再分类计算。否则黑名单 key 既不会展示也不会被删,但会被错误计入 totalBytes 误导用户。
|
||
|
||
#### 3.4.4 内存层清理(详情页 + cleanAll 同步触发)
|
||
|
||
`preloadApi/core.js` 的 `memoryMap` 是进程级单例 Map —— **所有用户的 preload 数据都在同一个 Map 里**。切账号不清会导致老用户条目挤占内存。
|
||
|
||
**`cacheManager.cleanAll()`** 在 storage/sandbox handler 全部完成后,**同步**调:
|
||
|
||
```js
|
||
import { invalidateAll } from '@/utils/preloadApi/core'
|
||
// ...
|
||
await invalidateAll() // 清空 memoryMap + inFlightMap
|
||
```
|
||
|
||
**详情页 `cleanCategory('preload')` / `cleanCategoryGroup('preload', ...)`** 完成后,**同样**同步调 `invalidateAll()`。原因:用户从详情页清 preload 后,下一次进列表页如果 preload handler 仍命中 `memoryMap` 老数据,用户感知不到清理效果。
|
||
|
||
注意:`invalidateAll()` 不动 storage,只清内存层。storage 清理由 `preload.clean()` / `preload.cleanGroup()`(§5.2)负责。
|
||
|
||
#### 3.4.5 活跃创作页面保护(MVP 不做)
|
||
|
||
用户编辑中点清理导致草稿丢失,仅靠 UI ConfirmModal 文案警示,不做活跃检测(YAGNI)。后续优化可加页面级 dirty 标记。
|
||
|
||
---
|
||
|
||
## 四、UI 页面结构
|
||
|
||
### 4.1 入口
|
||
|
||
`pages/profile/profile.vue` 加菜单项「存储空间」点击进入 `cache-cleanup` 列表页。
|
||
|
||
### 4.2 列表页(pages/profile/cache-cleanup.vue)
|
||
|
||
**采用 WeChat 风格:顶部大字 + 百分比 + 进度条 + 一行 3 标签 + 缓存分类 + 其他 section**。**没有任何清理按钮**。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ ← 返回 存储空间 │ ← navbar
|
||
├─────────────────────────────────────────────┤
|
||
│ │
|
||
│ Topfans 已用空间 │ ← 小字 label
|
||
│ │
|
||
│ 49.7 MB │ ← 大字号主指标(主色)
|
||
│ │
|
||
│ ▓▓▓▓░░░░░░░░░░░░░░░░░░░░░ 16% │ ← 进度条(当前用量占配额比例)
|
||
│ │
|
||
│ ┌────────────┬─────────────┬────────────┐ │
|
||
│ │ Topfans 已用空间 │ 内存缓存空间 │ 总内存空间 │ │ ← 一行 3 个 chip 标签(无数值)
|
||
│ └────────────┴─────────────┴────────────┘ │ 数值已分别显示在:大字 / 缓存分类 sum / 总配额
|
||
│ │
|
||
│ ┌──────────────────────────────────────┐ │
|
||
│ │ 设备总空间 │ 设备可用 │ │ ← 新增:HTML5+ 设备级信息行
|
||
│ │ 128.0 GB │ 45.2 GB │ │ (plus.io.getStorageInfo;非 APP-PLUS 显示 "—")
|
||
│ └──────────────────────────────────────┘ │
|
||
│ │
|
||
│ 占设备 0.04% 存储空间 │ ← 百分比副文字(分母=设备总存储,2 位小数)
|
||
├─────────────────────────────────────────────┤
|
||
│ 缓存分类 │ ← section 标题(次要色)
|
||
├─────────────────────────────────────────────┤
|
||
│ 预加载数据缓存 > │ ← 分类行(点击进详情)
|
||
│ 15.2 MB · 142 项 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 创作中的草稿 > │
|
||
│ 2.4 MB · 3 项 ⚠ 按账号分组清理 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 活动进度缓存 > │
|
||
│ 0.4 MB · 5 项 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 引导记录 > │
|
||
│ 0.2 MB · 8 项 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 临时文件 > │
|
||
│ 1.5 MB · 15 项 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 其他业务缓存 > │
|
||
│ 0.7 MB · 12 项 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 其他 > │ ← 新增:黑名单/不可清理数据
|
||
│ 4.1 MB │
|
||
│ 包含运行 Topfans 的必要数据、账号会话、 │
|
||
│ 其他账号的数据等 │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
**3 个标签的语义对应**(与 WeChat 同款:"微信已用空间/磁盘已用空间/磁盘可用空间"):
|
||
- **Topfans 已用空间** = 大字 `appUsedBytes`(顶部已显示)
|
||
- **内存缓存空间** = 下方"缓存分类"的总和 = `totalBytes`(可清理部分)
|
||
- **总内存空间** = `uni.getStorageInfoSync().limitSize` × 1024 = `quotaTotalBytes`(配额上限)
|
||
|
||
3 个标签**无数据值**(数据值已分别在:大字、缓存分类、总配额三处展示),纯起"标签导航"作用。
|
||
|
||
**"其他" section 的来源**:
|
||
- 数据 = 黑名单 keys 大小 + 黑名单沙盒文件大小(**所有不该/不能清理的数据**)
|
||
- 包括:登录会话(access_token/user/star_id/cid/deviceFp 等)、设备状态(needs_welcome/has_seen_welcome/is_new_user/daily_login_completed_*)、用户行为(liked_assets_exhibition)、头像缓存(avatar_file_* + savedFilePath)、注册中状态(temp_register_*)、其他账号的草稿/引导残留等
|
||
- 文案(仿 WeChat):"包含运行 Topfans 的必要数据、账号会话、其他账号的数据等"
|
||
- 该行**不可点击进入详情**(没有可清理内容)
|
||
|
||
**关于"磁盘" vs "配额"**:
|
||
- **进度条**仍以 `uni.getStorageInfoSync().limitSize`(app 自己的 SQLite 配额)为 100%,表达"app 缓存压力"(用户的清理决策依据)
|
||
- **百分比分母** = `plus.io.getStorageInfo().totalSize`(设备总存储),即"占设备 X% 存储空间"——HTML5+ 标准 API,Android/iOS 均原生支持,**不需要 native plugin**
|
||
- **设备信息行** = `plus.io.getStorageInfo()` 直接拿 `{ totalSize, availableSize }`,无权限要求;非 APP-PLUS 平台或调用失败时显示 `—`
|
||
|
||
**草稿/引导的二级提示**:行右侧副文字"按账号分组清理",警示用户详情页会有分组。
|
||
|
||
### 4.3 详情页(pages/profile/cache-cleanup-detail.vue?id=xxx)
|
||
|
||
**通用结构**:每个分类的详情页头部展示分类说明,中间展示分组(按需),底部展示清理按钮。
|
||
|
||
#### 4.3.1 简单详情页(preload / progress / sandbox-tmp / others)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ ← 返回 预加载数据缓存 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 列表页提前拉取的数据 │ ← description
|
||
├─────────────────────────────────────────────┤
|
||
│ │
|
||
│ 占用 18.2 MB │ ← 大字号
|
||
│ 共 142 项缓存 │
|
||
│ │
|
||
├─────────────────────────────────────────────┤
|
||
│ │
|
||
│ [ 清 理 缓 存 ] │ ← 主按钮
|
||
│ │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
点击「清理缓存」→ ConfirmModal(带 warning 时显示警告)→ 确认 → 执行 → toast + 返回列表页(自动刷新)。
|
||
|
||
#### 4.3.2 分组详情页(draft / guide)
|
||
|
||
**按 uid 分组展示,每组独立清理按钮**。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ ← 返回 创作中的草稿 │
|
||
├─────────────────────────────────────────────┤
|
||
│ 未提交的创作表单/生成结果 │
|
||
├─────────────────────────────────────────────┤
|
||
│ │
|
||
│ 👤 我的草稿(uid: 10001) │
|
||
│ 1.2 MB · 2 项 │
|
||
│ [ 清 理 ] │
|
||
│ ───────────────────────────────── │
|
||
│ 👤 其他用户的草稿(uid: 10002) │
|
||
│ 0.8 MB · 1 项 │
|
||
│ [ 清 理 ] ⚠ │ ← 更强警告
|
||
│ ───────────────────────────────── │
|
||
│ 👤 其他用户的草稿(uid: 10003) │
|
||
│ 0.4 MB · 0 项 │ ← 0 项时按钮置灰
|
||
│ [ 清 理 ] │
|
||
│ │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
**关键行为**:
|
||
- 「我的草稿」按钮 → ConfirmModal 普通警告("将清空你未提交的草稿")
|
||
- 「其他用户的草稿」按钮 → ConfirmModal **更强警告**("将清空 uid 10002 的草稿,对方下次登录不会看到。是否继续?")
|
||
- 0 项分组的按钮置灰禁用
|
||
- 列表按 `sizeBytes` 降序排序,大占用在前
|
||
|
||
**引导详情页同结构**,但 description 与分组标题文案不同。
|
||
|
||
### 4.4 交互流程
|
||
|
||
1. **列表页 onLoad**:调 `cacheManager.getCacheInfo()` → 渲染分类列表(带 loading skeleton)
|
||
2. **点击分类行**:`uni.navigateTo` 进 `cache-cleanup-detail?id=xxx`
|
||
3. **详情页 onLoad**:调 `cacheManager.getCategoryBreakdown(id)`(简单 handler 返回 `null`,UI 走简单页模板;分组 handler 返回 `Array<GroupInfo>`,UI 走分组页模板)
|
||
4. **详情页点清理按钮**(分组页每组一个,简单页一个):弹 ConfirmModal(按需 warning/strongWarning)→ 确认 →
|
||
- 简单页:调 `cacheManager.cleanCategory(id)`
|
||
- 分组页:调 `cacheManager.cleanCategoryGroup(id, { uid: 'self' | '具体uid' | null })`(uid='self' 表示当前用户,uid=null 表示"其他用户聚合")
|
||
- → `uni.showLoading` → toast → 返回列表页(`uni.navigateBack`,列表页 `onShow` 自动重算 `getCacheInfo`)
|
||
5. **下拉刷新**(列表页):onPullDownRefresh 重新调 `getCacheInfo()` 刷新(页面级 enablePullDownRefresh)
|
||
6. **下拉刷新**(详情页):重新调 `cacheManager.getCategoryBreakdown(id)`
|
||
|
||
### 4.5 视觉规范
|
||
|
||
- 复用项目已有 `components/ConfirmModal.vue`
|
||
- 分类行用 `pages/components/Header.vue` 同款 row 风格
|
||
- 警示文字用 `#FAAD14`(项目里的 warning 色);强警告用 `#FF4D4F`
|
||
- 分组卡片用 `border-radius: 12rpx` 圆角,与列表项视觉区分
|
||
|
||
---
|
||
|
||
## 五、数据流与加载策略
|
||
|
||
### 5.1 首次加载
|
||
|
||
```
|
||
pages/profile/cache-cleanup.vue onLoad
|
||
↓
|
||
cacheManager.getCacheInfo()
|
||
├─ [并行 1] 遍历所有注册 handler 调 computeSize() Promise.all
|
||
│ ├─ preload.computeSize → 遍历所有 preload:* key
|
||
│ ├─ draft.computeSize → 遍历白名单内 `*_${currentUid}` key(详见 §3.2 实现说明)
|
||
│ ├─ progress.computeSize → 遍历 progress_*
|
||
│ ├─ guide.computeSize → 遍历 guide_*
|
||
│ ├─ sandbox-tmp.computeSize → 调用 ioPath.scanSandboxTmpFiles()(新增只读 API,返回 tmp 文件大小总和)
|
||
│ └─ others.computeSize → 上述未匹配的 key 汇总
|
||
├─ [并行 2] 读取存储配额
|
||
│ ├─ uni.getStorageInfoSync() → { currentSize (KB), limitSize (KB) }
|
||
│ ├─ ioPath.getSandboxTotalSize() → 沙盒 doc 目录下所有文件总字节数(新增 ioPath 只读 API;与 scanSandboxTmpFiles 不同,前者含白名单 preload/share/image 所有文件,后者仅统计 tmp/)
|
||
│ └─ ioPath.getDeviceStorageInfo() → 设备级 { totalSize (KB), availableSize (KB) },HTML5+ API,非 APP-PLUS 返回 {0,0}
|
||
↓
|
||
聚合:
|
||
totalBytes = sum(categories.sizeBytes) // 可清理总量(=内存缓存空间 chip)
|
||
appUsedBytes = currentSize * 1024 + sandboxBytes // 软件占用(大字 + Topfans已用空间 chip)
|
||
// 注:currentSize 是 uni storage 全量(含黑名单),这是 WeChat "已用空间" 口径
|
||
quotaTotalBytes = limitSize * 1024 // 配额总量(=总内存空间 chip,进度条 100% 基准)
|
||
quotaAvailableBytes = quotaTotalBytes - appUsedBytes // 配额可用
|
||
usagePercent = (appUsedBytes / quotaTotalBytes) * 100 // 配额百分比(进度条用)
|
||
deviceTotalBytes = deviceTotalKB * 1024 // 设备总存储(设备信息行"设备总空间")
|
||
deviceFreeBytes = deviceFreeKB * 1024 // 设备可用(设备信息行"设备可用")
|
||
deviceUsagePercent = (appUsedBytes / deviceTotalBytes) * 100 // 设备百分比(百分比副文字"占设备 X%"用)
|
||
othersBytes = sum(黑名单 keys size) + 不可清理文件大小 // "其他" section
|
||
↓
|
||
渲染页面(大字 + 百分比 + 进度条 + 3 chip 标签 + 6 缓存分类行 + 1 其他 section)
|
||
```
|
||
|
||
### 5.2 清理流程
|
||
|
||
**用户从详情页清理(主要路径)**:
|
||
|
||
```
|
||
用户在详情页点清理按钮(简单页单按钮 / 分组页每组按钮)
|
||
↓
|
||
ConfirmModal 显示预估 freedBytes(含 warning/strongWarning 时显示对应警告文案)
|
||
↓
|
||
用户确认
|
||
↓
|
||
cacheManager.cleanCategory(id) ← 简单页
|
||
或
|
||
cacheManager.cleanCategoryGroup(id, {uid}) ← 分组页(uid='self' 或具体 uid 或 null=其他用户聚合)
|
||
├─ 调对应 handler.clean() 或 handler.cleanGroup()
|
||
├─ 错误容错(单 handler 失败不影响)
|
||
↓
|
||
返回 { freedBytes, keyCount }
|
||
↓
|
||
UI toast + uni.navigateBack(列表页 onShow 自动重算 getCacheInfo)
|
||
```
|
||
|
||
**编程式 cleanAll(登出/测试用,UI 不调用)**:
|
||
|
||
```
|
||
cacheManager.cleanAll()
|
||
├─ 顺序执行每个 handler.clean()(非 Promise.all,更安全)
|
||
│ ├─ preload.clean → 遍历所有 `preload:` 前缀 key 删除(**所有用户**,不限于 currentUid;详见 §1 决策 6)
|
||
│ ├─ draft.clean → 删除 `*_${currentUid}` 后缀的草稿 key(uid 改造完成后;详见 §11);迁移期内老无 uid 后缀 key 归 others
|
||
│ ├─ progress.clean → 删除 progress_* key
|
||
│ ├─ guide.clean → 删除其他用户的 `guide_*_${otherUid}_*` + 当前用户的 `guide_done_*`/`guide_step_*`/...(详见 §3.4.2)
|
||
│ ├─ sandbox-tmp.clean → clearAllSandboxTmpFiles()
|
||
│ └─ others.clean → 删除未分类 key(含迁移期遗留的无 uid 后缀草稿 key)
|
||
├─ ★ 同步清理内存层:core.invalidateAll()(详见 §3.4.4)
|
||
↓
|
||
返回 { freedBytes, perCategory[] }
|
||
```
|
||
|
||
### 5.3 关键设计决策
|
||
|
||
1. **getCacheInfo 异步并行**:6 个 handler 用 `Promise.all` 并行计算
|
||
2. **cleanAll 顺序执行**:部分清理会互斥,顺序更安全;用 `uni.showLoading({mask:true})` 包裹
|
||
3. **失败容错**:单个 handler clean() 失败不影响其他 handler,外层 try/catch 记录 error 并跳过
|
||
4. **空状态**:若 totalBytes = 0,列表页显示 "✓ 当前无缓存可清理",所有分类行点击无响应(toast 提示"暂无缓存")
|
||
5. **重新计算时机**:清理完成后**必须**重算 `getCacheInfo()`,不能用 `info.totalBytes - freedBytes` 简单相减
|
||
6. **cleanCategory/cleanCategoryGroup 单例保护**:`cleanCategory(id)` 与 `cleanCategoryGroup(id, opts)` 各维护一个 `_inFlight` Map(key 为 `id` 或 `${id}#${uid}`);并发触发同一目标返回同一 Promise,不重复执行。UI 通过 `uni.showLoading({mask:true})` 阻隔误触。`cleanAll()` 不再被 UI 调用,保留作为登出流程/测试的编程式入口,不需 in-flight 保护。
|
||
|
||
---
|
||
|
||
## 六、错误处理 & 边界条件
|
||
|
||
### 6.1 错误分类与处理
|
||
|
||
| 场景 | 行为 |
|
||
|------|------|
|
||
| 单个 handler `computeSize()` 抛错 | 该分类显示 `sizeBytes: -1`、label 旁加 `?` 标记,主页面其他分类正常显示,console.warn 记录 |
|
||
| 单个 handler `clean()` / `cleanGroup()` 抛错 | 该分类跳过,perCategory 中标记 `{id, freedBytes: 0, error: 'msg'}`,其他继续;最终 toast 显示「已清理 XX MB(X 项失败)」 |
|
||
| `getCacheInfo()` 整体超时(>5s)| 兜底显示空列表 + 「加载失败,点此重试」按钮 |
|
||
| `uni.getStorageInfoSync()` 失败(罕见)| 整个 storage 维度显示 `— MB`,仅沙盒维度可清理 |
|
||
| 沙盒文件不存在(登出后清理等场景)| `clearAllSandboxTmpFiles` 已实现"目录不存在视为成功",无需额外处理 |
|
||
|
||
### 6.2 边界条件
|
||
|
||
- **页面打开瞬间用户被踢登录**:`getCacheInfo()` 不依赖 token,可正常返回;若清理中触发退出,回调里捕错即可
|
||
- **清理进行中用户退出页面**:用 `cacheManager.cleanCategory()` 的 in-flight 单例保护 —— 重复触发返回同一 Promise
|
||
- **草稿清理有未提交内容**:UI 在 ConfirmModal 文案写明"将清空你未提交的创作草稿";不做活跃页面编辑检测(YAGNI)
|
||
- **黑名单 key 误触**:内置黑名单在 `cleanCategory` / `cleanCategoryGroup` 入口拦截,永不删
|
||
- **大小格式化**:用 `KB/MB/GB` 自适应,1 位小数(避免 `0.0 MB`),< 1KB 显示 `< 1 KB`
|
||
- **顶部数据加载失败**:配额读取失败(罕见)时,UI 显示"配额数据暂不可用",分类列表仍正常渲染;getCacheInfo() 内部 try/catch 单点失败不影响其他字段
|
||
- **详情页无 uid 数据**:草稿/引导详情页若当前用户 uid 取不到(未登录),全部归为"其他用户"分组,**不允许清理**(按钮置灰 + tooltip"请先登录")
|
||
- **未登录态只能清理简单页**:preload / progress / sandbox-tmp / others 在未登录态允许清理;draft / guide 在未登录态**禁止清理**(按钮置灰)
|
||
|
||
### 6.3 关键日志
|
||
|
||
所有 handler 失败必须 `console.warn('[cacheManager] xxx failed:', id, err.message)`,便于线上排查。
|
||
|
||
---
|
||
|
||
## 七、测试策略
|
||
|
||
### 7.1 单元测试
|
||
|
||
- `utils/cacheManager.js` 纯函数(黑名单匹配、key 分类、大小格式化、GroupInfo 构造)单测
|
||
- 6 个 handler 的 `computeSize` / `clean` / `computeBreakdown` / `cleanGroup` 单测(mock `uni.getStorageSync`)
|
||
- `draftStorage.js` 双读单写逻辑单测(mock storage)
|
||
|
||
### 7.2 集成测试
|
||
|
||
mock `uni.getStorageSync` / `clearAllSandboxTmpFiles` / `core.invalidateAll`,验证:
|
||
- `cleanAll()` 顺序执行 + invalidateAll 同步
|
||
- 失败容错(单个失败不影响其他)
|
||
- 黑名单永不删
|
||
- `cleanCategory('preload')` 完成后 memoryMap 已清空
|
||
|
||
### 7.3 手工验证场景
|
||
|
||
| 场景 | 期望 |
|
||
|------|------|
|
||
| 登录态清理(简单页) | token/user/star_id/cid 不变,console 二次确认 |
|
||
| 登录态清理(分组页-我的草稿) | 仅删 currentUid 草稿;普通警告 |
|
||
| 登录态清理(分组页-其他用户) | 仅删 otherUid 草稿;强警告;对方下次登录看不到 |
|
||
| 未登录态清理(简单页) | 全部业务缓存正常清理 |
|
||
| 未登录态清理(分组页) | 草稿/引导按钮置灰,不可清理 |
|
||
| 清理中退出页面 | 不重复清理,不报错 |
|
||
| 空缓存状态 | 列表页显示"✓ 当前无缓存可清理",无跳转入口 |
|
||
| 草稿详情页(仅自己) | 仅显示"我的草稿"分组,清理按钮触发普通警告 |
|
||
| 草稿详情页(多账号切换后) | 显示"我的草稿" + 多个"其他用户的草稿"分组;强警告+正常清理流程 |
|
||
| 草稿详情页(未登录) | 所有数据归"其他用户",按钮置灰 |
|
||
| **账号 A 写草稿 → 退出 → 账号 B 登录 → 打开创作页** | **B 看不到 A 的草稿**(依赖 §11 改造) |
|
||
| **账号 A 产生 preload 缓存 → 退出 → 账号 B 登录 → B 进 preload 详情页清理** | **A 的 preload 也被清掉**(§1 决策 6) |
|
||
| **清理 preload 后立刻再访问同一列表页** | **不会从 memoryMap 命中老数据**(§3.4.4 内存层清理) |
|
||
|
||
---
|
||
|
||
## 八、文件清单
|
||
|
||
### 8.1 新增文件
|
||
|
||
| 路径 | 说明 |
|
||
|------|------|
|
||
| `frontend/utils/cacheManager.js` | 统一封装层(核心,约 250-300 行) |
|
||
| `frontend/utils/handlers/preloadHandler.js` | preload 缓存 handler(简单型) |
|
||
| `frontend/utils/handlers/draftHandler.js` | 创作草稿 handler(分组型,含 computeBreakdown + cleanGroup) |
|
||
| `frontend/utils/handlers/progressHandler.js` | 进度缓存 handler(简单型) |
|
||
| `frontend/utils/handlers/guideHandler.js` | 引导记录 handler(分组型,含 computeBreakdown + cleanGroup) |
|
||
| `frontend/utils/handlers/sandboxTmpHandler.js` | 沙盒临时文件 handler(基于 ioPath.js,简单型) |
|
||
| `frontend/utils/handlers/othersHandler.js` | 兜底分类 handler(简单型) |
|
||
| `frontend/pages/profile/cache-cleanup.vue` | 缓存清理列表页(只展示,无按钮) |
|
||
| `frontend/pages/profile/cache-cleanup-detail.vue` | 缓存清理详情页(按 `id` 分发到简单/分组模板) |
|
||
|
||
### 8.2 修改文件
|
||
|
||
| 路径 | 改动 |
|
||
|------|------|
|
||
| `frontend/pages/profile/profile.vue` | 加菜单项「存储空间」点击进入 `cache-cleanup` |
|
||
| `frontend/pages.json` | 注册两个新页面(cache-cleanup 列表 + cache-cleanup-detail 详情) |
|
||
| `frontend/utils/ioPath.js` | 新增 `getSandboxTotalSize()` 只读 API(§5.1 并行 2 用于计算沙盒文件总字节)|
|
||
|
||
### 8.3 实施顺序(5 个 milestone,M0 是 M2 前置)
|
||
|
||
1. **M0 — 草稿 key 改造(前置迁移)**:`utils/draftStorage.js` 封装 + 7 个文件读写侧改造 + 双读单写兼容(§11)
|
||
2. **M1 — 核心封装层**:`cacheManager.js` + 黑名单 + 大小格式化 + `getCacheInfo()` / `getCategoryBreakdown()` / `cleanCategory()` / `cleanCategoryGroup()` 框架(handlers 暂时用空 stub);包含 in-flight 单例保护
|
||
3. **M2 — 6 个 handler 实现**:每个 handler 的 computeSize / clean(或 computeBreakdown + cleanGroup);preload 跨用户清理;preload handler 清理完成后同步调 `invalidateAll()`
|
||
4. **M3 — UI 页面(列表 + 详情)**:`cache-cleanup.vue`(列表)+ `cache-cleanup-detail.vue`(简单/分组模板)+ ConfirmModal + profile.vue 入口 + pages.json 注册
|
||
5. **M4 — 测试 & 回归**:单元测试 + 13 个手工场景验证(含跨账号分组清理、未登录态分组禁用、清理后内存即清等)+ 对照 CLAUDE.md 完成自检清单
|
||
|
||
---
|
||
|
||
## 九、验收标准(DoD)
|
||
|
||
- [ ] 个人中心出现「存储空间」入口,点击进入列表页
|
||
- [ ] 列表页顶部显示:**大字(Topfans 已用空间)+ 百分比(占据配额 X%)+ 进度条 + 3 个 chip 标签(无数值)+ 6 个分类行 + 1 个"其他"section**
|
||
- [ ] 列表页**没有任何清理按钮**;点击分类行进入详情页;"其他"section 不可点击
|
||
- [ ] **简单详情页**(preload / progress / sandbox-tmp / others):单按钮 + ConfirmModal(含 warning 时显示警告文案)
|
||
- [ ] **分组详情页**(draft / guide):按 uid 分组卡片,每组独立清理按钮;当前用户组普通警告,他人组**强警告**;0 项组按钮置灰
|
||
- [ ] 未登录态打开草稿/引导详情页:所有数据归"其他用户",按钮置灰 + tooltip "请先登录"
|
||
- [ ] 详情页清理 preload 后立即进列表页:memoryMap 已清空,重新拉数据
|
||
- [ ] 详情页清理成功后 toast + 返回列表页(onShow 自动重算)
|
||
- [ ] 单个 handler 失败不影响其他 handler
|
||
- [ ] 登录 token、用户信息、设备指纹等黑名单 key **绝对不会被清理**(用 console.log 二次确认)
|
||
- [ ] `pages.json` 已注册两个新页面(列表 + 详情),`unpackage/dist/` 未手动改
|
||
- [ ] 走 CLAUDE.md 自检清单(API 工程化、前端规范、接口规范均过)
|
||
|
||
---
|
||
|
||
## 十、风险与回滚
|
||
|
||
| 风险 | 影响 | 缓解 |
|
||
|------|------|------|
|
||
| 黑名单遗漏某个登录态 key | 用户被错误退出 | M2 完成后 grep 全量 storage key 与黑名单比对;M4 手工验证登录态清理 |
|
||
| handler 顺序错误 | sandbox 清理在 preload 清理之前引用失效 | 已设计为顺序执行;单测覆盖 |
|
||
| 草稿清理误删活跃数据 | 用户创作内容丢失 | ConfirmModal 文案警示;后续可加活跃检测(MVP 不做) |
|
||
| `clearAllSandboxTmpFiles` 性能 | 启动时已实现,启动清理通常 0 个 tmp | 实测 50ms 内完成;超过 1s 加 loading |
|
||
|
||
回滚:删除 `pages/profile/cache-cleanup.vue` + `pages.json` 注册项 + profile.vue 菜单项即可,业务无侵入。
|
||
|
||
---
|
||
|
||
## 十一、草稿 key 改造迁移项
|
||
|
||
> **范围外工作但与本 spec 强耦合**:为了让 §3.2 的 draft handler 准确按 uid 分类,必须先把草稿 storage key 改为 `*_${uid}` 形式。本节作为 M2 启动前的**前置依赖**。
|
||
|
||
### 11.1 现状问题
|
||
|
||
当前草稿 key 不带 uid:
|
||
- `castlove_form_data`
|
||
- `CASTLOVE_FORM_KEY`
|
||
- `temp_nft_data`
|
||
- `GENERATED_IMAGES_KEY`
|
||
- `GENERATION_RESULT_META_KEY`
|
||
- `LENTICULAR_STUDIO_STORAGE_KEY`
|
||
- `CRAFT_SELECTED_IMAGE_KEY`
|
||
|
||
**结果**:账号 A 的草稿在设备上 → 退出登录 → B 账号登录 → 打开创作页 → **看到 A 的草稿**。这是隐私 + 体验问题。
|
||
|
||
### 11.2 改造方案
|
||
|
||
将所有写入侧(`pages/castlove/create.vue`、`pages/castlove/index.vue`、`pages/castlove/success.vue`、`composables/useLaserSegment.js`、`composables/useLaserBatchGenerate.js` 等)改为:
|
||
|
||
```js
|
||
// 旧
|
||
uni.setStorageSync('castlove_form_data', JSON.stringify(formData))
|
||
// 新
|
||
uni.setStorageSync(`castlove_form_data_${currentUid}`, JSON.stringify(formData))
|
||
```
|
||
|
||
读取侧同样需改造。
|
||
|
||
### 11.3 迁移策略
|
||
|
||
**双读单写**:
|
||
- 读:先读新 key `*_${currentUid}`;若不存在,**fallback** 读老 key;读到后将数据写到新 key,老 key 删除
|
||
- 写:只写新 key `*_${currentUid}`
|
||
|
||
实现位置:建议封装一个 `utils/draftStorage.js`,所有读写都走它,业务侧无感。
|
||
|
||
### 11.4 与 cacheManager 的关系
|
||
|
||
- **迁移完成前**:draft handler.computeSize() 同时计入新 key(`*_${currentUid}`)+ 老 key(无 uid 后缀),所有都按 uid 分组(无后缀 key 归 `__legacy__` 分组)
|
||
- **迁移完成后**:draft handler 仅匹配 `*_${currentUid}` + 老无 uid 后缀 key(理论上不应再有,但万一有仍归 `__legacy__` 分组);**不**走 `others` 兜底
|
||
- **验证**:M4 手工场景新增"切换账号后看不到对方草稿"
|
||
|
||
### 11.5 工作量估算
|
||
|
||
**M0 范围限定为「核心 castlove 创作流」(约 7 个文件)**:
|
||
- `pages/castlove/create.vue`、`pages/castlove/index.vue`、`pages/castlove/success.vue`、`pages/castlove/lenticular/lenticular-result.vue`、`pages/castlove/laser/laser-result.vue`、`composables/useLaserSegment.js`、`composables/useLaserBatchGenerate.js`
|
||
- 包含:`castlove_form_data` / `CASTLOVE_FORM_KEY` / `temp_nft_data` / `GENERATED_IMAGES_KEY` / `GENERATION_RESULT_META_KEY` / `LENTICULAR_STUDIO_STORAGE_KEY` / `CRAFT_SELECTED_IMAGE_KEY`
|
||
|
||
**discover / mint / self-created 等流程(约 10 个其他文件)**涉及相同 key 但属于次级路径,**列为 M0 后续批次(M0.5)**,MVP 上线后补;估算额外 1 天。
|
||
|
||
**核心 castlove 7 文件改造工作量**:
|
||
- 改造 7 个文件读写侧:0.5 天
|
||
- `draftStorage.js` 封装:0.5 天
|
||
- 联调验证:0.5 天
|
||
|
||
合计 **1.5 天**,作为 M2 启动前置。
|
||
|
||
---
|
||
|
||
## 十二、自检清单
|
||
|
||
按 CLAUDE.md 要求:
|
||
|
||
- [ ] 文档开头「方案概述」含:要解决的问题 / 实现路径 / 关键决策 / 核心架构图 ✅
|
||
- [ ] MVP 先行:未引入超出当前业务需要的抽象(统一封装层是必要的,避免 UI 上帝化) ✅
|
||
- [ ] 文件清单与目录结构对齐:`utils/handlers/` 是新目录需提前创建 ✅
|
||
- [ ] 跨章节引用一致性:§3.2 → §4.2 分类一致;§4.3 → §5 流程一致;§6 → §7 测试场景一致 ✅
|
||
- [ ] CLAUDE.md 自检清单(前端规范):新组件用 `<script setup>` 组合式 API、所有原生 API 包 `#ifdef`、接口走 `utils/api.js`、新页面已在 `pages.json` 注册、敏感权限失败有"去设置"出口、不动 `unpackage/dist/` ✅ |