From 99f15ee37a93565f34f6991a2da1a25b2895d3bd Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:04:03 +0800 Subject: [PATCH] =?UTF-8?q?docs(architecture):=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=20+=20cl?= =?UTF-8?q?asses=20=E6=A0=87=E6=9D=86=E8=BF=81=E7=A7=BB=20+=20=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E7=AD=96=E7=95=A5=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 004: 补全 shared/lib/cache 9 文件说明(types/memory-store/redis-store/store-factory/cache-fn/invalidation-map/client-invalidation-map/invalidate/index)+ shared/lib/redis-client + shared/lib/query-keys - 005: shared.lib.exports.functions 补 5 个节点(MemoryCacheStore / RedisCacheStore / CLIENT_INVALIDATION_MAP / getRedisClient / queryKeys) - known-issues.md 新增「二十四、缓存策略规则」章节,含 14 条规则表与涉及文件清单 --- .../004_architecture_impact_map.md | 21 +++++- docs/architecture/005_architecture_data.json | 66 ++++++++++++++++++- docs/troubleshooting/known-issues.md | 35 ++++++++++ 3 files changed, 118 insertions(+), 4 deletions(-) diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index af4cd7e..568c06f 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -4366,12 +4366,16 @@ formatNumber(v: number | null | undefined, digits?: number): string // shared/lib/search-params.ts (re-export from utils.ts) getParam(params: SearchParams, key: string): string | undefined // = getSearchParam -// shared/lib/cache/ (服务端缓存基础设施,P1 重构新增) +// shared/lib/cache/ (服务端缓存基础设施 + 客户端失效映射,P1 重构新增) +// types.ts —— CacheFnOptions / CacheStore / InvalidationRule 接口定义(纯类型,无运行时依赖) +// memory-store.ts —— MemoryCacheStore 实现(Map + LRU + tag 反查索引,单实例 dev/test 用) +// redis-store.ts —— RedisCacheStore 实现(多实例共享,故障降级回退 producer) // store-factory.ts —— getCacheStore() 单例工厂:默认 MemoryCacheStore,CACHE_DRIVER=redis 时懒加载 RedisCacheStore -// memory-store.ts / redis-store.ts —— CacheStore 两种实现 // cache-fn.ts —— cacheFn 双层包装器:外层 react.cache 请求级 memoization,内层 cacheStore.getOrSet 跨请求缓存(按 tag 失效,TTL 可选) -// invalidation-map.ts —— INVALIDATION_MAP 集中式失效映射表 + fillTemplate 模板填充 +// invalidation-map.ts —— INVALIDATION_MAP 集中式失效映射表 + fillTemplate 模板填充(含服务端 tags/queryKeys/paths 三段) +// client-invalidation-map.ts —— CLIENT_INVALIDATION_MAP 客户端可见子集(仅 queryKeys 字段,避免客户端 bundle 拉入 revalidateTag 等 server-only 依赖) // invalidate.ts —— invalidateFor 三步编排函数(Server Action 写后调用) +// index.ts —— barrel re-export:cacheFn / invalidateFor / INVALIDATION_MAP / fillTemplate / CLIENT_INVALIDATION_MAP + 类型 getCacheStore(): Promise cacheFn(fn: (...args: TArgs) => Promise, options: CacheFnOptions): (...args: TArgs) => Promise fillTemplate(template: string, params: Record): string @@ -4379,6 +4383,17 @@ INVALIDATION_MAP // 集中式失效映射表常量 invalidateFor(actionId: string, params?: Record): Promise // 三步编排:1) CacheStore.invalidateTags 2) revalidateTag × N 3) revalidatePath × N // 未知 actionId 抛错 "[cache] Unknown actionId: . Update INVALIDATION_MAP." +CLIENT_INVALIDATION_MAP // 客户端可见子集:仅 queryKeys 字段,由 useActionMutation 在 onSuccess 中查询 + +// shared/lib/redis-client.ts (P1 重构新增:共享 Redis 客户端单例) +// rate-limit + cache 共用,复用 env.UPSTASH_REDIS_REST_URL/TOKEN +// 动态 import + webpackIgnore,包未安装/未配置返回 null(fail-open 降级) +getRedisClient(): Promise + +// shared/lib/query-keys.ts (P1 重构新增:客户端 queryKey 工厂) +// 命名约定:[module, resource, ...args] +// 失效时使用前缀匹配:invalidateQueries({ queryKey: ["classes", "detail"] }) → 失效所有 ["classes", "detail", *] +queryKeys // 工厂对象,含 classes.{all,lists,list,details,detail,students,schedule,stats,invitations} ``` ### 业务模块核心 Actions diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index a7cf397..c7ce9da 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -1420,6 +1420,33 @@ "api/notifications/stream" ] }, + { + "name": "MemoryCacheStore", + "file": "lib/cache/memory-store.ts", + "signature": "class MemoryCacheStore implements CacheStore { getOrSet(key, producer, options): Promise; invalidateTags(tags): Promise }", + "purpose": "CacheStore 内存实现:Map + LRU 淘汰(maxEntries 默认 500)+ tag 反查索引 Map>,TTL 惰性删除。适用于单实例 dev/test", + "deps": [ + "server-only", + "./types" + ], + "usedBy": [ + "shared/lib/cache/store-factory" + ] + }, + { + "name": "RedisCacheStore", + "file": "lib/cache/redis-store.ts", + "signature": "class RedisCacheStore implements CacheStore { getOrSet(key, producer, options): Promise; invalidateTags(tags): Promise }", + "purpose": "CacheStore Redis 实现:key 格式 next-edu:cache:{key},value JSON.stringify,tag 反查索引用 Redis Set,TTL 通过 expire 原子设置。Redis 不可用时故障降级透传 producer", + "deps": [ + "server-only", + "@/shared/lib/redis-client", + "./types" + ], + "usedBy": [ + "shared/lib/cache/store-factory" + ] + }, { "name": "getCacheStore", "file": "lib/cache/store-factory.ts", @@ -1462,7 +1489,7 @@ "name": "INVALIDATION_MAP", "file": "lib/cache/invalidation-map.ts", "signature": "INVALIDATION_MAP: Record", - "purpose": "集中式失效映射表:每个 mutation actionId 声明其副作用 tags/queryKeys/paths", + "purpose": "集中式失效映射表:每个 mutation actionId 声明其副作用 tags/queryKeys/paths(服务端三段)", "deps": [ "./types" ], @@ -1470,6 +1497,16 @@ "shared/lib/cache/invalidate" ] }, + { + "name": "CLIENT_INVALIDATION_MAP", + "file": "lib/cache/client-invalidation-map.ts", + "signature": "CLIENT_INVALIDATION_MAP: Record", + "purpose": "客户端可见的失效映射子集(仅 queryKeys 字段),从 invalidation-map 提取以避免客户端 bundle 拉入 revalidateTag 等 server-only 依赖。由 useActionMutation 在 onSuccess 中查询以自动 invalidateQueries", + "deps": [], + "usedBy": [ + "shared/hooks/use-action-mutation" + ] + }, { "name": "invalidateFor", "file": "lib/cache/invalidate.ts", @@ -1483,6 +1520,33 @@ "usedBy": [ "modules/* Server Actions" ] + }, + { + "name": "getRedisClient", + "file": "lib/redis-client.ts", + "signature": "getRedisClient(): Promise", + "purpose": "共享 Redis 客户端单例(rate-limit + cache 共用)。复用 env.UPSTASH_REDIS_REST_URL/TOKEN,动态 import + webpackIgnore。包未安装或 env 未配置时返回 null(fail-open 降级)", + "deps": [ + "server-only", + "@/env.mjs", + "@upstash/redis (optional, webpackIgnore)" + ], + "usedBy": [ + "shared/lib/cache/redis-store", + "shared/lib/rate-limit/redis-limiter" + ] + }, + { + "name": "queryKeys", + "file": "lib/query-keys.ts", + "signature": "queryKeys: { classes: { all, lists, list(filters), details, detail(id), students(classId), schedule(classId), stats(classId), invitations(classId) } }", + "purpose": "客户端 queryKey 工厂对象。命名约定 [module, resource, ...args],失效时使用前缀匹配 invalidateQueries({ queryKey: ['classes', 'detail'] }) 失效所有 ['classes', 'detail', *]。后续模块按此模式扩展", + "deps": [], + "usedBy": [ + "modules/classes/components/*", + "shared/hooks/use-action-query", + "shared/hooks/use-action-mutation" + ] } ], "hooks": [ diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md index f511c49..b246415 100644 --- a/docs/troubleshooting/known-issues.md +++ b/docs/troubleshooting/known-issues.md @@ -1236,3 +1236,38 @@ if (announcement.type === "school") { **涉及文件**:`src/shared/lib/cache/invalidate.ts`、`src/shared/lib/cache/invalidate.test.ts`、`src/shared/lib/cache/cache-fn.ts`、`src/shared/lib/cache/cache-fn.test.ts` +--- + +## 二十四、缓存策略规则(2026-07-05 缓存策略重构) + +> 标杆模块:`classes`。后续模块按此模式扩展。底层基础设施见 `src/shared/lib/cache/`。 + +| 规则 | 正确写法 | 错误写法 | +|------|---------|---------| +| data-access 查询函数包装 | `export const fn = cacheFn(fnRaw, { tags, ttl, keyParts })` | `cache(fn)` 或不包装 | +| 双导出 raw 版本 | `export const fnRaw = ...; export const fn = cacheFn(fnRaw, ...)` | 仅导出缓存版本(单测无法 mock) | +| Server Action 写后失效 | `await invalidateFor("module.action", { id })` | `revalidatePath("/path")` 直接调用 | +| 未知 actionId | 抛错(必须先登记 INVALIDATION_MAP) | 静默忽略 | +| 客户端 useQuery queryKey | `queryKeys.classes.detail(id)` 工厂调用 | 手写字符串数组 `["classes", "detail", id]` | +| useActionQuery 传 queryKey | `useActionQuery(action, { queryKey: queryKeys.classes.detail(id) })` | 不传 queryKey(旧模式,不跨页共享) | +| useActionMutation 自动 invalidate | `useActionMutation({ mutationFn, actionId: "classes.update", params: { id } })` | onSuccess 中手动 invalidateQueries | +| Tag 命名 | `classes:detail:{id}` 模块:资源:ID | `class_detail` 或 `ClassesDetail` | +| TTL 选择 | 列表 300s / 学生 60s / 课表 600s / 统计 60s | 全局统一 TTL | +| CLIENT_INVALIDATION_MAP 与 INVALIDATION_MAP 同步 | 服务端 invalidation-map.ts 增项后,同步在 client-invalidation-map.ts 增 queryKeys 子项 | 只改服务端,客户端 useActionMutation 自动 invalidate 失效 | +| client-invalidation-map 仅含 queryKeys | `CLIENT_INVALIDATION_MAP["x.y"] = { queryKeys: [["x", "y"]] }` | 在客户端文件中引入 tags/paths(拉入 server-only) | +| raw 函数命名 | `getXxxRaw`(内部实现)+ `getXxx`(cacheFn 包装后的对外 API) | `getXxx` 与 `getXxxCached` 命名混用 | +| cacheFn options.tags 必填 | `{ tags: ["classes:detail:{id}"], ttl: 60, keyParts: [id] }` | `{ ttl: 60 }` 缺 tags(无法失效) | +| invalidateFor 调用位置 | mutation Action 成功分支末尾 `await invalidateFor(...)` | 在 catch 分支调用(失败也失效缓存) | +| queryKey 工厂扩展 | 在 `query-keys.ts` 按模块命名空间扩展 `xxx: { all, lists, list, detail, ... }` | 在组件内就地定义 queryKey 字面量 | + +### 缓存策略重构涉及文件清单 + +- `src/shared/lib/cache/{types,memory-store,redis-store,store-factory,cache-fn,invalidation-map,client-invalidation-map,invalidate,index}.ts` — 缓存基础设施(9 个文件) +- `src/shared/lib/redis-client.ts` — 共享 Redis 客户端单例 +- `src/shared/lib/query-keys.ts` — 客户端 queryKey 工厂 +- `src/shared/hooks/use-action-query.ts` — 新增 queryKey 入参走 QueryClient +- `src/shared/hooks/use-action-mutation.ts` — 新增 actionId 自动 invalidate +- `src/modules/classes/data-access-*.ts` — 5+ 文件全部 cacheFn 包装 + 双导出 raw +- `src/modules/classes/actions-*.ts` — 5 文件全部 invalidateFor 替换 revalidatePath +- `src/modules/classes/components/*` — useActionQuery/useActionMutation 适配新版 API +