47 KiB
缓存清理功能设计(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 前置迁移 + 详情页)。
关键决策
- 统一封装层模式(方案 A)——新建
frontend/utils/cacheManager.js,UI 只调 manager,永远不直接调uni.removeStorageSync等底层 API。这与项目里frontend/utils/preloadApi/core.js已采用的封装模式一致。 - 必须有二级详情页(用户决策)——MVP 阶段没有一键清理按钮,每类缓存必须进详情页才能清理。理由:
- 草稿(draft)和引导(guide)天然有"我的 / 其他用户的"分组(一台设备多账号切换常见),一键清理会把前任账号数据也清掉,体验突兀
- 用户进入详情页可以看到每个分组的占用与项数,明确知道清的是什么
- 清理"其他用户的草稿"需要单独更强的警告(避免误操作),而清理"自己的草稿"只需要普通警告
- 其他分类(preload / progress / sandbox-tmp / others)详情页可简化(一组 + 单按钮)
- 永远不清的 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。 - 范围不包含头像/图片缓存——
avatarCache.js的avatar_file_*storage key 与uni.saveFile的savedFilePath均纳入黑名单(§3.4.1)。原因:用户误清会导致头像全部重新下载,体验差;且与本设计目标(清理临时性业务缓存)不符。MVP 不纳入清理,仅作黑名单保护。 - 范围包含创作草稿(带警告)——草稿 key 改造(§11 迁移项)后形式为
*_${currentUid}。所有草稿 key(无论当前 uid / 其他 uid / legacy 无后缀)统一归drafthandler,通过computeBreakdown()按 uid 分组展示;强警告只在清理"其他用户/legacy"组时触发。UI ConfirmModal 在清理自己草稿时弹普通警告(handler.warning = true),清理他人时弹强警告(handler.strongWarning = true)。不走othershandler 兜底,避免双重计入 totalBytes 且强警告无法生效。 - preload handler 清所有用户——
preload:${oldUid}:*是跨账号的真正垃圾。spec 的preload.clean()通过遍历所有preload:前缀 key 一次性删,不依赖 currentUid。 - cleanAll 同步调
invalidateAll()——preloadApi/core.js的memoryMap是进程级共享,切账号不清会残留老用户数据。cacheManager.cleanAll()在 storage/sandbox 清理完成后调core.invalidateAll()清空整个内存层(含inFlightMap)。 - 草稿 key 改造为 uid 绑定(新增迁移项)——将
castlove_form_data等草稿 key 改为castlove_form_data_${currentUid}等 uid 后缀形式(详见 §11 迁移项),账号切换后看不到对方草稿(隐私 + 体验)。所有草稿 key(无论 currentUid / 其他 uid / legacy 无后缀)统一归drafthandler,通过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 类型定义
/**
* @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
// 注册(通常在 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
// 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 绝对不会被清理,保证:
- 不会误踢登录(token/user/star_id/cid)
- 不会误清点赞(用户主动行为,有业务价值)
- 不会误清头像文件(避免全部重新下载,这是 MVP 范围外的关键回归点)
- 不会误清注册中状态导致注册流程断掉 + 避免密码明文暴露风险
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 的归属判定 必须按以下优先级:
- 黑名单前缀(§3.4.1)→ 永不删(不计入 totalBytes,不计入"其他"section,仅用于计算
othersBytes) preload:前缀 →preloadhandler- 创作草稿白名单(§3.2 draft) →
drafthandler progress_前缀 →progresshandlerguide_前缀 →guidehandler(含 §3.4.2 的保留判断)- 其余非黑名单 key →
othershandler
关键:§5.1 的 getCacheInfo() 必须先过滤黑名单 key,再分类计算。否则黑名单 key 既不会展示也不会被删,但会被错误计入 totalBytes 误导用户。
3.4.4 内存层清理(详情页 + cleanAll 同步触发)
preloadApi/core.js 的 memoryMap 是进程级单例 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().limitSize(app 自己的 SQLite 配额),文案写"配额"而非"磁盘",不误导用户 - 设备级"磁盘总空间/可用空间"需要 native plugin(iOS
NSFileManager/ AndroidStatFs),MVP 不引入,列为后续优化(§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 交互流程
- 列表页 onLoad:调
cacheManager.getCacheInfo()→ 渲染分类列表(带 loading skeleton) - 点击分类行:
uni.navigateTo进cache-cleanup-detail?id=xxx - 详情页 onLoad:调
cacheManager.getCategoryBreakdown(id)(简单 handler 返回null,UI 走简单页模板;分组 handler 返回Array<GroupInfo>,UI 走分组页模板) - 详情页点清理按钮(分组页每组一个,简单页一个):弹 ConfirmModal(按需 warning/strongWarning)→ 确认 →
- 简单页:调
cacheManager.cleanCategory(id) - 分组页:调
cacheManager.cleanCategoryGroup(id, { uid: 'self' | '具体uid' | null })(uid='self' 表示当前用户,uid=null 表示"其他用户聚合") - →
uni.showLoading→ toast → 返回列表页(uni.navigateBack,列表页onShow自动重算getCacheInfo)
- 简单页:调
- 下拉刷新(列表页):onPullDownRefresh 重新调
getCacheInfo()刷新(页面级 enablePullDownRefresh) - 下拉刷新(详情页):重新调
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}` 后缀的草稿 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 关键设计决策
- getCacheInfo 异步并行:6 个 handler 用
Promise.all并行计算 - cleanAll 顺序执行:部分清理会互斥,顺序更安全;用
uni.showLoading({mask:true})包裹 - 失败容错:单个 handler clean() 失败不影响其他 handler,外层 try/catch 记录 error 并跳过
- 空状态:若 totalBytes = 0,列表页显示 "✓ 当前无缓存可清理",所有分类行点击无响应(toast 提示"暂无缓存")
- 重新计算时机:清理完成后必须重算
getCacheInfo(),不能用info.totalBytes - freedBytes简单相减 - cleanCategory/cleanCategoryGroup 单例保护:
cleanCategory(id)与cleanCategoryGroup(id, opts)各维护一个_inFlightMap(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单测(mockuni.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 前置)
- M0 — 草稿 key 改造(前置迁移):
utils/draftStorage.js封装 + 7 个文件读写侧改造 + 双读单写兼容(§11) - M1 — 核心封装层:
cacheManager.js+ 黑名单 + 大小格式化 +getCacheInfo()/getCategoryBreakdown()/cleanCategory()/cleanCategoryGroup()框架(handlers 暂时用空 stub);包含 in-flight 单例保护 - M2 — 6 个 handler 实现:每个 handler 的 computeSize / clean(或 computeBreakdown + cleanGroup);preload 跨用户清理;preload handler 清理完成后同步调
invalidateAll() - M3 — UI 页面(列表 + 详情):
cache-cleanup.vue(列表)+cache-cleanup-detail.vue(简单/分组模板)+ ConfirmModal + profile.vue 入口 + pages.json 注册 - 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_dataCASTLOVE_FORM_KEYtemp_nft_dataGENERATED_IMAGES_KEYGENERATION_RESULT_META_KEYLENTICULAR_STUDIO_STORAGE_KEYCRAFT_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 等)改为:
// 旧
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/✅