docs(architecture): 同步缓存基础设施 + classes 标杆迁移 + 缓存策略规则

- 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 条规则表与涉及文件清单
This commit is contained in:
SpecialX
2026-07-05 19:04:03 +08:00
parent a3dc22cb9e
commit 99f15ee37a
3 changed files with 118 additions and 4 deletions

View File

@@ -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() 单例工厂:默认 MemoryCacheStoreCACHE_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-exportcacheFn / invalidateFor / INVALIDATION_MAP / fillTemplate / CLIENT_INVALIDATION_MAP + 类型
getCacheStore(): Promise<CacheStore>
cacheFn<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>, options: CacheFnOptions): (...args: TArgs) => Promise<TResult>
fillTemplate(template: string, params: Record<string, string>): string
@@ -4379,6 +4383,17 @@ INVALIDATION_MAP // 集中式失效映射表常量
invalidateFor(actionId: string, params?: Record<string, string>): Promise<void>
// 三步编排1) CacheStore.invalidateTags 2) revalidateTag × N 3) revalidatePath × N
// 未知 actionId 抛错 "[cache] Unknown actionId: <id>. 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包未安装/未配置返回 nullfail-open 降级)
getRedisClient(): Promise<unknown | null>
// 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

View File

@@ -1420,6 +1420,33 @@
"api/notifications/stream"
]
},
{
"name": "MemoryCacheStore",
"file": "lib/cache/memory-store.ts",
"signature": "class MemoryCacheStore implements CacheStore { getOrSet<T>(key, producer, options): Promise<T>; invalidateTags(tags): Promise<void> }",
"purpose": "CacheStore 内存实现Map + LRU 淘汰maxEntries 默认 500+ tag 反查索引 Map<tag, Set<key>>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<T>(key, producer, options): Promise<T>; invalidateTags(tags): Promise<void> }",
"purpose": "CacheStore Redis 实现key 格式 next-edu:cache:{key}value JSON.stringifytag 反查索引用 Redis SetTTL 通过 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<string, InvalidationRule>",
"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<string, { queryKeys: readonly (readonly string[])[] }>",
"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<unknown | null>",
"purpose": "共享 Redis 客户端单例rate-limit + cache 共用)。复用 env.UPSTASH_REDIS_REST_URL/TOKEN动态 import + webpackIgnore。包未安装或 env 未配置时返回 nullfail-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": [

View File

@@ -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