topfans/docs/specs/2026-07-28-cache-cleanup-design.md
2026-07-29 16:03:33 +08:00

47 KiB
Raw Blame History

缓存清理功能设计2026-07-28

配套前置分析:本次会话中已扫描 frontend/ 全量代码,识别出 4 大类存储uni 本地持久化、内存缓存、临时文件、沙盒文件系统)。本文把"统一清理入口"这一需求转化为可执行的 MVP 实施方案


一、方案概述(必读)

要解决的问题

业务问题

  • 用户遇到"App 占用过大"、"切换账号后旧账号残留"、"创作草稿一直清不掉"、"活动进度缓存过期"等场景时,没有统一的清理入口
  • 当前只能在登出时被动清一部分(store/modules/user.js#CLEAR_AUTH),普通用户无法主动触发。
  • 多个 utils 各自实现清理逻辑(avatarCache.jslikeHelper.jspreloadApi/storage.jsioPath.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.jsUI 只调 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.jsavatar_file_* storage key 与 uni.saveFilesavedFilePath 均纳入黑名单§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.jsmemoryMap 是进程级共享,切账号不清会残留老用户数据。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 ea39ee1feat:修改图片尺寸和uni配置
  • 目标读者:前端工程师、测试、产品。
  • 后续优化(不在 MVP
    • 二级详情页MVP 包含)
    • 头像/图片缓存清理(需评估用户体验)
    • 清理历史记录(用户看到"上次清理于 X 分钟前"
    • 自动清理策略(按 LRU 自动触发)

三、Handler 接口契约

3.1 类型定义

/**
 * @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 接口选择规则

  • 简单 handlerpreload / progress / sandbox-tmp / others实现 computeSize + clean,不实现 computeBreakdown/cleanGroup
  • 分组 handlerdraft / 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。包含需保护的工作流关键 keygeneration_flow_payload__package_info__ 等)的白名单排除——这些 key 即使不在黑名单也不归 others 清理(避免破坏进行中的生成流程 / 升级包)。

3.3 cacheManager 公共 API

// 注册(通常在 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-1001 位小数)= appUsedBytes / quotaTotalBytes * 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_modeguide_first_show)→ 全部保留(与登录态无关)
    • 其他用户的 guide_done_${otherUid}_* / guide_step_${otherUid}_* / guide_rewards_claimed_${otherUid} / guide_completed_steps_${otherUid}_* → 仅登录态可清理(未登录态禁止清理,按钮置灰 + tooltip"请先登录"
  • 未登录态currentUid = null全部 guide_*_${anyUid}_* 都视作"其他用户"(按"其他用户的引导记录"清理规则处理,但未登录时不可清理);无 userId 段 guide_* 仍保留。
  • owner 段识别:支持数字 uidguide_done_10001_*)和 defaultguide_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 draftdraft 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.jsmemoryMap 是进程级单例 Map —— 所有用户的 preload 数据都在同一个 Map 里。切账号不清会导致老用户条目挤占内存。

cacheManager.cleanAll() 在 storage/sandbox handler 全部完成后,同步调:

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% 存储空间                         │  ← 百分比副文字(次要色)
│                                             │
│  ▓▓▓▓░░░░░░░░░░░░░░░░░░░░░ 16%             │  ← 进度条(当前用量占配额比例)
│                                             │
│  ┌────────────┬─────────────┬────────────┐  │
│  │ Topfans 已用空间 │ 内存缓存空间 │ 总内存空间 │  │  ← 一行 3 个 chip 标签(无数值)
│  └────────────┴─────────────┴────────────┘  │     数值已分别显示在:大字 / 缓存分类 sum / 总配额
├─────────────────────────────────────────────┤
│  缓存分类                                     │  ← 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 "配额"

  • MVP 用 uni.getStorageInfoSync().limitSizeapp 自己的 SQLite 配额),文案写"配额"而非"磁盘",不误导用户
  • 设备级"磁盘总空间/可用空间"需要 native pluginiOS NSFileManager / Android StatFsMVP 不引入列为后续优化§2

草稿/引导的二级提示:行右侧副文字"按账号分组清理",警示用户详情页会有分组。

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.navigateTocache-cleanup-detail?id=xxx
  3. 详情页 onLoad:调 cacheManager.getCategoryBreakdown(id)(简单 handler 返回 nullUI 走简单页模板;分组 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/
  ↓
聚合:
  totalBytes          = sum(categories.sizeBytes)             // 可清理总量(=内存缓存空间 chip
  appUsedBytes        = currentSize * 1024 + sandboxBytes     // 软件占用(大字 + Topfans已用空间 chip
                                                                          // 注currentSize 是 uni storage 全量(含黑名单),这是 WeChat "已用空间" 口径
  quotaTotalBytes     = limitSize * 1024                        // 配额总量(=总内存空间 chip
  quotaAvailableBytes = quotaTotalBytes - appUsedBytes        // 配额可用
  usagePercent        = (appUsedBytes / quotaTotalBytes) * 100 // 百分比
  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}` 后缀的草稿 keyuid 改造完成后;详见 §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 Mapkey 为 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 MBX 项失败)」
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 个 milestoneM0 是 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 + cleanGrouppreload 跨用户清理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.vuepages/castlove/index.vuepages/castlove/success.vuecomposables/useLaserSegment.jscomposables/useLaserBatchGenerate.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.vuepages/castlove/index.vuepages/castlove/success.vuepages/castlove/lenticular/lenticular-result.vuepages/castlove/laser/laser-result.vuecomposables/useLaserSegment.jscomposables/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.5MVP 上线后补;估算额外 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/