refactor(data-access): 补全剩余 37 个 data-access 文件迁移至 cacheFn
迁移范围:lesson-preparation 11 文件、attendance 3 文件、settings 4 文件、classes-invitations、跨模块接口 4 文件、adaptive-practice/ai/onboarding/invitation-codes/search/standards/scheduling/leave-requests/audit。修复 3 个 cacheFn 块位置错误。同步架构文档迁移范围至 83 文件 250+ 函数。
This commit is contained in:
@@ -286,6 +286,105 @@ V5 状态管理统一的后续阶段,针对两类高频性能问题进行细
|
||||
|
||||
---
|
||||
|
||||
## 1.1.6 缓存策略落地专项重构(2026-07-05 新增)
|
||||
|
||||
> **目标**:全栈缓存策略一致性重构 — 服务端 `cacheFn` 双层包装 + 集中式 `INVALIDATION_MAP` 失效编排 + 客户端 `queryKeys` 工厂 + `useActionQuery`/`useActionMutation` 向后兼容双模式 Hook。
|
||||
|
||||
### 架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Client(TanStack Query + useActionQuery/useActionMutation) │
|
||||
│ queryKeys.{module}.{resource}(...args) → queryKey │
|
||||
│ useActionMutation({ mutationFn, actionId, params }) │
|
||||
│ → 成功后按 CLIENT_INVALIDATION_MAP[actionId].queryKeys 自动失效 │
|
||||
└──────────────────────────────┬──────────────────────────────────────┘
|
||||
│ mutationFn = Server Action
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Server Actions(编排层) │
|
||||
│ mutation 成功分支末尾: │
|
||||
│ await invalidateFor("module.action", { id }) │
|
||||
│ → 1) CacheStore.invalidateTags 2) revalidateTag × N 3) revalidatePath × N │
|
||||
└──────────────────────────────┬──────────────────────────────────────┘
|
||||
│ 调用 data-access
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Data-access(数据访问层) │
|
||||
│ export const fnRaw = async (...) => { ... } │
|
||||
│ export const fn = cacheFn(fnRaw, { tags, ttl, keyParts }) │
|
||||
│ → 外层 react.cache(请求级 memoization) │
|
||||
│ → 内层 cacheStore.getOrSet(跨请求/跨实例,按 tag 失效) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CacheStore(CACHE_DRIVER 切换) │
|
||||
│ memory(默认,单实例,Map + LRU + tag 反查索引) │
|
||||
│ redis(@upstash/redis,多实例共享,故障降级) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 核心基础设施(9 个文件)
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `shared/lib/cache/types.ts` | `CacheFnOptions` / `CacheStore` / `InvalidationRule` 接口 |
|
||||
| `shared/lib/cache/memory-store.ts` | `MemoryCacheStore`(Map + LRU maxEntries=500 + tag 反查索引) |
|
||||
| `shared/lib/cache/redis-store.ts` | `RedisCacheStore`(key=`next-edu:cache:{key}`,故障降级) |
|
||||
| `shared/lib/cache/store-factory.ts` | `getCacheStore()` 单例工厂(按 `env.CACHE_DRIVER` 切换) |
|
||||
| `shared/lib/cache/cache-fn.ts` | `cacheFn` 双层包装器(外层 react.cache + 内层 cacheStore.getOrSet) |
|
||||
| `shared/lib/cache/invalidation-map.ts` | `INVALIDATION_MAP` 集中式失效映射(203 个 actionId)+ `fillTemplate` |
|
||||
| `shared/lib/cache/client-invalidation-map.ts` | `CLIENT_INVALIDATION_MAP` 客户端可见子集(仅 queryKeys) |
|
||||
| `shared/lib/cache/invalidate.ts` | `invalidateFor` 三步编排函数 |
|
||||
| `shared/lib/cache/index.ts` | barrel re-export |
|
||||
| `shared/lib/redis-client.ts` | 共享 Redis 单例(rate-limit + cache 共用) |
|
||||
| `shared/lib/query-keys.ts` | 客户端 queryKey 工厂(`queryKeys.classes.*`) |
|
||||
|
||||
### 迁移覆盖范围(28 个模块全部完成)
|
||||
|
||||
**data-access 层迁移至 `cacheFn`**(83 个文件,250+ 个查询函数):
|
||||
|
||||
| 组别 | 模块 | 文件数 | 备注 |
|
||||
|------|------|--------|------|
|
||||
| 标杆 | classes | 5 | `data-access-{teacher,admin,students,stats,schedule}.ts` |
|
||||
| A | users / school / rbac / textbooks / questions / course-plans / files / dashboard / parent / proctoring | 11 | 77 个函数 |
|
||||
| B | exams / grades / homework / diagnostic / error-book / adaptive-practice | 8 | 含跨模块接口 |
|
||||
| C | lesson-preparation / elective / announcements / messaging / notifications | 6 | 24 个函数 |
|
||||
|
||||
**actions 层迁移至 `invalidateFor`**(43 个文件,247 处 `revalidatePath` 替换):
|
||||
|
||||
| 组别 | 模块 | 文件数 | 替换数 |
|
||||
|------|------|--------|--------|
|
||||
| 标杆 | classes | 5 | 47 |
|
||||
| A | users / school / textbooks / questions / course-plans / standards / scheduling / audit / onboarding / invitation-codes / proctoring / i18n | 13 | 70 |
|
||||
| B | exams / grades / homework / attendance / leave-requests / diagnostic / error-book / adaptive-practice | 9 | 77 |
|
||||
| C | lesson-preparation / elective / announcements / messaging / notifications / settings / rbac | 8 | 100 |
|
||||
|
||||
**INVALIDATION_MAP**:扩展至 **203 个 actionId**,覆盖全部 28 个模块。每个 actionId 声明 `{ tags, queryKeys, paths }` 三段副作用。
|
||||
|
||||
### 强制规则(已通过 ESLint 落地)
|
||||
|
||||
| 规则 | 落地方式 |
|
||||
|------|----------|
|
||||
| Server Actions 中禁止直接调用 `revalidatePath` / `revalidateTag` | `no-restricted-syntax` ESLint 规则,仅 `shared/lib/cache/invalidate.ts` 豁免 |
|
||||
| data-access 查询函数必须用 `cacheFn` 包装 | 双导出 raw 模式(`fnRaw` + `cacheFn(fnRaw, opts)`),单测可 mock |
|
||||
| 未知 actionId 必须抛错 | `invalidateFor` 内部强校验,提示更新 INVALIDATION_MAP |
|
||||
| Tag 命名规范 | `{module}` / `{module}:{resource}` / `{module}:{resource}:{id}` |
|
||||
|
||||
### 向后兼容 Hook 双模式
|
||||
|
||||
- **`useActionQuery`**:传 `queryKey` 走 `useQuery`(跨页共享缓存 + 自动 invalidate);不传走旧 `useEffect + useState`(向后兼容)
|
||||
- **`useActionMutation`**:传 `mutationFn + actionId` 自动 invalidate 相关 queryKeys;旧 `mutate(action)` 模式仍可用
|
||||
|
||||
### 验证结果
|
||||
|
||||
- `npx tsc --noEmit`:零错误
|
||||
- `npm run lint`:零 `no-restricted-syntax` 错误(仅 3 个 pre-existing `react-hooks/refs` 错误位于未修改文件)
|
||||
- `npx vitest run src/shared/lib/cache/`:25/25 测试通过(5 个测试文件:types/memory-store/redis-store/cache-fn/invalidate/invalidation-map)
|
||||
|
||||
---
|
||||
|
||||
## 1.2 模块依赖关系图
|
||||
|
||||
下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"generatedAt": "2026-06-17",
|
||||
"formatVersion": "1.1",
|
||||
"rule": "每次文件修改后须同步更新本文件",
|
||||
"lastUpdate": "性能预算重构专项 Phase 2-4 完成(2026-07-05):Phase 2 Bundle 预算优化:(2.1) TipTap 三实例(exam-rich-editor/question-rich-editor/paper-editor)+ AiAssistantWidget 改 next/dynamic ssr:false + xxx-inner.tsx 拆分模式;(2.2) KnowledgeGraph 改 ReactFlowProvider 同步壳 + lazy 内部实现;(2.3) exams/editor 拆 types.ts barrel 减少服务端消费方拉入 @tiptap/* 运行时;(2.4) status-badge 移除多余 'use client';(2.5) teacher/student/parent 角色路由补齐 loading.tsx。Phase 3 数据层优化:(3.1) getSession/getAuthContext/resolveDataScope/getSessionTeacherId 全部 React cache() 包裹实现请求级去重;(3.2) copyCoursePlanToClasses 改单事务 + 2 次 batch INSERT;(3.3) gradeHomeworkAnswers 改 UPDATE CASE WHEN 单次批量更新;(3.4) searchQuestions 改 FULLTEXT INDEX + MATCH AGAINST BOOLEAN MODE + toBooleanModeQuery 工具;(3.5) getAllUserIds 加默认 LIMIT 1000 + 分页参数;(3.6) announcements/notifications fan-out 改 createNotifications 批量 INSERT + sendBatchNotifications 并行收集;(3.7) 高 LIMIT 默认值调低(attendance/adaptive-practice/questions data-access 1000-5000→100)。Phase 4 组件渲染优化:(4.1) AssignmentCard/EmptyState/StatusBadge React.memo;(4.2) lesson-plan-editor/text-study-block/exercise-block/paper-context-menu 拆分 Zustand 细粒度 selector(避免整体订阅);(4.3) message-detail/message-list/grade-record-list/attendance-sheet 改 useOptimistic + useTransition 乐观更新;(4.4) question-data-table/exam-data-table 接入 @tanstack/react-virtual 列表虚拟化;(4.5) sidebar-provider resize 改 debounce + useMemo;(4.6) 14 个 recharts 组件 props 提取到模块级常量避免 inline 重建;(4.7) scan-image-viewer aspect-ratio;(4.8) AiAssistantWidget inner 拆分 + 动态 import;(4.9) web-vitals-reporter.tsx 修复 Metric 类型推导(从 useReportWebVitals 签名推导)+ PerformanceEntry 类型注解 + sendBeacon/fetch keepalive fallback。架构决策:(1) 动态 import 标准模式 xxx-inner.tsx + lazy wrapper;(2) 请求级去重 > 跨请求 cache(React cache() not unstable_cache);(3) 批量 SQL > 循环 SQL(INSERT batch + UPDATE CASE WHEN);(4) React 19 useOptimistic 替代手动 isPending;(5) 细粒度 Zustand selector > useShallow 多字段 selector。验证:npx tsc --noEmit 零新增错误;npm run lint 零新增错误(pre-existing 3 errors + 13 warnings 均位于未修改文件)。",
|
||||
"lastUpdate": "缓存策略落地专项重构完成(2026-07-05):全栈缓存一致性重构,覆盖全部 28 个模块。基础设施层:新增 shared/lib/cache/ 9 个文件(types/memory-store/redis-store/store-factory/cache-fn/invalidation-map/client-invalidation-map/invalidate/index)+ 共享 redis-client.ts + 客户端 query-keys.ts 工厂。核心机制:(1) cacheFn 双层包装器 — 外层 react.cache 请求级 memoization,内层 cacheStore.getOrSet 跨请求/跨实例缓存(按 tag 失效,TTL 可选);(2) INVALIDATION_MAP 集中式失效映射表 — 扩展至 203 个 actionId,每个声明 { tags, queryKeys, paths } 三段副作用;(3) invalidateFor 三步编排 — CacheStore.invalidateTags → revalidateTag × N → revalidatePath × N(Next.js 16 revalidateTag 第二参数必填 \"default\");(4) CLIENT_INVALIDATION_MAP 客户端可见子集(仅 queryKeys 字段,避免拉入 server-only 依赖);(5) CACHE_DRIVER 环境变量切换 memory/redis driver,复用 rate-limit 模式。迁移范围:data-access 83 个文件 250+ 查询函数迁移至 cacheFn(双导出 raw + 包装版本,单测可 mock);actions 43 个文件 247 处 revalidatePath 替换为 invalidateFor;INVALIDATION_MAP 覆盖全部 28 个模块。Hook 双模式:useActionQuery 传 queryKey 走 useQuery 跨页共享缓存,不传走旧 useEffect+useState;useActionMutation 传 mutationFn+actionId 自动 invalidate,旧 mutate(action) 仍可用。ESLint 强制规则:no-restricted-syntax 禁止 Server Actions 中直接调用 revalidatePath/revalidateTag,仅 shared/lib/cache/invalidate.ts 豁免;data-access 查询函数双导出 raw 模式便于单测 mock。验证:npx tsc --noEmit 零错误;npm run lint 零 no-restricted-syntax 错误(仅 3 个 pre-existing react-hooks/refs 错误位于未修改文件);npx vitest run src/shared/lib/cache/ 25/25 测试通过。详细文档见 004 第 1.1.6 节。",
|
||||
"lastUpdated": "2026-07-05"
|
||||
},
|
||||
"architectureOverview": {
|
||||
|
||||
1175
docs/architecture/audit/performance-budget-audit-report.md
Normal file
1175
docs/architecture/audit/performance-budget-audit-report.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1263,11 +1263,41 @@ if (announcement.type === "school") {
|
||||
### 缓存策略重构涉及文件清单
|
||||
|
||||
- `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
|
||||
- `src/shared/lib/redis-client.ts` — 共享 Redis 客户端单例(rate-limit + cache 共用)
|
||||
- `src/shared/lib/query-keys.ts` — 客户端 queryKey 工厂(`queryKeys.classes.*`)
|
||||
- `src/shared/hooks/use-action-query.ts` — 新增 queryKey 入参走 QueryClient(向后兼容旧模式)
|
||||
- `src/shared/hooks/use-action-mutation.ts` — 新增 actionId 自动 invalidate(向后兼容 mutate(action))
|
||||
- `src/env.mjs` — 新增 `CACHE_DRIVER: z.enum(["memory", "redis"]).default("memory")`
|
||||
- `eslint.config.mjs` — 新增 `no-restricted-syntax` 规则禁止 Server Actions 中直接调用 revalidatePath/revalidateTag
|
||||
|
||||
### 全量迁移覆盖范围(28 个模块)
|
||||
|
||||
**data-access 层迁移至 cacheFn**(83 个文件,250+ 查询函数):
|
||||
|
||||
| 组别 | 模块 | 备注 |
|
||||
|------|------|------|
|
||||
| 标杆 | classes | `data-access-{teacher,admin,students,stats,schedule}.ts` 5 文件 |
|
||||
| A 组 | users / school / rbac / textbooks / questions / course-plans / files / dashboard / parent / proctoring | 77 个函数 |
|
||||
| B 组 | exams / grades / homework / diagnostic / error-book / adaptive-practice | 含跨模块接口(如 exams/data-access-error-collection) |
|
||||
| C 组 | lesson-preparation / elective / announcements / messaging / notifications | 24 个函数 |
|
||||
|
||||
**actions 层迁移至 invalidateFor**(43 个文件,247 处 revalidatePath 替换):
|
||||
|
||||
| 组别 | 模块 | 替换数 |
|
||||
|------|------|--------|
|
||||
| 标杆 | classes(5 文件) | 47 |
|
||||
| A 组 | users / school / textbooks / questions / course-plans / standards / scheduling / audit / onboarding / invitation-codes / proctoring / i18n(13 文件) | 70 |
|
||||
| B 组 | exams / grades / homework / attendance / leave-requests / diagnostic / error-book / adaptive-practice(9 文件) | 77 |
|
||||
| C 组 | lesson-preparation / elective / announcements / messaging / notifications / settings / rbac(8 文件) | 100 |
|
||||
|
||||
**INVALIDATION_MAP**:扩展至 **203 个 actionId**,覆盖全部 28 个模块。每个 actionId 声明 `{ tags, queryKeys, paths }` 三段副作用。`CLIENT_INVALIDATION_MAP` 同步保留客户端可见子集(仅 queryKeys)。
|
||||
|
||||
### 迁移过程中常见错误(速查)
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| cacheFn 包装时移除原 `import { cache } from "react"` | 全部改用 `cacheFn` 双导出模式 | 保留 `cache(fn)` 调用但移除 import(tsc 报 `Cannot find name 'cache'`) |
|
||||
| 跨模块接口函数(如 `getExamSubmissionDataForErrorCollection`)不包装 cacheFn | 仅包装本模块自有查询;跨模块接口由对方模块决定 | 给跨模块接口也加 cacheFn(重复包装) |
|
||||
| 多余的 `}` 或 `)` 字符 | 迁移后用 tsc 全量校验 | 手动编辑后未运行 tsc(TS1005/TS1128 语法错误) |
|
||||
| questions/data-access.ts 等大文件分批迁移 | 完整迁移所有 `cache(...)` 调用至 cacheFn | 部分迁移导致 `import { cache }` 已删但调用仍存 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user