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:
SpecialX
2026-07-06 15:12:30 +08:00
parent 22c2e6459d
commit 9ce8d6d3fd
112 changed files with 18094 additions and 1188 deletions

View File

@@ -286,6 +286,105 @@ V5 状态管理统一的后续阶段,针对两类高频性能问题进行细
---
## 1.1.6 缓存策略落地专项重构2026-07-05 新增)
> **目标**:全栈缓存策略一致性重构 — 服务端 `cacheFn` 双层包装 + 集中式 `INVALIDATION_MAP` 失效编排 + 客户端 `queryKeys` 工厂 + `useActionQuery`/`useActionMutation` 向后兼容双模式 Hook。
### 架构总览
```
┌─────────────────────────────────────────────────────────────────────┐
│ ClientTanStack 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 失效) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ CacheStoreCACHE_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 模块依赖关系图
下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。

View File

@@ -5,7 +5,7 @@
"generatedAt": "2026-06-17",
"formatVersion": "1.1",
"rule": "每次文件修改后须同步更新本文件",
"lastUpdate": "性能预算重构专项 Phase 2-4 完成2026-07-05Phase 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) 请求级去重 > 跨请求 cacheReact cache() not unstable_cache(3) 批量 SQL > 循环 SQLINSERT 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 × NNext.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 + 包装版本,单测可 mockactions 43 个文件 247 处 revalidatePath 替换为 invalidateForINVALIDATION_MAP 覆盖全部 28 个模块。Hook 双模式useActionQuery 传 queryKey 走 useQuery 跨页共享缓存,不传走旧 useEffect+useStateuseActionMutation 传 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": {

File diff suppressed because it is too large Load Diff

View File

@@ -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 替换):
| 组别 | 模块 | 替换数 |
|------|------|--------|
| 标杆 | classes5 文件) | 47 |
| A 组 | users / school / textbooks / questions / course-plans / standards / scheduling / audit / onboarding / invitation-codes / proctoring / i18n13 文件) | 70 |
| B 组 | exams / grades / homework / attendance / leave-requests / diagnostic / error-book / adaptive-practice9 文件) | 77 |
| C 组 | lesson-preparation / elective / announcements / messaging / notifications / settings / rbac8 文件) | 100 |
**INVALIDATION_MAP**:扩展至 **203 个 actionId**,覆盖全部 28 个模块。每个 actionId 声明 `{ tags, queryKeys, paths }` 三段副作用。`CLIENT_INVALIDATION_MAP` 同步保留客户端可见子集(仅 queryKeys
### 迁移过程中常见错误(速查)
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| cacheFn 包装时移除原 `import { cache } from "react"` | 全部改用 `cacheFn` 双导出模式 | 保留 `cache(fn)` 调用但移除 importtsc 报 `Cannot find name 'cache'` |
| 跨模块接口函数(如 `getExamSubmissionDataForErrorCollection`)不包装 cacheFn | 仅包装本模块自有查询;跨模块接口由对方模块决定 | 给跨模块接口也加 cacheFn重复包装 |
| 多余的 `}` 或 `)` 字符 | 迁移后用 tsc 全量校验 | 手动编辑后未运行 tscTS1005/TS1128 语法错误) |
| questions/data-access.ts 等大文件分批迁移 | 完整迁移所有 `cache(...)` 调用至 cacheFn | 部分迁移导致 `import { cache }` 已删但调用仍存 |