Files
NextEdu/docs/architecture/004_architecture_impact_map.md
SpecialX 9ce8d6d3fd 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+ 函数。
2026-07-06 15:12:30 +08:00

4588 lines
579 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Next_Edu 架构影响地图
> 重写日期2026-06-17
> 目标:一次阅读即可直观理解整个项目架构
> 规则:源码修改后须同步更新本文档与 `005_architecture_data.json`
> 审查依据:`docs/architecture/audit/` 下 4 份审查报告
---
## 目录
- [第一部分:全局架构概览](#第一部分全局架构概览)
- [1.1 分层架构图](#11-分层架构图)
- [1.2 模块依赖关系图](#12-模块依赖关系图)
- [1.3 数据流向图(考试流程)](#13-数据流向图考试流程)
- [1.4 核心调用链路](#14-核心调用链路)
- [第二部分:模块清单](#第二部分模块清单)
- [第三部分:已知架构问题和技术债](#第三部分已知架构问题和技术债)
- [附录 A模块间依赖矩阵](#附录-a模块间依赖矩阵)
- [附录 B关键参数影响链](#附录-b关键参数影响链)
- [附录 C核心函数签名索引](#附录-c核心函数签名索引)
- [相关文档](#相关文档)
---
# 第一部分:全局架构概览
## 1.1 分层架构图
项目采用**严格三层架构**,依赖方向必须单向:`app → modules → shared`
```
┌─────────────────────────────────────────────────────────────────────┐
│ app/ (路由层) │
│ ───────────────────────────────────────────────────────────────── │
│ (auth)/ login / register / privacy / terms │
│ (dashboard)/ admin / teacher / student / parent / management │
│ api/ REST API (auth, ai, upload, files, search, ...) │
└──────────────────────────────┬──────────────────────────────────────┘
│ 调用 Server Actions / data-access
┌─────────────────────────────────────────────────────────────────────┐
│ modules/ (业务模块层) │
│ ───────────────────────────────────────────────────────────────── │
│ 核心教学: exams · homework · questions · textbooks · grades │
│ · lesson-preparation │
│ 教学管理: classes · school · scheduling · attendance · course-plans│
│ 用户与沟通users · messaging · notifications · parent · audit │
│ 扩展功能: elective · proctoring · diagnostic · dashboard │
│ 权限管理: rbac │
│ 其他: announcements · files · settings · auth · layout · student│
│ │
│ 每个模块标准结构actions.ts (编排) → data-access.ts (DB) → schema.ts│
└──────────────────────────────┬──────────────────────────────────────┘
│ 依赖
┌─────────────────────────────────────────────────────────────────────┐
│ shared/ (基础设施层) │
│ ───────────────────────────────────────────────────────────────── │
│ db/ schema.ts (54 张表) + relations.ts + index.ts │
│ lib/ auth-guard · permissions · ai · audit-logger · │
│ change-logger · login-logger · password-policy · │
│ rate-limit · excel · file-storage · ... │
│ hooks/ use-permission · use-aria-live · ... │
│ components/ ui/ (shadcn) · a11y/ · global-search · ... │
│ types/ permissions · action-state │
└─────────────────────────────────────────────────────────────────────┘
│ 反向依赖(违规,见 1.2
┌──────────────────────────────┴──────────────────────────────────────┐
│ 根模块src/auth.ts (NextAuth 配置) · src/proxy.ts (中间件) │
│ ✅ shared/lib/{audit-logger, change-logger, auth-guard} 已通过 │
│ shared/lib/session.ts 单一入口获取 session不再直依赖 @/auth │
│ (详见第三部分 P0-2
└─────────────────────────────────────────────────────────────────────┘
```
**分层规则**
- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access不直接访问 DB
- `modules/` 之间通过对方 data-access 通信,不直接查询对方表
- `shared/` 是被依赖方,不应反向依赖 `app/``modules/`
- `src/auth.ts``src/proxy.ts` 位于根目录,属于应用层
---
## 1.1.1 M7 移动端 PWA 支持2026-07-01 新增)
- **PWA 清单**:新增 `src/app/manifest.ts`Next.js App Router 元数据路由),生成 `/manifest.webmanifest`,声明 `name`/`short_name=NextEdu`/`start_url=/`/`display=standalone`/`theme_color=#1976d2`/`icons=/icon.svg`
- **根布局**`src/app/layout.tsx` 新增 `viewport` 导出(`width=device-width``initialScale=1``maximumScale=5``userScalable=true``themeColor` 按明暗主题切换)+ `metadata` 新增 `manifest`/`appleWebApp` 配置,支持添加到主屏幕与独立显示。
- **触控友好**`lesson-preparation` 模块 `node-editor.tsx``lesson-plan-readonly-view.tsx``<ReactFlow>` 组件新增 `panOnDrag`/`zoomOnPinch`/`zoomOnDoubleClick={false}`/`selectionOnDrag={false}` 属性,防止移动端双击误缩放、优化触控拖拽体验。
---
## 1.1.2 设计令牌体系2026-07-04 新增)
项目采用**双层令牌架构**Primitive + SemanticTailwind v4 通过 `@theme inline` 暴露为业务可用的类。
### 文件分布
```
src/app/
├─ globals.css # @import 入口 + base layer + 组件硬编码样式
└─ styles/tokens/
├─ primitive.css # Layer 1: 原始色板(zinc/stone/indigo)、字号/间距/阴影/字体家族阶梯
├─ semantic-light.css # Layer 2: :root 语义令牌(shadcn + 扩展 + chart/sidebar + diff/graph)
├─ semantic-dark.css # Layer 2: .dark 语义令牌(完整暗色)
├─ lesson-preparation.css # --lp-* 令牌(明暗双份,HEX→HSL)
├─ tailwind-theme.css # @theme inline 暴露所有令牌为 Tailwind 类
└─ index.css # 汇总 @import 入口
```
### 令牌分类
| 分类 | 命名 | 暴露为 Tailwind 类 |
|---|---|---|
| shadcn 标准 | `--background`/`--foreground`/`--primary`/... | `bg-background`/`text-foreground`/`bg-primary`/... |
| 扩展层级 | `--background-elevated`/`--text-secondary`/`--border-strong`/... | `bg-background-elevated`/`text-text-secondary`/... |
| chart | `--chart-1~5` | `bg-chart-1`/... |
| sidebar | `--sidebar-*` | `bg-sidebar`/`text-sidebar-foreground`/... |
| lp-* 命名空间 | `--lp-paper`/`--lp-inline-node-text`/`--lp-dot-*` | `bg-lp-paper`/`text-lp-inline-node-text`/... |
| 字体家族 | `--font-family-sans/serif/mono` | `font-sans`/`font-serif`/`font-mono` |
| 字号阶梯 | `--font-size-1~9`(含 `--font-size-0` 微字号) | `text-size-1`~`text-size-9` |
| 间距阶梯 | `--space-0~18` | `w-[length:var(--space-N)]` 等 |
| 阴影阶梯 | `--shadow-1~6` | `shadow-1`~`shadow-6` |
| 圆角阶梯 | `--radius-sm/md/lg/xl/2xl/full` | `rounded-sm`/`rounded-md`/... |
| 动效 | `--duration-*`/`--ease-*` | `duration-fast`/`ease-in`/... |
| z-index | `--z-dropdown/sticky/modal/popover/toast` | `z-dropdown`/`z-modal`/... |
### 强制约束
- ESLint 规则 `no-restricted-syntax` 禁止 `#hex` 字面量
- 自定义规则 `design-tokens/no-hardcoded-fonts` 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
- 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML`manifest.ts`PWA
- 任意值 `w-[Npx]` 等需通过 `// eslint-disable-next-line no-restricted-syntax -- <reason>` 注释豁免(如 Dialog 固定宽度、图表画布、loading.tsx 骨架等)
---
## 1.1.3 Phase 4.2 + Phase 4.5 状态管理优化2026-07-05 新增)
V5 状态管理统一的后续阶段,针对两类高频性能问题进行细粒度优化:**Zustand 整体订阅导致的级联 re-render** 与 **useState + 等待 Server Action 模式导致的 UI 不即时反馈**
### Phase 4.2lesson-plan-editor Zustand selector 拆分
`useLessonPlanEditor` 是组合了 5 个 slice 的 zustand store。原消费方使用 `const editor = useLessonPlanEditor()` 整体订阅,导致任一字段变化都触发整体 re-render。
| 文件 | 修改要点 |
|------|---------|
| `modules/lesson-preparation/components/lesson-plan-editor.tsx` | 拆分为 9 个细粒度 selector`isDirty`/`isOnline`/`doc`/`title`/`isSaving`/`saveError`/`lastSavedAt`/`setTitle` + `canUndo`/`canRedo` 派生 boolean移除 `editor.canUndo()` 重复调用,更新 `useEffect` 依赖与 `printablePlan` useMemo 使用拆分后的变量 |
| `modules/lesson-preparation/components/blocks/text-study-block.tsx` | `const { updateNode } = useLessonPlanEditor()``useLessonPlanEditor((s) => s.updateNode)` |
| `modules/lesson-preparation/components/blocks/exercise-block.tsx` | 拆为 `useLessonPlanEditor((s) => s.updateNode)` + `useLessonPlanEditor((s) => s.planId)` |
| `modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx` | 拆为 7 个细粒度 selector`toggleExpand`/`expandedNodeIds`/`updateNode`/`removeNode`/`addAnchor`/`duplicateNode`/`nodes`),特别地将 `doc` 改为 `s.doc.nodes` 更精细订阅(`addAnchor` 等仅修改 anchors 不会触发本组件 re-render |
**关键决策**
- `canUndo`/`canRedo` 改为 `useLessonPlanEditor((s) => s.canUndo())` 订阅派生 boolean避免每次渲染时调用函数
- 不使用 `useShallow` 包装多字段 selector因为单字段 selector 已经够细,且避免了 `useShallow` 自身的浅比较开销
- 函数引用(`setTitle`/`updateNode` 等)天然稳定,无需 `useShallow`
### Phase 4.5useOptimistic + useTransition 乐观更新
将 4 个组件从 `useState` + 等待 Server Action 模式迁移到 React 19 `useOptimistic` + `useTransition`,自动管理乐观状态与回滚。
| 文件 | 修改要点 |
|------|---------|
| `modules/messaging/components/message-detail.tsx` | 星标 `useState``useOptimistic<boolean>` + `useTransition``handleToggleStar``startStarTransition``addOptimisticStarred(nextStarred)` + `await toggleMessageStarAction` + `router.refresh()`。JSX 中所有 `isStarred` 引用改为 `optimisticIsStarred`Button variant/aria-pressed/Star className/星标徽章) |
| `modules/messaging/components/message-list.tsx` | 星标 `starredOverride` useState → `useOptimistic<Map<string, boolean>>`key=messageId, value=isStarred`getIsStarred` 改为 `optimisticStarredMap.get(m.id)`。撤回 `recalledOverride` 仍保留 useState不需要乐观更新 |
| `modules/grades/components/grade-record-list.tsx` | 编辑保存 `isSaving` useState → `useOptimistic<GradeRecordListItem[], GradeRecordListItem>` + `useTransition``handleEditSave` 构造 `optimisticRecord``startSaveTransition``addOptimisticRecord` + `await updateGradeRecordAction` + `router.refresh()`。两处 `records.map` 改为 `optimisticRecords.map`(桌面表格 + 移动端卡片视图)即时显示乐观记录 |
| `modules/attendance/components/attendance-sheet.tsx` | `isSubmitting` useState → `useOptimistic<boolean>`。保留 `<form action={handleSubmit}>` 模式 + `useFormStatus` SubmitButtonprogressive enhancement`setOptimisticSubmitting(true)` 在 form action 内调用action 完成后 `optimisticSubmitting` 自动恢复 false移除 `finally` 块 |
**关键决策**
- attendance-sheet 保留 form action 模式而非改用 `useTransition` 包裹,因为 `useFormStatus().pending` 依赖 form action 的 promise 状态,若用 `startTransition` 包裹会导致 form action 立即返回、`useFormStatus` 不可靠
- `useOptimistic``addOptimistic` 必须在 transition 或 action 内调用form action 也算 action
- 成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值
- 不为撤回recall操作使用 useOptimistic因为撤回需要根据服务端返回判断是否成功2 分钟窗口校验),失败时不应乐观显示已撤回
### 验证结果
- `npx tsc --noEmit`零新增错误pre-existing 错误:`providers.tsx` 缺少 `@tanstack/react-query-devtools``web-vitals-reporter.tsx` Metric 导出、`homework/data-access-write.ts` MySqlTransaction.run、`paper-context-menu.tsx` 第 58 行 `duplicateNode` 调用签名不匹配——此为 pre-existing bug`duplicateNode(id: string)` 只接受 1 参数但调用方传 2 个、`print-view.tsx` Object.entries 类型推断)
- `npm run lint`零新增错误8 个 modified 文件均未出现在 lint 输出中pre-existing 3 errors + 13 warnings 均位于未修改文件:`scripts/check-db-state.mjs``scripts/seed-grade5-chinese.ts``practice-result-view.tsx``exam-rich-form.tsx``use-exam-preview-state.ts``use-exam-preview-tasks.ts``homework/data-access-write.ts``unread-message-badge.tsx`
---
## 1.1.4 Phase 3.6 + Phase 3.7 + Phase 3.8 DB 性能优化2026-07-05 新增)
针对数据库查询性能的三阶段优化:**FULLTEXT 全文检索**、**公告 fan-out 批量化**、**高 LIMIT 默认值调低**。
### Phase 3.6searchQuestions 改 FULLTEXT 索引
将 questions 表的模糊查询从 `LIKE '%xxx%'` 升级为 MySQL FULLTEXT 索引 + `MATCH AGAINST ... IN BOOLEAN MODE`,提升大表检索性能。
| 文件 | 修改要点 |
|------|---------|
| `src/shared/db/schema.ts` | 添加 `import { sql } from "drizzle-orm"`;在 `questions` 表添加 STORED 生成列 `contentText``text("content_text").generatedAlwaysAs(sql\`CAST(content AS CHAR)\`, { mode: "stored" })`);补齐 5 个先前未同步的索引typeDifficultyIdx/statusCreatedIdx/creatorIdx/examStatusIdx/submittedAtIdx |
| `drizzle/0001_questions_fulltext_search.sql` | drizzle-kit 生成的迁移CREATE TABLE lesson_plan_schedules + content_text 生成列 + 8 个索引;末尾手动追加 `CREATE FULLTEXT INDEX questions_content_text_ft_idx ON questions (content_text)` |
| `drizzle/meta/0001_snapshot.json` | questions 表 indexes 节点添加 `questions_content_text_ft_idx` 声明type: "fulltext" |
| `src/modules/search/data-access.ts` | `searchQuestions(kw, limit?)` → `searchQuestions(q, limit?)`;新增 `toBooleanModeQuery` 工具函数将原始查询转为 `+word1* +word2*` 格式;查询改用 `MATCH(questions.contentText) AGAINST(? IN BOOLEAN MODE)` |
| `src/app/api/search/route.ts` | 本地 `searchQuestions` 函数同步改用 MATCH AGAINST + `toBooleanModeQuery`;调用点从 `searchQuestions(kw, pageSize)` 改为 `searchQuestions(q, pageSize)` |
**关键决策**
- 使用 STORED 生成列 `content_text` 而非 VIRTUAL因为 FULLTEXT 索引在 MySQL 5.7+ 仅支持 STORED 生成列
- `CAST(content AS CHAR)` 将 JSON 字段序列化为纯文本,避免 MySQL FULLTEXT 无法直接索引 JSON 列的限制
- drizzle-kit 无法生成 FULLTEXT 索引声明,迁移文件中需手动追加 `CREATE FULLTEXT INDEX` 语句,并同步更新 snapshot 的 indexes 节点
- `toBooleanModeQuery` 转义 BOOLEAN MODE 特殊字符(+ - < > ( ) ~ * " ')避免用户输入破坏查询语法
- 保留 textbooks/exams/announcements 的 LIKE 模糊匹配FULLTEXT 索引需 InnoDB + utf8mb4且这些表查询量较小
### Phase 3.7getAllUserIds 加 LIMIT + 公告 fan-out 批量化
| 文件 | 修改要点 |
|------|---------|
| `src/modules/users/data-access.ts` | `getAllUserIds()` → `getAllUserIds(limit=1000, offset=0)`,添加默认 LIMIT 防止 OOM + 分页参数支持 |
| `src/modules/announcements/data-access.ts` | `resolveAnnouncementTargetUserIds` school 分支改为分页循环遍历 `getAllUserIds(PAGE_SIZE, offset)`PAGE_SIZE=1000聚合所有用户 ID |
| `src/modules/notifications/data-access.ts` | 新增 `createNotifications(items)` 批量 INSERT 函数(单次 `db.insert().values([...])`),返回生成的 ID 数组 |
| `src/modules/notifications/channels/in-app-channel.ts` | `InAppChannelSender.sendBatch` 从 `Promise.all(items.map(send))` 改为调用 `createNotifications(inputs)` 单次批量 INSERT预校验 recipient/userId 一致性;失败时整体回滚并返回失败结果 |
| `src/modules/notifications/dispatcher.ts` | `sendBatchNotifications` 重构:并行获取所有用户偏好+联系方式 → 收集 in_app 渠道 payload 单次 `sendBatch` → 其他渠道保持 per-payload 并行 → 按 payload 聚合日志 |
| `src/modules/notifications/index.ts` | 导出 `createNotifications` |
**关键决策**
- `getAllUserIds` 默认 LIMIT 1000 是平衡单次查询压力与 fan-out 完整性的折中值;调用方通过分页遍历获取全部用户
- `createNotifications` 使用 drizzle 的 `db.insert().values(array)` 一次 INSERT 多行,相比 N 次单行 INSERT 显著降低网络往返
- in_app 渠道批量化其他渠道sms/email/wechat保持原并行模式因为这些渠道的发送成本主要在网络 IO 而非 DB 写入
- 批量 INSERT 失败时整体回滚drizzle 默认事务行为),每条记录返回失败结果
### Phase 3.8:调低高 LIMIT 默认值
在 `src/modules/*/data-access*.ts` 中搜索 LIMIT > 100 的默认值并调低至 ≤100保留分页场景
| 文件 | 原 LIMIT | 新 LIMIT | 函数 |
|------|---------|---------|------|
| `src/modules/attendance/data-access-correlation.ts` | 5000 | 100 | `getAttendanceGradeCorrelation` 调用 `getGradeRecords` |
| `src/modules/adaptive-practice/data-access-strategy.ts` | 1000 | 100 | `getStudentAnsweredQuestionIds` |
| `src/modules/questions/data-access.ts` | 1000 | 100 | `exportQuestions` |
**未修改(不在 data-access*.ts 范围内)**
- `src/modules/homework/stats-service.ts:276`limit: 200和 `:345`limit: 5000不在 `data-access*.ts` glob 范围内,本次未修改
### 验证结果
- `npx tsc --noEmit`零新增错误pre-existing 错误:`providers.tsx`、`web-vitals-reporter.tsx`、`homework/data-access-write.ts`、`paper-context-menu.tsx`、`print-view.tsx` 均为未修改文件)
- `npm run lint`零新增错误16 个 pre-existing problems 均位于未修改文件:`scripts/check-db-state.mjs`、`scripts/seed-grade5-chinese.ts`、`practice-result-view.tsx`、`exam-rich-form.tsx`、`use-exam-preview-state.ts`、`use-exam-preview-tasks.ts`、`homework/data-access-write.ts`、`unread-message-badge.tsx`
---
## 1.1.5 性能预算重构专项2026-07-05 新增)
> 完整审计报告见 `docs/architecture/audit/performance-budget-audit-report.md`。
> 本章节记录本次重构对架构产生的关键变更,以便后续维护时定位。
### 影响范围概览
| Phase | 改造内容 | 关键文件 |
|-------|---------|---------|
| 2.1 | tiptap 动态导入(3 处) | `shared/components/ui/rich-text-editor.tsx` + `modules/exams/editor/exam-rich-editor.tsx` + `modules/lesson-preparation/components/blocks/rich-text-block.tsx` |
| 2.2 | ReactFlow 动态导入 | `modules/textbooks/components/knowledge-graph.tsx` 拆为 `knowledge-graph-inner.tsx` + lazy wrapper,ReactFlowProvider 留外层 |
| 2.3 | exams/editor barrel 拆分 | 新增 `modules/exams/editor/types.ts` 作为纯类型入口,供 server-only 消费方引用 |
| 2.4 | chart.tsx barrel + status-badge 移除 use client | chart.tsx 已是具名导出;`status-badge.tsx` 移除 `"use client"`(纯展示组件) |
| 2.5 | 4 个角色根路由 loading.tsx | 新增 `teacher/loading.tsx` + `student/loading.tsx` + `parent/loading.tsx`(admin 已有) |
| 3.1 | unstable_cache 评估 | 评估后决定不引入,与项目现有 React `cache()` 模式一致 |
| 3.2 | auth 函数 cache 包装 | `shared/lib/session.ts:getSession` + `shared/lib/auth-guard.ts:resolveDataScope` + `getAuthContext` + `modules/classes/data-access.ts:getSessionTeacherId` 全部用 React `cache()` 包装,`getSessionTeacherId` 改走 `getAuthContext()` |
| 3.3 | copyCoursePlanToClasses 批量化 | `modules/course-plans/data-access.ts:347` 改为单事务 + 2 次批量 INSERT,O(N*M) → O(1) |
| 3.4 | gradeHomeworkAnswers 批量 UPDATE | `modules/homework/data-access-write.ts:356` 改为单次 `UPDATE ... CASE WHEN` + `sql.join`,替代 N 次串行 UPDATE |
| 3.5 | exams 表索引补齐 | `shared/db/schema.ts` 新增 5 个索引,迁移 `drizzle/0001_questions_fulltext_search.sql` |
| 3.6 | searchQuestions FULLTEXT | `shared/db/schema.ts` 新增 `contentText` STORED 生成列 + `questions_content_text_ft_idx` FULLTEXT 索引;`search/data-access.ts` 改用 `MATCH(...) AGAINST(... IN BOOLEAN MODE)` |
| 3.7 | 公告 fan-out 批量化 | `getAllUserIds` 加分页参数;新增 `notifications/data-access.ts:createNotifications` 批量 INSERT;`dispatcher.sendBatchNotifications` 重构为 in_app 单次批量 + 其他渠道并行 |
| 3.8 | 高 LIMIT 调低 | attendance 5000→100、adaptive-practice 1000→100、questions 1000→100 |
| 4.1 | SidebarProvider memoize | `modules/layout/components/sidebar-provider.tsx` resize debounce 200ms + useMemo context + useCallback toggleSidebar |
| 4.2 | lesson-plan-editor selector 拆分 | `lesson-plan-editor.tsx` + 3 个子组件改为单字段 selector |
| 4.3 | 题库表 + 考试表虚拟化 | 新增 `@tanstack/react-virtual` 依赖;`question-data-table.tsx` + `exam-data-table.tsx` 引入 `useVirtualizer` |
| 4.4 | recharts props 模块级常量 | 14 个 chart 文件提取 `CHART_MARGIN`/`AXIS_TICK`/`TOOLTIP_CURSOR` 等模块级常量 |
| 4.5 | useOptimistic 改造 | `message-detail.tsx` + `message-list.tsx` + `grade-record-list.tsx` + `attendance-sheet.tsx` 改用 `useOptimistic` + `useTransition` |
| 4.6 | 高频子组件 React.memo | `AssignmentCard` + `StatusBadge` + `EmptyState` 添加 React.memo(ClassCard/GraphKpNode 已有) |
| 4.7 | AiAssistantWidget 懒加载 | 新增 `modules/ai/components/ai-assistant-widget-inner.tsx`,外层改为 lazy wrapper |
| 4.8 | scan-image-viewer aspect-ratio | `modules/homework/components/scan-image-viewer.tsx` `<img>` 加 `aspectRatio: "4 / 3"` + width/height 属性 |
| 4.9 | metadata 增强 | `app/layout.tsx` 补 `metadataBase`/`openGraph`/`robots`/`applicationName`/`authors`/`creator` |
### 关键架构决策
1. **动态导入模式标准化**:重 UI 库(tiptap / @xyflow/react / AI SDK)统一采用 `xxx-inner.tsx` + `xxx.tsx` lazy wrapper 模式。ReactFlowProvider 等 context provider 留在外层同步渲染,保证 lazy 子组件的 `useReactFlow()` 等 hook 可用。
2. **请求级去重优先于跨请求缓存**:auth 相关 4 个核心函数(getSession/resolveDataScope/getAuthContext/getSessionTeacherId)全部用 React `cache()` 包装。不引入 `unstable_cache`,因权限数据易变,跨请求缓存风险高于收益。
3. **批量 SQL 优于循环 SQL**:
- INSERT 场景:用 `db.insert().values([...])` 批量
- UPDATE 场景:用 `sql.join` + `CASE WHEN` 单次执行
- 复杂事务:用 `db.transaction` 包裹多个批量操作
4. **React 19 useOptimistic 替代手动 isPending**:消息星标/出勤/成绩录入统一改为 `useOptimistic` + `useTransition`,移除手动 `useState<boolean>` 跟踪 pending。
5. **细粒度 Zustand selector**:单字段 selector 优先于 `useShallow` 多字段 selector,因为函数引用天然稳定。
---
## 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 模块依赖关系图
下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。
### 图例
- `───▶` 合理依赖(通过 data-access 或类型导入)
- `═══▶` 违规依赖(直接查询对方 DB 表)
- `` 循环依赖
- `──▷` UI 组件组合(合理)
### 1.2.1 核心业务模块依赖
```
┌──────────────┐
│ textbooks │ ◀── 标杆模块(无跨模块 DB 访问)
└──────┬───────┘
│ ──▷ UI 组合knowledge-point-dialogs
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌──────────┐ ┌────────────┐
│ questions │ │ exams │ │ homework │
└─────┬──────┘ └────┬─────┘ └─────┬──────┘
│ ✅ P1-1 │ ✅ P1-1 │ ✅ P1-1 已修复
│ 通过 textbooks│ 通过 questions│ 通过 exams/classes/
│ data-access │ data-access │ school/users data-access
│ │ 通过 classes │
│ │ data-access │
┌─┴────────┐ │ │
│ grades │◀────┘ 仅外键引用(合理)
│ (成绩) │◀──── exams data-access✅ 2026-06-24 新增getExamsForGradeEntry/getExamForGradeEntry 按试卷录入成绩)
└────┬─────┘
│ ✅ P1-1 已修复
│ 通过 classes/school/users data-access
┌──────────┐
│ classes │ ✅ P1-1 已修复:通过 homework/data-access-classes
└────┬─────┘ 获取作业数据,不再直查 homework/exams 表
│ ✅ P0-1 已修复data-access.ts 已拆分为 5 文件
│ ✅ P0-5 已修复classSchedule 写入口统一到 scheduling
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────┐ ┌──────────┐
│scheduling│ │school│ │ attendance│
└────┬─────┘ └──────┘ └──────────┘
│ ✅ P0-5 已修复
│ classSchedule 写入口统一到 scheduling
│ 通过 classes/data-access 校验归属
│ ✅ P1-1 已修复:通过 users data-access
```
### 1.2.2 扩展模块依赖
```
┌─────────────┐ ✅ P0-3 已修复:通过各模块 data-access ┌──────────────┐
│ dashboard │ 获取数据getUsersDashboardStats 等) │ users/classes│
│ (聚合层) │──────────────────────────────────────▶│ /exams/... │
└─────────────┘ ✅ P0-4 已修复Promise.all 并行调用 └──────────────┘
┌─────────────┐ ─── 调用 data-access合理 ┌──────────────┐
│ parent │──────────────────────────────▶│ classes/ │
│ (聚合层) │ ✅ P1-1 已修复:通过各模块 │ homework/grades│
└─────────────┘ data-access 获取数据 └──────────────┘
┌─────────────┐ ✅ P1-1 已修复:通过 exams/questions/ ┌──────────────┐
│ diagnostic │ classes/users data-access 获取数据 │ exams/questions│
│ │──────────────────────────────────────▶│ /classes/users│
└─────────────┘ └──────────────┘
┌─────────────┐ ✅ P1-1 已修复:通过 exams/users ┌──────────────┐
│ proctoring │ data-access 获取数据 │ exams/users │
└─────────────┘──────────────────────────────────────▶└──────────────┘
┌─────────────┐ ─── 调用 notifications dispatcher ┌──────────────┐
│ messaging │ (通知偏好/CRUD 已迁移至 notifications│ notifications │
│ │──────────────────────────────────────▶│ (拥有 │
│ │ ✅ P0-4 / P1-5 已修复:单向依赖 │ messageNotif│
└─────────────┘ │ ications + │
│ preferences)│
└──────────────┘
┌─────────────┐ ─── 调用 messaging Action ┌──────────────┐
│ settings │ (通知偏好表单) │ messaging │
└─────────────┘──────────────────────────────▶└──────────────┘
┌─────────────────┐ ───▶ data-access合理 ┌──────────────┐
│ lesson-prep │──────────────────────────────▶│ textbooks │
│ (备课聚合层) │ 只读章节/知识点树 │ (章节/KP 树) │
│ │──────────────────────────────▶└──────────────┘
│ │──────────────────────────────▶┌──────────────┐
│ │ 创建/查询题目 │ questions │
│ │──────────────────────────────▶└──────────────┘
│ │──────────────────────────────▶┌──────────────┐
│ │ 创建 exam 草稿 │ exams │
│ │──────────────────────────────▶└──────────────┘
│ │──────────────────────────────▶┌──────────────┐
│ │ 创建作业下发 │ homework │
│ │──────────────────────────────▶└──────────────┘
│ │──────────────────────────────▶┌──────────────┐
│ │ 查询教师班级 │ classes │
│ │──────────────────────────────▶└──────────────┘
│ │──────────────────────────────▶┌──────────────┐
│ │ 附件 │ files │
└─────────────────┘──────────────────────────────▶└──────────────┘
```
### 1.2.3 循环依赖详情 ✅ 已修复
```
shared/lib/audit-logger.ts ──┐
shared/lib/change-logger.ts ──┼──▶ shared/lib/session.ts ──▶ (dynamic import) @/auth
shared/lib/auth-guard.ts ──┘
src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
──▶ import { ... } from "@/shared/lib/login-logger"
──▶ import { ... } from "@/shared/lib/password-policy"
──▶ import { ... } from "@/shared/lib/rate-limit"
──▶ import { ... } from "@/shared/lib/role-utils" # P1-3 拆出
──▶ import { ... } from "@/shared/lib/bcrypt-utils" # P1-3 拆出
──▶ import { ... } from "@/shared/lib/http-utils" # P1-3 拆出
──▶ import { ... } from "@/shared/lib/password-security-service" # P1-3 拆出
──▶ import { db, schema } from "@/shared/db"
✅ 修复shared/lib/* 不再静态 import @/auth统一通过 session.ts 单一入口
session.ts 内部使用 dynamic import("@/auth") 打破模块级静态循环
运行时调用链保持不变,模块加载图无环
```
---
## 1.3 数据流向图(考试流程)
以"考试流程"为例,展示数据从创建到成绩统计的完整流向。
```
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 1教师创建考试 │
│ ───────────────────────────────────────────────────────────────── │
│ teacher/exams/create/page.tsx │
│ └─▶ exams/actions.createExamAction │
│ ├─▶ requirePermission(EXAM_CREATE) [shared/auth-guard] │
│ ├─▶ persistExamDraft() [exams/data-access] │
│ │ └─▶ db.insert(exams) [shared/db] │
│ │ └─▶ db.insert(examQuestions) [shared/db] │
│ └─▶ revalidatePath("/teacher/exams") │
│ │
│ 数据写入exams 表 + examQuestions 表 │
│ ✅ P0-1 已修复persistAiGeneratedExamDraft 改为调用 questions/data-access.createQuestionWithRelations不再直查 questions 表 │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 2学生作答作业化考试
│ ───────────────────────────────────────────────────────────────── │
│ student/learning/assignments/[assignmentId]/page.tsx │
│ └─▶ homework/actions.startHomeworkSubmissionAction │
│ ├─▶ requirePermission(HOMEWORK_SUBMIT) │
│ ├─▶ data-access-write.startHomeworkSubmission ✅ P1-2 已修复 │
│ │ └─▶ db.insert(homeworkSubmissions) [shared/db] │
│ └─▶ 返回 submissionId │
│ │
│ └─▶ homework/actions.saveHomeworkAnswerAction │
│ └─▶ data-access-write.saveHomeworkAnswer ✅ P1-2 已修复 │
│ └─▶ db.transaction(insert homeworkAnswers) [shared/db] │
│ │
│ └─▶ homework/actions.submitHomeworkAction │
│ └─▶ data-access-write.submitHomework ✅ P1-2 已修复 │
│ └─▶ db.update(homeworkSubmissions.status="submitted") │
│ │
│ 数据写入homeworkSubmissions 表 + homeworkAnswers 表 │
│ ✅ P1-2 已修复actions 层不再直接 DB 操作,已下沉到 data-access-write│
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 3教师批改 │
│ ───────────────────────────────────────────────────────────────── │
│ teacher/homework/submissions/[submissionId]/page.tsx │
│ └─▶ homework/actions.gradeHomeworkSubmissionAction │
│ ├─▶ requirePermission(HOMEWORK_GRADE) │
│ └─▶ data-access-write.gradeHomeworkSubmission ✅ P1-2 已修复│
│ └─▶ db.update(homeworkAnswers) 循环 [shared/db] │
│ │
│ 数据更新homeworkAnswers.isCorrect / score / feedback │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段 4成绩统计与诊断 │
│ ───────────────────────────────────────────────────────────────── │
│ teacher/grades/page.tsx │
│ └─▶ grades/data-access.getGradeRecords │
│ └─▶ ✅ P1-1 已修复:通过 classes/school/users data-access │
│ │
│ teacher/diagnostic/page.tsx │
│ └─▶ diagnostic/data-access.updateMasteryFromSubmission │
│ ├─▶ ✅ P1-1 已修复:通过 exams data-access 获取提交 │
│ ├─▶ ✅ P1-1 已修复:通过 questions data-access 获取知识点 │
│ └─▶ 更新 knowledgePointMastery │
│ │
│ 数据读取homeworkSubmissions → grades → knowledgePointMastery │
└─────────────────────────────────────────────────────────────────────┘
```
**关键观察**:考试流程横跨 4 个模块exams → homework → grades → diagnostic✅ P1-1 已修复:所有跨模块查询已改为通过对方 data-access 接口,模块封装性已恢复。
---
## 1.4 核心调用链路
### 1.4.1 调用链路:创建考试(含 AI 出题)
```
[Client] exam-form.tsx
│ FormData
[Route] POST /teacher/exams/create (Server Action)
[Action] exams/actions.createAiExamAction
├─▶ requirePermission(EXAM_CREATE)
│ └─▶ shared/lib/auth-guard.getAuthContext()
│ ├─▶ auth() [src/auth.ts]
│ ├─▶ db.query.usersToRoles [shared/db]
│ ├─▶ db.query.classSubjectTeachers
│ └─▶ 返回 { userId, roles, permissions, dataScope }
├─▶ generateAiCreateDraftFromSource()
│ └─▶ exams/ai-pipeline.ts (912 行)
│ ├─▶ shared/lib/ai.createAiChatCompletion()
│ │ └─▶ OpenAI SDK + db.query.aiProviders
│ └─▶ JSON 解析 + Zod 校验
├─▶ persistAiGeneratedExamDraft()
│ └─▶ exams/data-access.ts
│ ├─▶ db.insert(exams) ✅ 合理
│ ├─▶ db.insert(examQuestions) ✅ 合理
│ └─▶ questions/data-access.createQuestionWithRelations ✅ P0-1 已修复:通过 data-access
└─▶ revalidatePath("/teacher/exams")
```
### 1.4.2 调用链路:学生提交作业
```
[Client] homework-take-view.tsx
[Action] homework/actions.submitHomeworkAction
├─▶ requirePermission(HOMEWORK_SUBMIT)
├─▶ data-access-write.submitHomework ✅ P1-2 已修复
│ (校验 submission 归属 + 更新状态)
│ └─▶ db.update(homeworkSubmissions)
│ SET status = "submitted", submittedAt = now()
└─▶ 返回 ActionState<{ submissionId }>
```
### 1.4.3 调用链路:管理员仪表盘聚合
```
[Route] /admin/dashboard/page.tsx (Server Component)
[DataAccess] dashboard/data-access.getAdminDashboardData
├─▶ users/data-access.getUsersDashboardStats() ✅ 通过模块 data-access
│ ├─ userCount / activeSessionsCount / userRoleCounts
│ └─ recentUsers (含角色解析)
├─▶ classes/data-access.getClassesDashboardStats() ✅ 通过模块 data-access
│ └─ classCount
├─▶ textbooks/data-access.getTextbooksDashboardStats() ✅ 通过模块 data-access
│ └─ textbookCount / chapterCount
├─▶ questions/data-access.getQuestionsDashboardStats() ✅ 通过模块 data-access
│ └─ questionCount
├─▶ exams/data-access.getExamsDashboardStats(scope?) ✅ 通过模块 data-access
│ └─ examCount (含 scope 过滤)
└─▶ homework/stats-service.getHomeworkDashboardStats(scope?) ✅ 通过模块 data-access
├─ homeworkAssignmentCount / homeworkAssignmentPublishedCount
└─ homeworkSubmissionCount / homeworkSubmissionToGradeCount
✅ P0-4 已修复dashboard 改为并行调用各模块 dashboard stats 函数,不再直查跨模块表
```
### 1.4.4 调用链路admin 路由组统一权限守卫
```
[Layout] app/(dashboard)/admin/layout.tsx (Server Component)
[AuthGuard] shared/lib/auth-guard.getAuthContext()
└─▶ getSession() → 校验已登录(未登录抛 PermissionDeniedError
[Children] 各 admin/* 页面page.tsx在函数体首行调用 requirePermission(XXX)
├─▶ /admin/school/schools → requirePermission(SCHOOL_MANAGE)
├─▶ /admin/school/academic-year → requirePermission(SCHOOL_MANAGE)
├─▶ /admin/school/classes → requirePermission(SCHOOL_MANAGE)
├─▶ /admin/school/departments → requirePermission(SCHOOL_MANAGE)
├─▶ /admin/school/grades → requirePermission(SCHOOL_MANAGE)
├─▶ /admin/school/grades/insights → requirePermission(SCHOOL_MANAGE)
├─▶ /admin/users/import → requirePermission(USER_MANAGE)
├─▶ /admin/users → requirePermission(USER_MANAGE)
├─▶ /admin/roles → requirePermission(ROLE_READ)
├─▶ /admin/roles/[id] → requirePermission(ROLE_READ)
├─▶ /admin/permissions → requirePermission(PERMISSION_READ)
├─▶ /admin/scheduling/auto → requirePermission(SCHEDULE_AUTO)
├─▶ /admin/scheduling/changes → requirePermission(SCHEDULE_ADJUST)
├─▶ /admin/scheduling/rules → requirePermission(SCHEDULE_ADJUST)
├─▶ /admin/announcements → requirePermission(ANNOUNCEMENT_MANAGE)
├─▶ /admin/announcements/[id] → requirePermission(ANNOUNCEMENT_MANAGE)
├─▶ /admin/audit-logs → requirePermission(AUDIT_LOG_READ)
└─▶ /admin/invitation-codes → requirePermission(USER_MANAGE) ✅ audit-P2-3 新增
✅ P0 安全修复admin/layout.tsx 提供登录态统一守卫,
各页面 requirePermission() 提供细粒度权限校验
teacher/classes/* 路由权限校验2026-06-22 审计修复):
├─▶ /teacher/classes/my → requirePermission(CLASS_READ) ✅ 已修复
├─▶ /teacher/classes/my/[id] → requirePermission(CLASS_READ) ✅ 已修复
├─▶ /teacher/classes/schedule → requirePermission(CLASS_READ) ✅ 已修复
└─▶ /teacher/classes/students → requirePermission(CLASS_READ) ✅ 已修复
student/learning + student/schedule 路由权限校验2026-06-26 审计修复):
├─▶ /student/learning → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit
├─▶ /student/learning/courses → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit
├─▶ /student/learning/courses/[classId]→ requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit
└─▶ /student/schedule → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit
```
---
# 第二部分:模块清单
> 每个模块包含:职责 · 导出函数 · 依赖关系 · 已知问题 · 文件清单
## 2.1 shared基础设施层
**职责**:提供全项目共享的 DB Schema、工具函数、权限系统、UI 基础组件、通用 Hooks。
**导出函数**(核心):
- `getAuthContext()` / `requirePermission(p)` / `requireAuth()` — 认证与权限
- `resolvePermissions(roles)` / `resolveDataScope(userId, roles)` — 权限解析
- `logAudit()` / `logLoginEvent()` / `logDataChange()` — 日志记录
- `createAiChatCompletion()` / `parseAiChatPayload()` — AI 调用
- `validatePassword()` / `isAccountLocked()` / `rateLimit()` — 安全策略
- `exportToExcel()` / `parseExcel()` / `generateTemplate()` — Excel 工具
- `cn()` / `formatDate()` / `formatFileSize()` — 通用工具
- `getInitials(name)` / `formatDateForFile(d?)` — 通用工具P1-c / P1-a 重构新增:从 parent/lib/utils.ts、grades/export-button.tsx 等多处重复实现抽取)
- `downloadBase64File(base64, filename, mimeType?)` / `downloadBlob(blob, filename)` — 客户端文件下载P1-c 重构新增:从 grades/export-button、users/user-import-dialog、audit/audit-log-export-button 三处重复实现抽取,位于 `lib/download.ts`
- `handleActionError(e)` / `safeActionCall(action, options?)` / `safeJsonParse(json, msg)` / `safeParseDate(v, field)` / `safeParseNumber(v, field)` / `escapeLikePattern(input)` — Server Action 错误处理与客户端调用包装2026-06-23 审计修复新增:位于 `lib/action-utils.ts`,统一所有 Server Action catch 块错误处理,避免内部错误消息暴露给客户端)
- `BusinessError` / `NotFoundError` / `ValidationError` — 错误类2026-06-23 审计修复新增:位于 `lib/action-utils.ts`已知业务错误基类message 可安全返回客户端)
- `extractQuestionPreview(content, fallback?, maxLength?)` — 题目内容纯文本预览提取(✅ 2026-06-24 新增:位于 `lib/question-content.ts`,从 `unknown` 类型题目内容递归提取纯文本,支持字符串/数组/对象格式,可选截断;被 error-book、homework 等模块共享使用,避免各模块重复实现题目内容解析逻辑)
- `isRecord(v)` — 通用类型守卫P1-6 代码去重新增:位于 `lib/type-guards.ts`,判断未知值是否为非空对象 `v is Record<string, unknown>`;消除 homework/stats-service、lesson-preparation/node-summary、lesson-preparation/ai-suggest 等 12+ 个文件中重复定义的 `isRecord` 局部实现,从 `unknown` 安全收窄类型,替代 `as Record<string, unknown>` 断言)
- `trackEvent(name, payload)` / `trackExamEvent(name, payload)` / `trackAuthEvent(name, payload)` — 监控埋点接口(位于 `lib/track-event.ts`server-only当前输出到 console.info预留接入外部监控服务EventName 联合类型覆盖 announcement/message/notification/attendance/elective/exam/homework/ai/audit/files/auth 模块事件;✅ P3 新增事件名 `homework.excellent_viewed`、`homework.remind_unsubmitted`;✅ audit-P1-9 新增 auth 模块 8 个事件 `auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.2fa_disabled`/`auth.account_locked`/`auth.rate_limited`,并新增 `trackAuthEvent` 便捷函数自动设置 targetType="user"
- `ROLE_NAMES` — 角色名常量对象(✅ 2026-06-24 新增:位于 `types/permissions.ts`,包含 ADMIN/TEACHER/STUDENT/PARENT/GRADE_HEAD/TEACHING_HEAD 键,替代 DB 查询中硬编码的角色名字符串;被 error-book/data-access-analytics.getAllStudentIds 使用 `ROLE_NAMES.STUDENT`
- `apiSuccess(data, init?)` / `apiError(message, status, errorCode?, init?)` / `apiFromAction(result, options?)` / `handleApiError(error, init?)` / `withApiErrorHandler(handler)` / `parseJsonBody<T>(req)` / `errorToStatus(error)` — API 路由层统一响应工具(✅ 2026-07-04 API 规范化重构新增:位于 `lib/api-response.ts`,仅用于 `app/api/.../route.ts`;统一响应信封 `{ success, message?, errorCode?, data? }` 与 `ActionState<T>` 对齐集中错误→HTTP 状态码映射替代字符串匹配推导;`withApiErrorHandler` HOF 统一异常处理消除每个 route 重复 try/catch`apiFromAction` 通过 `errorCode` 驱动状态码替代 `result.message?.includes("not found")` 脆弱模式;`errorToStatus` 映射规则PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500
- `formatSseEvent(data)` / `formatSseError(message)` / `formatSseDone()` / `createSseResponse(stream)` / `createSseError(message, status)` — SSE 共享工具(✅ 2026-07-04 API 规范化重构新增:位于 `lib/sse.ts`,统一 `app/api/.../stream/route.ts` 的事件格式与响应头;标准 SSE 头 `Content-Type: text/event-stream` + `Cache-Control: no-cache, no-transform` + `Connection: keep-alive`;客户端约定错误统一 `{ type: "error", message }`、流结束信号 `[DONE]`
**共享组件导出**P0-b / P1-a / P1-b / P1-c / P2-a / P2-b / P3-a / P3-b / P3-c / P3-d / 第二轮 P0-1/P0-2/P0-3/P1-1/P1-2/P1-3/P1-4 重构新增,按类别组织):
| 类别 | 组件 | 文件 | 用途 | 消费方数量 |
|------|------|------|------|-----------|
| **UI 组件** | `StatCard` | `components/ui/stat-card.tsx` | 统计卡片(标题+数值+图标+描述+跳转+骨架屏) | 8 个P1-a |
| **UI 组件** | `StatItem` | `components/ui/stat-item.tsx` | 紧凑统计项label+icon+value+hint用于统计面板网格 | 8 个P1-a |
| **UI 组件** | `ChipNav` | `components/ui/chip-nav.tsx` | 芯片导航组(通过 URL search params 切换筛选维度Link 跳转) | 3 个P1-b |
| **UI 组件** | `PageHeader` | `components/ui/page-header.tsx` | 页面头部(标题+描述+icon+actions响应式布局 | 2 个P2-b: profile/page.tsx, settings/security/page.tsx |
| **UI 组件** | `FilterBar` / `FilterSearchInput` / `FilterResetButton` | `components/ui/filter-bar.tsx` | 筛选栏容器+搜索框+重置按钮统一布局壳URL 状态由各模块处理) | 5 个P3-b: exam/textbook/question/audit-log/login-log filters |
| **UI 组件** | `ConfirmDeleteDialog` | `components/ui/confirm-delete-dialog.tsx` | 通用删除确认对话框AlertDialog 包装,支持自定义 confirmText/cancelText | 5 个P0-1: announcement-detail, message-detail, course-plan-detail, grade-classes-view, students-table |
| **UI 组件** | `Pagination` | `components/ui/pagination.tsx` | 通用分页 UIShowing X-Y of Z + Page X of Y + 上一页/下一页按钮) | 3 个P0-2: audit-log-table, login-log-table, data-change-log-table |
| **UI 组件** | `EmptyTableRow` | `components/ui/empty-table-row.tsx` | 表格空状态行TableRow + TableCell 居中显示空状态文案) | 3 个P0-3: audit-log-table, login-log-table, data-change-log-table |
| **UI 组件** | `ResizablePanel` | `components/ui/resizable-panel.tsx` | 自实现可拖拽分栏容器(左右两栏 + 中间分隔条 pointer 拖拽调整宽度,无新依赖;用于试卷富文本编辑器左编辑右预览、阅卷式批改左题目右扫描图等场景) | 2 个exams/components/exam-rich-form, homework/components/homework-scan-grading-view |
| **UI 组件** | `ErrorState` | `components/error-state.tsx` | 通用错误边界展示组件("use client",统一 error.tsx 渲染逻辑AlertTriangle 图标 + i18n title/description + retry 按钮props`{ error, reset, namespace }`;翻译键查找规则:在传入 namespace 下查找 `error.title`/`error.description`/`error.retry` | 10+ 个2026-06-26 审计修复新增classes/student 等模块的 error.tsx |
| **UI 组件** | `StatusBadge` | `components/ui/status-badge.tsx` | 通用状态徽章Badge + 状态→variant/label/className 映射表,修复 in_progress 颜色不一致 bug | 9+ 个P1-1: audit 3 文件, grades 2 文件, student/learning/assignments, parent/child-homework-summary, student-upcoming-assignments-card, question-columns |
| **表单字段** | `TextField` | `components/form-fields/text-field.tsx` | 通用文本字段FormField + Input 包装,支持 text/number/password/datetime-local 类型 + value 转换器) | 3 个文件 16 处P1-2: profile-settings-form 6, exam-basic-info-form 4, ai-provider-settings-card 4 |
| **表单字段** | `SelectField` | `components/form-fields/select-field.tsx` | 通用选择字段FormField + Select 包装,支持 toSelectValue/fromSelectValue 处理 number↔string | 4 个文件 8 处P1-2: exam-basic-info-form 3, ai-provider-settings-card 1, create-question-dialog 2, profile-settings-form 1 |
| **表单字段** | `TextareaField` | `components/form-fields/textarea-field.tsx` | 通用多行文本字段FormField + Textarea 包装) | 1 个P1-2: create-question-dialog |
| **图表组件** | `ChartCardShell` | `components/charts/chart-card-shell.tsx` | 图表卡片外壳Card+Header+EmptyState+Content 统一结构) | 8 个P3-c |
| **图表组件** | `TrendLineChart` | `components/charts/trend-line-chart.tsx` | 趋势折线图LineChart 统一配置,支持单/多系列) | 8 个P3-c: grade-trend-chart 等) |
| **图表组件** | `SimpleBarChart` | `components/charts/simple-bar-chart.tsx` | 柱状图BarChart 统一配置,支持单/多 Bar + Cell 分桶着色 + defs 自定义 SVG 图案) | 8 个P3-c: grade-distribution-chart 等) |
| **图表组件** | `ComparisonRadarChart` | `components/charts/comparison-radar-chart.tsx` | 对比雷达图RadarChart 统一配置,支持双 Radar 对比) | 8 个P3-c: subject-comparison-chart, mastery-radar-chart 等) |
| **课表组件** | `ScheduleList` / `ScheduleListItem` | `components/schedule/schedule-list.tsx` | 课表列表+列表项(课程+时间+地点+班级徽章separator/card 两种变体) | 3 个P3-a: student-today-schedule-card, child-schedule-card, student-schedule-view |
| **题库组件** | `QuestionBankFilters` | `components/question/question-bank-filters.tsx` | 题库筛选栏(搜索+题型+难度default/compact 两种布局) | 2 个P3-d: exam-assembly, question-bank-picker |
| **题库组件** | `QuestionCascadeFilter` | `modules/questions/components/question-cascade-filter.tsx` | ✅ 审计新增教材→章节→知识点三级级联筛选nuqs URL 状态同步,清除上级自动清除下级) | 1 个question-filters |
| **题库组件** | `KnowledgePointSelector` | `modules/questions/components/knowledge-point-selector.tsx` | ✅ 审计新增:知识点选择器(支持搜索过滤,从 create-question-dialog 拆分) | 1 个create-question-dialog |
| **题库组件** | `OptionsEditor` | `modules/questions/components/options-editor.tsx` | ✅ 审计新增:选项编辑器(单选/多选模式,增删改选项,从 create-question-dialog 拆分) | 1 个create-question-dialog |
| **题库组件** | `QuestionBankResultsClient` | `modules/questions/components/question-bank-results-client.tsx` | ✅ 审计新增RSC 与客户端 Hook 桥接(解决 useQuestionColumns Hook 在客户端组件中使用) | 1 个teacher/questions/page |
| **题库组件** | `QuestionContentRenderer` | `modules/questions/components/question-content-renderer.tsx` | ✅ 审计新增:题目结构化渲染(题干/选项/答案/解析,单选圆点/多选方框,正确答案高亮) | 1 个question-actions 查看详情) |
| **题库组件** | `BatchOperations` | `modules/questions/components/batch-operations.tsx` | ✅ 审计新增:批量操作工具栏(批量删除,权限感知,事务原子性) | 1 个question-data-table |
| **题库组件** | `ImportExportButtons` | `modules/questions/components/import-export-buttons.tsx` | ✅ 审计新增导入导出组件JSON 格式,导出下载/上传导入,导入前预览确认) | 2 个teacher/admin questions page |
| **设置组件** | `SettingsView` | `modules/settings/components/settings-view.tsx` | 统一设置页布局5 标签页General/Notifications/Appearance/Security/AI角色差异通过 props 注入Tab URL 持久化,登出二次确认) | 4 个P2-a: admin/teacher/student/parent 设置页) |
**共享 Hooks 导出**(第二轮 P1-4 重构新增):
| Hook | 文件 | 签名 | 用途 | 消费方 |
|------|------|------|------|--------|
| `useActionMutation` | `hooks/use-action-mutation.ts` | `useActionMutation<T>(options?): { isWorking, mutate }`。Options 联合类型:旧版 `{ successMessage?, errorMessage?, onSuccess?, onError? }` 或新版 `{ mutationFn?, actionId?, params?, invalidateQueryKeys?, ... }`。新版接入 QueryClient成功后按 `CLIENT_INVALIDATION_MAP[actionId]` 自动 invalidateQueries。`mutate(action?)` 参数可选:传 action 走旧模式,不传则使用 options.mutationFn | 通用 Server Action mutation Hook替代 50+ 文件中重复的 setIsWorking + try/catch/finally + toast 模式。V5 缓存策略重构新增 QueryClient + actionId 自动失效,向后兼容旧调用方 | 示范P1-4 schools-viewTask 19 classes/class-invitation-managerrevoke mutation + actionId 自动失效)。潜在影响 50+ 文件 |
| `useActionQuery` | `hooks/use-action-query.ts` | `useActionQuery<T>(action, options?): { data, loading, error, refetch }`。Options 联合类型:旧版 `{ deps?, enabled?, errorMessage? }` 或新版 `UseActionQueryWithCacheOptions`(含 `queryKey` + `UseQueryOptions`)。传入 `queryKey` 走 useQuery 跨页共享缓存;不传走旧 useEffect + useState。Hook 内部始终声明 useQuery + useState/useEffect 以遵守 React Hooks 规则 | 通用 Server Action 查询 Hook替代 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式内置竞态防护。V5 缓存策略重构新增 queryKey 入参走 QueryClient向后兼容旧调用方 | 示范P1-4 create-question-dialogTask 19 classes/class-invitation-managerlistClassInvitationCodesAction + queryKeys.classes.invitations(classId))。潜在影响 11 个文件 |
**V5 状态管理统一专项基础设施**(阶段 1 新增,零业务行为变更):
| 资产 | 文件 | 签名 | 用途 | 后续消费方 |
|------|------|------|------|--------|
| `createQueryClient` | `lib/query-client.ts` | `createQueryClient(): QueryClient` | TanStack Query 工厂,默认 staleTime 30s / retry 1 / refetchOnWindowFocus false / mutations 全局 onError 兜底 | `app/providers.tsx` |
| `configureNotify` + `notify` + `rawToast` | `lib/notify.ts` | `configureNotify(resolver); notify.{success,error,info,warning,promise}; rawToast` | 统一 toast 接口,包装 sonner 并内置 i18n key 解析。`configureNotify` 由 `Providers` 的 `NotifyConfigurator` 注入 next-intl `t` 函数 | `query-client.ts` 全局兜底 + 业务组件迁移后由各模块使用(替换 100+ 处 `import { toast } from "sonner"` |
| `useDialogState` | `hooks/use-dialog-state.ts` | `useDialogState(): readonly [boolean, () => void, () => void, () => void]` | Dialog/Sheet 开关状态 Hook替换 30+ 处 `const [open, setOpen] = useState(false)` 模式 | 待迁移30+ 处 Dialog 组件 |
| `useUiStore` + `useModal` | `stores/ui-store.ts` | `useUiStore: { modals, openModal, closeModal, isOpen, closeAll }; useModal(name)` | 全局 UI storezustand集中管理 modal registry。命令式 API + Hook 双入口。与 sonner 的分工toast 走 notifymodal 走本 store | 待迁移30+ 处 Dialog 组件切换为 `useModal(name)` |
| `createServiceProvider` | `lib/create-service-provider.tsx` | `createServiceProvider<T>(displayName): { Provider, useService, Context }` | Service DI Provider 工厂,替换 14 处重复的 `createContext(null)` + Provider + useService 抛错模板 | 待迁移announcements / attendance / audit / dashboard / messaging / settings 等 14 处 Service DI Provider |
| `Providers` | `app/providers.tsx` | `Providers({ children, locale, messages, session }): React.JSX.Element` | 客户端 Provider 聚合。嵌套顺序QueryClientProvider → NextIntlClientProvider → NotifyConfigurator → ThemeProvider → SessionProvider → NuqsAdapter → children + Toaster。DevTools 仅 dev 环境 | `app/layout.tsx` |
**V5 状态分层模型**5 层):
| 层 | 责任 | 选型 |
|----|------|------|
| URL 状态 | 列表过滤、tab 持久化、深链接 | `nuqs useQueryState`(单一来源,逐步替换 `useSearchParams` + `window.location.search` |
| 服务端状态 | 列表/详情/分页数据mutation + 缓存失效 | TanStack Query混合模式RSC 拿首屏 propsuseQuery 负责交互数据) |
| 客户端业务状态 | 模块级状态机(如 exam preview、lesson editor | zustand slice按模块拆参考 lesson-preparation 现有模式) |
| 全局 UI 状态 | modal registry | zustand ui-store |
| 表单状态 | 表单密集模块 | react-hook-form + zodResolver |
**共享工具函数导出**(第二轮 P1-3 重构新增):
| 函数 | 文件 | 签名 | 用途 | 消费方 |
|------|------|------|------|--------|
| `formatDateTime` | `lib/utils.ts` | `formatDateTime(date, locale?): string` | 国际化日期+时间格式化(含小时、分钟) | 4 个P1-3: lesson-plan-card, version-history-drawer, proctoring-dashboard, exam-ai-generator |
| `formatLongDate` | `lib/utils.ts` | `formatLongDate(date, locale?): string` | 国际化长日期格式化(含星期、完整月份名),默认 zh-CNweekday=short | 1 个P1-3: teacher-dashboard-header |
> 注:`SettingsView` 位于 `modules/settings/components/`(非 shared 层),因仅被 settings 模块消费,未下沉到 shared。此处列出以完整反映本次重构的组件抽取范围。
**依赖关系**
- 被依赖方:**所有模块**依赖 shared
- ✅ 反向依赖已修复:`shared/lib/{audit-logger, change-logger, auth-guard}` 通过 `shared/lib/session.ts` 单一入口获取 session不再直接依赖 `@/auth`
**已知问题**
- ✅ P0~~`shared/lib/*` ↔ `@/auth` 循环依赖~~ 已修复(新增 `shared/lib/session.ts` 封装 session 获取3 个文件改为 `import { getSession } from "@/shared/lib/session"`
- ⚠️ P1`schema.ts` 1111 行54 张表混合,超 1000 硬上限)
- ✅ P1~~`auth.ts` 293 行混合 5 类职责~~ 已拆分4 个辅助函数组迁移至 `shared/lib/{role-utils,bcrypt-utils,http-utils,password-security-service}`auth.ts 仅保留 NextAuth 配置)
- ✅ P2-2 已修复:~~`ai.ts` 218 行混合 5 类职责~~ 已拆分为 `ai/` 目录payload-parser.ts/api-key-crypto.ts/provider-config.ts/client.ts/errors.ts/index.ts原 `ai.ts` 保留为向后兼容的重导出文件9 行)
- ✅ P2-4 已修复:~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ 已迁移至 `modules/onboarding/`actions/data-access/schema/types/components引导流程改为独立路由 `/onboarding` + middleware 重定向 + Server Action
- ✅ P0-2/P0-3/P0-4/P0-5/P1-1/P1-2/P1-4/P1-5 已修复v3 对标 PowerSchool/Veracross/Auth0家长绑定三因子验证邮箱+生日+手机号后4位、教师多科目循环绑定、审计日志、服务端幂等、URL query 持久化步骤、局部错误收集、家长多子女动态行、跳过机制明确化
- ✅ v3 i18n 体系引入:采用 next-intl 4.xwithout i18n routing 模式cookie 驱动 locale 切换,字典放在 `shared/i18n/messages/{locale}/`,支持 zh-CN/en 两种语言,不破坏现有路由组结构
- ✅ v3 班级邀请码体系引入(对标 Google Classroom / 钉钉教育 / 智学网):新增 `class_invitation_codes` 表(独立表,支持有效期/次数限制/审计/多码并存6 位字母数字(剔除歧义字符 0/O/1/I/L空间 1.13 亿rate limit 防爆破10 次/5 分钟),审计日志全链路记录,懒清理过期码
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `db/schema.ts` | 1111 | 54 张表定义(超硬上限) |
| `db/relations.ts` | - | 表关系定义 |
| `db/index.ts` | - | Drizzle 客户端 |
| `lib/auth-guard.ts` | - | 认证上下文 + 权限校验 + DataScope |
| `lib/permissions.ts` | - | 角色-权限映射 |
| `lib/session.ts` | 38 | session 获取单一入口getSessionserver-onlydynamic import 打破循环) |
| `lib/ai.ts` | 9 | 向后兼容重导出P2-2 已拆分到 `ai/` 目录) |
| `lib/ai/payload-parser.ts` | 78 | 请求负载解析 |
| `lib/ai/api-key-crypto.ts` | 28 | API Key 加密/解密 |
| `lib/ai/provider-config.ts` | 132 | Provider 配置查询V3.1 增强:基于 session 用户的可见性/所有权校验public + own private 过滤) |
| `lib/ai/client.ts` | 58 | AI 客户端创建与调用 |
| `lib/ai/errors.ts` | 8 | 错误格式化 |
| `lib/ai/index.ts` | 5 | 聚合导出 |
| `lib/audit-logger.ts` | - | 操作日志(通过 session.ts 获取 sessionhttp-utils 获取 IP/UA |
| `lib/change-logger.ts` | - | 数据变更日志(通过 session.ts 获取 sessionhttp-utils 获取 IP |
| `lib/login-logger.ts` | - | 登录日志(通过 http-utils 获取 IP/UA |
| `lib/password-policy.ts` | - | 密码策略纯函数 |
| `lib/rate-limit/` | - | 速率限制器抽象audit-P1-6 新增:抽象 `RateLimiter` 接口;默认内存实现 `MemoryRateLimiter`;可选 Redis 实现 `RedisRateLimiter` 基于 @upstash/ratelimit通过 `RATE_LIMIT_DRIVER=memory\|redis` 环境变量切换;`rateLimit()` / `resetRateLimit()` 改为 Promise 返回以支持异步后端;`rateLimitKey` / `RATE_LIMIT_RULES` / `rateLimitHeaders` 保持同步纯函数) |
| `lib/role-utils.ts` | 51 | 角色规范化纯函数normalizeRole / resolvePrimaryRole✅ P2-8 修改:保留 grade_head/teaching_head 独立性,不再映射为 teacher优先级 admin>grade_head>teaching_head>teacher>student>parent |
| `lib/bcrypt-utils.ts` | 18 | bcrypt 哈希前缀规范化纯函数 |
| `lib/breached-password.ts` | 125 | 已泄露密码检测(✅ audit-P2-4 新增HIBP k-anonymity API 范围查询;仅发送 SHA-1 前 5 字符到 `api.pwnedpasswords.com`本地比对完整哈希后缀不泄露用户密码3 秒超时 + AbortControllerfail-open 策略API 不可用时返回 `checkSkipped=true` 不阻断注册/改密server-only |
| `lib/http-utils.ts` | 44 | 请求头解析resolveClientIp / getUserAgentserver-only |
| `lib/password-security-service.ts` | 84 | 密码安全 DB 操作(账户锁定/失败登录追踪server-only |
| `lib/route-resolver.ts` | 21 | 角色默认路径解析纯函数 `resolveDefaultPath(roles: string[]): string`(✅ audit-P0-6 新增:合并 `proxy.ts` 与 `onboarding/data-access.resolveDefaultPathByRoles` 重复实现;纯函数无 `server-only`,可在 edge/proxy 运行时使用;优先级 admin>grade_head/teaching_head>teacher>student>parent>fallback `/dashboard` |
| `lib/route-permissions.ts` | 82 | 路由权限配置常量(✅ audit-P1-10 + audit-P1-12 新增:从 proxy.ts 抽取,配置驱动设计;导出 4 个 `Record<string, Permission>` 常量:`SPECIFIC_ROUTE_PERMISSIONS`15 个精确路由权限audit-P1-10 扩展自 3 个至 15 个覆盖 `/admin/*` 子路径)/ `ROUTE_PREFIX_PERMISSIONS`5 个角色路由前缀权限)/ `DASHBOARD_ROUTE_PERMISSIONS`4 个仪表盘细粒度权限防跨角色访问)/ `API_ROUTE_PERMISSIONS`1 个 API 路由权限);纯常量无 `server-only`,新增路由权限只需修改此文件无需改 proxy.ts 逻辑) |
| `lib/permission-bitmap.ts` | 222 | 权限位图编解码(✅ audit-P1-7 新增:将 67 个权限点字符串数组约 1.1KB 压缩为约 13 字符 base36 字符串,体积减少 99%`PERMISSION_BITMAP_ORDER` 顺序固定不可变,新增权限只能追加末尾;纯函数无 `server-only`,可在 edge runtime 使用;导出 `encodePermissionsBitmap(permissions)` / `decodePermissionsBitmap(bitmap)` / `hasPermissionInBitmap(bitmap, permission)`BigInt 精度安全:手工按位累加 base36 → BigInt避免 `parseInt(bitmap, 36)` 的 Number 53 bit 精度丢失) |
| `lib/excel.ts` | - | Excel 导入导出 |
| `lib/file-storage.ts` | - | 文件存储抽象 |
| `hooks/use-permission.ts` | - | 客户端权限 Hook |
| `components/ui/*` | 35 文件 | shadcn/ui 标准组件(✅ P2-3 新增 alert.tsx |
| `components/ui/stat-card.tsx` | 95 | StatCard 统计卡片P1-a 新增) |
| `components/ui/stat-item.tsx` | 38 | StatItem 紧凑统计项P1-a 新增) |
| `components/ui/chip-nav.tsx` | 78 | ChipNav 芯片导航P1-b 新增) |
| `components/ui/page-header.tsx` | 44 | PageHeader 页面头部P2-b 新增,含 icon 属性) |
| `components/ui/filter-bar.tsx` | 124 | FilterBar + FilterSearchInput + FilterResetButtonP3-b 新增) |
| `components/ui/resizable-panel.tsx` | - | ResizablePanel 可拖拽分栏容器(自实现,无新依赖;用于富文本编辑器与阅卷式批改的左右分栏布局) |
| `components/error-state.tsx` | 35 | ErrorState 通用错误边界展示组件("use client"2026-06-26 审计修复新增:统一 30+ error.tsx 渲染逻辑props `{ error, reset, namespace }`,基于 EmptyState + AlertTriangle + next-intl useTranslations |
| `components/charts/chart-card-shell.tsx` | 90 | ChartCardShell 图表卡片外壳P3-c 新增) |
| `components/charts/trend-line-chart.tsx` | 153 | TrendLineChart 趋势折线图P3-c 新增) |
| `components/charts/simple-bar-chart.tsx` | 162 | SimpleBarChart 柱状图P3-c 新增) |
| `components/charts/comparison-radar-chart.tsx` | 143 | ComparisonRadarChart 对比雷达图P3-c 新增) |
| `components/schedule/schedule-list.tsx` | 112 | ScheduleList + ScheduleListItem 课表列表P3-a 新增) |
| `components/question/question-bank-filters.tsx` | 137 | QuestionBankFilters 题库筛选栏P3-d 新增) |
| `modules/questions/components/question-cascade-filter.tsx` | 130+ | QuestionCascadeFilter 三级级联筛选(✅ 审计新增教材→章节→知识点nuqs URL 状态) |
| `modules/questions/components/knowledge-point-selector.tsx` | 80+ | KnowledgePointSelector 知识点选择器(✅ 审计新增:从 create-question-dialog 拆分) |
| `modules/questions/components/options-editor.tsx` | 120+ | OptionsEditor 选项编辑器(✅ 审计新增:从 create-question-dialog 拆分) |
| `modules/questions/components/question-bank-results-client.tsx` | 30+ | QuestionBankResultsClient RSC 桥接(✅ 审计新增) |
| `modules/questions/utils/parse-content.ts` | 60+ | parseQuestionContent 类型守卫(✅ 审计新增:替代 `as` 断言) |
| `modules/questions/utils/track-event.ts` | 30+ | trackQuestionEvent 埋点接口(✅ 审计新增no-op 实现) |
| `modules/questions/components/question-content-renderer.tsx` | 110+ | QuestionContentRenderer 题目结构化渲染(✅ 审计新增) |
| `modules/questions/components/batch-operations.tsx` | 110+ | BatchOperations 批量操作工具栏(✅ 审计新增) |
| `modules/questions/components/import-export-buttons.tsx` | 180+ | ImportExportButtons 导入导出组件(✅ 审计新增) |
| `lib/download.ts` | 47 | downloadBase64File + downloadBlob 客户端下载工具P1-c 新增) |
| `lib/type-guards.ts` | - | 通用类型守卫集合P1-6 新增 `isRecord(v): v is Record<string, unknown>`,消除 12+ 个文件重复定义;含 grades/lesson-preparation 等模块的类型守卫 `isGradeDraftData`/`isExerciseBlockData`/`isPermission` 等) |
| `lib/track-event.ts` | - | 监控埋点接口server-onlyno-op console.info 实现EventName 联合类型覆盖 11 个模块事件P3 新增 `homework.excellent_viewed`、`homework.remind_unsubmitted`audit-P1-9 新增 auth 模块 8 个事件 + `trackAuthEvent` 便捷函数) |
| `lib/utils.ts` | - | 通用工具P1-a/P1-c 新增 getInitials + formatDateForFile |
| ~~`components/onboarding-gate.tsx`~~ | ~~312~~ | ✅ audit-P0-4 已删除(引导流程业务逻辑已迁移至 `modules/onboarding/`,原文件无任何 import 引用,删除以避免文件膨胀) |
| `components/global-search.tsx` | 221 | 全局搜索(业务泄漏) |
| `types/permissions.ts` | 157 | 67 个权限点常量 + Role/DataScope/AuthContext 类型(✅ rbac 新增:`Role` 类型从固定联合类型改为 `string` 支持动态角色;新增 `BUILTIN_ROLES` 常量、`BuiltinRole` 类型、`isBuiltinRole()` 类型守卫;`isRole()` 标记为 deprecated |
| `lib/api-response.ts` | 212 | ✅ 2026-07-04 API 规范化重构新增API 路由层统一响应工具apiSuccess/apiError/apiFromAction/handleApiError/withApiErrorHandler/parseJsonBody/errorToStatus统一响应信封 + 集中错误→HTTP 状态码映射,消除每个 route 重复 try/catch 与字符串匹配推导状态码 |
| `lib/sse.ts` | 81 | ✅ 2026-07-04 API 规范化重构新增SSE 共享工具formatSseEvent/formatSseError/formatSseDone/createSseResponse/createSseError统一 stream 路由的事件格式与响应头,消除 `ai/chat/stream` 与 `notifications/stream` 重复实现 |
---
## 2.2 exams考试模块
**职责**:考试全生命周期管理(创建/编辑/预览/发布/删除/复制)+ AI 辅助出题。
**导出函数**
- Actions`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增年级仪表盘维度3按 gradeId 查询年级下所有考试 + 提交统计EXAM_READ 权限)/ `autoMarkExamAction`(✅ 2026-06-24 新增AI 自动标记试卷文本,将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档EXAM_AI_GENERATE 权限,返回 `{ doc, title }` 供富文本编辑器载入。✅ 2026-06-24 拆分:迁移至 `ai-pipeline/auto-mark.ts`actions.ts re-export 保持向后兼容)/ `createExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器保存试卷草稿,将 Tiptap JSONContent 通过 `editor/editor-to-structure.editorDocToStructure` 转换为 questions + structure 后复用 `persistAiGeneratedExamDraft` 持久化EXAM_CREATE 权限;✅ 2026-06-24 升级:递归 `convertStructureNode` 支持 section/group/question 三级层级section 数据不再丢失。✅ 2026-06-24 拆分:迁移至 `actions-rich-editor.ts`actions.ts re-export 保持向后兼容)(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access / `updateExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器更新已有试卷,校验所有权后递归转换 EditorDoc→structure为无 questionId 的题目创建 questions 记录,调用 `data-access.updateExamWithRichEditorData` 持久化EXAM_UPDATE 权限revalidatePath 列表页与组卷页。✅ P0-4 修复2026-06-24原直接 db.transaction 写 exams/examQuestions 表,现已下沉到 data-access.updateExamWithRichEditorData。✅ 2026-06-24 拆分:迁移至 `actions-rich-editor.ts`actions.ts re-export 保持向后兼容)
- Data-access`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增年级仪表盘维度3exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)/ `getExamsForGradeEntry`(✅ 2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷,供成绩录入页试卷选择器使用,返回 id/title/subjectName/gradeName/questionCount/totalScore/ `getExamForGradeEntry`(✅ 2026-06-24 新增获取单个试卷详情含题目列表innerJoin questions 获取 type含 scope 校验,返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}],供 grades 模块按试卷录入成绩使用)/ `updateExamWithRichEditorData`(✅ 2026-06-24 新增P0-4 修复:从富文本编辑器更新已有试卷的 DB 事务,单事务内更新 exams.title/structure + 删除旧 examQuestions + 插入新 examQuestions原 updateExamFromRichEditorAction 直接 db.transaction 的逻辑下沉至此)
- AI Pipeline`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` / `loadAiDraftQuestionsAndStructure`(✅ 2026-06-24 新增:从 actions.ts 下沉,加载 AI 草稿题目与结构,优先从 rawAiQuestions JSON 解析,否则从源文本生成)
- Utils`normalizeStructure`v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取;✅ 2026-06-24 升级:支持 section/group/question 三级层级递归校验section 节点含 title/level/childrengroup 节点含 title/instruction/children
- Preview Utils`components/exam-preview-utils.ts`,✅ 2026-06-24 补充记录):`buildPreviewNodes` / `parseEditableContent` / `flattenPreviewQuestions` / `findPreviewQuestionNode` / `updatePreviewQuestionNodeInList` / `buildPreviewSignature` / `buildPreviewPayload` / `buildPreviewRequestData`(以上 8 个为预览纯函数,供 Hook 与组件复用)+ ✅ 2026-06-24 新增 5 个(从 use-exam-preview-tasks Hook 提取以控制行数):`buildTaskFormValues`(提取表单字段子集供任务回填)/ `isPreviewBackgroundTask`(类型守卫,校验 localStorage 恢复数据)/ `persistPreviewTasksToStorage`(持久化任务到 localStorage/ `parseStoredPreviewTasks`(解析 localStorage 任务queued/running 标记为 failed 防僵尸)/ `executeBackgroundPreviewTask`(执行单个后台预览任务的纯函数)
- Hooks✅ 2026-06-24 拆分记录):`useExamPreview`主组合器68 行,组合三个子 Hook + handlePreview 编排,返回 API 与拆分前一致)/ `usePreviewState`(状态子 Hook67 行10 个 useState + 3 个更新函数)/ `usePreviewBackgroundTasks`(后台任务子 Hook80 行PQueue 队列 + localStorage 持久化 + 入队/打开任务)/ `usePreviewRewrite`AI 重写子 Hook79 行handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction
- Stats-serviceV3-8 新增):`getExamAnalytics`cache 包装,聚合考试所有作业的已批改提交,计算平均分/及格率/分数段分布/逐题错误率与难度等级,对标智学网考试分析)+ `ExamAnalyticsSummary` 类型
- Types✅ 2026-06-24 新增成绩录入相关类型):`ExamQuestionItem`(试卷中单个题目的精简结构 { id, order, score, type }/ `ExamForGradeEntry`(成绩录入用的试卷详情,含题目列表)/ `ExamOptionForEntry`(成绩录入页试卷选择器选项 { id, title, subjectName, gradeName, questionCount, totalScore }
- Editor 子模块(✅ 2026-06-24 新增 `editor/` 目录,基于 Tiptap 的试卷富文本编辑器):
- 组件:`ExamRichEditor`富文本编辑器主体forwardRef 暴露 `ExamRichEditorHandle` 以便父组件获取 JSONContent/ `SelectionToolbar`(浮动选择工具栏,对选中文本应用 DottedMark 等标记)
- 自定义 Tiptap 扩展(`editor/extensions/``DottedMark`(加点字标记,拼音注音题)/ `BlankNode`(填空空位节点,原子节点)/ `ImageNode`(图片节点,含 fileId/url/alt 属性)/ `QuestionBlock`(题目块节点,含 type/score 等 attrs/ `GroupBlock`(大题分组节点,含 title/ `SectionBlock`(✅ 2026-06-24 新增:分卷节点,含 title/level attrsisolating: true对应第卷/第Ⅱ卷层级)
- 类型(`editor/exam-rich-editor-types.ts``RichQuestionType``single_choice | multiple_choice | judgment | text | composite`/ `RichQuestionContent`(题干文本+选项+填空+图片+子题+正确答案)/ `EditorQuestion` / `EditorStructureNode`(✅ 2026-06-24 升级section|group|question 三级递归section 含 title/level/childrengroup 含 title/instruction/children/ `EditorDoc`{ title, questions, structure }/ `EditorJSONContent`JSONContent 别名)
- 转换工具:`editorDocToStructure``editor/editor-to-structure.ts`Tiptap JSONContent → EditorDoc供 `createExamFromRichEditorAction`/`updateExamFromRichEditorAction` 持久化使用)/ `structureToEditorDoc``editor/structure-to-editor.ts`EditorDoc → Tiptap JSONContent✅ 2026-06-24 升级支持 section 节点回填,供编辑器回填已保存试卷使用)/ `examNodesToEditorDoc`(✅ 2026-06-24 新增:`editor/exam-nodes-to-editor-doc.ts`BuildExam 的 `ExamNode[]` + `Question[]` → EditorDoc 桥梁函数,递归转换 section/group/question供 edit-rich 页面加载已有试卷到富文本编辑器)
- Barrel 导出:`editor/index.ts` 聚合导出上述所有组件、扩展、类型与转换函数
- ComponentsV3-8 新增):`ExamAnalyticsDashboard`(考试分析仪表盘:汇总卡片+分数段分布+逐题分析表)
- Components✅ 2026-06-24 新增):`ExamRichForm`(富文本编辑器试卷创建/编辑表单,使用 `ResizablePanel` 左编辑右预览布局,集成 `autoMarkExamAction` 一键 AI 标记;✅ 2026-06-24 升级支持 `mode="create"|"edit"` 双模式create 模式调用 `createExamFromRichEditorAction` 保存草稿edit 模式调用 `updateExamFromRichEditorAction` 更新已有试卷,编辑模式隐藏科目/年级/难度等字段并显示"Back to Build"按钮)
- Components✅ P3 新增记录):`ExamPreview`(试卷结构预览组件,从 exam-rich-form.tsx 提取,渲染 section/group/question 三级嵌套P3 修改:题目图片 `<img>` → `next/image` `<Image>`,新增 `next/image` 导入;`useTranslations('examHomework')` 国际化)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`questions`(✅ P0-1 已修复:通过 data-access.createQuestionWithRelations、`classes`(✅ P0-2 已修复:通过 data-access.getClassGradeIdsByClassIds、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions、`homework`V3-8 新增stats-service 通过 `homework/data-access.getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId` 获取作业与提交数据,合理跨模块调用)
- 被依赖:`homework`(通过 sourceExamId 外键,合理)、`dashboard`(通过 data-accessP0-4 已修复)、`proctoring`(✅ P1-1 已修复:通过 exams data-access、`diagnostic`(✅ P1-1 已修复:通过 exams data-access、`grades`(✅ 2026-06-24 新增:通过 data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用)
**已知问题**
- ✅ P0-1 已修复:~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`,通过 ID 映射保持 structure 引用一致
- ✅ P0-2 已修复:~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds`
- ✅ P1-1 已修复:~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions`
- ✅ P1-2 已修复:~~`actions.ts` 832 行(超 800 建议),多处直接 DB 操作~~ DB 操作已下沉到 data-accessactions.ts 现 691 行
- ✅ P0-4 已修复2026-06-24~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData
- ✅ actions.ts 拆分2026-06-24~~`actions.ts` 1525 行(超 1000 硬上限)~~ 拆分为 4 个文件actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action)actions.ts re-export 保持向后兼容
- ✅ P0-8 已修复:~~`data-access.ts` 1036 行(超 1000 硬上限)~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口),文末 re-export 保持向后兼容
- ⚠️ P1`ai-pipeline.ts` 857 行(超 800 建议),混合 4 类职责
- ✅ P2 已修复:`ai-pipeline.ts` 中 3 处非空断言清理(`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`
- ✅ V3-5/V3-8/V3-12`exam-actions.tsx` 增强V3-5 角色化菜单按角色显示不同操作项、V3-8 新增 analytics 菜单项BarChart3 图标,跳转 `/teacher/exams/[id]/analytics`、V3-12 移动端触摸目标尺寸优化
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 633 | 10 个核心 Server Action✅ 2026-06-24 拆分:从 1525 行降至 633 行re-export autoMarkExamAction/createExamFromRichEditorAction/updateExamFromRichEditorAction 保持向后兼容P0-4 已修复:无直接 DB 操作) |
| `actions-helpers.ts` | 96 | ✅ 2026-06-24 新增跨文件共享辅助函数getStringValue/getBoolValue/parseExamModeConfig/failState/successState/invalidFormState/prepareExamCreateContext |
| `actions-rich-editor.ts` | 250 | ✅ 2026-06-24 新增:富文本编辑器 Server ActioncreateExamFromRichEditorAction/updateExamFromRichEditorAction+ 共享辅助 convertStructureNode + RichExamCreateSchema/RichExamUpdateSchema。P0-4 修复updateExamFromRichEditorAction 调用 data-access.updateExamWithRichEditorData 不再直访 DB |
| `ai-pipeline/auto-mark.ts` | 356 | ✅ 2026-06-24 新增autoMarkExamAction Server Action + 纯转换辅助buildTiptapDocFromAiResponse/splitByDottedTexts/extractTitleFromAiResponse/AutoMarkSchema/AutoMarkResult。从 actions.ts 迁出 |
| `ai-pipeline.ts` | 857 | AI 出题管线(超限) |
| `data-access.ts` | 542 | 考试 CRUD✅ 2026-06-24 拆分:跨模块接口迁移至 data-access-cross-module.ts原 1036 行降至 542 行;含 P1-2 写/查询函数P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信;✅ P0-4 修复:新增 updateExamWithRichEditorData 将富文本编辑器更新事务下沉;导出 parseExamMeta/getString/getNumber/toExamStatus 供 cross-module 复用;文末 re-export 跨模块接口保持向后兼容) |
| `data-access-cross-module.ts` | 462 | ✅ 2026-06-24 新增:跨模块查询接口(供 homework/grades/proctoring/diagnostic/dashboard/lesson-preparation 等模块调用,避免直查 exams/examSubmissions 表)。包含 getExamsDashboardStats/getExamIdsByGradeIds/getExamWithQuestionsForHomework/getExamSubjectIdMap/getExamTitleById/getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionWithAnswers/getExamSubmissionsForExam/addExamQuestions/getExamsByGradeId/getExamsForGradeEntry/getExamForGradeEntry标记 `server-only`,从 data-access.ts 导入 parseExamMeta/getString/getNumber/toExamStatus 复用 |
| `stats-service.ts` | - | V3-8 新增:考试分析数据聚合(`getExamAnalytics` + `ExamAnalyticsSummary` 类型) |
| `types.ts` | 50+ | 类型定义2026-06-24 新增ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用。注:`AutoMarkResult`/`AutoMarkSchema` 已迁移至 `ai-pipeline/auto-mark.ts``RichExamCreateSchema`/`RichExamUpdateSchema` 已迁移至 `actions-rich-editor.ts` |
| `editor/index.ts` | 23 | 2026-06-24 新增:富文本编辑器 barrel 导出(聚合组件/扩展/类型/转换函数) |
| `editor/exam-rich-editor.tsx` | - | 2026-06-24 新增ExamRichEditor 富文本编辑器主体forwardRef + ExamRichEditorHandle |
| `editor/selection-toolbar.tsx` | - | 2026-06-24 新增SelectionToolbar 浮动选择工具栏 |
| `editor/exam-rich-editor-types.ts` | 42 | 2026-06-24 新增富文本编辑器类型定义RichQuestionType/RichQuestionContent/EditorQuestion/EditorStructureNode/EditorDoc/EditorJSONContent |
| `editor/editor-to-structure.ts` | - | 2026-06-24 新增editorDocToStructureTiptap JSONContent → EditorDoc |
| `editor/structure-to-editor.ts` | - | 2026-06-24 新增structureToEditorDocEditorDoc → Tiptap JSONContent |
| `editor/extensions/index.ts` | 5 | 2026-06-24 新增Tiptap 扩展 barrel 导出 |
| `editor/extensions/dotted-mark.ts` | - | 2026-06-24 新增DottedMark 加点字标记扩展 |
| `editor/extensions/blank-node.tsx` | - | 2026-06-24 新增BlankNode 填空空位节点扩展 |
| `editor/extensions/image-node.ts` | - | 2026-06-24 新增ImageNode 图片节点扩展 |
| `editor/extensions/group-block.tsx` | - | 2026-06-24 新增GroupBlock 大题分组节点扩展 |
| `editor/extensions/question-block.tsx` | - | 2026-06-24 新增QuestionBlock 题目块节点扩展 |
| `hooks/use-exam-preview.ts` | 68 | 预览 Hook 主组合器(✅ 2026-06-24 拆分:从 318 行降至 68 行,组合 state/tasks/rewrite 三个子 Hook保留 handlePreview 编排逻辑,返回 API 与拆分前完全一致,调用方 exam-form.tsx 无需修改) |
| `hooks/use-exam-preview-state.ts` | 67 | ✅ 2026-06-24 新增:预览状态子 Hook10 个 useState + updatePreviewQuestionNode/updateSelectedQuestionFromAi/applyPreviewResult 三个更新函数) |
| `hooks/use-exam-preview-tasks.ts` | 80 | ✅ 2026-06-24 新增:预览后台任务子 HookPQueue 并发队列 + localStorage 持久化恢复 + handleBackgroundPreview/handleOpenPreviewTask |
| `hooks/use-exam-preview-rewrite.ts` | 79 | ✅ 2026-06-24 新增:预览 AI 重写子 HookhandleRewriteSelectedQuestion 调用 regenerateAiQuestionAction |
| `components/exam-preview-utils.ts` | 293 | 预览纯函数工具集buildPreviewNodes/parseEditableContent/flattenPreviewQuestions/findPreviewQuestionNode/updatePreviewQuestionNodeInList/buildPreviewSignature/buildPreviewPayload/buildPreviewRequestData + ✅ 2026-06-24 新增 5 个buildTaskFormValues/isPreviewBackgroundTask/persistPreviewTasksToStorage/parseStoredPreviewTasks/executeBackgroundPreviewTask从 use-exam-preview-tasks Hook 提取以控制行数) |
| `components/exam-preview.tsx` | - | 试卷结构预览组件use client从 exam-rich-form.tsx 提取,渲染试卷富文本编辑器的结构预览,支持分卷(section)/大题分组(group)/题目(question)三级嵌套递归渲染。✅ P3 修改:题目块中图片将原生 `<img>` 替换为 `next/image` 的 `<Image>` 组件(自动优化、懒加载、防布局偏移),新增导入 `next/image`;使用 `useTranslations('examHomework')` 国际化 section/group 标题与统计文案 |
| `utils/normalize-structure.ts` | 57 | v3 新增exam.structure 运行时校验与归一化(从 build/page.tsx 提取) |
| `components/exam-analytics-dashboard.tsx` | - | V3-8 新增:考试分析仪表盘组件 |
| `components/exam-actions.tsx` | - | V3-5/V3-8/V3-12 增强:角色化菜单+analytics 链接+移动端触摸优化 |
| `components/exam-rich-form.tsx` | - | 2026-06-24 新增:富文本编辑器试卷创建/编辑表单ResizablePanel 左编辑右预览 + AI 自动标记 + 保存草稿)。✅ 2026-06-24 拆分后import 路径直连 `ai-pipeline/auto-mark.ts`autoMarkExamAction和 `actions-rich-editor.ts`createExamFromRichEditorAction/updateExamFromRichEditorAction支持 create/edit 双模式 |
| `components/exam-boundaries.tsx` | 189 | ✅ 2026-06-25 新增P2-3模块统一边界组件。导出 `ExamErrorBoundary`(薄包装 SectionErrorBoundary固定 examHomework 命名空间)、`ExamEmptyState`i18n 键驱动的空状态)、`ExamSkeleton`list/card/detail/analytics/editor 五种变体)。复用 shared 层 EmptyState/Skeleton/SectionErrorBoundary避免每页重复实现。新增 i18n 键 `exam.error.boundaryTitle/boundaryDescription/retry` |
| `components/*` | 19+ 文件 | 考试表单/组卷/预览/分析组件 |
| `services/exam-service-port.ts` | 95 | ✅ 2026-06-25 新增P2-4模块对内的服务端口契约。导出 `ExamServicePort` 接口listExams/getExam/createExam/updateExam/deleteExam/duplicateExam/autoMarkExam/getExamPreview/getAnalytics/getSubjects/getGrades 共 12 个方法)+ `ExamDetail`/`ExamCreateInput`/`ExamUpdateInput` 类型。区别于 shared 层的 ExamHomeworkServicePort跨模块调用方契约单例注册本接口面向模块内部组件通过 React Context 注入 |
| `services/exam-service-context.tsx` | 72 | ✅ 2026-06-25 新增P2-4ExamServicePort 的 React Context 注入点。导出 `ExamServiceProvider`Provider 组件)、`useExamService`(消费者 Hook未注入时抛错、`useTryExamService`(测试用,未注入时返回 null。当前为骨架具体实现将在 P2-6+ 落地 |
| `services/index.ts` | 23 | ✅ 2026-06-25 新增services 子模块 barrel 导出 |
| `config/exam-widgets.ts` | 192 | ✅ 2026-06-25 新增P2-5配置驱动的 Widget 布局。导出 `ExamWidgetConfig`/`ExamWidgetLayoutConfig` 类型、四个角色的默认配置TEACHER_EXAM_WIDGET_CONFIG/ADMIN_EXAM_WIDGET_CONFIG/STUDENT_EXAM_WIDGET_CONFIG/PARENT_EXAM_WIDGET_CONFIG、`getExamWidgetConfig(role)` 与 `getWidgetsBySlot(config, slot)` 工具函数。与 dashboard/config/widget-configs.ts 模式对齐 |
**已知问题**
- ✅ P0-1 已修复:~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`,通过 ID 映射保持 structure 引用一致
- ✅ P0-2 已修复:~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds`
- ✅ P1-1 已修复:~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions`
- ✅ P1-2 已修复:~~`actions.ts` 832 行(超 800 建议),多处直接 DB 操作~~ DB 操作已下沉到 data-accessactions.ts 现 691 行
- ✅ P0-4 已修复2026-06-24~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData
- ✅ actions.ts 拆分2026-06-24~~`actions.ts` 1525 行(超 1000 硬上限)~~ 拆分为 4 个文件actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action)actions.ts re-export 保持向后兼容
- ✅ P0-8 已修复:~~`data-access.ts` 1036 行(超 1000 硬上限)~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口),文末 re-export 保持向后兼容
- ⚠️ P1`ai-pipeline.ts` 857 行(超 800 建议),混合 4 类职责
- ✅ P2 已修复:`ai-pipeline.ts` 中 3 处非空断言清理(`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`
- ✅ V3-5/V3-8/V3-12`exam-actions.tsx` 增强V3-5 角色化菜单按角色显示不同操作项、V3-8 新增 analytics 菜单项BarChart3 图标,跳转 `/teacher/exams/[id]/analytics`、V3-12 移动端触摸目标尺寸优化
- ✅ 2026-06-25 P2 落地exams 审计报告实施):
- ✅ P2-1在 `editor/exam-rich-editor-types.ts` 新增 `isRichQuestionType`/`isStandaloneQuestionType`/`toRichQuestionType`/`toStandaloneQuestionType` 类型守卫;`editor/editor-to-structure.ts` 与 `editor/exam-nodes-to-editor-doc.ts` 改用 `toRichQuestionType``actions-rich-editor.ts` 两处 `as "single_choice"|"multiple_choice"|"text"|"judgment"` 断言改为 `toStandaloneQuestionType` 安全收窄(消除 4 处 `as` 违规)
- ✅ P2-3新增 `components/exam-boundaries.tsx`ExamErrorBoundary/ExamEmptyState/ExamSkeleton 三组合单元5 种骨架变体)
- ✅ P2-4新增 `services/exam-service-port.ts` + `services/exam-service-context.tsx`ExamServicePort 接口 + Context 注入骨架,与 shared 层 ExamHomeworkServicePort 区分定位)
- ✅ P2-5新增 `config/exam-widgets.ts`(四角色 Widget 配置 + getExamWidgetConfig/getWidgetsBySlot
---
## 2.3 homework作业模块
**职责**:作业全生命周期(创建/发布/作答/批改/分析)。
**导出函数**
- Actions`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` / `batchAutoGradeSubmissionsAction`V3-7 新增批量自动批改HOMEWORK_GRADE 权限+非管理员仅可批改自己创建的作业)/ `getScansAction`(✅ 2026-06-24 新增:获取某次提交的所有答题扫描图,扫描图存储在 fileAttachments 表中 targetType="homework"/targetId=submissionId支持两类访问者——教师 HOMEWORK_GRADE 仅可访问自己创建的作业的提交,学生 HOMEWORK_SUBMIT 仅可访问自己的提交)/ `deleteScanAction`(✅ 2026-06-24 新增:删除答题扫描图,仅允许提交者本人删除且仅在提交状态为 started 时允许,调用 `files/data-access.deleteFileAttachment` 删除 DB 记录)/ `getExcellentSubmissionsAction`(✅ P3-1 新增查询优秀作业列表HOMEWORK_GRADE 权限,按 ctx.dataScope 过滤,调用前埋点 `homework.excellent_viewed`/ `remindUnsubmittedAction`(✅ P3-2 新增催交提醒HOMEWORK_GRADE 权限,查询未提交学生后通过动态 import 调用 `notifications/data-access.createNotification` 创建 type=homework/priority=high 站内通知Promise.allSettled 容错统计,埋点 `homework.remind_unsubmitted`)(✅ P1-2 已修复actions 层不再直接访问 DB全部下沉到 data-access/data-access-write
- Data-access`getHomeworkAssignments` / `getHomeworkAssignmentById` / `getHomeworkSubmissions` / `getStudentHomeworkAssignments` / `getStudentHomeworkTakeData` / `getHomeworkAssignmentReviewList` / `getHomeworkSubmissionDetails` / `getDemoStudentUser`(已迁移至 users 模块 `getCurrentStudentUser`,此处为 re-export 向后兼容)/ `toQuestionContent` / `getAssignmentMaxScoreById`(后两者供 stats-service 使用;`isRecord` 已于 2026-06-24 迁移至 `@/shared/lib/type-guards` 统一复用)/ `getHomeworkAssignmentsByExamId`V3-8 新增:按考试 ID 查作业+目标/提交/批改计数)/ `getGradedSubmissionsByExamId`V3-8 新增:按考试 ID 查已批改提交,按学生去重)/ `getStudentSubmissionResult`V3-9 新增:查学生指定作业最新提交,用于结果页)/ `getStudentExamResults`V3-11 新增:查学生考试结果列表,供家长端展示)/ `getExcellentSubmissions`(✅ P3-1 新增:查优秀作业列表,按得分率过滤 minPercentage 默认 80同一学生取最高分按得分率降序支持 scope 过滤)/ `getUnsubmittedStudents`(✅ P3-2 新增:查未提交作业的学生列表,对比 targets 与已提交学生集合返回差集,供催交提醒使用)
- Data-access-classes`getAssignmentIdsForStudents` / `getHomeworkAssignmentsWithSubject` / `getHomeworkAssignmentsByIds` / `getAssignmentTargetCounts` / `getHomeworkSubmissionsForStudents` / `getPublishedHomeworkAssignmentsWithSubject` / `getHomeworkSubmissionsForAssignments`P0-7 新增,供 classes 模块跨模块调用;✅ P1-1 已修复:内部不再直查 exams/subjects 表,改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`
- Data-access-write11 个写操作函数P1-2 新增 10 个从 actions 下沉 + V3-7 新增 `batchAutoGradeSubmissions`
- Stats-service`getTeacherGradeTrends` / `getHomeworkAssignmentAnalytics` / `getStudentDashboardGrades`(从 data-access.ts re-export 以保持向后兼容)
- ComponentsV3-7/V3-9 新增):`HomeworkBatchGradingView`(批量批改视图:勾选+一键批改+toast 反馈)/ `HomeworkSubmissionResult`(提交后即时反馈:分数汇总+对错分布+错题预览)
- Components✅ 2026-06-24 新增扫描图相关):`ScanUploader`(学生扫描图上传组件,调用 /api/upload 上传图片targetType="homework",支持增删/排序,导出 `ScanImage` 类型)/ `ScanImageViewer`(扫描图查看器,支持翻页/缩放/旋转/全屏,用于阅卷式批改时查看学生答题图片)/ `HomeworkScanGradingView`(教师阅卷式批改视图,使用 `ResizablePanel` 左侧题目+批改表单、右侧扫描图查看器,集成 `getScansAction` 拉取扫描图、`gradeHomeworkSubmissionAction` 批改,支持上一份/下一份提交快速切换)
- Components✅ P3-1 新增):`ExcellentSubmissions`(优秀作业展示 async 服务端组件,按得分率降序,前三名金/银/铜徽章,迟交 Clock 标注,仅展示姓名不展示学号保护隐私,直接调用 `getExcellentSubmissions` 避免多余 action 往返)/ `ExcellentSubmissionsWithBoundary`(带 `SectionErrorBoundary` + `Suspense` 包裹版本,提供 Skeleton 骨架屏与局部错误降级,避免本区块故障影响整个作业详情页)
- Types✅ 2026-06-24 新增):`ScanAttachment`(扫描图附件结构 { fileId, url, filename, originalName, page },由 `getScansAction` 返回)/ `ScanImage`(扫描图 UI 结构,由 `ScanUploader` 导出供 `ScanImageViewer` 复用)
- Types✅ P3-1 新增):`ExcellentSubmissionItem`(优秀作业展示项 { submissionId, assignmentId, assignmentTitle, studentName, totalScore, maxScore, percentage, submittedAt, isLate }percentage 为得分率 0-100isLate 标注迟交但质量优秀样例)/ `ExcellentSubmissionQuery`(优秀作业查询参数 { assignmentId, minPercentage?, limit? }minPercentage 默认 80limit 默认 10
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamForProctoringCrossModule、`classes`(✅ P1-1 已修复:通过 classes data-access.getStudentIdsByClassId 等 7 个函数)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getSubjectNameMapByIds、`users`(✅ P1-1 已修复:通过 users data-access.getUserWithRole/getUserNamesByIds、`files`(✅ 2026-06-24 新增:`deleteScanAction` 调用 `files/data-access.deleteFileAttachment` 删除扫描图 DB 记录,合理跨模块调用)、`notifications`(✅ P3-2 新增:`remindUnsubmittedAction` 通过动态 import 调用 `notifications/data-access.createNotification` 为未提交学生创建催交通知,合理跨模块调用)
- 被依赖:`dashboard`(通过 data-access合理、`parent`(通过 data-access合理V3-11 新增 `getStudentExamResults` 供 parent 调用)、`classes`(✅ P0-7 已修复classes 通过 `homework/data-access-classes` 获取作业数据,不再反向直查 homework/exams 表)、`exams`V3-8 新增:`exams/stats-service.getExamAnalytics` 调用 `getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId`,合理跨模块调用)
**已知问题**
- ✅ P0 已解决:`data-access.ts` 已拆分至 598 行(原 1038 行超 1000 硬上限),统计函数迁移至 `stats-service.ts`
- ✅ P0 已解决:`getStudentDashboardGrades` 排名计算逻辑迁移至 `stats-service.ts`
- ✅ P0 已解决:`getHomeworkAssignmentAnalytics` 错误率统计逻辑迁移至 `stats-service.ts`
- ✅ P0-7 已修复:新增 `data-access-classes.ts`,将 classes 模块对 homework/exams 表的直查封装为 homework 模块的导出函数,恢复三层架构
- ✅ P1-1 已修复:~~5 处直查 `exams` 表~~ 改为调用 `exams/data-access.getExamIdsByGradeIds` / `getExamSubjectIdMap` / `getExamWithQuestionsForHomework``data-access-classes.ts` 内部 JOIN exams+subjects 也改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`,彻底消除跨模块直查
- ✅ P0-竞品已修复:新增 `hooks/use-exam-countdown.ts` 倒计时 Hook对标智学网/猿题库),`homework-take-view.tsx` 集成限时/监考模式倒计时显示与到时自动提交;`data-access.ts` 的 `getStudentHomeworkTakeData` 新增 `examModeConfig` + `startedAt` 字段供客户端计算截止时间
- ✅ P1-2 已修复:~~`actions.ts` 多处直接 DB 操作(`createHomeworkAssignmentAction` 157 行)~~ DB 操作已下沉到 `data-access-write.ts`actions.ts 现 239 行
- ✅ V3-7新增 `batchAutoGradeSubmissionsAction` + `batchAutoGradeSubmissions` + `HomeworkBatchGradingView`,提交列表页接入批量批改
- ✅ V3-8新增 `getHomeworkAssignmentsByExamId` + `getGradedSubmissionsByExamId`,供 exams 模块跨模块调用
- ✅ V3-9新增 `getStudentSubmissionResult` + `HomeworkSubmissionResult` + 路由 `/student/learning/assignments/[assignmentId]/result``homework-take-view.tsx` 提交后跳转结果页
- ✅ V3-12`homework-take-view.tsx` 移动端触摸目标尺寸优化
- ✅ 2026-06-23 审计修复data-access 层 4 个查询函数新增 `scope?: DataScope` 参数(`getHomeworkAssignments`/`getHomeworkAssignmentById`/`getHomeworkSubmissions`/`getHomeworkAssignmentReviewList`),教师仅查看自己创建的作业,学生/家长仅查看相关作业Server Action catch 块改用 `handleActionError` 统一错误处理
- ✅ 2026-06-24 i18n 完成11 个文件6 个客户端组件 + 3 个服务端页面 + 2 个已有 `t` 的页面)全部接入 `next-intl`,硬编码中文/英文文案替换为 `examHomework.homework.*` 翻译键zh-CN + en 双语);客户端组件使用 `useTranslations("examHomework")`,服务端页面使用 `getTranslations("examHomework")`
- ✅ 2026-06-25 审计修复 P013 个缺失的 `loading.tsx`/`error.tsx` 路由文件全部补全teacher/homework 根 + assignments + submissions + submissions/[submissionId] + scan-gradingstudent/learning/assignments + result`homework-take-view.tsx` 拆分为 5 个组件(`HomeworkTakeView` 主容器 396 行 + `HomeworkTakeSidebar` 侧边栏 + `HomeworkTakeConfirmDialog` 提交确认对话框);`schema.ts` 错误消息改为 i18n 键 `homework.form.error.*`Server Action 通过 `translateZodErrors` 翻译后返回 `fieldErrors``ScanAttachment` 类型从 `actions.ts` 迁移至 `types.ts`
- ✅ 2026-06-25 审计修复 P1`homework-grading-view.tsx` 删除本地 wrapper 函数(`isAutoGradable`/`applyAutoGrades`/`getCorrectnessState`/`extractQuestionText`),直接调用 `lib/question-content-utils``data-access.ts` 末尾 re-export`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId`/`getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData`)已删除,调用方改为直接 import 子模块;`teacher/homework/assignments/[id]/page.tsx` Analytics 区块接入 `SectionErrorBoundary + Suspense + Skeleton``getScansAction` 中的 DB 查询下沉到新建的 `data-access-scans.ts::getScansBySubmissionId`
- ✅ 2026-06-25 审计修复 P2`startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `deleteScanAction` 补全 `trackExamEvent` 埋点(`homework.started` / `homework.answer_saved` / `homework.scan_deleted``extractQuestionText` 从 `homework-grading-view.tsx` 迁移到 `lib/question-content-utils.ts`
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 716 | 作业 CRUD + 批改(✅ 2026-06-25删除末尾 re-export调用方改为直接 import 子模块;✅ P3-1 新增 `getExcellentSubmissions`:按得分率过滤优秀作业,同一学生取最高分;✅ P3-2 新增 `getUnsubmittedStudents`:查询未提交作业学生供催交提醒使用) |
| `data-access-write.ts` | 455 | 作业写操作P1-2 新增 10 个写函数从 actions 下沉V3-7 新增 `batchAutoGradeSubmissions` |
| `data-access-classes.ts` | 285 | 跨模块查询封装P0-7 新增;✅ P1-1 已修复:内部通过 exams/school data-access 获取考试科目信息,不再直查 exams/subjects 表) |
| `data-access-student.ts` | 293 | 学生端只读查询(✅ 2026-06-25从 data-access.ts 拆出 `getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData` |
| `data-access-exam-cross.ts` | 119 | 考试跨模块查询(`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId` |
| `data-access-scans.ts` | 44 | ✅ 2026-06-25 审计新增:扫描图查询(`getScansBySubmissionId`,从 actions.ts 下沉的 DB 查询) |
| `data-access-error-collection.ts` | 121 | 错题采集查询 |
| `data-access-utils.ts` | 8 | 内部工具函数 |
| `stats-service.ts` | 414 | 统计分析(教师趋势/作业分析/学生仪表盘成绩) |
| `actions.ts` | 688 | 10 个 Server ActionP1-2 已修复,无直接 DB 操作;✅ 2026-06-25`ScanAttachment` 类型迁移至 types.tsschema.ts 错误消息改为 i18n 键,新增 `translateZodErrors` 翻译辅助函数;补全 `trackExamEvent` 埋点;✅ P3-1 新增 `getExcellentSubmissionsAction`查询优秀作业列表HOMEWORK_GRADE 权限 + scope 过滤 + 埋点 `homework.excellent_viewed`;✅ P3-2 新增 `remindUnsubmittedAction`:催交提醒,调用 `notifications/data-access.createNotification` 创建站内通知,埋点 `homework.remind_unsubmitted` |
| `types.ts` | 282 | 类型定义(✅ 2026-06-25新增 `ScanAttachment` 接口,从 actions.ts 迁入;✅ P3-1 新增 `ExcellentSubmissionItem`/`ExcellentSubmissionQuery` 类型) |
| `schema.ts` | 63 | Zod 校验(✅ 2026-06-25错误消息改为 i18n 键 `homework.form.error.*` |
| `lib/question-content-utils.ts` | 321 | 题目内容工具函数(✅ 2026-06-25`extractQuestionText` 从 grading-view 迁入) |
| `lib/question-content-utils.test.ts` | 415 | 工具函数单测 |
| `hooks/use-exam-countdown.ts` | 106 | P0-竞品新增:考试倒计时 Hook每秒更新、紧急状态高亮、到时回调自动提交 |
| `hooks/use-debounced-auto-save.ts` | 182 | 答案防抖自动保存 HooklocalStorage 离线缓存) |
| `components/homework-take-view.tsx` | 396 | ✅ 2026-06-25 重构:从 520 行拆分为 3 个组件(主容器 + Sidebar + ConfirmDialogV3-9/V3-12提交后跳转结果页+移动端触摸优化P0-竞品:集成限时/监考模式倒计时useExamCountdown+ 到时自动提交 |
| `components/homework-take-sidebar.tsx` | 169 | ✅ 2026-06-25 新增:从 homework-take-view 拆出(自动保存状态/作业信息/截止时间/尝试次数/描述/进度网格/提交按钮) |
| `components/homework-take-confirm-dialog.tsx` | 62 | ✅ 2026-06-25 新增:从 homework-take-view 拆出(提交确认对话框,含未作答数量警告) |
| `components/homework-grading-view.tsx` | 472 | ✅ 2026-06-25删除 4 个本地 wrapper 函数,直接调用 lib/question-content-utils |
| `components/homework-batch-grading-view.tsx` | 165 | V3-7 新增批量批改视图use client |
| `components/homework-submission-result.tsx` | 224 | V3-9 新增:提交后即时反馈页 |
| `components/homework-scan-grading-view.tsx` | 262 | 2026-06-24 新增教师阅卷式批改视图ResizablePanel 左题目右扫描图 + 上一份/下一份切换) |
| `components/scan-uploader.tsx` | 254 | 2026-06-24 新增:学生扫描图上传组件(调用 /api/uploadtargetType="homework",导出 `ScanImage` 类型) |
| `components/scan-image-viewer.tsx` | 194 | 2026-06-24 新增:扫描图查看器(翻页/缩放/旋转/全屏) |
| `components/question-renderer.tsx` | 339 | 题目渲染器(支持单选/多选/判断/文本题型) |
| `components/excellent-submissions.tsx` | 206 | P3-1优秀作业展示服务端组件 + SectionErrorBoundary + Suspense + 骨架屏;✅ P3-5 修复:`getTranslations` 从 `next-intl/server` 导入) |
| `components/student-homework-review-view.tsx` | 206 | 学生作业回顾视图 |
| `components/homework-assignment-form.tsx` | 219 | 作业创建/编辑表单 |
| `components/assignment-filters.tsx` | 46 | 作业列表筛选器use client基于 nuqs 的 URL 状态管理(✅ 2026-06-24 i18n接入 `useTranslations` |
| `components/homework-assignment-exam-content-card.tsx` | 29 | 作业考试内容卡片(✅ 2026-06-24 i18n接入 `useTranslations` |
| `components/homework-assignment-exam-preview-pane.tsx` | 39 | 作业考试预览面板(✅ 2026-06-24 i18n接入 `useTranslations` |
| `components/homework-assignment-exam-error-explorer.tsx` | 39 | 作业错误分析主组件 |
| `components/homework-assignment-exam-error-explorer-lazy.tsx` | 76 | 作业错误分析懒加载组件(✅ 2026-06-24 i18nFallback 接入 `useTranslations` |
| `components/homework-assignment-question-error-detail-panel.tsx` | 132 | 题目错误详情面板(✅ 2026-06-24 i18n接入 `useTranslations``formatAnswer` 移入组件内闭包以访问 `t` |
| `components/homework-assignment-question-error-overview-card.tsx` | 89 | 题目错误概览卡片(✅ 2026-06-24 i18n接入 `useTranslations` |
---
## 2.4 questions题库模块
**职责**:题库管理(题目 CRUD、知识点关联、题型支持、教材→章节→知识点级联筛选
**导出函数**
- Actions11 个,全部调用 `requirePermission()``getQuestionsAction` / `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction` / `getTextbookOptionsAction`(✅ 审计新增:级联第一级)/ `getChapterOptionsAction(textbookId)`(✅ 审计新增:级联第二级)/ `getKnowledgePointOptionsByChapterAction(chapterId)`(✅ 审计新增:级联第三级)/ `deleteQuestionsBatchAction`(✅ 审计新增:批量删除)/ `exportQuestionsAction`(✅ 审计新增:导出)/ `importQuestionsAction`(✅ 审计新增:导入)
- Data-access`getQuestions`(✅ 审计升级:支持 `textbookId`/`chapterId` 级联筛选参数,通过 textbooks data-access 获取知识点 ID 集合)/ `createQuestionWithRelations` / `updateQuestionById` / `deleteQuestionByIdRecursive` / `getKnowledgePointOptions` / `getTextbookOptions`(✅ 审计新增)/ `getChapterOptions(textbookId)`(✅ 审计新增)/ `getKnowledgePointOptionsByChapter(chapterId)`(✅ 审计新增)/ `deleteQuestionsBatch`(✅ 审计新增:批量删除,事务原子性)/ `exportQuestions`(✅ 审计新增:导出,权限范围过滤)/ `importQuestions`(✅ 审计新增:批量导入,事务原子性)
- Types`Question` / `QuestionWithRelations` / `GetQuestionsParams`(✅ 审计升级:新增 `textbookId`/`chapterId` 字段)/ `CreateQuestionInput` / `KnowledgePointOption` / `TextbookOption`(✅ 审计新增)/ `ChapterOption`(✅ 审计新增)/ `QuestionContent`(✅ 审计新增:类型守卫接口)/ `QuestionOption`(✅ 审计新增)/ `QUESTION_TYPE_I18N_KEY`(✅ 审计新增:题型 i18n 键映射)/ `DIFFICULTY_I18N_KEY`(✅ 审计新增:难度 i18n 键映射)
- Utils`parseQuestionContent(raw)`(✅ 审计新增:类型守卫,替代 `as` 断言)/ `getQuestionPreview` / `getQuestionOptions` / `getQuestionText` / `trackQuestionEvent`(✅ 审计新增:埋点接口 no-op/ `trackQuestionCreated` / `trackQuestionUpdated` / `trackQuestionDeleted` / `trackQuestionSearched`
- Components`QuestionFilters`(✅ 审计重写:组合 QuestionCascadeFilter/ `QuestionCascadeFilter`(✅ 审计新增三级级联筛选nuqs 管理 URL 状态)/ `QuestionDataTable`(✅ 审计重写i18n + 集成 BatchOperations✅ Phase 4.3:引入 @tanstack/react-virtual 虚拟滚动替换分页)/ `useQuestionColumns`(✅ 审计改为 Hook因 useTranslations 是 Hook/ `CreateQuestionButton`(✅ 审计重写:权限感知)/ `QuestionActions`(✅ 审计重写:权限感知 + 类型守卫 + QuestionContentRenderer 结构化预览)/ `CreateQuestionDialog`(✅ 审计重写:拆分为 281 行)/ `KnowledgePointSelector`(✅ 审计新增:从 dialog 拆分)/ `OptionsEditor`(✅ 审计新增:从 dialog 拆分)/ `QuestionBankResultsClient`(✅ 审计新增RSC 与客户端 Hook 桥接)/ `QuestionContentRenderer`(✅ 审计新增:题目结构化渲染)/ `BatchOperations`(✅ 审计新增:批量删除工具栏)/ `ImportExportButtons`(✅ 审计新增JSON 导入导出)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`textbooks`(✅ 审计升级:通过 textbooks data-access 的 `getTextbooks` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` 实现级联筛选)
- 被依赖:`exams`(通过类型导入,合理)、`textbooks`UI 组合,合理)、`error-book`(通过 data-access合理、`adaptive-practice`(通过 data-access合理
**已知问题**
- ✅ P1-2 已修复:~~写操作函数错放在 `actions.ts`~~ 已下沉到 data-access
- ✅ P1-1 已修复:~~`getKnowledgePointOptionsAction` 直查 textbooks 模块表~~ 改为调用 textbooks data-access
- ✅ P2 已解决:~~`data-access.ts` 仅 129 行~~ 扩充至 260+ 行
- ✅ 审计 P0 已修复:~~按名字搜索等于无用~~ 新增教材→章节→知识点三级级联筛选QuestionCascadeFilter + URL 状态同步)
- ✅ 审计 P0 已修复:~~前端权限硬编码~~ 全部改用 `usePermission().hasPermission()`
- ✅ 审计 P0 已修复:~~`as` 断言违反 TypeScript 严格模式~~ 提取 `parseQuestionContent` 类型守卫函数
- ✅ 审计 P0 已修复:~~i18n 完全缺失~~ 新增 `questions.json` 翻译文件zh-CN + en所有展示文本使用 i18n 键
- ✅ 审计 P0 已修复:~~缺 Error Boundary~~ 新增 `app/(dashboard)/teacher/questions/error.tsx`
- ✅ 审计 P1 已修复:~~create-question-dialog.tsx 453 行超规范~~ 拆分为 KnowledgePointSelector + OptionsEditor + 主对话框281 行)
- ✅ 审计 P1 已修复:~~列定义为纯函数无法使用 useTranslations~~ 改为 `useQuestionColumns` Hook通过 `QuestionBankResultsClient` 客户端桥接
- ✅ 审计 P1 已修复:~~缺监控埋点~~ 预留 `trackQuestionEvent` no-op 接口
- ✅ 审计 P2 已修复:~~Server Action 缺权限校验~~ 全部 8 个 Action 均调用 `requirePermission()`
- ✅ 审计 P2 已实施:~~仅支持教师角色~~ 新增 admin/questions 页面,复用全部组件,权限感知自动适配
- ✅ 审计 P2 已实施:~~无题目结构化预览~~ 新增 QuestionContentRenderer 组件(题干/选项/答案/解析,单选圆点/多选方框,正确答案高亮)
- ✅ 审计 P2 已实施:~~无批量操作~~ 新增 deleteQuestionsBatch data-access + deleteQuestionsBatchAction + BatchOperations 组件(事务原子性,权限感知)
- ✅ 审计 P2 已实施:~~无导入导出~~ 新增 exportQuestions/importQuestions data-access + exportQuestionsAction/importQuestionsAction + ImportExportButtons 组件JSON 格式,权限范围过滤,导入预览确认)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 380+ | 11 个 Server Action全部 requirePermission含 3 个级联筛选 + 批量删除 + 导入导出) |
| `data-access.ts` | 600+ | 题目 CRUD + 知识点选项 + 级联筛选 + 批量删除 + 导入导出 |
| `schema.ts` | 18 | Zod 校验 |
| `types.ts` | 60+ | 类型定义(含 TextbookOption/ChapterOption/QuestionContent/i18n 键映射) |
| `utils/parse-content.ts` | 60+ | 类型守卫parseQuestionContent + 便捷方法) |
| `utils/track-event.ts` | 30+ | 埋点接口no-op 实现) |
| `components/question-cascade-filter.tsx` | 130+ | 三级级联筛选nuqs URL 状态) |
| `components/question-filters.tsx` | 40+ | 筛选栏(组合 QuestionCascadeFilter |
| `components/question-data-table.tsx` | 180+ | 数据表格i18n + 集成 BatchOperations + ✅ Phase 4.3@tanstack/react-virtual 虚拟滚动,替换分页避免大数据渲染卡顿) |
| `components/question-columns.tsx` | 100+ | 列定义 HookuseQuestionColumns |
| `components/create-question-button.tsx` | 30+ | 创建按钮(权限感知) |
| `components/question-actions.tsx` | 200+ | 行操作(权限感知 + 类型守卫 + QuestionContentRenderer |
| `components/create-question-dialog.tsx` | 281 | 创建对话框(组合 KnowledgePointSelector + OptionsEditor |
| `components/knowledge-point-selector.tsx` | 80+ | 知识点选择器(从 dialog 拆分) |
| `components/options-editor.tsx` | 120+ | 选项编辑器(从 dialog 拆分) |
| `components/question-bank-results-client.tsx` | 30+ | RSC 与客户端 Hook 桥接 |
| `components/question-content-renderer.tsx` | 110+ | 题目结构化渲染(✅ 审计新增) |
| `components/batch-operations.tsx` | 110+ | 批量操作工具栏(✅ 审计新增) |
| `components/import-export-buttons.tsx` | 180+ | 导入导出组件(✅ 审计新增) |
---
## 2.5 textbooks教材模块— 标杆模块data-access 层)
**职责**:教材与知识体系管理(教材/章节树形结构、知识点 CRUD、Markdown 内容编辑、知识图谱)。
**导出函数**
- Actions14 个,均为写操作;读操作由 RSC 页面直接调用 data-access`createTextbookAction` / `updateTextbookAction` / `deleteTextbookAction` / `createChapterAction` / `updateChapterContentAction` / `deleteChapterAction` / `reorderChaptersAction` / `createKnowledgePointAction` / `updateKnowledgePointAction` / `deleteKnowledgePointAction` / `getKnowledgePointsByChapterAction` / `getKnowledgeGraphDataAction`(✅ Task 7 新增:知识图谱数据查询,支持 structure/student-mastery/class-mastery 三种视图)/ `createPrerequisiteAction`(✅ Task 7 新增:声明前置依赖,含循环检测)/ `deletePrerequisiteAction`(✅ Task 7 新增:删除前置依赖)
- Data-access`getTextbooks` / `getTextbookById` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` / `createTextbook` / `updateTextbook` / `deleteTextbook` / `createChapter` / `updateChapterContent` / `deleteChapter` / `createKnowledgePoint` / `updateKnowledgePoint` / `deleteKnowledgePoint` / `reorderChapters` / `getTextbooksDashboardStats` / `getKnowledgePointOptions`(跨模块接口,供 questions 调用)/ `getTextbooksWithScope`P1-1 新增:按数据范围获取教材列表,学生端强制按年级过滤)/ `verifyChapterBelongsToTextbook`P0-4 新增:资源归属校验)/ `verifyKnowledgePointBelongsToTextbook`P0-4 新增:资源归属校验)/ `verifyKnowledgePointBelongsToChapter`P0-4 新增:校验知识点是否属于指定章节)/ `createPrerequisite`(✅ Task 7 新增:创建前置依赖)/ `deletePrerequisite`(✅ Task 7 新增:删除前置依赖)/ `getPrerequisiteEdgesForTextbook`(✅ Task 7 新增:获取教材下所有前置依赖边,用于循环检测)/ `getSubjectLabelKey` / `getGradeLabelKey`i18n 标签键)
- Data-access-graph✅ Task 5 新增,图谱只读查询):`getKnowledgePointsWithRelations`(知识点+依赖+题目数聚合查询)/ `getStudentKpMastery`(学生掌握度)/ `getClassKpMastery`(班级平均掌握度)/ `getPrerequisitesForKp` / `getSuccessorsForKp`
- Constants✅ 新增):`SUBJECTS` / `GRADES` / `SUBJECT_COLORS` / `getSubjectColor` / `getSubjectLabelKey` / `getGradeLabelKey`
- ✅ v1 测试修复:`SUBJECTS` 新增 `Chinese` 学科、`GRADES` 新增 `Grade 1`/`Grade 2`,与 seed 数据对齐
- ✅ v1 测试修复:`SUBJECT_COLORS` 新增 `Chinese` 主题色rose
- Utils✅ 新增,纯函数 + 单测):`sortChapters` / `buildChapterTree` / `buildChapterIndex` / `findChapterParent` / `filterKnowledgePointsByChapter` / `normalizeOptional` / `highlightKnowledgePoints` / `hasCycleAfterAddingEdge`Task 4 新增循环依赖检测DFS 算法)/ `findChapterById`V3 续审计新增:递归查找章节树中指定 ID 的章节,替代各模块本地重复实现)
- Graph-layout✅ 新增,纯函数 + 单测):`computeGraphLayout`
- Components✅ Task 13 新增):`GraphNodeDetailPanel` — 知识图谱节点详情侧边面板,展示知识点名称/描述/掌握度/关联题目/前置后置知识点,支持跳转与前置关系增删
- Analytics✅ 新增):`TextbookAnalytics` / `TextbookAnalyticsProvider` / `useTextbookAnalytics`
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`@xyflow/react`(知识图谱可视化)、`@dagrejs/dagre`(图谱分层布局算法)
- 被依赖:`questions`(✅ P1-1 已修复:通过 textbooks data-access、`exams`(通过类型)、`dashboard`(通过 data-accessP0-4 已修复)
- ✅ UI 层跨模块依赖已解耦:`textbooks/components/knowledge-point-dialogs.tsx` 不再直接 import questions 模块,改为通过 render prop 注入创建题目入口
**已知问题**
- ✅ 无跨模块 DB 访问data-access 层)
- ✅ actions 层编排模式标杆(权限校验 → 调用 data-access → revalidatePath
- ✅ data-access 层职责单一
- ✅ P2 已修复:`data-access.ts` 中 `byId.get(pid)!.children.push` 非空断言清理为安全守卫;`or(...)!` 非空断言清理为条件 push
- ✅ P0 跨模块 UI 依赖已修复:`knowledge-point-dialogs.tsx` 通过 render prop 解耦,不再直接 import questions 模块
- ✅ P0 前端权限硬编码已修复:改用 `usePermission().hasPermission()`
- ✅ P0 i18n 已基本接入:`chapter-sidebar-list.tsx` / `actions.ts` / `section-error-boundary.tsx`(默认值改英文)均已接入 next-intl
- ✅ P1 Server Action 资源归属校验已修复:新增 `verifyChapterBelongsToTextbook` / `verifyKnowledgePointBelongsToTextbook`
- ✅ P1 data-access 数据范围过滤已修复:新增 `getTextbooksWithScope`,学生端强制按年级过滤
- ✅ P1 Error Boundary 已补齐:新增 `section-error-boundary.tsx`
- ✅ P1 知识点列表/弹窗重复实现已清理:移除无调用方的 `knowledge-point-panel.tsx`
- ✅ P1 学科/年级选项统一:抽取到 `constants.ts``SUBJECTS` / `GRADES` / `SUBJECT_COLORS`
- ✅ v1 测试修复:`textbook-reader.tsx` 移除 `SheetTrigger`(错误置于 `Sheet` 外部导致 `DialogTrigger must be used within Dialog`),改用受控 `Button` + `onClick` 打开移动端抽屉
- ✅ v1 测试修复:新增 `teacher-textbook-reader.tsx` 客户端包装组件,解决 Server Component 向 Client Component 传递函数 prop`renderQuestionCreator`)违反 Next.js 序列化约束的问题
- ✅ v1 测试修复:`scripts/seed.ts` 教材 subject/grade 改为英文 value与 `constants.ts` 对齐),消除 i18n `MISSING_MESSAGE` 错误
- ✅ P1 纯逻辑已导出并补单测:新增 `utils.ts` / `graph-layout.ts` 及对应 `.test.ts`
- ✅ v3 审计修复2026-06-24学生端两个页面添加 `requirePermission(Permissions.TEXTBOOK_READ)` 显式权限校验
- ✅ v3 审计修复2026-06-24`getKnowledgeGraphDataAction` 添加学生年级 scope 过滤,防止跨年级越权查看图谱数据
- ✅ v3 审计修复2026-06-24`getKnowledgeGraphDataAction` 的 class-mastery 模式限制为教师/管理员使用
- ✅ v3 审计修复2026-06-24`deleteChapter` 级联删除 `knowledgePointPrerequisites` 孤儿记录,保证数据完整性
- ✅ v3 审计修复2026-06-24`knowledge-graph.tsx` 前置依赖操作添加 try/catch/finally防止 loading 状态卡死
- ✅ v3 审计修复2026-06-24`textbook-reader.tsx` `onCreateKnowledgePoint` 添加 try/finally
- ✅ v3 审计修复2026-06-24`section-error-boundary.tsx` 添加 `componentDidCatch` 错误日志记录 + `role="alert"` a11y
- ✅ v3 审计修复2026-06-244 个 error.tsx 抽取为共享 `RouteError` 组件,消除重复代码
- ✅ v3 审计修复2026-06-24所有 loading.tsx 添加 `role="status"` / `aria-busy` a11y 标记
- ✅ v3 审计修复2026-06-24`chapter-sidebar-list.tsx` 章节项添加 `role="button"` / `tabIndex` / `onKeyDown` 键盘支持
- ✅ v3 审计修复2026-06-24`graph-node-detail-panel.tsx` 图标按钮添加 `aria-label`
- ✅ v3 审计修复2026-06-24`data-access.ts` `as [string, string]` 断言改为返回类型标注
- ✅ v3 审计修复2026-06-24`knowledge-point-dialogs.tsx` `as` 断言改为 void 包装
- ✅ v3 审计修复2026-06-24`graph-kp-node.tsx` 双重 `as unknown as` 断言改为类型守卫函数 `extractNodeData`
- ✅ v3 审计修复2026-06-24`graph-layout.ts` `GraphLayoutNodeData` 新增 `graphData` 字段类型定义
- ✅ v3 审计修复2026-06-24删除死代码 `verifyKnowledgePointBelongsToChapter`、`ReorderChaptersSchema`、deprecated `canEdit` prop
- ✅ v3 审计修复2026-06-24`textbook-settings-dialog.tsx` 新增 `redirectAfterDelete` prop 消除硬编码 URL
- ✅ v3 审计修复2026-06-24`teacher/textbooks/[id]/loading.tsx` 骨架屏布局改为匹配阅读器布局
- ✅ v3 P2 修复2026-06-24`data-access.ts` 3 处硬编码英文异常文案改为 `NotFoundError`(教材/章节),消息可安全返回客户端
- ✅ v3 P2 修复2026-06-24`graph-layout.ts` O(n²) `some()` 查找改为 `Set` 查找,性能降为 O(n)
- ✅ v3 P2 修复2026-06-24抽取 `textbook-form-fields.tsx` 共享组件,消除 `textbook-form-dialog` 与 `textbook-settings-dialog` 表单字段重复
- ✅ v3 P2 修复2026-06-24`textbook-content-panel.tsx` 17 个 props 按 `chapter`/`editing`/`selection`/`highlight` 四组重构,提升可读性
- ✅ v3 P2 修复2026-06-24`textbook-reader.tsx` 移除命令式 `classList` 操作,视觉高亮改为 `isHighlighted` 状态驱动,仅保留 `scrollIntoView`
- ✅ v3 P2 修复2026-06-24`use-kp-crud.ts`122 行)拆分为 `use-kp-create`/`use-kp-delete`/`use-kp-update` 三个子 Hook + 门面,均在 80 行限制内
- ⚠️ i18n 覆盖率约 99%v3 新增 `notFound`/`noClassMasteryPermission`/`close`/`prerequisiteAddFailed`/`prerequisiteRemoveFailed` 等键data-access 异常文案已错误码化)
- ⚠️ P2 图谱方向键导航未实现
- ⚠️ P2 data-access 函数未导出非缓存版本,单测难以 mock
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 502 | 14 个 Server Action写操作含 Zod 校验 + 资源归属校验 + 知识图谱查询/前置依赖管理 + class-mastery 视图) |
| `data-access.ts` | 593 | 教材/章节/知识点 CRUD + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUDv3 P2硬编码异常改为 NotFoundError |
| `data-access-graph.ts` | 188 | 知识图谱只读查询(✅ Task 5 新增:知识点关联聚合、学生/班级掌握度、前置后置知识点,标记 `server-only`v2 修复:前置依赖双向 IN 过滤防跨教材污染) |
| `types.ts` | 94 | 类型定义含知识图谱类型GraphViewMode/GraphLayoutMode/MasteryInfo/KpWithRelations/GraphNodeData/GraphEdgeData/KnowledgeGraphData/MasteryLevel |
| `schema.ts` | 56 | Zod 校验(含 CreatePrerequisiteSchema/DeletePrerequisiteSchemav2 修复refine 消息改为英文v3 修复:移除死代码 ReorderChaptersSchema |
| `constants.ts` | 91 | 学科/年级常量与颜色映射(✅ 新增v1 测试修复:新增 Chinese/Grade 1/Grade 2 |
| `utils.ts` | 226 | 章节树构建/排序/查找等纯函数 + 循环检测(✅ 新增,含单测) |
| `graph-layout.ts` | 109 | 知识图谱布局计算纯函数(✅ Task 8 重写为 dagre 集成,输出 React Flow 格式含单测v3 P2O(n²) 改为 Set 查找) |
| `analytics.tsx` | 37 | 教材分析 Context/Provider/Hook✅ 新增) |
| `hooks/use-knowledge-point-actions.ts` | 45 | 知识点操作门面 Hookv2 拆分:组合 useKpDialogState + useKpCrud对外 API 不变) |
| `hooks/use-kp-dialog-state.ts` | 33 | 知识点对话框状态管理 Hookv2 新增:从 use-knowledge-point-actions 拆分) |
| `hooks/use-kp-crud.ts` | 52 | 知识点 CRUD 门面 Hookv3 P2 拆分:组合 useKpCreate/useKpDelete/useKpUpdate对外 API 不变) |
| `hooks/use-kp-create.ts` | 43 | 知识点创建 Hookv3 P2 新增:从 use-kp-crud 拆分) |
| `hooks/use-kp-delete.ts` | 49 | 知识点删除 Hookv3 P2 新增:从 use-kp-crud 拆分) |
| `hooks/use-kp-update.ts` | 36 | 知识点更新 Hookv3 P2 新增:从 use-kp-crud 拆分) |
| `hooks/use-text-selection.ts` | 49 | 文本选区捕获 Hook |
| `hooks/use-graph-data.ts` | 65 | 知识图谱数据加载 Hook✅ Task 11 新增:派生值模式避免 effect 中 setStatev2 修复:区分 isLoading/isRefreshing 避免 UI 闪烁) |
| `components/knowledge-graph.tsx` | 408 | 知识图谱主组件(✅ Task 10/15 重写:全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色v2 新增:添加/删除前置依赖 Dialogv4 新增hierarchical/force 布局模式切换,根据布局模式渲染分层 ReactFlow 或 Obsidian 风格力导向图,状态/详情面板/依赖增删对话框共享) |
| `components/graph-kp-node.tsx` | 92 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条v2 修复:节点宽度常量化 NODE_WIDTHv3 修复:类型守卫替代 as 断言) |
| `components/graph-prerequisite-edge.tsx` | 43 | React Flow 自定义边(✅ Task 9 新增:虚线+箭头表示前置依赖v2 修复GraphEdgeData 类型安全转换) |
| `components/graph-toolbar.tsx` | 121 | 图谱工具栏(✅ Task 9 新增:视图模式切换 + 搜索 + 重置视图v2 新增isRefreshing 轻量指示器v4 新增hierarchical/force 布局模式切换 Select |
| `components/force-graph.tsx` | 451 | Obsidian 风格力导向图谱组件v4 新增:基于 react-force-graph-2d + d3-forcev5 视觉对齐 Obsidian Graph View小圆点节点+极细半透明边线+标签右侧带背景+hover/选中邻居高亮+紫色外圈焦点环;通过 next/dynamic ssr:false + import type 加载避免 SSR canvas/window 报错) |
| `components/graph-node-detail-panel.tsx` | 171 | 节点详情侧边面板(✅ Task 13 新增:描述/掌握度/关联题目/前置后置列表v2 修复:移除未使用 textbookId propv3 修复aria-label |
| `components/chapter-sidebar-list.tsx` | 325 | 章节侧边栏列表v2 修复canEdit 默认 false + orderUpdateFailed i18n keyv3 修复:键盘导航) |
| `components/section-error-boundary.tsx` | 53 | 章节内容错误边界v2 修复:默认值改为空字符串 + 条件渲染v3 修复componentDidCatch + a11y |
| `components/textbook-form-fields.tsx` | 103 | 教材表单字段共享组件v3 P2 新增:消除 form-dialog 与 settings-dialog 表单重复) |
| `components/textbook-content-panel.tsx` | 199 | 章节内容面板v3 P217 props 按 chapter/editing/selection/highlight 四组重构) |
| `components/textbook-form-dialog.tsx` | 68 | 教材创建对话框v3 P2复用 TextbookFormFields |
| `components/textbook-settings-dialog.tsx` | 136 | 教材设置对话框v3 P2复用 TextbookFormFields |
| `components/textbook-reader.tsx` | 458 | 教材阅读器主组件v3 P2移除命令式 classList高亮改为状态驱动v5 桌面端改用 ResizablePanel 可拖拽分栏,图谱 tab 时图谱初始占 60%,切换 tab 用 key 重置比例) |
| `components/*` | 17 文件 | 教材编辑/知识图谱组件(新增 `textbook-form-fields.tsx`、5 个 graph-* 组件、3 个 use-kp-* 子 Hook`teacher-textbook-reader.tsx` 已移至 app 层) |
---
## 2.6 grades成绩模块— 标杆模块(拆分范例)
**职责**:成绩分析(录入/查询/统计/导出/趋势对比分析)。
**导出函数**
- Actions`getGradeRecordsAction` / `createGradeRecordAction`v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`(✅ v4-P2-7 新增年级仪表盘维度1按 gradeId 查询年级整体 + 按班级拆分的成绩分布GRADE_RECORD_READ 权限)/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `batchCreateGradeRecordsByExamAction`(✅ 2026-06-24 新增,✅ 2026-06-25 增强错题采集:按试卷批量录入每题得分,流程 requirePermission → getExamForGradeEntry scope 校验 → assertClassInScope → safeJsonParse → BatchGradeEntryByExamSchema 校验 → batchCreateGradeRecordsByExam 单事务写入(返回 {gradeRecordId, studentId, submissionId}[])→ Promise.allSettled 并行执行 updateMasteryFromExamScore + collectFromExamSubmission考试错题采集失败不阻断→ notifyGradeEntered → revalidatePath返回 gradeRecordId 列表供撤销)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限)。注:`assertClassInScope` 原位于 actions.ts✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用),✅ v4-P2-6 修复:因 "use server" 文件要求所有 export 为 async而 `assertClassInScope` 是同步函数,已迁移至独立文件 `lib/scope-check.ts`actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入
- Data-access`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`(✅ v4-P2-7 新增年级仪表盘维度1通过 getClassesByGradeId 获取年级下所有班级inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats 纯函数,返回整体分布 + 按班级拆分)/ `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUDupsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总)/ `batchCreateGradeRecordsByExam`(✅ 2026-06-24 新增,✅ 2026-06-25 更新返回类型:按试卷批量录入成绩,单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩,返回 `BatchGradeRecordResult[]`{ gradeRecordId, studentId, submissionId })供调用方触发错题采集等后置钩子)
- Types✅ v3-P2 新增,✅ v4-P2-7 新增年级分布类型,✅ 2026-06-24 新增按试卷录入类型):`SchoolWideGradeSummaryItem` / `SchoolWideGradeSummary` / `GradeDraftData`(草稿数据接口)/ `GradeDistributionByGradeResult`(✅ v4-P2-7 新增:年级维度成绩分布结果,含 overall 整体分布 + stats 统计 + byClass 按班级拆分数组)/ `GradeDistributionByGradeClassItem`(✅ v4-P2-7 新增按班级拆分的分布项classId/className/distribution/stats/ `GradeRecordAnswer`(✅ 2026-06-24 新增:成绩记录-题目得分明细,对应 grade_record_answers 表)/ `BatchGradeEntryByExamQuestion`(✅ 2026-06-24 新增:按试卷录入时单个题目得分 { questionId, score }/ `BatchGradeEntryByExamItem`(✅ 2026-06-24 新增:按试卷录入时单个学生所有题目得分 { studentId, answers }
- Lib✅ P1-2 新增,✅ P3 更新签名,✅ P3-26 拆分,✅ v4-P2-6 新增 scope-check`toNumber` / `normalize`(位于 `lib/grade-utils.ts``buildScopeClassFilter(scope, currentUserId?)`P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`P3 修复:`class_members` scope 内置 studentId 过滤,需传入 currentUserId 参数);`assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 actions.ts 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因actions.ts 是 "use server" 文件要求所有 export 为 async而 assertClassInScope 是同步函数)
- Stats-service✅ P1-1 新增):`computeGradeStats` / `computeAverageScore` / `buildGradeTrendPoints` / `computeTrendAverage` / `computeClassComparisonStats` / `computeSubjectComparisonStats` / `computeGradeDistribution` / `buildRankingTrendPoints`(从 3 个 data-access 文件抽取的纯函数,使数据层专注 DB I/O统计逻辑可独立测试
- Export✅ v4-P1-12 新增):`exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` / `exportStudentGradeRecordsToExcel`v4-P1-12 新增:导出单个学生成绩单家长视角,仅含成绩明细 + 个人统计不含班级数据scope 为 children 自动按 studentId 过滤)/ `formatDateForFile`(已迁移至 shared/lib/utils
- Components✅ P1-5 新增):`WidgetBoundary`Error Boundary + Suspense + Skeleton 组合,含 a11y 属性)/ `SchoolWideSummaryCard`(✅ v3-P2 新增管理员全校成绩汇总卡片4 个统计卡片 + 各年级对比表格)/ `ScoreCell`(✅ v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%/黄60-84%/绿≥85%,使用语义化 Tailwind 类名避免动态拼接fullScore<=0 时不着色)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/getClassGradeIdsByClassIds、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds、`exams`(✅ 2026-06-24 新增:通过 exams data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用)、`error-book`(✅ 2026-06-25 新增:通过 error-book data-access.collectFromExamSubmission 在成绩录入后自动采集考试错题)、`diagnostic`(通过 diagnostic data-access.updateMasteryFromExamScore 更新掌握度)
- 被依赖:`parent`(通过 data-access合理、`dashboard`
**已知问题**
- ✅ P1-1 已修复:~~多处直查 `classes`/`classEnrollments`/`subjects`/`users` 表~~ 改为调用对应模块 data-access 函数classes/school/users
- ✅ P1-1 已修复:~~统计计算业务逻辑混入 data-access`getClassGradeStats` / `getGradeDistribution`~~ 抽取为纯函数到 `stats-service.ts`,数据层专注 DB I/O
- ✅ P1-2 已修复:~~`toNumber`/`normalize`/`buildScopeClassFilter` 在 3 个 data-access 文件中重复定义~~ 抽取到 `lib/grade-utils.ts` 统一维护
- ✅ P1-3 已修复:~~12 个查询/分析 Action 缺少 Zod 校验~~ 新增 12 个查询 schemaDeleteGradeRecordSchema/GetGradeRecordByIdSchema/GradeQuerySchema/ClassGradeStatsQuerySchema/StudentGradeSummaryQuerySchema/ClassRankingQuerySchema/ExportGradesSchema/GradeTrendQuerySchema/ClassComparisonQuerySchema/SubjectComparisonQuerySchema/GradeDistributionQuerySchema/RankingTrendQuerySchema所有 Action 使用 safeParse 校验
- ✅ P1-4 已修复:~~`batch-grade-entry.tsx`/`grade-record-form.tsx`/`grade-distribution-chart.tsx` 中存在 `as` 断言~~ 改用类型守卫函数isGradeType/isSemester/isDistributionTooltipPayload
- ✅ P1-5 已修复:~~teacher/grades 与 teacher/diagnostic 路由缺少 loading.tsx/error.tsx~~ 已为 7 个路由补齐 loading.tsx + error.tsx并新增 `WidgetBoundary` 通用组件
- ✅ P2-1 已修复:~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 4 个成绩图表添加 `role="img"` + `aria-label`2 个表格添加 `<caption>`3 个图标按钮添加 `aria-label`
- ✅ P2-2 已修复:~~diagnostic 组件中存在 Tailwind 任意值~~ 改用标准 Tailwind 类
- ✅ P2-4 已修复:~~`buildScopeClassFilter` 中 `grade_managed` scope 返回 `sql\`1=0\``(空数据)~~ 改为子查询 `classId IN (SELECT id FROM classes WHERE grade_id IN (...))` 过滤所管年级的班级
- ✅ P2-6 已修复:~~student/grades 和 management/grade/insights 各自重复定义 `SearchParams` 类型与 `getParam` 函数~~ 改为统一从 `@/shared/lib/search-params` 导入
- ✅ actions 层无直接 DB 访问(标杆)
- ✅ data-access 按职责拆分为 3 个文件(标杆)
- ✅ P2 已修复:`export.ts` 中 `scoreMap.get(r.studentId)!` 非空断言清理为安全守卫(`if (!subjMap) continue`
- ✅ P3 修复2026-06-22~~`schema.ts` 缺少输入边界约束~~ score 字段添加 `.max(1000)`records 数组添加 `.max(500)`,查询 schema 补全 studentId/semester/examId 字段
- ✅ P3 修复2026-06-22~~`buildScopeClassFilter` 的 `class_members` scope 未应用 studentId 过滤~~ 新增 `currentUserId` 参数,`class_members` scope 内置 `eq(gradeRecords.studentId, currentUserId)` 过滤
- ✅ P3 修复2026-06-22~~`getGradeRecords` 内存分页(全量查询后切片)~~ 改为 DB 层分页limit/offset并行查询总数与当前页数据返回 `PaginatedGradeRecords { records, total }`
- ✅ P3 修复2026-06-22~~`batchCreateGradeRecords` 非原子操作~~ 包裹 `db.transaction` 确保原子性
- ✅ P3 修复2026-06-22~~`updateGradeRecord`/`deleteGradeRecord` 未检查记录是否存在~~ 新增存在性检查,不存在时抛 `NotFoundError("成绩记录")`
- ✅ P3 修复2026-06-22~~`getClassGradeStats`/`getStudentGradeSummary`/`getClassRanking`/`getClassStudentsForEntry`/`getClassGradeStatsWithMeta` 缺少 scope 过滤~~ 所有函数新增 `scope?`/`currentUserId?` 参数,应用 `buildScopeClassFilter``getStudentGradeSummary`/`getClassStudentsForEntry` 对 `class_taught` scope 校验学生/班级归属
- ✅ P3 修复2026-06-22~~`getClassRanking` 未处理并列排名~~ 相同平均分获得相同名次,下一名次跳过占位
- ✅ P3 修复2026-06-22~~`getClassComparison`/`getRankingTrend` 缺少 scope 过滤~~ 应用 `buildScopeClassFilter``getRankingTrend` 对 `class_taught` scope 校验
- ✅ P3 修复2026-06-22~~actions.ts/actions-analytics.ts catch 块直接返回 `e.message`~~ 改用 `handleActionError(e)` 统一错误处理;`batchCreateGradeRecordsAction` 使用 `safeJsonParse` 解析 JSON
- ✅ P3 修复2026-06-22~~actions-analytics.ts 缺少 class scope 校验~~ `getGradeTrendAction`/`getSubjectComparisonAction`/`getGradeDistributionAction` 新增 `assertClassInScope` 校验
- ✅ P3 修复2026-06-22~~组件直接 try/catch 调用 Server Action~~ 改用 `safeActionCall` 包装器onError/onFinally 回调);`batch-grade-entry.tsx` localStorage 访问包裹 `typeof window !== "undefined"` 检查;区分"未录入"与"录入 0"
- ✅ P3 修复2026-06-22~~`grade-trend-card.tsx` 排序未处理 NaN 日期、除法未检查 fullScore > 0~~ 新增日期有效性检查与 `fullScore > 0` 守卫
- ✅ P3 修复2026-06-22~~页面文件缺少 scope 传递~~ teacher/grades/page.tsx 使用 DB 层分页analytics/page.tsx 添加 EmptyStateentry/page.tsx 与 stats/page.tsx 过滤 class_taught scope 班级student/grades/page.tsx 与 parent/grades/page.tsx 传递 `ctx.dataScope` 到 data-access
- ✅ v3-P2 改进2026-06-23新增 `grade_drafts` 表成绩录入草稿userId+classId+subjectId+type 唯一键content JSON 存储 scores24 小时过期schema.ts 第 1444-1469 行);新增 `saveGradeDraft`/`getGradeDraft`/`deleteGradeDraft` data-access 函数 + 对应 3 个 Server Actions支持成绩批量录入草稿服务端持久化
- ✅ v3-P2 改进2026-06-23新增 `getExamOptionsForGrades` data-access 函数,获取指定班级/科目下有成绩记录的考试列表;`AnalyticsFilters` 组件新增 `exams`/`currentExamId`/`currentSemester` props添加学期和考试筛选 ChipNavteacher/grades/analytics/page.tsx 新增 semester/examId 搜索参数解析
- ✅ v3-P2 改进2026-06-23`getGradeTrend`/`getGradeDistribution`/`getSubjectComparison`/`getClassComparison` 均新增 `semester` 和 `examId` 可选参数,支持按学期和考试筛选分析
- ✅ v3-P2 改进2026-06-23新增 `getSchoolWideGradeSummary` data-access 函数 + `SchoolWideSummaryCard` 组件 + `SchoolWideGradeSummary`/`SchoolWideGradeSummaryItem` 类型,管理员全校成绩汇总视图(按年级聚合平均分/及格率/优秀率/学生数/班级数加权平均计算全校汇总admin/school/grades/insights/page.tsx 顶部新增 SchoolWideSummaryCard
- ✅ v3-P2 改进2026-06-23parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`,渲染 GradeTrendCard
- ✅ v3-P3-4 改进2026-06-23GradeTrendCard 新增日期范围选择器(全部/近7天/近30天/近90天通过 nuqs `trendRange` URL 参数持久化useEffect 中计算截止时间戳避免渲染阶段调用 Date.now()
- ✅ v4-P3-4 改进2026-06-23GradeDistributionChart 色盲友好双重编码——每个分数段使用不同 SVG pattern条纹/点状/交叉线/反向条纹/网格)+ 颜色,`SimpleBarChart` 新增 `defs` prop 支持自定义 SVG 图案
- ✅ P3 修复2026-06-23~~`lib/grade-utils.ts` 72 行超 40 行工具函数上限~~ P3-26 将 `buildScopeClassFilter` 迁移至 `lib/scope-filter.ts`grade-utils.ts 仅保留 toNumber/normalize20 行)
- ✅ P3 修复2026-06-23~~`stats-service.ts` createDefaultBuckets 不必要导出~~ P3-10 移除 export 关键字,改为内部函数
- ✅ P3 修复2026-06-23~~`stats-service.ts` buildGradeTrendPoints 使用 as 断言~~ P3-24 新增 isGradeTrendType 类型守卫函数替代 as 断言
- ✅ P3 修复2026-06-23~~`export.ts` 局部 avg 函数与 stats-service.computeAverageScore 重复~~ P3-6 删除局部 avg复用 computeAverageScore
- ✅ P3 修复2026-06-23~~`export.ts` TYPE_LABELS 硬编码中文~~ P3-7 改用 next-intl getTranslations新增 export.sheets/columns/metrics i18n 键zh-CN/en 同步)
- ✅ v4-P1-6 改进2026-06-23`createGradeRecordAction` / `batchCreateGradeRecordsAction` 成绩录入后通知学生和家长(调用 `notifyGradeEntered`,内部使用 `parent/data-access.getParentIdsByStudentIds` 批量查询家长 ID通知失败不阻断成绩录入
- ✅ v4-P1-7 改进2026-06-23新增 `ScoreCell` 组件components/score-cell.tsx根据得分率着色红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名避免动态拼接
- ✅ v4-P1-10 改进2026-06-23`grade-record-list.tsx` 使用 `ScoreCell` 替代纯文本分数展示 + 表格 `overflow-x-auto` 水平滚动
- ✅ v4-P1-12 改进2026-06-23`exportGradesAction` 新增可选 `studentId` 参数,支持按学生导出(家长视角);新增 `exportStudentGradeRecordsToExcel` 导出函数(仅含成绩明细 + 个人统计不含班级数据parent/grades/page.tsx 传 studentId 到 ParentExportButtonParentExportButton 接入 exportGradesAction
- ✅ v3-P3-1 改进2026-06-23`batch-grade-entry.tsx` 新增"下载模板"按钮,客户端生成 CSV 模板(含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8教师可下载填好后粘贴到录入表格
- ✅ v3-P3-2 改进2026-06-23`grade-record-list.tsx` 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;新增 `bulkDeleteGradeRecords` data-access 函数(使用 inArray 一次性删除避免 N+1+ `bulkDeleteGradeRecordsAction` Server Action限制单次最多 500 条)
- ✅ v4-P3-2 改进2026-06-23`batch-grade-entry.tsx` 顶部新增可折叠新手引导提示框4 步使用说明),使用 localStorage 记住用户关闭状态避免重复显示
- ✅ v4-P2-6 修复2026-06-23~~`assertClassInScope` 是同步函数但位于 "use server" 文件 actions.ts 中~~ Next.js 要求 "use server" 文件中所有 export 必须为 async同步 export 会导致构建错误。修复:将 `assertClassInScope` 迁移至独立文件 `lib/scope-check.ts`(含 `import "server-only"`actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入
- ✅ 2026-06-24 重新设计:批量录入从"只录总分"改为"按试卷录入每题得分"。新增 `grade_record_answers` 表存储每题得分明细(迁移 0010_grade_record_answers.sql。`batchCreateGradeRecordsByExam` data-access 单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩(学生只看到对错,不知道学生答案)。`BatchGradeEntryByExam` 组件完全重写为 Excel 式表格(行=学生,列=题目,末列=总分自动计算支持多行多列粘贴、Enter 跳下一行、Tab 跳下一格、分数校验、撤销机制sessionStorage 5 分钟有效)。新增对 `exams` 模块的依赖getExamsForGradeEntry/getExamForGradeEntry。i18n 新增 batchByExam 章节zh-CN/en ~35 键)
- ✅ 审计修复2026-06-22~~`school-wide-summary-card.tsx` 整个组件无 i18n13 处硬编码中文)~~ 改为 async 组件使用 `getTranslations("grades")`,新增 `schoolWide` 翻译键节zh-CN/en 同步 14 键)
- ✅ 审计修复2026-06-22~~5 个 teacher/grades 页面文件大量硬编码中文/英文~~ `page.tsx`/`analytics/page.tsx`/`entry/page.tsx`/`stats/page.tsx`/`error.tsx` 全部使用 `getTranslations("grades")` 或 `useTranslations("grades")`,新增 `page` 翻译键节zh-CN/en 同步 ~30 键)
- ✅ 审计修复2026-06-22~~`actions.ts` 15+ 处硬编码中文错误消息/通知内容~~ `notifyGradeEntered` 使用 `getTranslations("grades")` 获取通知标题和内容;`batchCreateGradeRecordsByExamAction` 错误消息改用 `t("action.*")` 翻译键;`exportGradesAction` 文件名和错误消息改用翻译键,新增 `action` 翻译键节zh-CN/en 同步 ~20 键)
- ✅ 审计修复2026-06-22~~`data-access.ts` 2 处硬编码中文 throw Error~~ 改为英文错误消息data-access 层为防御性检查action 层已先行校验)
- ✅ 审计修复2026-06-22~~`data-access.ts:782` `as` 断言无类型守卫~~ 新增 `isGradeDraftData` 类型守卫函数lib/type-guards.ts从 unknown 安全验证 DB JSON 结构
- ✅ 审计修复2026-06-22~~`data-access-analytics.ts:360-362` 非空断言 `!`~~ 改用类型谓词 `filter((r): r is typeof r & { stats: NonNullable<typeof r.stats> } => r.stats !== null)` 消除非空断言
- ✅ 审计修复2026-06-22~~`export.ts` 内存过滤而非 DB 层过滤~~ `exportStudentGradeRecordsToExcel` 传入 `studentId` 到 `getGradeRecords` DB 层过滤
- ✅ 审计修复2026-06-22~~`teacher/grades/page.tsx` 内联类型守卫 + `as` 断言~~ 改用 `lib/type-guards.ts` 的 `isGradeType`/`isSemester` 函数
- ✅ 审计修复2026-06-22~~11 个组件函数缺少返回值类型标注~~ 全部添加 `: JSX.Element` 或 `: Promise<JSX.Element>` 返回值类型
- ✅ 审计修复2026-06-24~~17 处 Tailwind 任意值(`w-[140px]`/`h-[280px]`/`min-w-[80px]` 等)~~ 图表组件移除匹配默认值的 props 或改用标准类(`h-60`/`w-52`/`w-60`),过滤器改用 `w-36`/`w-32`,表格改用 `min-w-32`/`min-w-20`
- ✅ 审计修复2026-06-24~~`batch-grade-entry.tsx` 2 处 `window.confirm()`~~ 改用声明式 AlertDialog + `pendingSwitch` 状态管理,新增 3 个 i18n 键confirmTitle/confirmAction/cancelAction
- ✅ 审计修复2026-06-24~~`class-grade-report.tsx` 缺少返回值类型标注~~ 补充 `: Promise<JSX.Element>` 返回值类型
- ✅ 审计修复2026-06-25 P0~~3 个 error.tsx 硬编码中文parent/grades、admin/school/grades、admin/school/grades/insights~~ 改为 `useTranslations("grades")` + 新增 `error.*` 翻译键zh-CN/en 同步);~~7 个 loading/error.tsx 函数缺返回值类型~~ 补全 `: JSX.Element` 返回值类型标注
- ✅ 审计修复2026-06-25 P1-1~~`actions.ts` 超 800 行856 行)~~ 拆分草稿相关 3 个 Action 到 `actions-draft.ts`106 行);通知逻辑 `notifyGradeEntered` 抽取至 `lib/notify.ts`60 行actions.ts 降至 646 行
- ✅ 审计修复2026-06-25 P1-2~~`data-access.ts` 超 800 行816 行)~~ 拆分草稿 CRUD 到 `data-access-drafts.ts`107 行);拆分按试卷录入到 `data-access-exam-entry.ts`167 行data-access.ts 降至 536 行
- ✅ 审计修复2026-06-25 P1-3~~`actions.ts` 多处英文错误消息未 i18n~~ 新增 `action.error.*` 翻译键,所有错误消息改用 `t("action.error.*")`scope 校验错误改用 `t("action.scopeError.{code}")`
- ✅ 审计修复2026-06-25 P1-4~~`lib/scope-check.ts` 返回硬编码英文消息~~ 改为返回 `ScopeCheckErrorCode` 枚举(`"class_taught" | "class_members" | "denied"`),由 Action 层通过 i18n 键 `grades.action.scopeError.{code}` 翻译
- ✅ 审计修复2026-06-25 P1-5~~`actions-analytics.ts` scope 校验消息未 i18n~~ 所有 6 个 Action 使用 `getTranslations("grades")` 获取翻译;新增 `ownRankingTrendOnly`/`childrenRankingTrendOnly` 翻译键
- ✅ 审计修复2026-06-25 P1-6/P1-7~~`batch-grade-entry.tsx` 超 500 行604 行)+ as 断言~~ 拆分为 `BatchGradeEntryTable` + `BatchGradeEntryStats` + `BatchGradeEntryDialog` 子组件 + `useBatchGradeEntryUndo` Hook主组件降至 422 行;撤销数据使用 `isUndoData` 类型守卫in 运算符)替代 `as { ids: string[]; timestamp: number }` 断言
- ✅ 审计修复2026-06-25 P1-8~~`parent/grades/page.tsx` 第 63 行 as 断言~~ 改用 `for` 循环 + 类型守卫 `if (summary === null) continue` 替代 `.filter().map()` + `as NonNullable<...>` 断言
- ✅ 审计修复2026-06-25 P1-9~~`teacher/grades/page.tsx` 列表与统计未 WidgetBoundary 包裹~~ 新增 `<WidgetBoundary title={t("title.list")}>` 包裹 `<GradeRecordList>`,避免单数据区块失败导致整页崩溃
- ✅ 审计修复2026-06-25 P2-2~~`grade-filters.tsx`/`grade-query-filters.tsx`/`export-button.tsx` 函数缺返回值类型~~ 补全 `: JSX.Element` / `: Promise<void>` 返回值类型标注
- ✅ 审计修复2026-06-25 P2-3~~`updateGradeRecord` 使用 `Record<string, unknown>` 累积更新字段~~ 改用 `Partial<typeof gradeRecords.$inferInsert>` 类型安全更新
- ✅ 审计修复2026-06-25 P2-4~~`getStudentGradeSummary` 内部调用 `getClassRanking` 形成 data-access 层内部耦合~~ 新增 `getStudentRankInClass` 独立函数(两步法:查学生平均分 → 查所有学生平均分 → 计算严格高于的不同平均分数),避免获取整个班级排名(含姓名查询)只为查单个学生排名
- ✅ 审计修复2026-06-25 P2-9~~`lib/type-guards.ts` 第 8/12 行 as 断言~~ `isGradeType`/`isSemester` 改用 `GRADE_TYPES.some((t) => t === v)` 替代 `(GRADE_TYPES as readonly string[]).includes(v)``isGradeDraftData` 改用 `in` 运算符替代 `as Record<string, unknown>` 断言
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 646 | 17 个 Server Action含 Zod 校验,含 v2-P1-5 安全修复assertClassInScope + 行级 scope 校验P3 修复handleActionError + safeJsonParse + scope 传递 + DB 层分页v4-P1-12exportGradesAction 新增 studentId 参数v3-P3-2 新增bulkDeleteGradeRecordsAction 批量删除v4-P2-6assertClassInScope 迁移至 lib/scope-check.ts2026-06-24 新增batchCreateGradeRecordsByExamAction 按试卷录入每题得分;**P1-1 重构2026-06-25**:草稿相关 3 个 ActionsaveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction迁移至 actions-draft.ts通知逻辑 notifyGradeEntered 抽取至 lib/notify.ts错误消息全部 i18nt(\"action.*\")scope 校验错误改用 ScopeCheckErrorCode + t(\"action.scopeError.{code}\") 翻译) |
| `actions-draft.ts` | 106 | **P1-1 新增2026-06-25**:成绩录入草稿 3 个 Server ActionsaveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction从 actions.ts 拆分以使 actions.ts 降至 800 行以内。权限分别为 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE调用 data-access-drafts错误消息使用 i18n 翻译键 |
| `actions-lock.ts` | 197 | **P3-6 新增2026-06-26**:协同录入锁 4 个 Server ActiongetDraftLockStatusAction/acquireDraftLockAction/renewDraftLockAction/releaseDraftLockAction。权限 GRADE_RECORD_READ查询/GRADE_RECORD_MANAGE获取/续期/释放),调用 data-access-drafts 锁函数 + assertClassInScope 校验 + getUserNamesByIds 解析锁持有者姓名(避免跨模块 DB join。releaseDraftLockAction 失败不阻断主流程 |
| `actions-import.ts` | 297 | **P3-10 新增2026-06-26**Excel 批量导入成绩 3 个 Server ActiondownloadGradeImportTemplateAction/importGradesFromExcelAction/previewGradeImportAction。权限 GRADE_RECORD_MANAGE + assertClassInScope调用 import-export.ts 解析 Excel + 匹配学生姓名 + 批量入库。支持模板下载(含班级学生姓名)、预校验、正式导入 |
| `actions-appeal.ts` | 197 | **P3-7 新增2026-06-26**:成绩申诉 5 个 Server ActioncreateGradeAppealAction/withdrawGradeAppealAction/getStudentAppealsAction/reviewGradeAppealAction/getPendingAppealsForReviewAction。学生权限 GRADE_RECORD_READ提交/撤回/查看自己申诉),教师权限 GRADE_RECORD_MANAGE复核。审核通过且提供 adjustedScore 时同步更新 grade_records.score |
| `data-access-appeals.ts` | 261 | **P3-7 新增2026-06-26**成绩申诉数据访问层。7 个函数hasPendingAppeal/createGradeAppeal/getStudentAppeals/getPendingAppealsForReview/getAppealById/reviewGradeAppeal/withdrawGradeAppeal+ GradeAppeal/GradeAppealWithRecord 接口。申诉状态机 pending→approved/rejected/withdrawnoriginalScore 快照留痕reviewGradeAppeal 同步更新成绩记录 |
| `import-export.ts` | 278 | **P3-10 新增2026-06-26**Excel 导入成绩逻辑。generateGradeImportTemplate 生成含班级学生姓名的模板parseGradeExcelBuffer 解析 Excel支持中英双语表头parseGradeImportData 匹配学生姓名到 ID处理重名buildBatchInputFromImport 转换为批量录入输入。依赖 @/shared/lib/excel |
| `actions-analytics.ts` | 219 | 6 个分析 Action含 Zod 校验P3 修复handleActionError + assertClassInScope 校验v4-P2-6assertClassInScope 改从 ./lib/scope-check 导入;**P1-5 重构2026-06-25**:所有 Action 使用 getTranslations(\"grades\") 获取翻译,错误消息改用 t(\"action.invalidQuery\")/t(\"action.scopeError.{code}\");学生/家长排名趋势访问校验使用 t(\"action.ownRankingTrendOnly\")/t(\"action.childrenRankingTrendOnly\") |
| `data-access.ts` | 536 | 成绩 CRUD + 统计(含 v2-P2-9 修复recorderName 批量查询P3 修复PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名v3-P3-2 新增bulkDeleteGradeRecords 使用 inArray 批量删除;**P1-2 重构2026-06-25**:草稿 CRUDsaveGradeDraft/getGradeDraft/deleteGradeDraft迁移至 data-access-drafts.ts按试卷录入batchCreateGradeRecordsByExam迁移至 data-access-exam-entry.ts**P2-3**updateGradeRecord 类型强化为 Partial<typeof gradeRecords.$inferInsert>**P2-4**:新增 getStudentRankInClass 解耦 getStudentGradeSummary 与 getClassRanking |
| `data-access-drafts.ts` | 405 | **P1-2 新增2026-06-25**:成绩录入草稿 CRUDsaveGradeDraft/getGradeDraft/deleteGradeDraft+ GradeDraftData 接口,从 data-access.ts 拆分。使用 cache() memoize getGradeDraftisGradeDraftData 类型守卫安全解析 DB JSON 列24 小时过期。**P3-6 扩展2026-06-26**:新增协同录入锁机制 5 个函数getDraftLockStatus/acquireDraftLock/renewDraftLock/releaseDraftLock/cleanupExpiredDraftLocks+ DraftLockStatus/AcquireLockResult 接口LOCK_TTL_MS=5 分钟,悲观锁语义+乐观 TTL+令牌续期,锁以 grade_drafts 行为载体lockedBy/lockedAt/lockToken 列) |
| `data-access-exam-entry.ts` | 167 | **P1-2 新增2026-06-25**按试卷批量录入成绩batchCreateGradeRecordsByExam+ BatchGradeRecordResult 接口,从 data-access.ts 拆分。单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),返回 BatchGradeRecordResult[]{gradeRecordId, studentId, submissionId})供调用方触发错题采集等后置钩子 |
| `data-access-analytics.ts` | 417 | 趋势/对比分析P3 修复getClassComparison 应用 buildScopeClassFilterv3-P2 新增getExamOptionsForGrades/getSchoolWideGradeSummarygetGradeTrend/getClassComparison/getSubjectComparison/getGradeDistribution 新增 semester/examId 可选参数) |
| `data-access-ranking.ts` | 166 | 排名查询P3 修复getRankingTrend 接受 scope 参数 + class_taught 校验) |
| `stats-service.ts` | 285 | 统计计算纯函数P1-1 新增8 个纯函数 + 2 个常量 + 2 个接口P3-10createDefaultBuckets 改为内部函数P3-24buildGradeTrendPoints 使用 isGradeTrendType 类型守卫替代 as 断言) |
| `export.ts` | 290+ | Excel 导出v2-P1-5 修复:传递 currentUserId 到 data-accessP3 修复:适配 PaginatedGradeRecords 结构 + 传递 scopeP3-6复用 stats-service.computeAverageScore 替代局部 avgP3-7硬编码中文改用 next-intl getTranslationsv4-P1-12 新增exportStudentGradeRecordsToExcel 家长视角单学生导出) |
| `schema.ts` | 237 | Zod 校验(含 12 个查询 schemaP3 修复score .max(1000) + records .max(500) + 补全查询字段v3-P2 新增grade_drafts 表定义2026-06-24 新增BatchGradeEntryByExamSchema**P3-10 新增2026-06-26**ExcelImportRowSchema/ExcelImportGradesSchema + 导出 ExcelImportRowInput/ExcelImportGradesInput 类型;**P3-7 新增2026-06-26**GradeAppealStatusEnum/CreateGradeAppealSchema/ReviewGradeAppealSchema + 导出 CreateGradeAppealInput/ReviewGradeAppealInput 类型) |
| `lib/grade-utils.ts` | 20 | 公共工具函数toNumber/normalizeP3-26buildScopeClassFilter 迁移至 scope-filter.ts |
| `lib/scope-filter.ts` | 56 | DB 行级权限过滤buildScopeClassFilterP3-26 从 grade-utils.ts 迁移v2-P2-2 修复:改用 classes data-access 子查询P3 修复:新增 currentUserId 参数) |
| `lib/scope-check.ts` | 34 | v4-P2-6 新增:班级 scope 校验工具assertClassInScope 同步函数,从 actions.ts 迁移至此独立文件以避开 "use server" 文件要求 export 必须为 async 的限制;含 `import "server-only"`**P1-4 重构2026-06-25**:返回类型从 string \| null 改为 ScopeCheckErrorCode \| null\"class_taught\" \| \"class_members\" \| \"denied\"),消除硬编码英文字符串,调用方通过 i18n 键 grades.action.scopeError.{code} 翻译) |
| `lib/notify.ts` | 60 | **P1-1 新增2026-06-25**成绩录入通知工具notifyGradeEntered从 actions.ts 抽取。使用 next-intl getTranslations 获取通知标题和内容i18n**P2-7 改进**:使用 Promise.allSettled 并行化通知写入,避免批量录入 30+ 学生时串行等待;依赖 notifications/data-access.createNotification + parent/data-access.getParentIdsByStudentIds + school/data-access.getSubjectNameById |
| `lib/report-card.ts` | 380 | **P3-1 新增2026-06-26**成绩报告卡数据访问层。getReportCardDatacached聚合学生成绩+班级信息+班主任+学年+科目排名+总体统计computeSubjectRank 计算单科排名。三层 scope 校验owned学生自己/children家长子女/class_taught教师班级学生。静态导入 data-accessgetStudentRankInClass/getClassNamesByIds+ stats-servicecomputeAverageScore |
| `lib/type-guards.ts` | 25 | 类型守卫纯函数isGradeType/isSemester/isGradeDraftData**P2-9 修复2026-06-25**isGradeType/isSemester 使用 GRADE_TYPES.some((t) => t === v) 替代 (GRADE_TYPES as readonly string[]).includes(v),消除 as 断言isGradeDraftData 使用 in 运算符替代 as Record<string, unknown> 断言) |
| `types.ts` | 200+ | 类型定义v3-P2 新增SchoolWideGradeSummaryItem/SchoolWideGradeSummary2026-06-24 新增GradeRecordAnswer/BatchGradeEntryByExamQuestion/BatchGradeEntryByExamItem |
| `components/widget-boundary.tsx` | 136 | Widget 边界组件P1-5 新增v2-P1-1 已在 3 个页面应用;**P1-92026-06-25**teacher/grades/page.tsx 列表与统计卡片新增 WidgetBoundary 包裹,避免单数据区块失败导致整页崩溃) |
| `components/school-wide-summary-card.tsx` | - | v3-P2 新增管理员全校成绩汇总卡片4 个统计卡片 + 各年级对比表格) |
| `components/score-cell.tsx` | 41 | v4-P1-7 新增:成绩单元格组件,根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名 |
| `components/grade-trend-card.tsx` | 69 | 趋势卡片v2-P2-9 修复a11yv2-P1-4i18nP3 修复NaN 日期检查 + fullScore > 0 守卫) |
| `components/grade-record-list.tsx` | 200+ | 成绩记录列表v2-P1-4i18nP3 修复safeActionCall 包装删除操作v4-P1-7使用 ScoreCellv4-P1-10overflow-x-autov3-P3-2 新增:多选复选框 + 全选 + 批量删除工具栏 + 批量删除确认对话框,接入 bulkDeleteGradeRecordsAction |
| `components/grade-distribution-chart.tsx` | 100 | 分数分布图v2-P1-4i18n |
| `components/subject-comparison-chart.tsx` | 62 | 科目对比图v2-P1-4i18n |
| `components/class-comparison-chart.tsx` | 194 | 班级对比图v2-P1-4i18nv3-P3-5 新增:显著性分析区域,基于极差和样本量的经验规则判断班级间差异,含可折叠详细分析) |
| `components/grade-trend-chart.tsx` | 59 | 趋势图v2-P1-4i18n |
| `components/grade-record-form.tsx` | 177 | 录入表单v2-P2-7 修复Label htmlForv2-P1-4i18nP3 修复safeActionCall 包装提交) |
| `components/batch-grade-entry.tsx` | 422 | 2026-06-24 完全重写:导出名改为 BatchGradeEntryByExam按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴Tab 分隔Enter 跳下一行同一列Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低分数校验超过题目满分标红撤销机制sessionStorage 5 分钟有效)。**P1-6/P1-7 重构2026-06-25**:拆分为 3 个子组件 + 1 个 HookBatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo主组件从 604 行降至 422 行;撤销数据使用 isUndoData 类型守卫替代 as { ids: string[]; timestamp: number } 断言window.confirm() 已替换为声明式 AlertDialog。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?(原 v2-P2-7 Label htmlForv2-P1-4 i18nP3 safeActionCall + localStorage 安全检查v3-P2 服务端草稿v3-P3-1 下载 CSV 模板v4-P3-2 新手引导均已替换) |
| `components/batch-grade-entry-table.tsx` | 115 | **P1-6 新增2026-06-25**:批量录入 Excel 式表格子组件(行=学生,列=题目,末列=总分自动计算)。支持多行多列 Excel 粘贴、Enter 跳下一行、Tab 跳下一格、分数校验(超过题目满分标红)。接受 inputRefs 供父组件管理输入焦点 |
| `components/batch-grade-entry-stats.tsx` | 68 | **P1-6 新增2026-06-25**:批量录入统计栏子组件,显示已录入/总数/均分/最高/最低/无效分数徽章,含学生搜索框过滤。导出 BatchGradeEntryStatsData 接口 |
| `components/batch-grade-entry-dialog.tsx` | 58 | **P1-6 新增2026-06-25**:批量录入切换确认对话框子组件(替代 window.confirm())。导出 PendingSwitch 联合类型 { kind: "exam"; examId; message } \| { kind: "class"; classId; message } \| null |
| `hooks/use-batch-grade-entry-undo.ts` | 86 | **P1-6/P1-7 新增2026-06-25**:批量录入撤销 Hook。封装撤销令牌的序列化、sessionStorage 读写、5 分钟 TTL 过期检查、撤销 Action 调用。包含 isUndoData 类型守卫函数(替代 as 断言),使用 in 运算符做安全属性访问 |
| `components/grade-filters.tsx` | 76 | 过滤器v2-P1-4i18n**P2-22026-06-25**:补全 : JSX.Element 返回值类型) |
| `components/student-grade-summary.tsx` | 107 | 学生成绩摘要v2-P1-4i18n |
| `components/export-button.tsx` | 79 | 导出按钮v2-P1-4i18nP3 修复safeActionCall 包装导出操作;**P2-22026-06-25**handleExport 补全 : Promise<void> 返回值类型) |
| `components/analytics-filters.tsx` | 86+ | 分析过滤器v2-P1-4i18nv3-P2 新增exams/currentExamId/currentSemester props添加学期和考试筛选 ChipNav |
| `components/stats-class-selector.tsx` | 40 | 统计班级选择器v2-P1-4i18n |
| `components/grade-stats-card.tsx` | 74 | 统计卡片v2-P1-4i18n |
| `components/class-grade-report.tsx` | 90 | 班级成绩报告v2-P1-4i18n |
| `components/grade-query-filters.tsx` | 96 | 查询过滤器v2-P2-7 修复Label htmlForv2-P1-4i18n**P2-22026-06-25**:补全 : JSX.Element 返回值类型) |
| `components/report-card-view.tsx` | 323 | **P3-1 新增2026-06-26**A4 打印格式成绩报告卡服务端组件。含表头(学校名+标题+学期、学生信息网格、科目成绩表格rowSpan 合并科目名/排名列)、汇总统计(均分/班排名/及格率/优秀率)、教师评语+签字区。使用 globals.css `@media print` 规则控制打印布局 |
| `components/report-card-print-button.tsx` | 106 | **P3-1 新增2026-06-26**:跳转至报告卡页面的客户端按钮,支持学年/学期筛选 |
| `components/report-card-print-action.tsx` | 48 | **P3-1 新增2026-06-26**:调用 window.print() 的客户端按钮组件,含 loading 状态和错误 toast |
| `components/excel-import-dialog.tsx` | 518 | **P3-10 新增2026-06-26**Excel 批量导入成绩对话框3 步流程:下载模板→填写参数→上传文件)。展示导入结果(成功/失败计数、未匹配学生、无效行表格前 20 条)。使用 safeActionCall 包装 Action 调用 |
| `components/draft-lock-banner.tsx` | 75 | **P3-6 新增2026-06-26**协同录入锁状态提示条。三种状态acquiringloading/conflict锁被他人持有警告+重试按钮)/acquired不渲染。使用 Alert 组件 |
| `components/knowledge-point-mastery-chart.tsx` | 232 | **P3-3 新增2026-06-26**:知识点掌握度柱状图组件,集成 diagnostic 模块的 getClassMasterySummary 数据。使用 SimpleBarChart + masteryBarColor 函数着色 + isMasteryTooltipPayload 类型守卫 |
| `hooks/use-draft-lock.ts` | 184 | **P3-6 新增2026-06-26**:协同录入锁管理 Hook。职责进入页面自动获取锁、3 分钟心跳续期TTL 5 分钟)、组件卸载/提交成功释放锁、锁冲突状态暴露。使用 ref 保存最新 token 避免闭包捕获旧值,防重复 acquire/release |
> 架构变更2026-07-05Phase 4.5 — grade-record-list.tsx 编辑保存改用 useOptimistic + useTransition
> - **grade-record-list.tsx**:编辑保存 `isSaving` useState → `useOptimistic<GradeRecordListItem[], GradeRecordListItem>` + `useTransition`。`handleEditSave` 构造 `optimisticRecord` 在 `startSaveTransition` 内 `addOptimisticRecord` + `await updateGradeRecordAction` + `router.refresh()`;两处 `records.map` 改为 `optimisticRecords.map`(桌面表格 + 移动端卡片视图)即时显示乐观记录
> - **关键决策**:成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值;不为删除操作使用 useOptimistic因为删除需要服务端确认外键约束、关联记录检查等
---
## 2.7 classes班级模块— 耦合最严重
**职责**:班级 CRUD + 学生/教师管理 + 邀请码注册。
**导出函数**
- Actions`createTeacherClassAction` / `updateTeacherClassAction` / `deleteTeacherClassAction` / `createAdminClassAction` / `updateAdminClassAction` / `deleteAdminClassAction` / `createGradeClassAction` / `updateGradeClassAction` / `deleteGradeClassAction`
- Data-access`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `verifyTeacherOwnsClass` / `getTeacherIdsByClassIds`(获取多个班级的所有教师 ID班主任 + 任课教师,跨模块接口,供 messaging 模块调用)/ `getClassIdByScheduleId(scheduleId: string): Promise<string | null>`(✅ 2026-06-26 审计修复新增:根据课表项 ID 获取所属班级 ID供 actions-schedule 的 create/update/delete 三个 Action 做越权校验,防止教师通过 scheduleId 修改他人班级课表)(✅ P0-5 已修复classSchedule 写函数 createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem 已迁移至 scheduling/data-access-class-schedule.tsclasses 模块仅保留 classSchedule 读函数;✅ P2 已修复:`getAccessibleClassIdsForTeacher` 使用 `Promise.all` 并行化 ownedIds 与 assignedIds 查询)
- Schema`CreateTeacherClassSchema` / `UpdateTeacherClassSchema` / `DeleteTeacherClassSchema` / `CreateAdminClassSchema` / `UpdateAdminClassSchema` / `DeleteAdminClassSchema` / `CreateGradeClassSchema` / `UpdateGradeClassSchema` / `DeleteGradeClassSchema` / `CreateClassScheduleItemSchema` / `UpdateClassScheduleItemSchema` / `DeleteClassScheduleItemSchema` / `EnrollStudentByEmailSchema`
- Components`AdminClassesClient` / `GradeClassesClient`P1-D 重构:降至 ≤160 行,组合使用共享组件)/ `ClassListTable`admin/grade 共享班级列表表格P1-D 抽取)/ `ClassFormDialog`admin/grade 共享创建/编辑表单弹窗,`schools` 可选控制学校选择显隐P1-D 抽取)/ `ClassDeleteDialog`admin/grade 共享删除确认弹窗,基于 ConfirmDeleteDialogP1-D 抽取)/ `ClassListToolbar`admin/grade 共享工具栏:新建按钮+计数徽章P1-D 抽取)/ `class-form-utils``formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型P1-D 抽取)/ `ClassErrorBoundary`P1-C 新增class component Error Boundary + i18n + router.refresh 重试)/ `ClassListSkeleton` / `ClassCardSkeleton` / `ClassGridSkeleton` / `StudentListSkeleton` / `ScheduleGridSkeleton`P1-C 新增5 个骨架屏组件)/ `MyClassesGrid`P0-A 修复:已接入 i18n/ `ScheduleView`(✅ P2 修复:已拆分出 `ScheduleCreateDialog`/`ScheduleEditDialog`/`ScheduleDeleteDialog` 3 个对话框子组件,主组件从 464 行降至 175 行;不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态下沉到子对话框WEEKDAYS 常量改用从 `./schedule-utils` 导入的 `SCHEDULE_WEEKDAYS`/ `ScheduleCreateDialog`P2 修复新增:课表新增对话框,从 schedule-view 拆分)/ `ScheduleEditDialog`P2 修复新增:课表编辑对话框,从 schedule-view 拆分)/ `ScheduleDeleteDialog`P2 修复新增:课表删除确认对话框,从 schedule-view 拆分)/ `ScheduleFilters` / `StudentsTable` / `StudentsFilters`P0-A 修复:已接入 i18n/ `ClassHeader` / `ClassOverviewStats` / `ClassQuickActions` / `ClassScheduleWidget` / `ClassAssignmentsWidget` / `ClassStudentsWidget` / `ClassTrendsWidget` / `EditClassDialog`P0-A 修复class-detail 下 7 个子组件 + 编辑对话框已接入 i18n
- 工具函数components/schedule-utils.ts2026-06-26 审计修复新增):`getSubjectColor(subject: string): string`(根据科目名返回对应颜色类名,中英文双语关键词匹配;修复原实现仅匹配英文关键词导致中文 `DEFAULT_CLASS_SUBJECTS` 全部落到 default 分支的颜色分组失效 bug/ `getPositionStyle(startTime: string, endTime: string): { top: string; height: string }`(根据课程开始/结束时间计算课表块在 8:00-18:00 时间轴上的定位百分比)/ `timeToMinutes(time: string): number`(将 "HH:MM" 时间字符串转换为当天从 0 点起的分钟数)/ `SCHEDULE_WEEKDAYS`(✅ P2 修复新增:课表周常量数组,供 schedule-view 及拆分出的 3 个对话框子组件复用,替换原 schedule-view.tsx 内联的 WEEKDAYS 常量)
- Hooks`useClassData`admin/grade 共享对话框+表单状态管理+重置逻辑P1-E 抽取)/ `useClassFilters`按学校筛选年级admin 模式使用P1-E 抽取)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`school`(✅ P1-1 已修复:通过 school data-access.isGradeHead/isGradeManager/findGradeIdByHeadAndName、`homework`(✅ P0-7 已修复:通过 `homework/data-access-classes` 暴露的函数获取作业数据,不再直查 homework/exams 表)
- 被依赖:`exams`/`homework`/`grades`/`attendance`/`scheduling`/`dashboard`(通过 data-accessP0-4 已修复)/`parent`/`course-plans`/`users`(✅ P1-1 已修复8+ 处直查 classes 表改为通过 classes data-access/`messaging`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId支持学生/家长给班级教师发消息)
**已知问题**
- ✅ P0-1 已修复:`data-access.ts` 已拆分为 5 个文件data-access/data-access-stats/data-access-schedule/data-access-students/data-access-admin所有文件均 ≤800 行
- ✅ P0-5 已修复classSchedule 写函数createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem已迁移至 scheduling/data-access-class-schedule.tsclasses 模块仅保留 classSchedule 读函数getStudentSchedule/getClassSchedule新增 verifyTeacherOwnsClass 供 scheduling 模块跨模块校验教师班级归属
- ✅ P0-7 已修复:`data-access-stats.ts` 和 `data-access-students.ts` 不再直查 `homeworkAssignmentQuestions`/`homeworkAssignmentTargets`/`homeworkAssignments`/`homeworkSubmissions`/`exams` 表,改为调用 `homework/data-access-classes.ts` 暴露的函数(`getAssignmentIdsForStudents`/`getHomeworkAssignmentsWithSubject`/`getHomeworkAssignmentsByIds`/`getAssignmentMaxScoreById`/`getAssignmentTargetCounts`/`getHomeworkSubmissionsForStudents`/`getPublishedHomeworkAssignmentsWithSubject`/`getHomeworkSubmissionsForAssignments`
- ✅ P1-1 已修复:~~`actions.ts` 直查 `grades` 表做权限校验~~ 改为调用 `school/data-access` 函数
- ✅ P1-1 已修复:~~`getSessionTeacherId` 在 data-access 调用 `auth()`~~ 改为通过 `shared/lib/auth-guard.getAuthContext()` 获取
- ✅ P2 已修复:`data-access.ts` 中 `idByName.get(name)!` 非空断言清理为 `flatMap` 安全过滤;`data-access-admin.ts` 中同类非空断言清理
- ✅ P0-3 修复2026-06-22~~`actions.ts` 974 行接近 1000 行硬上限~~ 拆分为 6 个文件actions-teacher/actions-admin/actions-grade/actions-invitations/actions-schedule/actions-shared原 `actions.ts` 改为 50 行 barrel re-export
- ✅ P1-1 修复2026-06-22~~`ctx.roles.includes("admin"/"teacher"/"student")` 角色硬编码~~ 改为 `hasAdminScope(ctx)`/`hasTeacherScope(ctx)`/`hasStudentScope(ctx)` 基于 `dataScope.type` 判断
- ✅ P1-4 修复2026-06-22`types.ts` 中 ClassHomeworkInsights 等跨领域类型保留在 classes 模块(因为是 classes 对 homework 数据的视图),添加注释说明归属决策
- ✅ P1-D 修复2026-06-24~~`admin-classes-view.tsx` 533 行与 `grade-classes-view.tsx` 439 行 ~80% 代码重复~~ 拆分为组合模式:抽取 `ClassListTable` / `ClassFormDialog` / `ClassDeleteDialog` / `ClassListToolbar` 4 个共享组件 + `class-form-utils``formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型admin-classes-view 降至 159 行grade-classes-view 降至 153 行admin 删除弹窗统一改用 `ConfirmDeleteDialog`(与 grade 一致)
- ✅ P1-E 修复2026-06-24抽取 `useClassData` hook对话框开关+表单字段+提交中标记+重置逻辑admin/grade 共享)+ `useClassFilters` hook按学校筛选年级admin 模式使用)
- ✅ P0-A 修复2026-06-24~~classes 模块 13/14 个组件未接入 i18n~~ 全部 13 个组件admin-classes-view/grade-classes-view/my-classes-grid/schedule-view/schedule-filters/students-filters/students-table/class-detail 下 7 个子组件/edit-class-dialog已接入 `useTranslations("classes")`my-classes-grid 中 5 处硬编码英文("Failed to regenerate"/"Failed to copy"/"No Room"/"vs last week")已替换为翻译键
- ✅ P0-B 修复2026-06-24~~classes.json 仅 55 行~~ 扩充至 ~370 行,覆盖 invitation/class/list/form/delete/filters/myClasses/students/schedule/detailheader/overview/trends/assignments/widgets/edit/empty/grade/bulk/errors 全部场景en 镜像同步
- ✅ P1-C 修复2026-06-24新增 `class-error-boundary.tsx`class component Error Boundary + i18n + router.refresh 重试)和 `class-skeleton.tsx`ClassListSkeleton/ClassCardSkeleton/ClassGridSkeleton/StudentListSkeleton/ScheduleGridSkeleton6 个页面均已包裹 Error Boundaryadmin/school/classes + teacher/classes/my + teacher/classes/my/[id] + teacher/classes/schedule + teacher/classes/studentsClassErrorBoundary、management/grade/dashboardSchoolErrorBoundaryschedule/students 页面 EmptyState 硬编码英文替换为 i18n
- ✅ 2026-06-26 审计修复i18n 全覆盖 + 越权校验 + 角色硬编码消除 + 错误处理统一 + 路由错误边界完善):
- **i18n 翻译键扩充**classes.json 新增 `actions` 命名空间42 个键,覆盖全部 Server Action 错误/成功消息)、`error` 命名空间3 个键title/description/retry供 ErrorState 组件使用)、`filters` 命名空间新增 10 个键、`detail` 命名空间新增 12 个键
- **越权校验加固**`actions-schedule.ts` 新增 `assertClassOwnership` 辅助函数(调用 `getClassIdByScheduleId` + `verifyTeacherOwnsClass`create/update/delete 三个 Action 增加班级归属校验,防止教师通过 scheduleId 修改他人班级课表;`actions-teacher.ts` 的 `updateTeacherClassAction` / `deleteTeacherClassAction` 增加越权校验
- **角色硬编码消除**`data-access.ts` / `data-access-teacher.ts` / `data-access-admin.ts` 中所有 `roles.name, "teacher"` / `roles.name, "student"` 改为 `ROLE_NAMES.TEACHER` / `ROLE_NAMES.STUDENT`(共 7 处),统一引用 `@/shared/types/permissions` 常量
- **错误处理统一**`actions-admin.ts` / `actions-grade.ts` / `actions-invitations.ts` / `actions-schedule.ts` / `actions-teacher.ts` 所有 catch 块改为使用 `handleActionError`(来自 `@/shared/lib/action-utils`),所有 message 改为使用 `await getTranslations("classes")` + t(),移除 `PermissionDeniedError` 直接 import`actions-invitations.ts` 的 `assertClassOwnership` 签名变更:新增 `t` 参数;`actions-shared.ts` 的 `toWeekday` / `parseSubjectTeachers` 改为 async使用 `ValidationError` 代替 `throw new Error`
- **DEFAULT_CLASS_SUBJECTS 回退逻辑**`data-access.ts` 的 `getClassSubjects()` 新增回退逻辑——subjects 表为空时返回 `DEFAULT_CLASS_SUBJECTS``data-access-teacher.ts` / `data-access-admin.ts` 中 `inArray(subjects.name, DEFAULT_CLASS_SUBJECTS)` 改为 `inArray(subjects.name, await getClassSubjects())`,确保筛选条件与回退数据源一致
- **DB 错误处理修正**`data-access-teacher.ts` 的 `getTeacherClasses()` DB 错误时 throw 而非返回空数组(避免静默吞错)
- **路由错误边界完善**10 个 error.tsx + 1 个 loading.tsx 新增teacher/classes/my/error.tsx、teacher/classes/my/[id]/error.tsx + loading.tsx、teacher/classes/students/error.tsx、teacher/classes/schedule/error.tsx、student/learning/error.tsx + loading.tsx、student/learning/courses/error.tsx、student/learning/courses/[classId]/error.tsx、student/schedule/error.tsx统一使用 shared 层 `ErrorState` 组件
- **新增工具函数文件**`components/schedule-utils.ts` 抽取 `getSubjectColor` / `getPositionStyle` / `timeToMinutes` 3 个纯函数(修复中文科目颜色分组失效 bug + 提升可测试性)
- **新增权限点使用**`student/learning/page.tsx` / `student/learning/courses/page.tsx` / `student/learning/courses/[classId]/page.tsx` / `student/schedule/page.tsx` 接入 `requirePermission(CLASS_READ)`
- **generateMetadata 接入**(✅ P2 修复新增8 个 page.tsx 新增 `generateMetadata` 导出(通过 next-intl `getTranslations` 生成页面标题):`teacher/classes/my/page.tsx`、`teacher/classes/my/[id]/page.tsx`、`teacher/classes/schedule/page.tsx`、`teacher/classes/students/page.tsx`、`student/learning/page.tsx`、`student/learning/courses/page.tsx`、`student/learning/courses/[classId]/page.tsx`、`student/schedule/page.tsx`
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 548 | 核心班级 CRUD + 邀请码 + 教师班级管理(含 re-export 向后兼容) |
| `data-access-stats.ts` | 513 | 作业统计查询(班级/年级作业洞察,通过 homework/data-access-classes 获取数据) |
| `data-access-schedule.ts` | 93 | 课表查询(学生/班级课表只读P0-5 已修复:写函数已迁移至 scheduling 模块) |
| `data-access-students.ts` | 253 | 学生相关查询(科目成绩、学生名单、学生班级,通过 homework/data-access-classes 获取数据) |
| `data-access-admin.ts` | 406 | 管理员班级管理(管理员班级 CRUD、年级管理班级查询 |
| `actions.ts` | 50 | Barrel re-exportP0-3 修复:从 974 行拆分为 6 个文件) |
| `actions-teacher.ts` | 100 | 教师班级 CRUD3 个 Action |
| `actions-admin.ts` | 120 | 管理员班级 CRUD3 个 Action |
| `actions-grade.ts` | 110 | 年级组长班级 CRUD3 个 Action |
| `actions-invitations.ts` | 502 | 邀请码与注册 + 批量操作10 个 ActionP2-4 新增批量导入学生/批量分配教师) |
| `actions-schedule.ts` | 90 | 班级课表 CRUD3 个 Action |
| `actions-shared.ts` | 60 | 共享工具hasAdminScope/hasTeacherScope/hasStudentScope/parseSubjectTeachers/toWeekday |
| `schema.ts` | 152 | Zod 校验13 个 schema教师/管理员/年级班级 CRUD + 课表 CRUD + 邮箱注册) |
| `types.ts` | 201 | 类型定义含跨领域类型说明注释P1-4 修复) |
| components/admin-classes-view.tsx | 159 | 管理员班级客户端P1-D 重构:组合共享组件) |
| components/grade-classes-view.tsx | 153 | 年级班级客户端P1-D 重构:组合共享组件) |
| components/class-list-table.tsx | 115 | 班级列表表格admin/grade 共享P1-D 抽取) |
| components/class-form-dialog.tsx | 200 | 班级创建/编辑表单弹窗admin/grade 共享P1-D 抽取) |
| components/class-delete-dialog.tsx | 35 | 班级删除确认弹窗admin/grade 共享P1-D 抽取) |
| components/class-list-toolbar.tsx | 30 | 班级列表工具栏admin/grade 共享P1-D 抽取) |
| components/class-form-utils.ts | 35 | formatSubjectTeachers + 表单类型P1-D 抽取) |
| components/schedule-utils.ts | 89 | 课表纯函数工具集2026-06-26 审计修复新增getSubjectColor/getPositionStyle/timeToMinutes✅ P2 修复新增 SCHEDULE_WEEKDAYS 周常量;修复中文科目颜色分组失效 bug + 提升可测试性) |
| components/class-error-boundary.tsx | 45 | 班级模块 Error BoundaryP1-C 新增) |
| components/class-skeleton.tsx | 80 | 班级模块骨架屏组件5 个List/Card/Grid/StudentList/ScheduleGridP1-C 新增) |
| components/my-classes-grid.tsx | 418 | 教师班级卡片网格P0-A 修复:已接入 i18n |
| components/schedule-view.tsx | 175 | 课表视图P0-A 修复:已接入 i18n✅ P2 修复:拆分出 ScheduleCreateDialog/ScheduleEditDialog/ScheduleDeleteDialog 3 个对话框子组件,主组件从 464 行降至 175 行;不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态下沉到子对话框WEEKDAYS 常量改用从 ./schedule-utils 导入的 SCHEDULE_WEEKDAYS |
| components/schedule-create-dialog.tsx | - | 课表新增对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleCreate 状态) |
| components/schedule-edit-dialog.tsx | - | 课表编辑对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleUpdate 状态) |
| components/schedule-delete-dialog.tsx | - | 课表删除确认对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleDelete 状态) |
| components/schedule-filters.tsx | 60 | 课表筛选器P0-A 修复:已接入 i18n |
| components/students-filters.tsx | 60 | 学生筛选器P0-A 修复:已接入 i18n |
| components/students-table.tsx | 120 | 学生表格P0-A 修复:已接入 i18n |
| components/class-detail/class-header.tsx | 100 | 班级详情头部P0-A 修复:已接入 i18n |
| components/class-detail/class-overview-stats.tsx | 80 | 班级概览统计P0-A 修复:已接入 i18n |
| components/class-detail/class-quick-actions.tsx | 60 | 快捷操作P0-A 修复:已接入 i18n |
| components/class-detail/class-schedule-widget.tsx | 120 | 课表 WidgetP0-A 修复:已接入 i18n |
| components/class-detail/class-assignments-widget.tsx | 100 | 作业 WidgetP0-A 修复:已接入 i18n |
| components/class-detail/class-students-widget.tsx | 120 | 学生 WidgetP0-A 修复:已接入 i18n |
| components/class-detail/class-trends-widget.tsx | 150 | 趋势 WidgetP0-A 修复:已接入 i18n |
| components/class-detail/edit-class-dialog.tsx | 145 | 编辑班级对话框P0-A 修复:已接入 i18n |
| hooks/use-class-data.ts | 95 | 对话框+表单状态管理 hookP1-E 抽取) |
| hooks/use-class-filters.ts | 30 | 按学校筛选年级 hookP1-E 抽取) |
**路由文件清单**`app/(dashboard)/`2026-06-26 审计修复新增错误边界 + 加载骨架):
| 文件 | 职责 |
|------|------|
| `teacher/classes/my/error.tsx` | 教师我的班级错误边界(使用 shared `ErrorState` + namespace="classes" |
| `teacher/classes/my/[id]/error.tsx` | 班级详情错误边界(使用 shared `ErrorState` |
| `teacher/classes/my/[id]/loading.tsx` | 班级详情加载骨架ClassGridSkeleton |
| `teacher/classes/students/error.tsx` | 学生管理错误边界(使用 shared `ErrorState` |
| `teacher/classes/schedule/error.tsx` | 课表错误边界(使用 shared `ErrorState` |
| `student/learning/error.tsx` | 学生学习中心错误边界(使用 shared `ErrorState` |
| `student/learning/loading.tsx` | 学生学习中心加载骨架 |
| `student/learning/courses/error.tsx` | 学生课程列表错误边界(使用 shared `ErrorState` |
| `student/learning/courses/[classId]/error.tsx` | 学生课程详情错误边界(使用 shared `ErrorState` |
| `student/schedule/error.tsx` | 学生课表错误边界(使用 shared `ErrorState` |
---
## 2.8 school学校模块
**职责**:学校/学年/部门/年级的 CRUD。
**导出函数**
- Actions`createSchoolAction` / `updateSchoolAction` / `deleteSchoolAction` / `createAcademicYearAction` / `updateAcademicYearAction` / `deleteAcademicYearAction` / `createDepartmentAction` / `updateDepartmentAction` / `deleteDepartmentAction` / `createGradeAction` / `updateGradeAction` / `deleteGradeAction` / `promoteGradesAction`(编排层:权限校验 + Zod 校验 + 调用 data-access + revalidatePath + after(logAudit)`promoteGradesAction` 年级升级,审计日志 `grade.promote`
- Data-access只读查询`getSchools` / `getGrades` / `getDepartments` / `getAcademicYears` / `getStaffOptions` / `getGradesForStaff` / `getOrgTree` / `getSubjectOptions` / `getGradeOptions` / `getSubjectNameMapByIds`P1-1 新增:批量科目名称映射,供 homework/data-access-classes 调用)/ `getGradeOverviewStats`(✅ v4-P2-6 新增:年级概览统计,返回 `GradeOverviewStats[]`,每个年级的 classCount/studentCount/teacherCount供年级管理卡片视图使用动态导入 classes/data-access 避免循环依赖))+ 写操作(`create/update/delete` × `Department/School/Grade/AcademicYear`+ `promoteGrades(schoolId)` 年级升级order +1 + 名称升级,辅助函数 `promoteGradeName`
- Types✅ v4-P2-6 新增):`GradeOverviewStats`(年级概览统计接口:{ gradeId, classCount, studentCount, teacherCount },位于 data-access.ts
- Components`SchoolsClient` / `SchoolFormDialog` / `SchoolDeleteDialog` / `SchoolListToolbar` / `SchoolErrorBoundary` / `SchoolListSkeleton` / `SchoolCardSkeleton` / `OrgTreeNav` / `GradesClient`(✅ v4-P2-6 更新:新增 `gradeStats: GradeOverviewStats[]` prop渲染年级概览卡片视图——每个年级卡片展示班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口)/ `GradeInsightsFilters`(✅ v4-P2-6 新增:年级洞察筛选器,使用 ChipNav 替代原生 form get点击 chip 即时通过 URL 参数切换,无整页刷新)/ `GradeDistributionPanel` / `GradeHomeworkPanel` / `GradeExamsPanel` / `GradeProgressPanel`(✅ v4-P2-7 新增:年级仪表盘 4 个维度面板组件,位于 `components/grade-dashboard/` 子目录,服务端组件直接渲染数据,无客户端交互)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`users`(⚠️ `getStaffOptions` 直查 users/roles可接受
- 被依赖:`exams`(✅ P1-1 已修复:通过 school data-access、`homework`(✅ P1-1 已修复:通过 school data-access、`grades`(✅ P1-1 已修复:通过 school data-access、`questions`(✅ P1-1 已修复:通过 textbooks data-access、`classes`(✅ P1-1 已修复:通过 school data-access、`course-plans`(合理)
**已知问题**
- ✅ P0-8 已修复:`actions.ts` 不再直接导入 `db` 和 schema所有 DB 写操作下沉到 `data-access.ts`,符合三层架构
- ✅ P2 已修复:`logAudit()` 通过 Next.js `after()` 异步非阻塞执行
- ✅ P2 已修复:`data-access.ts` 中 8 处 catch 块添加 `console.error` 输出错误上下文getDepartments/getAcademicYears/getSchools/getGrades/getStaffOptions/getGradesForStaff/getSubjectOptions/getGradeOptions
- ⚠️ P2审计日志不一致仅 school 实体记录department/academicYear/grade 未记录)
- ⚠️ P2`getStaffOptions`/`getGrades` 直查 users/roles展示用可接受
- ✅ P0-2 修复2026-06-22~~年级 CRUD 逻辑与 `grade-management` 模块重复定义~~ `grade-management` 死模块已删除,年级 CRUD 统一由 school 模块负责
- ✅ P0-5 修复2026-06-22~~`school/components/*` 4 个组件缺少 i18n~~ 全部 4 个组件schools-view/grades-view/departments-view/academic-year-view已接入 `useTranslations("school")``school.json` i18n 文件已创建并扩充
- ✅ P1-3 修复2026-06-22新增 school-error-boundary.tsxclass component Error Boundary + i18n + router.refresh 重试)和 school-skeleton.tsxSchoolListSkeleton 表格骨架 + SchoolCardSkeleton 卡片骨架4 个页面schools/grades/departments/academic-year均已包裹 SchoolErrorBoundaryschool.json 补充 errors.boundary.* 翻译键
- ✅ P1-5 修复2026-06-22~~`schools-view.tsx` 硬编码 Table+Dialog+AlertDialog~~ 拆分为组合模式SchoolListToolbar + SchoolFormDialog + SchoolDeleteDialog + useSchoolData hook
- ✅ P1-6 修复2026-06-22新增 `getSchoolsForUser(userId)` / `getGradesForUser(userId)` 权限感知查询函数,根据用户角色返回可见数据范围
- ✅ P2-1 修复2026-06-22抽取 `use-school-data` hook将对话框状态管理逻辑与 UI 分离
- ✅ P2-2 修复2026-06-23新增 `getOrgTree()` data-access 函数(学校→年级→班级三级树,通过 `import("@/modules/classes/data-access")` 动态导入获取班级数据避免循环依赖)+ `OrgTreeNode` 类型 + `OrgTreeNav` 组件(三级树形导航,支持搜索过滤/选中高亮/展开折叠/不同节点类型图标school.json 补充 `orgTree.*` 翻译键
- ✅ P2-3 修复2026-06-23新增 `promoteGradesAction` + `promoteGrades(schoolId)` + `promoteGradeName(name)` 辅助函数(中文数字 一→二…十二、阿拉伯数字 1→2…12 识别),按 order 降序逐条 +1 避免唯一约束冲突;含 `after(() => logAudit({ action: "grade.promote" }))` 审计日志
- ✅ P2-4 修复2026-06-23新增 `bulkEnrollStudentsAction`CSV 批量导入学生,复用 enrollStudentByEmail+ `bulkAssignSubjectTeachersAction`CSV 批量分配教师,简化实现含 TODO 待完善查找逻辑classes/actions.ts barrel 导出
- ✅ P2-5 修复2026-06-23为 department/academicYear/grade 的 9 个 CRUD Action 补充 `after(() => logAudit(...))` 审计日志action: department.create/update/delete、academicYear.create/update/delete、grade.create/update/delete与 school 实体审计日志策略一致
- ✅ v4-P2-6 改进2026-06-23年级管理体验增强——新增 `getGradeOverviewStats()` data-access 函数(返回 `GradeOverviewStats[]`:每个年级的 classCount/studentCount/teacherCount动态导入 classes/data-access 避免循环依赖);`GradesClient` 新增 `gradeStats` prop渲染年级概览卡片视图班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口admin/school/grades/page.tsx 新增 `getGradeOverviewStats()` 查询并传入 GradesClient
- ✅ v4-P2-6 改进2026-06-23新增 `GradeInsightsFilters` 组件(使用 ChipNav 替代原生 form get点击 chip 即时通过 URL 参数切换无整页刷新admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写,使用 `GradeInsightsFilters` 替代原生 form get添加 i18n表格添加 `overflow-x-auto` 水平滚动
- ✅ P1-A 修复2026-06-24~~`grades-view.tsx` 860 行超出组件行数限制~~ 拆分为组合模式:`GradeFormDialog`(创建/编辑表单对话框,含客户端校验+去重+isDirty 检测)+ `GradeDeleteDialog`(删除确认)+ `GradeListToolbar`(搜索/筛选/排序工具栏)+ `GradeOverviewCards`(年级概览卡片视图)+ `useGradeData` hook对话框状态管理主容器 `grades-view.tsx` 降至 290 行,参照 `schools-view.tsx` 组合模式
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 457 | 13 个 Server Action编排层无 DB 直访;含 `promoteGradesAction` 年级升级) |
| `data-access.ts` | 782 | 只读查询 + 12 个写操作 + `promoteGrades` 年级升级 + `getOrgTree` 组织架构树 + 跨模块查询接口 + 权限感知函数getSchoolsForUser/getGradesForUser |
| `schema.ts` | 56 | Zod 校验(含 `PromoteGradesSchema` |
| `types.ts` | 103 | 类型定义(含 Insert/Update 入参类型 + `OrgTreeNode` |
| components/schools-view.tsx | 132 | 学校列表容器组合模式P1-5 修复) |
| components/school-form-dialog.tsx | 80 | 学校创建/编辑对话框P1-5 修复) |
| components/school-delete-dialog.tsx | 50 | 学校删除确认对话框P1-5 修复) |
| components/school-list-toolbar.tsx | 30 | 学校列表工具栏P1-5 修复) |
| components/school-error-boundary.tsx | 72 | 共享 Error BoundaryP1-3 修复) |
| components/school-skeleton.tsx | 69 | 共享骨架屏P1-3 修复) |
| components/org-tree-nav.tsx | 134 | 学校→年级→班级三级树形导航P2-2 修复:搜索过滤 + 选中高亮 + 展开折叠 + 节点类型图标) |
| components/grades-view.tsx | 290 | 年级管理客户端P1-A 修复:拆分为组合模式,仅负责列表渲染+筛选+组合子组件) |
| components/grade-form-dialog.tsx | 280 | 年级创建/编辑表单对话框P1-A 新增:含客户端校验+去重+isDirty 检测useActionMutation 管理 mutation |
| components/grade-delete-dialog.tsx | 65 | 年级删除确认对话框P1-A 新增) |
| components/grade-list-toolbar.tsx | 115 | 年级列表工具栏P1-A 新增:搜索+学校筛选+主任筛选+排序+新建按钮) |
| components/grade-overview-cards.tsx | 165 | 年级概览卡片视图P1-A 新增:班级/学生/教师数+年级主任/教学主任+快捷操作) |
| components/grade-insights-filters.tsx | 48 | v4-P2-6 新增年级洞察筛选器ChipNav 替代原生 form get点击 chip 即时通过 URL 参数切换,无整页刷新) |
| hooks/use-school-data.ts | 40 | 学校数据管理 hookP2-1 修复) |
| hooks/use-grade-data.ts | 42 | 年级数据管理 hookP1-A 新增:对话框状态管理,参照 use-school-data 模式) |
---
## 2.8b grade-management年级管理模块— ✅ 已删除
> **2026-06-22 审计发现**该模块拥有完整的理想架构Service 接口 + Context DI + 角色配置 + Error Boundary + Skeleton + i18n + hooks 分离),但 **13 个相关页面中无任何一个导入此模块**。`management/grade/*` 页面实际依赖 `classes` 和 `school` 模块的 data-access。
>
> **2026-06-22 处置决策P0-1/P0-2 修复)**:该死模块已**完整删除**。年级 CRUD 统一由 `school` 模块负责(`school/actions.ts` + `school/data-access.ts`),避免两套重复实现。详见 `docs/architecture/audit/school-grade-class-audit-report.md`。
---
## 2.9 scheduling排课模块
**职责**:自动排课算法 + 课表调整 + 排课规则管理。
**导出函数**
- Actions`autoScheduleAction` / `applyAutoScheduleAction` / `getSchedulingRulesAction` / `updateSchedulingRulesAction` / `getScheduleChangesAction` / `createScheduleChangeAction` / `updateScheduleChangeAction` / `deleteScheduleChangeAction`
- Data-access`getSchedulingRules` / `getScheduleChanges` / `getAdminClassesForScheduling` / `getTeachersForScheduling` / `getClassroomsForScheduling` / `getClassSubjectsForScheduling`
- Data-access-class-schedule✅ P0-5 新增):`createClassScheduleItem` / `updateClassScheduleItem` / `deleteClassScheduleItem`(从 classes 模块迁移,含教师班级归属校验,通过 `classes/data-access.verifyTeacherOwnsClass` 跨模块校验)
- Data-access低级写入`insertClassScheduleItem` / `updateClassScheduleItemById` / `deleteClassScheduleItemById` / `replaceClassSchedule`(统一 classSchedule DB 写入口)
- 算法:`findOptimalSlot` / `validateSchedule` / `autoSchedule` / `buildDefaultTimeSlots`(纯函数,标杆)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`classes`(✅ P0-5 已修复:通过 `classes/data-access.verifyTeacherOwnsClass` / `getTeacherIdForMutations` 校验教师班级归属,不再直写 classSchedule 表的写入口分散在 classes 模块)、`school`(⚠️ 排课辅助查询,可接受)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds
- 被依赖:`classes/actions.ts`(✅ P0-5 已修复:通过 `scheduling/data-access-class-schedule` 调用写函数)
**已知问题**
- ✅ P0-5 已修复:~~`applyAutoScheduleAction` 直接 transaction 写 `classSchedule` 表(第三个写入口)~~ 改为调用 `replaceClassSchedule` 统一写入口classSchedule 所有写函数统一在 scheduling 模块
- ✅ P1-1 已修复:~~`autoScheduleAction` 直查 `users` 表~~ 改为调用 `users/data-access.getUserNamesByIds`
- ⚠️ P2`actions.ts` 末尾 re-export data-access 函数(反模式)
- ✅ P2 已修复:`data-access.ts` 中 3 处非空断言清理(`userIds[0]!`、`rows[i]!`、`rows[j]!``auto-scheduler.ts` 中 2 处非空断言清理(`schedule[i]!`、`schedule[j]!`
- ✅ `auto-scheduler.ts` 是算法独立化的最佳实践(纯函数、无 DB、可测试
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `auto-scheduler.ts` | 310 | 排课算法(纯函数,标杆) |
| `actions.ts` | 302 | 8 个 Server Action |
| `data-access.ts` | 398 | 排课辅助查询 + 规则/变更 CRUD + classSchedule 低级写入insert/update/delete/replace |
| `data-access-class-schedule.ts` | 165 | classSchedule 业务写入P0-5 新增,从 classes 模块迁移,含教师归属校验) |
| `schema.ts` | - | Zod 校验 |
| `types.ts` | - | 类型定义(含 P0-5 迁移的 CreateClassScheduleItemInput / UpdateClassScheduleItemInput |
---
## 2.10 attendance考勤模块— 结构典范
**职责**:考勤记录管理 + 统计分析 + 规则配置。✅ L-6 节次考勤:支持按节次(早读/上午/下午/晚自习/全日)记录与过滤。✅ L-9 关联分析: 考勤与成绩跨模块关联分析(Pearson 相关系数 + 风险分级)。
**导出函数**
- Actions5 个写 Action`recordAttendanceAction`(✅ L-6formData 读取 period/ `batchRecordAttendanceAction`(✅ L-2保存成功后调用 `notifyParentsOfAbsence` 异步通知家长,失败不阻断主流程;✅ L-6每条记录携带 period/ `updateAttendanceAction`(✅ L-6formData 读取 period/ `deleteAttendanceAction` / `saveAttendanceRulesAction`(✅ 审计修复:均调用 `requirePermission(ATTENDANCE_MANAGE)` + 资源归属校验 + `trackEvent` 埋点)
- Data-access`getAttendanceRecords`(✅ 审计修复:移除跨模块 JOIN委托 `getUserNamesByIds`/`getClassNamesByIds`;✅ L-6支持按 period 过滤)/ `getClassAttendanceForDate`(✅ L-6新增可选 period 参数)/ `buildPeriodFilter`(✅ L-6 新增纯函数full_day 匹配 NULL OR 'full_day' 向后兼容)/ `createAttendanceRecord`(✅ L-6写入 period 默认 full_day/ `batchCreateAttendanceRecords`(✅ L-6批量写入 period/ `updateAttendanceRecord`(✅ L-6支持更新 period/ `deleteAttendanceRecord` / `getClassStudentsForAttendance`(✅ 审计修复:委托 `classes/data-access.getClassActiveStudentsWithInfo`/ `getAttendanceStats`(✅ 审计修复SQL 聚合 + 返回统一 `AttendanceStats` 类型)/ `upsertAttendanceRules` / `getAttendanceRules` / `getAttendanceRecordClassId` / `syncExcusedFromLeaveRequest`(✅ L-5 新增:请假审批通过后遍历日期区间插入 excused 记录,幂等不覆盖已有记录;✅ L-6插入时 period: full_day被 `leave-requests/actions.reviewLeaveRequestAction` 调用)
- Data-access-stats`getStudentAttendanceSummary`(✅ 审计修复SQL 聚合 + 委托 data-access✅ L-6recentRecords 映射 period/ `getClassAttendanceStats`(✅ 审计修复SQL 聚合 + 委托 data-access✅ L-6studentRecords 映射 period/ `computeStats`(✅ 纯函数导出,可单测)
- Notifications✅ L-2 新增):`notifyParentsOfAbsence`批量考勤录入后多渠道通知家长in_app/sms/email/wechat一次性批量查询 parent-student 映射避免 N+1
- Import-export`exportAttendanceRecordsToExcel`(✅ 审计修复:类型守卫 + i18n 列头)
- Services✅ 审计新增 P1-12/P2-6`AttendanceDataService` 接口 / `AttendanceReadService` / `AttendanceWriteService` / `AttendanceRepository` / `AttendanceProvider` / `useAttendanceService` / `createAttendanceDataService` / `createAttendanceReadService`
- Components`AttendanceSheet`(✅ 审计修复AlertDialog + 常量统一 + 容器内 keydown + useMemo✅ L-6新增节次选择器3 列布局,提交时携带 period/ `AttendanceRecordList`(✅ 审计修复:`usePermission` 控制删除按钮 + `resolveActionError`;✅ L-6新增节次列null 视为 full_day 展示)/ `AttendanceFilters` / `AttendanceStatsCard` / `AttendanceStatsCards`(✅ 审计修复:统一 `AttendanceStats` 类型)/ `AttendanceStatsClassSelector` / `AttendanceRulesForm`(✅ 审计修复:`resolveActionError`/ `StudentAttendanceView` / `AttendancePageLayout`
- Data-access-correlation(✅ L-9 新增): `getAttendanceGradeCorrelation`(跨模块委托 classes/users/grades data-access 聚合数据) / `canAccessCorrelationAnalysis`(权限校验)
- Correlation-compute(✅ L-9 新增纯函数, 与 IO 解耦便于单测): `computePearsonCorrelation` / `classifyRiskLevel` / `interpretCorrelation` / `computeCorrelationSummary`
- Components L-9(✅ L-9 新增): `AttendanceGradeCorrelationCard`(散点图 + 风险表格, recharts ScatterChart 风险等级着色)
**依赖关系**
- 依赖:`shared/*`(含 `shared/constants/attendance-status` ✅ 审计新增:状态类型与视觉常量下沉至 shared、`@/auth`、`classes`(✅ 审计修复:通过 `classes/data-access` 获取班级学生/名称)、`users`(✅ 审计修复:通过 `users/data-access.getUserNamesByIds` 获取姓名)、`parent`(✅ L-2 新增:通过 `parent/data-access.getParentStudentMapByStudentIds` 获取家长-学生映射)、`notifications`(✅ L-2 新增:通过 `sendBatchNotifications` 多渠道分发)、`grades`(✅ L-9 新增:通过 `grades/data-access.getGradeRecords` 获取成绩数据用于关联分析)、`next-intl`
- 被依赖:`parent`(✅ 审计修复:通过 `createAttendanceReadService` 接口消费,不再直接 import attendance 组件;常量改从 `shared/constants/attendance-status` 导入)、`app/api/export`(通过 `exportAttendanceRecordsToExcel`)、`leave-requests`(✅ L-5 新增:审批通过后通过 `syncExcusedFromLeaveRequest` 同步 excused 考勤记录)
**L-6 节次考勤数据模型**
- DB`attendanceRecords.period` enum('morning_reading','morning','afternoon','evening','full_day') nullable向后兼容 NULL 视为 full_day
- 索引:`classDatePeriodIdx`class_id + date + period 复合索引)
- 迁移:`drizzle/0021_attendance_period.sql`
- 类型:`AttendancePeriod`types.ts`AttendanceRecord`/`AttendanceListItem`/`AttendanceQueryParams` 新增 period 字段
- Schema`AttendancePeriodEnum`Zod`RecordAttendanceSchema` 默认 full_day`UpdateAttendanceSchema` 可选 period
- i18n`attendance.period.{label,morning_reading,morning,afternoon,evening,full_day,allPeriods}` 中英文翻译
- 过滤策略:`buildPeriodFilter` 对 full_day 使用 `or(isNull(period), eq(period, "full_day"))` 匹配旧数据
- 对外统一mapListItem 中 `r.period ?? "full_day"`,前端展示 null 统一为 full_day
**L-9 考勤与成绩关联分析**
- 类型: `AttendanceGradeRiskLevel`(high/medium/low) / `AttendanceGradeCorrelationItem` / `AttendanceGradeCorrelationSummary`(types.ts)
- 纯函数(correlation-compute.ts) `computePearsonCorrelation`(Pearson r) / `classifyRiskLevel`(high=低出勤+低分, medium=中低出勤+中低分, low=其余) / `interpretCorrelation` / `computeCorrelationSummary`
- 数据访问(data-access-correlation.ts) `getAttendanceGradeCorrelation` 跨模块委托 classes/users/grades data-access; `canAccessCorrelationAnalysis` scope 校验
- UI(attendance-grade-correlation-card.tsx) recharts ScatterChart 散点图, 风险等级着色(red/amber/emerald)
- 集成: admin/attendance/page.tsx 指定班级时显示
- i18n attendance.correlation.* 中英文翻译
**已知问题**(详见 `docs/architecture/audit/attendance-audit-report.md`
- ✅ P0-1 已修复teacher/sheet、teacher/stats 页面增加 `requirePermission(ATTENDANCE_READ)`
- ✅ P0-2 已修复teacher 分页使用后端返回的 `totalPages`
- ✅ P0-3 已修复:`safeParseDate` fieldName 改为中性 key前端通过 `resolveActionError` 按 errorCode 本地化
- ✅ P0-4 已修复error.tsx 重试按钮改用 `t("actions.retry")`
- ✅ P0-5 已修复data-access 移除跨模块 JOIN改为委托 data-access
- ✅ P1-1 已修复teacher/student 主页面增加 `requirePermission(ATTENDANCE_READ)`
- ✅ P1-2 已修复parent 通过 `AttendanceReadService` 接口消费
- ✅ P1-3 已修复parent-attendance-calendar 改从 `shared/constants/attendance-status` 导入
- ✅ P1-6 已修复export.ts 类型守卫替代 `as` 断言
- ✅ P1-7 已修复calendar 使用 `useLocale()` 替代硬编码 locale
- ✅ P1-8 已修复teacher 子路由新增 error.tsx
- ✅ P1-9 已修复:常量统一从 `constants.ts`re-export shared导入
- ✅ P1-10 已修复4 个 error.tsx 抽取为 shared `RouteErrorBoundary`
- ✅ P1-11 已修复:`getClassAttendanceStats` 改用 SQL 聚合
- ✅ P1-12 已修复:新增 `AttendanceDataService` 接口 + Context 注入
- ✅ P2-1 已修复:删除按钮 `usePermission` 控制显隐
- ✅ P2-5 已修复stats 数据结构统一为 `AttendanceStats`
- ✅ P2-6 已修复:导出 `AttendanceRepository` 接口类型
- ✅ P2-7 已修复keydown 限容器内 + 排除 Select/contenteditable
- ✅ P2-8 已修复parent-attendance-warning/rate-card 改为 RSC
- ✅ P2-9 已修复statusCounts 使用 `useMemo`
- ✅ stats 独立拆分为 `data-access-stats.ts`(拆分范例)
- ✅ DataScope 完整接入 6 种 scope 类型
- ✅ actions 层无直接 DB 访问
- ✅ Server Action 均有 `trackEvent` 埋点
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 258 | 5 个写 Server Action权限校验 + 归属校验 + Zod + 埋点;✅ L-6 读取 period |
| `data-access.ts` | ~410 | 考勤 CRUD + 规则 upsert + 总览统计SQL 聚合 + 委托 data-access✅ L-6 period 过滤/写入/更新) |
| `data-access-stats.ts` | ~230 | 学生/班级考勤汇总SQL 聚合 + 委托 data-access✅ L-6 period 映射) |
| `export.ts` | 93 | Excel 导出(类型守卫 + i18n 列头) |
| `schema.ts` | ~60 | Zod 校验6 个 schema + ✅ L-6 AttendancePeriodEnum |
| `types.ts` | ~95 | 类型定义(状态常量已迁移至 shared✅ L-6 AttendancePeriod + period 字段) |
| `constants.ts` | 19 | 重导出 `shared/constants/attendance-status` |
| `services/types.ts` | 166 | 服务接口抽象AttendanceDataService/Repository |
| `services/attendance-context.tsx` | 46 | React Context 注入AttendanceProvider + hook |
| `services/attendance-data-service.ts` | 162 | 服务实现 + 工厂函数 |
| `components/attendance-sheet.tsx` | ~480 | 批量点名表单AlertDialog + 容器内 keydown + useMemo✅ L-6 节次选择器) |
| `components/attendance-record-list.tsx` | ~165 | 记录列表 + 删除对话框(权限控制 + resolveActionError✅ L-6 节次列) |
| `components/attendance-filters.tsx` | 94 | URL 同步筛选器 |
| `components/attendance-stats-card.tsx` | 82 | 单卡片统计8 指标) |
| `components/attendance-stats-cards.tsx` | 83 | 管理员总览 6 卡片网格(统一 AttendanceStats |
| `components/attendance-stats-class-selector.tsx` | 27 | 班级筛选 ChipNav |
| `components/attendance-rules-form.tsx` | 155 | 规则配置表单resolveActionError |
| `components/student-attendance-view.tsx` | 111 | 学生只读视图 |
| `components/attendance-page-layout.tsx` | 38 | 页面布局骨架(插槽模式) |
| `correlation-compute.ts` | ~90 | L-9 纯函数: Pearson 相关系数 + 风险分级 + 汇总 (与 IO 解耦) |
| `data-access-correlation.ts` | ~130 | L-9 跨模块关联分析: 委托 classes/users/grades data-access |
| `components/attendance-grade-correlation-card.tsx` | ~220 | L-9 散点图 + 风险表格 (recharts ScatterChart) |
> 架构变更2026-07-05Phase 4.5 — attendance-sheet.tsx 提交状态改用 useOptimistic
> - **attendance-sheet.tsx**`isSubmitting` useState → `useOptimistic<boolean>`。保留 `<form action={handleSubmit}>` 模式 + `useFormStatus` SubmitButtonprogressive enhancement`setOptimisticSubmitting(true)` 在 form action 内调用action 完成后 `optimisticSubmitting` 自动恢复 false移除 `finally` 块
> - **关键决策**:保留 form action 模式而非改用 `useTransition` 包裹,因为 `useFormStatus().pending` 依赖 form action 的 promise 状态,若用 `startTransition` 包裹会导致 form action 立即返回、`useFormStatus` 不可靠;`useOptimistic` 的 `addOptimistic` 必须在 transition 或 action 内调用form action 也算 action
---
## 2.11 users用户模块
**职责**:用户资料管理 + 批量导入导出 + 管理员用户列表管理。
**导出函数**
- Actions`getUserProfileAction` / `updateUserProfileAction` / `importUsersAction` / `exportUsersAction` / `downloadUserTemplateAction` / `updateUserRoleAction` / `deleteUserAction`
- Data-access`getUserProfile` / `getCurrentStudentUser`(✅ P2-20 已修复:从 homework 模块迁移而来6 个 student 页面通过此函数获取学生身份,不再依赖 homework 模块)/ `getAdminUsers`(管理员用户列表分页查询,支持搜索+角色聚合)/ `getAdminUserRoles`(角色名列表,用于筛选下拉框)/ `getAllUserIds(limit=1000, offset=0)`(✅ P3-7全校用户 ID 查询,默认 LIMIT 1000 防止 OOM + 分页参数支持)/ `getUserIdsByGradeId` / `getUserNamesByIds`
- Import-export`generateUserImportTemplate` / `parseUserImportData` / `exportUsersToExcel`+ re-export `batchImportUsers` / `UserImportResult` 保持向后兼容)
- User-service`batchImportUsers`(用户创建 + 密码哈希 + 角色分配)
- Class-registration`registerStudentByInvitationCode`(委托 classes/data-access 完成班级注册)
- Components`UserImportDialog`(批量导入对话框)/ `AdminUsersView`(管理员用户列表客户端组件,搜索+筛选+分页+删除)
**依赖关系**
- 依赖:`shared/*`(含 `shared/lib/role-utils`,✅ P2 已修复:删除本地 `normalizeRoleName`/`resolvePrimaryRole`/`rolePriority`,统一复用 `shared/lib/role-utils.resolvePrimaryRole`)、`@/auth`、`classes`(✅ P1-4 已修复:通过 `class-registration.ts` 调用 `classes/data-access.enrollStudentByInvitationCode`,不再直写 classEnrollments
- 被依赖:`dashboard`(通过 data-accessP0-4 已修复)、`grades`(✅ P1-1 已修复:通过 users data-access、`homework`(✅ P1-1 已修复:通过 users data-access、`scheduling`(✅ P1-1 已修复:通过 users data-access、`diagnostic`(✅ P1-1 已修复:通过 users data-access、`elective`(✅ P1-1 已修复:通过 users data-access、`proctoring`(✅ P1-1 已修复:通过 users data-access、`parent`(✅ P1-1 已修复:通过 users data-access
**已知问题**
- ✅ P1 已解决:`import-export.ts` 四重职责已拆分为 `import-export.ts`(解析/生成)+ `user-service.ts`(用户创建)+ `class-registration.ts`(班级注册)
- ✅ P1 已解决:`batchImportUsers` 不再跨模块直写 `classEnrollments`,改为调用 `classes/data-access.enrollStudentByInvitationCode`
- ✅ P2 已解决:删除本地 `normalizeRoleName`/`resolvePrimaryRole`/`rolePriority`,统一复用 `shared/lib/role-utils.resolvePrimaryRole`,消除重复代码
- ✅ P1-1 已修复:~~`updateUserProfile` 绕过 data-access 直接 DB 写~~ 已下沉到 data-access
- ✅ P2-20 已修复:新增 `getCurrentStudentUser` 函数(从 homework 模块迁移6 个 student 页面通过此函数获取学生身份,不再依赖 homework 模块
- ✅ P2 已解决:`data-access.ts` 已扩充写操作updateUserProfile 已下沉)
- ⚠️ 已知限制:`AdminUsersView` 客户端组件的删除操作通过 `fetch("/api/admin/users/:id")` 调用,对应 API 路由尚未实现(`deleteUserAction` Server Action 已就绪,可作为后续 API 路由的实现基础)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `import-export.ts` | 157 | 文件解析/生成(模板生成 + 解析校验 + Excel 导出)+ re-export 向后兼容 |
| `user-service.ts` | 82 | 用户创建(批量导入 + 密码哈希 + 角色分配) |
| `class-registration.ts` | 21 | 班级注册(委托 classes/data-access |
| `actions.ts` | 218 | 7 个 Server Actionprofile 更新 + 模板下载/导入/导出 + 角色更新 + 删除) |
| `data-access.ts` | 394 | getUserProfile + 用户查询 + 管理员用户列表分页查询getAdminUsers/getAdminUserRoles |
| `components/admin-users-view.tsx` | 290 | 管理员用户列表客户端组件(搜索+筛选+表格+分页+删除对话框) |
---
## 2.12 dashboard仪表盘模块
**职责**:管理员/教师/学生/家长仪表盘数据聚合 + 权限校验 + i18n + 纯逻辑工具函数 + 数据服务接口抽象 + 配置驱动 Widget 渲染 + 监控埋点接口 + 时间范围筛选 + 数据对比 + 自定义仪表盘 + 通知集成 + 实时更新 + 数据导出 + 移动端优化。
**导出函数**
- Actions`getAdminDashboardAction` / `getTeacherDashboardAction` / `getStudentDashboardAction` / `getParentDashboardAction`(均调用 `requirePermission()` 校验对应 `DASHBOARD_*_READ` 权限)
- Data-access`getAdminDashboardData`(并行调用 6 个模块 stats 函数V4 添加趋势数据 TODO 注释)
- Streams`getAdminDashboardStreams`V4 新增:返回各分区独立 Promise 供 React `use()` 流式消费)
- Lib 纯函数:`toWeekday` / `countStudentAssignments` / `sortUpcomingAssignments` / `filterTodaySchedule`V4 修复:移除 `as T[]` 断言,改为非泛型函数)/ `computeTeacherMetrics` / `getGreetingKey` / `timeToMinutes`V4 新增)/ `getScheduleStatus`V4 新增)/ `getDueUrgency`V4 新增)/ `getActionLabelKey`V4 新增)/ `getActionVariant`V4 新增)/ `computeComparison`L3 新增:数据对比计算)/ `toCSV`L7 新增CSV 导出)/ `downloadFile`L7 新增:浏览器下载)/ `exportDashboardCSV`L7 新增:仪表盘 CSV 导出便捷方法)
- ServicesV4 新增):`DashboardService` 接口(抽象数据依赖,含 4 个角色方法签名)/ `DashboardAnalytics` 接口监控埋点Widget 点击 / 空状态 / 错误重试 / 页面停留)/ `DashboardServiceProvider`React Context 依赖注入)/ `useDashboardService` / `useDashboardAnalytics`
- ConfigV4 新增):`ADMIN_WIDGET_CONFIG` / `TEACHER_WIDGET_CONFIG` / `STUDENT_WIDGET_CONFIG` / `PARENT_WIDGET_CONFIG` / `getWidgetConfig`(配置驱动 Widget 布局,新增角色只需修改配置)
- HooksL4/L6 新增):`useDashboardPreferences`L4 自定义仪表盘偏好localStorage 持久化)/ `useDashboardRealtime`L6 SSE 实时更新,指数退避重连)
- Components`AdminDashboardView` / `TeacherDashboardView` / `StudentDashboard` / `ParentDashboard`V4 P1-4 从 parent 模块迁移,使用 slots 组合注入)/ `UserGrowthChart` / `DashboardGreetingHeader`共享问候头部V2 抽象V4 支持空用户名 fallback/ `DashboardSection`(分区 Error Boundary + Suspense + 骨架屏V4 P3-1 新增 ariaLabel prop 支持键盘导航)/ `DashboardTimeRangeFilter`L2 时间范围筛选)/ `ComparisonBadge`L3 数据对比徽章)/ `DashboardNotificationWidget`L5 通知中心 Widget/ `DashboardResponsiveLayout`L8 移动端响应式布局)/ `MobileSwipeContainer`L8 移动端水平滑动)/ `DesktopGrid`L8 桌面端网格)
- TypesV4 新增):`DashboardRole` / `WidgetSkeletonVariant` / `DashboardWidgetConfig` / `DashboardLayoutConfig` / `ComparisonTrend` / `ComparisonResult`L3/ `TimeRange`L2/ `ConnectionStatus`L6/ `DashboardNotificationItem`L5/ `ExportRow` / `ExportColumn`L7
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`classes`(通过 data-access、`homework`(通过 data-access、`users`(通过 data-access、`parent`(通过 data-access.getParentDashboardData + 组件 slots 注入 ChildCard/ParentAttentionBanner、`textbooks`/`questions`/`exams`(通过各模块 dashboard stats 函数)、`recharts`、`next-intl`
- 被依赖:无
**权限点**
- `DASHBOARD_ADMIN_READ`admin
- `DASHBOARD_TEACHER_READ`teacher
- `DASHBOARD_STUDENT_READ`student
- `DASHBOARD_PARENT_READ`parent
**已知问题**
- ✅ P0-4 已修复:`getAdminDashboardData` 改为并行调用各模块 dashboard stats 函数,不再直查跨模块表
- ✅ P0 已修复2026-06-22所有仪表盘页面通过 `actions.ts` 调用 `requirePermission()` 进行权限校验,不再裸调 data-access
- ✅ P0 已修复2026-06-22根重定向页 `/dashboard` 改用 `resolvePermissions()` + 权限点判断,不再 `role === "xxx"` 硬编码
- ✅ P0 已修复2026-06-22所有仪表盘组件接入 next-intl`useTranslations` / `getTranslations`),翻译文件 `messages/{zh-CN,en}/dashboard.json`
- ✅ P1 已修复2026-06-22业务逻辑weekday 转换、作业统计、教师指标计算、问候语时段)抽取至 `lib/dashboard-utils.ts` 纯函数,与 UI 分离
- ✅ P2 已修复2026-06-22新增 `components/dashboard-section.tsx`,每个独立数据区块用 Error Boundary + Suspense + 骨架屏包裹,单区块崩溃/加载不波及整页5 种骨架变体stats/card/chart/table/list
- V1 新增:`AdminDashboardData` 类型含 `userGrowth`/`homeworkTrend` 字段,`data-access.ts` 当前返回空数组占位V4 P2-4 添加 TODO 注释),待后续接入真实统计
- ✅ V4 P1-4 已修复:`ParentDashboard` 组件从 `modules/parent/components/` 迁移至 `modules/dashboard/components/parent-dashboard/`,使用 slots 组合注入 ChildCard 和 ParentAttentionBannerparent 模块仅保留数据访问
- ✅ P0 已修复2026-06-22 V210 个子组件 i18n 遗漏全部补齐,新增 ~50 个翻译键
- ✅ P1 已修复2026-06-22 V2抽象共享组件 `DashboardGreetingHeader`,消除 teacher/student 头部 90% 重复代码
- ✅ P2 已修复2026-06-22 V2为 6 个纯函数添加 31 个单元测试
- ✅ P2 已修复2026-06-22 V2a11y 增强 — admin 表格 `<caption>`、teacher/student 视图语义化标签
- ✅ P0 已修复2026-06-22 V4`filterTodaySchedule` 移除 `as T[]` 类型断言,改为非泛型函数
- ✅ P1 已修复2026-06-22 V45 个组件内嵌纯函数抽取至 `lib/dashboard-utils.ts`
- ✅ P1 已修复2026-06-22 V4`teacherName` 硬编码 "Teacher" fallback 改为空字符串
- ✅ P1 已修复2026-06-22 V4`teacher-schedule.tsx` 移除本地类型定义,从 `types.ts` 导入
- ✅ P2 已修复2026-06-22 V4新增 `services/dashboard-service.tsx`,定义 `DashboardService` 接口 + `DashboardAnalytics` 监控埋点接口 + React Context 依赖注入
- ✅ P2 已修复2026-06-22 V4新增 `config/widget-configs.ts`4 个角色的 Widget 布局配置,支持配置驱动渲染
- ✅ V4 P2-4 已修复admin 趋势数据占位添加 TODO 注释,标记后续实现
- ✅ V4 P2-5 已修复4 个角色 StatCard 使用模式统一(均传 color + valueClassName="tabular-nums"
- ✅ V4 P3-1 已修复:`DashboardSection` 新增 `ariaLabel` prop渲染 `<section role="region" tabIndex={0}>`admin/teacher/student 视图全部接入
- ✅ V4 P3-2 已修复:`AdminTrendCharts` 添加 TODO 注释标记趋势数据待接入
- ✅ V4 P3-3 已修复:`TeacherTodoCard` 排序逻辑改用 `VARIANT_PRIORITY` 数值映射,提升可读性
- ✅ L1 已实施Widget 下钻导航 — admin StatCard 添加 href 跳转ContentRow 支持可选 href 下钻
- ✅ L2 已实施:时间范围筛选 — 新增 `DashboardTimeRangeFilter` 客户端组件URL search param 持久化,集成到 admin dashboard
- ✅ L3 已实施:数据对比 — 新增 `ComparisonBadge` 组件 + `computeComparison` 纯函数
- ✅ L4 已实施:自定义仪表盘 — 新增 `useDashboardPreferences` HooklocalStorage 持久化 Widget 显示/隐藏偏好
- ✅ L5 已实施:通知中心集成 — 新增 `DashboardNotificationWidget` 组件
- ✅ L6 已实施:实时更新 — 新增 `useDashboardRealtime` HookSSE + 指数退避重连)
- ✅ L7 已实施:数据导出 — 新增 `lib/export-utils.ts`CSV 导出 + 浏览器下载)
- ✅ L8 已实施:移动端优化 — 新增 `DashboardResponsiveLayout` / `MobileSwipeContainer` / `DesktopGrid` 组件
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 167 | 4 个 Server Action编排层含 `requirePermission()` 权限校验) |
| `data-access.ts` | 51 | admin 仪表盘数据聚合(并行调用各模块 stats 函数V4 添加趋势数据 TODO |
| `streams.ts` | 34 | admin 流式数据源(返回未解析 Promise 供 React `use()` 消费) |
| `lib/dashboard-utils.ts` | 279 | 纯逻辑工具函数weekday / 统计 / 排序 / 指标计算 / 问候语 / 课表状态 / 作业紧急度 / 操作按钮 V4 新增 5 个函数) |
| `lib/export-utils.ts` | 73 | L7 新增CSV 导出工具toCSV / downloadFile / exportDashboardCSV |
| `types.ts` | 107 | Admin / Teacher / Student 类型定义 + V4 新增 Widget 配置类型 |
| `services/dashboard-service.tsx` | 110 | V4 新增DashboardService 接口 + DashboardAnalytics 接口 + React Context 依赖注入 |
| `config/widget-configs.ts` | 62 | V4 新增4 个角色的 Widget 布局配置(配置驱动渲染) |
| `hooks/use-dashboard-preferences.ts` | 82 | L4 新增:自定义仪表盘偏好 HooklocalStorage 持久化) |
| `hooks/use-dashboard-realtime.ts` | 73 | L6 新增SSE 实时更新 Hook指数退避重连 |
| `components/dashboard-section.tsx` | 186 | 分区 Error Boundary + Suspense + 骨架屏V4 P3-1 新增 ariaLabel prop |
| `components/dashboard-greeting-header.tsx` | 36 | 共享问候头部组件V2 抽象V4 支持空用户名 fallback |
| `components/dashboard-time-range-filter.tsx` | 57 | L2 新增时间范围筛选器URL search param 持久化) |
| `components/comparison-badge.tsx` | 75 | L3 新增:数据对比徽章 + computeComparison 纯函数 |
| `components/dashboard-notification-widget.tsx` | 85 | L5 新增:通知中心 Widget |
| `components/dashboard-responsive-layout.tsx` | 72 | L8 新增:移动端响应式布局组件 |
| `components/parent-dashboard/parent-dashboard.tsx` | 115 | V4 P1-4 迁移家长仪表盘视图slots 组合注入) |
| `components/admin-dashboard/admin-dashboard.tsx` | 180 | 管理员仪表盘视图V4 接入时间范围筛选 + ariaLabel |
| `components/admin-dashboard/admin-sections.tsx` | 245 | 管理员 6 个分区组件V4 StatCard 下钻 + ContentRow href |
| `components/admin-dashboard/user-growth-chart.tsx` | 65 | recharts 折线图i18n |
| `components/teacher-dashboard/*.tsx` | 9 文件 | 教师仪表盘组件V4 修复 teacher-schedule 类型导入 + TeacherTodoCard 排序优化 + ariaLabel |
| `components/student-dashboard/*.tsx` | 6 文件 | 学生仪表盘组件V4 抽取纯函数 + ariaLabel |
| `tests/integration/dashboard/dashboard-utils.test.ts` | 408 | 6 个纯函数的 31 个单元测试V2 新增) |
---
## 2.13 messaging私信模块
**职责**站内私信messages 表 CRUD+ 消息草稿message_drafts 表 CRUD
**导出函数**
- Actions`sendMessageAction`(✅ 审计 V1-P0-1新增 `isReceiverAllowed` 二次校验收件人是否在 sender DataScope 内 + ✅ 审计 V1-P0-6校验 `parentMessageId` 属于当前用户会话 + ✅ 审计 V1-P1-9失败埋点 `message.send_failed`/ `markMessageAsReadAction` / `deleteMessageAction` / `getMessagesAction`(✅ 审计 V1参数新增 `starredOnly?: boolean`/ `getRecipientsAction` / `getUnreadMessageCountAction`(✅ P1-4 已修复:通知 CRUD Action 已迁移至 notifications 模块;✅ V3-P1-3 已修复:通知偏好 Action 已迁移至 notifications 模块messaging 仅保留私信 Action✅ V2-P2-13c 新增:`toggleMessageStarAction` 星标切换 / `getMessageDraftsAction` 草稿列表 / `saveMessageDraftAction` 草稿保存(✅ V3-P0-1使用 `MessageDraftSchema` Zod 校验替代 `as` 断言)/ `deleteMessageDraftAction` 草稿删除;✅ 审计 V1-P0-4 已删除:`getMessageDetailAction`V2-P1-3 重构遗留死代码,详情数据由 RSC 通过 `getMessageDetailPageData` 直接获取);✅ 审计 V1-P1-3 新增:`getMessageThreadAction` 获取消息线程(替代组件直接调用 data-access统一权限边界
- Data-access`getMessages`(✅ V2-P2-13c 新增 starredOnly 星标筛选)/ `getMessageById` / `getMessageThread`(✅ 审计 V1-P0-2签名变更为 `(messageId, userId)`,内部校验当前用户对根消息的访问权)/ `createMessage` / `markMessageAsRead` / `deleteMessage` / `toggleMessageStar`(✅ V2-P2-13c 新增:接收方星标切换)/ `getUnreadMessageCount` / `getRecipients`(✅ 审计 V1-P1-6返回 `role` 收紧为 `RecipientRole` 类型admin 分支补 role/ `isReceiverAllowed`(✅ 审计 V1-P0-1 新增:校验收件人是否在 sender DataScope 内)/ `getMessageDetailPageData`(✅ V2-P1-3 新增:消息详情页编排函数,获取详情并自动标记已读;✅ 审计 V1-P0-3`getMessagesPageData` 已删除,编排迁回 messages/page.tsx 页面层 `Promise.all` 调用 messaging + notifications data-access保持模块独立性/ `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`(✅ V2-P2-13c 新增:消息草稿 CRUD操作 message_drafts 表)
- Hooks`useMessageSearch`(✅ P1-7 新增:消息搜索 hook含防抖和请求竞态取消
- Lib`buildReplyHref`(✅ 审计 V1-P1-7 新增:构建回复消息 URL 纯函数,使用 URLSearchParams 安全编码,替代组件内字符串拼接)
- Notification-preferences~~re-export shim实际逻辑在 `notifications/preferences.ts`~~ ✅ P0-b 已修复:`notification-preferences.ts` 文件已删除(通知模块去重),消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`notifications`(✅ P0-4 / P1-5 已修复:通过 `sendNotification` dispatcher 发送通知,通知 CRUD 和偏好已迁移至 notifications 模块;✅ P1-4 已修复:通知 UI 组件已迁移至 notifications 模块;✅ 审计 V1-P0-3 已修复data-access 不再动态 import notifications编排迁回页面层、`classes`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID支持学生 class_members 和家长 children 数据范围)、`users`(通过 data-access.getUserNamesByIds 获取用户显示名称)
- 被依赖:`notifications`(✅ 已消除反向依赖)、`settings`(通知偏好表单)、`layout`(✅ P1-4 已修复:通知下拉组件改为从 notifications 模块导入)
**已知问题**
- ✅ P0-4 已修复:~~`sendMessageAction` 绕过 notifications dispatcher 直接调用 `createNotification`~~ 改为调用 `notifications.sendNotification`,通知 CRUD 已迁移至 notifications 模块
- ✅ P0 已修复:~~与 notifications 双向依赖 + 职责重叠~~ 通知相关表messageNotifications / notificationPreferences所有权已移交 notifications 模块messaging 仅保留 messages 表
- ✅ P1-5 已修复:~~同时管理 3 类数据messages + messageNotifications + notificationPreferences~~ 仅管理 messages 表,通知相关数据由 notifications 模块管理
- ✅ P1 已修复:~~通知相关 Action 使用 `requireAuth()` 而非 `requirePermission()`~~ 通知 Action 已迁移至 notifications 模块并改为 `requirePermission(Permissions.MESSAGE_READ)`
- ✅ P1 已修复:~~`markMessageAsReadAction` / `deleteMessageAction` / `getMessageDetailAction` 缺少 Zod 校验~~ 已添加 `MessageIdSchema` 校验 messageId 参数
- ✅ P1 已修复:~~`updateNotificationPreferencesAction` 缺少 Zod 校验~~ 已添加 `UpdateNotificationPreferencesSchema` 校验 8 个布尔字段
- ✅ P2 已修复:`data-access.ts` 中 3 处 `or(...)!` 非空断言清理为安全守卫(条件 push
- ✅ P0-b 已修复:~~`notification-preferences.ts` re-export shim 文件~~ 已删除通知模块去重8 个消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`,消除 messaging 模块对通知偏好的冗余 re-export 层
- ✅ P1 已修复:~~全模块零 i18n中英文案硬编码~~ 所有组件接入 next-intl`useTranslations("messages")`),新增 `src/shared/i18n/messages/{zh-CN,en}/messages.json` 翻译字典title/description/tabs/actions/form/status/meta/notificationType/search/empty/messages/error 共 13 个命名空间;✅ 审计 V1-P1-4 新增 `pagination` / `drafts` / `thread` 三个命名空间);所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata
- ✅ P1 已修复:~~缺 Error Boundary~~ 新增 3 个 `error.tsx` 错误边界(`/messages`、`/messages/[id]`、`/messages/compose`),统一使用 `EmptyState` + i18n 错误文案 + 重试按钮
- ✅ P2 已修复a11y 改进,`message-list.tsx` 添加 `aria-label` / `aria-hidden`;✅ 审计 V1-P1-4分页 `aria-label` i18n 化(替代硬编码 "Previous page" / "Next page"),星标按钮加 `aria-pressed`
- ✅ P1-4 已修复:~~通知组件notification-list.tsx / notification-dropdown.tsx放在 messaging/components 下,直接 import notifications 模块类型和 messaging/actions~~ 两个组件已迁移至 `notifications/components/`,通知 CRUD Action 已迁移至 `notifications/actions.ts`messaging 模块仅保留私信组件
- ✅ 审计 V1-P0-3 已修复:~~`getMessagesPageData` 在 data-access 层动态 `await import("@/modules/notifications/data-access")` 跨模块编排~~ 编排迁回 `messages/page.tsx` 页面层 `Promise.all([getMessages(...), getNotifications(...)])`,保持 messaging data-access 模块独立性
- ✅ 审计 V1-P0-4 已修复:~~`getMessageDetailAction` 死代码V2-P1-3 重构遗留,全代码库无引用)~~ 已彻底删除
- ✅ 审计 V1-P0-1 已修复:~~`sendMessageAction` 未校验 `receiverId` 是否在 sender DataScope 内~~ 新增 `isReceiverAllowed(userId, receiverId, scope)` data-access 函数Server Action 二次校验,防止越权发送
- ✅ 审计 V1-P0-2 已修复:~~`getMessageThread(messageId)` 缺少 userId 参数和权限校验~~ 签名变更为 `(messageId, userId)`where 子句加 `(senderId=userId OR receiverId=userId)`,纵深防御
- ✅ 审计 V1-P0-5 已修复:~~`MessageCompose` 自动保存 effect 在用户快速提交后仍会触发新草稿创建~~ 新增 `submittedRef` 标志位,提交后跳过自动保存,失败时重置
- ✅ 审计 V1-P0-6 已修复:~~`sendMessageAction` 未校验 `parentMessageId` 属于当前用户会话~~ 新增 `getMessageById(parentMessageId, ctx.userId)` 校验,防止将回复注入他人会话线程
- ✅ 审计 V1-P1-1 已修复:~~compose 页面无草稿列表 UI~~ 新增 `MessageDraftList` 组件compose page 通过 `getMessageDraftsAction` 获取草稿并渲染,支持"继续编辑"和"删除草稿"
- ✅ 审计 V1-P1-2 已修复:~~MessageList 缺少"星标" Tabdata-access `starredOnly` 筛选不可达~~ Tab 类型新增 `starred`,切换时调用 `getMessagesAction({type:"all", starredOnly:true})`
- ✅ 审计 V1-P1-3 已修复:~~MessageDetail 不渲染消息线程,`getMessageThread` 已实现但 UI 缺失~~ 新增 `getMessageThreadAction` + 线程渲染区块,时间正序展示回复
- ✅ 审计 V1-P1-6 已修复:~~`RecipientOption.role` 为宽松的 `string?`~~ 新增 `RecipientRole = "student" | "teacher" | "admin" | "parent"` 类型admin 分支补 role
- ✅ 审计 V1-P1-7 已修复:~~`replyHref` 构建逻辑散落在 MessageDetail 组件内URL 参数未安全编码~~ 抽取为 `lib/build-reply-href.ts` 纯函数,使用 `URLSearchParams` 编码
- ✅ 审计 V1-P1-9 已修复:~~`sendMessageAction` 失败路径无埋点~~ catch 块调用 `trackEvent({event:"message.send_failed"})`,权限拒绝除外避免噪音
- ✅ P1-7 已修复:~~消息列表客户端 `useEffect` + `setTimeout` 防抖搜索未取消已发出的请求~~ 搜索逻辑抽离为 `useMessageSearch` hook含防抖 + 请求竞态取消);~~无分页 UI~~ 新增客户端分页 UIPAGE_SIZE=20ChevronLeft/ChevronRight 按钮)
- ✅ P1-9 已修复:~~`deleteMessage` 两个独立 UPDATE 无事务~~ 改为 `db.transaction` 包裹 senderDeletedAt 和 receiverDeletedAt 更新,保证原子性
- ✅ P2-11 已修复:~~发送/删除消息无埋点~~ `sendMessageAction` / `markMessageAsReadAction` / `deleteMessageAction` 新增 `trackEvent` 埋点message.sent / message.marked_read / message.deleted
- ✅ V2-P0-2 已修复:~~通知标题硬编码~~ `sendMessageAction` 通过 `getTranslations('messages')` 生成 i18n 通知标题(`notification.messageTitle` / `messageTitleNoSubject`
- ✅ V2-P1-2 已修复:~~MessageList 客户端过滤冗余~~ 客户端过滤仅在初始数据type=all时执行搜索结果已由服务端按 tab 过滤
- ✅ V2-P1-3 已修复:~~消息详情页分散编排~~ 新增 `getMessageDetailPageData` 编排函数,替代 page.tsx 中 `after()` + `getMessageById` + `markMessageAsRead` 的分散编排
- ✅ V2-P1-4 已修复:~~表单无服务端校验错误展示~~ `message-compose.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示receiverId/subject/content
- ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `unread-message-badge.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量(~~60_000ms~~ → 30_000ms✅ V2-P3 与通知组件保持一致)
- ✅ V2-P2-13c 已实现:消息星标和草稿功能。`messages` 表新增 `isStarred` 字段(接收方可标记重要消息)+ `messages_receiver_starred_idx` 复合索引;新增 `message_drafts` 表userId/receiverId/subject/content/parentMessageId + updatedAt/createdAt含 `message_drafts_user_idx` 和 `message_drafts_user_updated_idx` 索引data-access 新增 `toggleMessageStar` / `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`actions 新增 `toggleMessageStarAction` / `getMessageDraftsAction` / `saveMessageDraftAction` / `deleteMessageDraftAction``getMessages` 支持 `starredOnly` 筛选i18n 新增 actions.star/unstar/saveDraft/deleteDraft + messages.draftSaved/draftDeleted/starToggled + empty.noDrafts/noStarred 翻译键;迁移文件 `drizzle/0008_message_star_draft.sql`
- ✅ V3-P0-1 已修复:~~`saveMessageDraftAction` 中 5 处 `formData.get("xxx") as string \| null` 断言~~ 改为使用 `MessageDraftSchema` Zod 校验安全转换 FormData 值
- ✅ V3-P1-3 已修复:~~`getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 放在 messaging 模块~~ 已迁移至 `notifications/actions.ts`messaging 仅保留私信 Action
- ✅ V3-P1-6 已修复:~~`getMessageDraftsAction` 和 `getMessagesPageData` 使用内联 `import("./types").XXX` 类型导入~~ 改为顶部 `import type` 语句
- ✅ V3-P2-7 已修复:~~`message-list.tsx` 中 `setTab(v as Tab)` 断言~~ 改为 `isTab` 类型守卫
**未实施项P1-5 / P1-8 / P2-* 中长期)**
- ⏳ P1-5`UnreadMessageBadge` 接入 SSE 实时推送(需扩展 `/api/notifications/stream` 端点 payload
- ✅ P1-8 已实施:`MessageList` 依赖注入化(定义 `MessageListService` 接口通过 `MessageListServiceProvider` Context 注入,`default-message-list-service.ts` 绑定现有 Server Actions
- ✅ P2-1 已实施:批量操作(`bulkMarkMessagesAsRead` / `bulkDeleteMessages` / `bulkToggleMessagesStar`,使用 inArray + 事务)
- ✅ P2-2 已实施群组消息fan-out on write 策略,`messages.groupMessageId` 列 + `sendGroupMessage` data-access + `sendGroupMessageAction`教师→全班家长DataScope 校验)
- ✅ P2-3 已实施:附件支持(复用 `fileAttachments` 表,`targetType="message"` + `getMessageAttachments` data-access + `getMessageAttachmentsAction`UI 含 `MessageAttachmentUploader` 上传 + `MessageAttachmentList` 展示下载)
- ✅ P2-4 已实施:消息撤回(`messages.recalledAt` 列 + `recallMessage` data-access2 分钟窗口校验UI 显示"已撤回"占位)
- ✅ P2-5 已实施:举报+屏蔽(新增 `message_reports` 表 + `user_blocks` 表,`reportMessage` / `blockUser` / `unblockUser` / `isEitherUserBlocked` data-access`reportMessageAction` / `blockUserAction` / `unblockUserAction` / `getUserBlocksAction``sendMessageAction` 集成 `isEitherUserBlocked` 屏蔽校验)
- ✅ P2-6 已实施:快捷回复模板(`message_templates` 表 + CRUD data-access + `MessageTemplatePicker` UI 组件集成到 compose
- ✅ P2-7 已实施:配置驱动的角色组合(`RecipientResolver` 注册表模式,`RECIPIENT_RESOLVERS` 按 DataScope.type 索引,新增角色仅新增配置项)
- ✅ P2-8 已实施Error Boundary 细化(列表区/详情区/草稿区分别 `SectionErrorBoundary` 包裹)
- ✅ P2-9 已实施React Suspense 流式渲染RSC 使用 `Suspense` 包裹 MessageList + NotificationList骨架屏 fallback
- ✅ P2-10 已实施:跨设备草稿同步(`message_drafts.deviceId` + `version` 列 + `userParentIdx` 索引,`updateMessageDraft` 带乐观锁返回 `"ok" | "not_found" | "conflict"``saveMessageDraftAction` 处理冲突与 not_found 重建)
- ✅ P2-11 已实施:组合搜索(`getMessages` 扩展 `senderId` / `dateFrom` / `dateTo` / `isRead` 参数)
- ✅ P1-9 已实施:失败埋点(`message.send_failed` / `message.group_send_failed` / `message.report_failed` 等)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~360 | 11 个私信 Server Action✅ 审计 V1-P0-4删除 `getMessageDetailAction` 死代码;✅ 审计 V1-P1-3新增 `getMessageThreadAction`;✅ 审计 V1-P0-1/P0-6/P1-9sendMessageAction 增加 receiverId 二次校验 + parentMessageId 校验 + 失败埋点;✅ V3-P1-3通知偏好 Action 已迁移至 notifications✅ V3-P0-1`saveMessageDraftAction` 使用 Zod 校验;✅ V2-P0-2通知标题 i18n 化) |
| `data-access.ts` | ~430 | 私信 CRUD + ✅ 审计 V1-P0-1 新增 `isReceiverAllowed` + ✅ 审计 V1-P0-2 `getMessageThread` 增加 userId 校验 + ✅ 审计 V1-P0-3 删除 `getMessagesPageData` 编排迁出至页面层 + `getMessageDetailPageData` 编排函数(✅ P1-5 新增;✅ V3-P1-6内联类型导入改为顶部 import type |
| `schema.ts` | 40 | 私信发送校验 + messageId 校验 + 消息草稿校验(✅ V3-P0-1 新增 `MessageDraftSchema`;✅ V3-P1-3`UpdateNotificationPreferencesSchema` 已迁移至 notifications/schema.ts |
| `types.ts` | 85 | 私信类型 + ✅ 审计 V1-P1-6 新增 `RecipientRole` 类型 + re-export 通知类型(向后兼容) |
| `hooks/use-message-search.ts` | ~60 | ✅ P1-7 新增:消息搜索 hook防抖 + 请求竞态取消) |
| `lib/build-reply-href.ts` | ~30 | ✅ 审计 V1-P1-7 新增:构建回复 URL 纯函数URLSearchParams 编码) |
| `lib/type-guards.ts` | ~40 | ✅ 类型守卫专项重构2026-07-04新增`isRecipientRole` / `isMessageReportReason` / `isMessageReportStatus` 守卫 + `toMessageReportReason` / `toMessageReportStatus` 安全窄化函数fallback 默认值),替代 `data-access.ts` 与 `message-report-block.tsx` 中的 `as` 断言 |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/message-list.tsx` | 消息列表(✅ P1-7使用 `useMessageSearch` hook + 客户端分页 UIPAGE_SIZE=20✅ V2-P1-2客户端过滤仅在初始数据时执行✅ 审计 V1-P1-2新增 `starred` Tab + 调用 `getMessagesAction({starredOnly:true})`;✅ 审计 V1-P1-4分页 aria-label i18n 化) |
| `components/message-detail.tsx` | 消息详情(含回复;✅ 审计 V1-P1-3渲染消息线程✅ 审计 V1-P1-7使用 `buildReplyHref` 纯函数;✅ 审计 V1-P1-4星标按钮加 `aria-pressed` |
| `components/message-compose.tsx` | 撰写新消息(✅ V2-P1-4fieldErrors + aria-invalid 字段级错误展示;✅ 审计 V1-P0-5`submittedRef` 防止提交后自动保存竞态) |
| `components/message-draft-list.tsx` | ✅ 审计 V1-P1-1 新增:草稿列表组件(继续编辑 + 删除草稿 + 乐观更新) |
| `components/unread-message-badge.tsx` | 未读消息计数徽章(侧边栏,每 30 秒轮询 `getUnreadMessageCountAction`;✅ V2-P2-1POLL_INTERVAL_MS 常量;✅ V2-P3间隔从 60s 缩短为 30s 与通知一致) |
**客户端行为**
- `message-list.tsx`:客户端调用 `getMessagesAction` 搜索消息useMessageSearch hook400ms 防抖请求竞态取消V2-P1-2 优化客户端过滤仅在初始数据type=all时执行✅ 审计 V1-P1-2starred Tab 单独加载 `getMessagesAction({type:"all", starredOnly:true})`
- `unread-message-badge.tsx`:每 `POLL_INTERVAL_MS`30_000ms轮询 `getUnreadMessageCountAction` 刷新未读计数
- `message-detail.tsx`:✅ 审计 V1-P1-3 客户端调用 `getMessageThreadAction(messageId)` 加载回复链
- `message-compose.tsx`:✅ 审计 V1-P0-5 `submittedRef` 标志位阻止发送后自动保存 effect 触发新草稿
> 架构变更2026-07-05Phase 4.5 — 消息星标改用 useOptimistic + useTransition
> - **message-detail.tsx**:星标 `useState` → `useOptimistic<boolean>` + `useTransition`。`handleToggleStar` 在 `startStarTransition` 内 `addOptimisticStarred(nextStarred)` + `await toggleMessageStarAction` + `router.refresh()`。JSX 中所有 `isStarred` 引用改为 `optimisticIsStarred`Button variant/aria-pressed/Star className/星标徽章)
> - **message-list.tsx**:星标 `starredOverride` useState → `useOptimistic<Map<string, boolean>>`key=messageId, value=isStarred。`getIsStarred` 改为 `optimisticStarredMap.get(m.id)``handleToggleStar` 重写为 `startStarTransition` + `addOptimisticStar` + `await router.refresh()`。撤回 `recalledOverride` 仍保留 useState不需要乐观更新
> - **关键决策**:成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值不为撤回recall操作使用 useOptimistic因为撤回需要根据服务端返回判断是否成功2 分钟窗口校验),失败时不应乐观显示已撤回
---
## 2.14 notifications通知分发模块
**职责**多渠道通知分发SMS/Email/WeChat/InApp+ 站内通知 CRUD + 通知偏好管理 + 通知 UI 组件。
**导出函数**
- Actions`sendNotificationAction` / `sendClassNotificationAction` / `getNotificationsAction` / `getUnreadNotificationCountAction` / `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` / `archiveNotificationAction`(✅ P1-4 新增:后 4 个通知 CRUD Action 从 messaging 模块迁移;✅ V2-P2-13b 新增archiveNotificationAction 归档 Action✅ V3-P1-3 新增:`getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 从 messaging 模块迁移)
- Dispatcher`sendNotification(payload)` / `sendBatchNotifications(payloads)`(✅ P3-7`sendBatchNotifications` 重构为 in_app 单次批量 INSERT其他渠道保持 per-payload 并行)
- Data-access`createNotification` / `createNotifications(items)`(✅ P3-7 新增:批量 INSERT 多行)/ `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount` / `archiveNotification` / `unarchiveNotification` / `getUserContactInfo` / `logNotificationSend` / `logNotificationSendBatch`(✅ P0-4 / P1-5 修复后从 messaging 迁移;✅ V2-P2-13b 新增archiveNotification / unarchiveNotification 归档函数)
- Preferences`getNotificationPreferences` / `upsertNotificationPreferences`(✅ P0-4 / P1-5 修复后从 messaging 迁移)
- Channels`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender`
- Components`NotificationList` / `NotificationDropdown`(✅ P1-4 新增:从 messaging/components 迁移;✅ V2-P2-13bNotificationList 支持优先级 Badge 显示和归档操作;✅ V2-P2-13cNotificationList 支持按类型筛选)
- Hooks`useNotificationStream`(✅ V2-P3 新增SSE 实时推送 + 轮询降级 Hook、`useDesktopNotifications`(✅ V2-P2-13c 新增:浏览器桌面推送 Hook支持权限管理和新通知桌面提醒
- Types`NotificationPriority`(✅ V2-P2-13b 新增:通知优先级类型 low/normal/high/urgent
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getStudentIdsByClassId
- 被依赖:`messaging`(✅ P0-4 / P1-5 已修复messaging 通过 `sendNotification` dispatcher 发送通知;✅ P1-4 已修复:通知 UI 组件由 notifications 模块自持messaging 不再反向依赖)、`layout`(✅ P1-4 已修复:通知下拉组件直接从 notifications 模块导入)、`app/(dashboard)/messages`(✅ P1-4 已修复:通知列表组件直接从 notifications 模块导入)
**已知问题**
- ✅ P0-4 已修复:~~不拥有任何数据,全部依赖 messaging 模块~~ messageNotifications 和 notificationPreferences 表所有权已从 messaging 迁移至 notifications 模块
- ✅ P0 已修复:~~与 messaging 双向依赖~~ notifications 不再反向依赖 messagingin-app-channel 改为静态导入本地 createNotification
- ✅ P1-1 已修复:~~`sendClassNotificationAction` 直查 `classes`/`classEnrollments`~~ 改为调用 `classes/data-access.getClassExists` / `getStudentIdsByClassId`
- ✅ P1-4 已修复:~~通知 UI 组件放在 messaging/components 下,直接 import notifications 类型和 messaging/actions~~ `notification-list.tsx` 和 `notification-dropdown.tsx` 已迁移至 `notifications/components/`,通知 CRUD Action 已从 messaging 迁移至 `notifications/actions.ts`,消除 UI 层跨模块耦合
- ✅ P2-11 已修复:~~通知标记已读无埋点~~ `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` 新增 `trackEvent` 埋点notification.marked_read / notification.marked_all_read
- ✅ V2-P0-1 已修复:~~通知 i18n 键混在 messages.json 中~~ 新增独立的 `notifications.json` 命名空间zh-CN/en通知组件 `useTranslations` 从 `"messages"` 切换到 `"notifications"``src/i18n/request.ts` 新增 notifications 命名空间加载
- ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `notification-dropdown.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量30_000ms
- ✅ V2-P3 已优化:~~30 秒轮询~~ `notification-dropdown.tsx` 改为 SSE 实时推送(`/api/notifications/stream`SSE 不可用时自动降级为轮询(调用 Server Actions间隔 30 秒)
- ✅ V2-P2-13b 新增通知优先级和归档功能。schema.ts `messageNotifications` 表新增 `priority`low/normal/high/urgent默认 normal和 `isArchived`(默认 false字段 + 2 个索引types.ts 新增 `NotificationPriority` 类型data-access.ts 新增 `archiveNotification` / `unarchiveNotification` 函数,`getNotifications` 支持归档和优先级筛选(默认仅返回未归档),`createNotification` 支持 priorityactions.ts 新增 `archiveNotificationAction`(含 trackEvent 埋点 notification.archivedNotificationList 组件支持优先级 Badge 显示和归档按钮i18n 新增 priority/actions.archive/messages.archiveFailed 翻译键
- ✅ V2-P2-13c 新增:通知分类筛选 + 浏览器桌面推送。NotificationList 组件新增按类型筛选按钮组all/message/announcement/homework/grade空状态区分"无通知"和"无筛选结果";新增 `useDesktopNotifications` Hook浏览器 Notification API支持权限管理、自动请求权限、新通知桌面推送、点击跳转NotificationDropdown 集成桌面推送监听新通知并触发桌面提醒首次加载不批量推送i18n 新增 filter.all / empty.noFilterResults / empty.noFilterResultsDesc 翻译键
- ⚠️ P1发送日志仅 console无 `notification_logs` 表
- ✅ 渠道抽象优秀(接口 + 工厂 + Mock 实现)
- ✅ V3-P1-3 已修复:~~通知偏好 Action 放在 messaging 模块~~ `getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 已迁移至 `notifications/actions.ts`,新增 `notifications/schema.ts``UpdateNotificationPreferencesSchema` 从 messaging 迁移)
- ✅ V3-P1-4 已修复:~~`sendBatchNotifications` 重复日志~~ 移除 `sendBatchNotifications` 中的重复 `logNotificationSendBatch` 调用(日志已在 `sendNotification` 内部记录)
- ✅ V3-P1-5 已修复:~~腾讯云短信 `SmsSdkAppId` 使用 `templateCode`~~ 新增 `SMS_SDK_APP_ID` 环境变量和 `smsSdkAppId` 配置字段,`SmsSdkAppId` 使用独立配置值
- ✅ V3-P2-7 已修复:~~`notification-list.tsx` 中 `Object.keys(TYPE_ICON) as NotificationType[]` 断言~~ 改为 `TYPE_KEYS` 常量数组
- ✅ V3-P2-8 已标记:`logNotificationSend` 中 `console.info` / `console.error` 添加 TODO 注释,预留统一日志服务接入点
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `dispatcher.ts` | ~225 | 渠道选择 + 并行分发(✅ V3-P1-4移除 `sendBatchNotifications` 重复日志;✅ P3-7`sendBatchNotifications` 重构为 in_app 批量 INSERT + 其他渠道并行) |
| `data-access.ts` | ~290 | 站内通知 CRUD + 用户联系方式 + 发送日志持久化notification_logs 表P0-4 / P1-5 修复后新增通知 CRUD✅ V2-P2-13b新增归档函数 + 优先级/归档筛选;✅ V3-P2-8console 日志添加 TODO 统一日志服务标记;✅ P3-7新增 `createNotifications` 批量 INSERT 函数) |
| `preferences.ts` | 166 | 通知偏好 CRUDP0-4 / P1-5 修复后从 messaging 迁移) |
| `actions.ts` | ~380 | 9 个 Server Action✅ P1-4新增 4 个通知 CRUD Action✅ V2-P2-13b新增 archiveNotificationAction✅ V3-P1-3新增 getNotificationPreferencesAction / updateNotificationPreferencesAction 从 messaging 迁移) |
| `schema.ts` | 22 | ✅ V3-P1-3 新增:通知偏好校验 Schema从 messaging/schema.ts 迁移) |
| `types.ts` | ~130 | 通知负载 + 渠道配置 + 通知记录 + 偏好类型P0-4 / P1-5 修复后扩充;✅ V2-P2-13b新增 NotificationPriority 类型 + priority/isArchived 字段) |
| `index.ts` | ~85 | 对外导出入口(✅ P1-4新增组件和 CRUD Action 导出;✅ V2-P2-13b新增归档函数/Action/类型导出;✅ V2-P2-13c新增 useNotificationStream / useDesktopNotifications Hook 导出;✅ V3-P1-3新增通知偏好 Action 和 Schema 导出) |
| `channels/*` | 5 文件 | 4 个渠道实现 |
| `components/notification-list.tsx` | ~190 | ✅ P1-4 新增(从 messaging 迁移):通知列表组件;✅ V2-P2-13b支持优先级 Badge 显示和归档操作;✅ V2-P2-13c支持按类型筛选 + 空状态区分 |
| `components/notification-dropdown.tsx` | ~180 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件;✅ V2-P3改用 SSE 实时推送 + 轮询降级;✅ V2-P2-13c集成 useDesktopNotifications 桌面推送 |
| `hooks/use-notification-stream.ts` | ~195 | ✅ V2-P3 新增SSE 实时推送 HookEventSource + 轮询降级) |
| `hooks/use-desktop-notifications.ts` | ~100 | ✅ V2-P2-13c 新增:浏览器桌面推送 HookNotification API + 权限管理 + 点击跳转) |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/notification-list.tsx` | 通知列表(消息页底部,展示所有通知,支持标记已读;✅ V2-P0-1useTranslations 命名空间从 "messages" 切换到 "notifications";✅ V2-P2-13b支持优先级 Badge 显示和归档操作;✅ V2-P2-13c支持按类型筛选 + 空状态区分"无通知"/"无筛选结果" |
| `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,✅ V2-P3改用 SSE 实时推送 `/api/notifications/stream` + 轮询降级;✅ V2-P0-1useTranslations 命名空间切换;✅ V2-P2-1POLL_INTERVAL_MS 常量;✅ V2-P2-13c集成 useDesktopNotifications 桌面推送,监听新通知并触发桌面提醒) |
**客户端行为**
- `notification-dropdown.tsx`:✅ V2-P3 改用 `useNotificationStream` Hook 消费 SSE 实时推送(`/api/notifications/stream`SSE 不可用时自动降级为轮询(调用 `getNotificationsAction` + `getUnreadNotificationCountAction`,间隔 30 秒);✅ V2-P2-13c 集成 `useDesktopNotifications` Hook通过 `prevNotificationIds` 对比检测新通知并触发桌面推送(首次加载不批量推送)
- `hooks/use-notification-stream.ts`:✅ V2-P3 新增,管理 SSE 连接生命周期EventSource onopen/onmessage/onerror降级时通过 `pollFnRef` 调用 Server Actions 轮询
- `hooks/use-desktop-notifications.ts`:✅ V2-P2-13c 新增,封装浏览器 Notification API权限管理、自动请求权限、桌面推送、点击跳转回调SSR 安全(`typeof window !== "undefined"` 检查)
---
## 2.15 audit审计模块
**职责**:操作日志 + 登录日志 + 数据变更日志查询与导出 + 审计概览仪表盘 + 数据保留策略(含 i18n + Error Boundary + 骨架屏 + 埋点 + Service 接口抽象)。
**导出函数**
- Actions`getDataChangeLogsAction` / `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` / `getAuditRetentionConfigAction` / `saveAuditRetentionConfigAction` / `purgeAuditLogsAction`(✅ 审计重构:导出逻辑下沉到 `export.ts`actions 仅编排;新增 `trackEvent` 埋点 `audit.exported`;✅ P2-5 新增保留策略 3 个 Action✅ v2 修正:保留策略操作 event 改为 `audit.retention`;✅ v2 新增 `revalidatePath` 精确刷新缓存)
- Data-access`getAuditLogs` / `getLoginLogs` / `getDataChangeLogs` / `getAuditModuleOptions` / `getDataChangeStats` / `getDataChangeTableOptions` / `getAuditLogsForExport` / `getLoginLogsForExport` / `getDataChangeLogsForExport` / `getAuditOverviewStats` / `getAuditTrend` / `getDataChangeActionStats`(✅ P2-4 新增概览统计 3 个函数)
- Export✅ 审计重构新增):`buildAuditLogExport` / `buildLoginLogExport` / `buildDataChangeLogExport`Excel 列定义 + 行映射纯函数,从 actions 抽取)
- Retention✅ P2-5 新增):`getAuditRetentionConfig` / `saveAuditRetentionConfig` / `purgeExpiredAuditLogs` / `calculateRetentionThreshold` / `isValidRetentionDays`(保留策略配置读写 + 清理 + 纯函数)
- Hooks✅ 审计重构新增):`useLogPagination`(分页 URL 状态管理,三处重复逻辑抽取)
- Services✅ P2-8 新增,✅ v2 接线完成):`AuditService` 接口 + `AuditServiceProvider` Context + `useAuditService` / `useAuditAnalytics` hook + `adminAuditService` 默认实现v2 改造为 Client-safe查询方法抛错强制走 RSC props写操作委托 Server Action+ `mockAuditService` 测试用实现v2 修正:消除 9 处 `as` 断言,为所有方法添加显式返回类型标注)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`next-intl`i18n、`shared/lib/track-event`(埋点)、`settings`(保留策略配置存储于 `system_settings` 表 `audit_retention` 分类)
- 被依赖:无
**已知问题**
- ✅ 审计重构 P0 已修复:~~Excel 导出逻辑内联在 actions 层~~ 已抽取到 `export.ts`144 行actions.ts 从 212 行精简到 139 行
- ✅ 审计重构 P0 已修复:~~三个导出 Action 结构高度重复~~ 列定义 + 行映射统一到 export.ts 的 `buildXxxExport` 函数
- ✅ 审计重构 P0 已修复:~~全模块零 i18n~~ 所有组件接入 `useTranslations("audit")` + `useLocale()`formatDate 从硬编码 `zh-CN` 改为 locale 变量;新增 `src/shared/i18n/messages/{zh-CN,en}/audit.json`(含 table/filter/empty/export/error/detail/overview/retention 命名空间)
- ✅ 审计重构 P0 已修复:~~缺 Error Boundary + 骨架屏~~ 新增 `AuditErrorBoundary`class 组件,捕获错误展示 EmptyState + 重试按钮4 个页面新增 `loading.tsx`(骨架屏)+ `error.tsx`(错误边界)
- ✅ 审计重构 P1 已修复:~~DataChangeLogTable 281 行过大~~ 拆分为 `data-change-log-table.tsx`146 行,仅表格)+ `data-change-log-filters.tsx`138 行,独立筛选器)+ `data-change-log-view.tsx`53 行,视图组合)
- ✅ 审计重构 P1 已修复:~~三处分页逻辑重复~~ 抽取 `useLogPagination` hook27 行)
- ✅ 审计重构 P1 已修复:~~导出无埋点~~ `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` 新增 `trackEvent` 埋点(`audit.exported`
- ✅ 审计重构 P1 已修复a11y 改进,筛选器添加 `aria-label`、`aria-expanded` 属性
- ✅ 审计重构 P1 已修复:~~api/export/route.ts 中 `as Record<string, string>` 类型断言~~ 改为 for 循环 + 类型守卫
- ✅ P2 已修复:`data-access.ts` 中 6 处 catch 块添加 `console.error` 输出错误上下文;`toIso`/`clampPageSize`/`clampPage` 工具函数补齐显式返回类型
- ✅ P2-1 已修复:~~data-access 错误吞没~~ 所有 catch 块改为 `throw error`不再返回空数组UI 通过 ErrorBoundary 捕获
- ✅ P2-2 已修复:~~无日志详情视图~~ 新增 `AuditLogDetailDialog`discriminated union 模式,支持 audit/login/dataChange 三种类型)
- ✅ P2-3 已修复:~~无用户维度筛选~~ 三个筛选器均新增 `userId` Input + URL 状态管理
- ✅ P2-4 已修复:~~无审计概览仪表盘~~ 新增 `/admin/audit-logs/overview` 页面(统计卡片 + 趋势图 + 分布图 + 快捷入口 + 保留策略配置)
- ✅ P2-5 已修复:~~无数据保留策略~~ 新增 `retention.ts`(配置读写 + 清理纯函数)+ 3 个 Server Action + cron API 路由 + `AuditRetentionSettings` 组件
- ✅ audit-P2-5auth 审计)已修复:~~`login_logs` 与 `audit_logs`/`data_change_logs` 共用同一保留期180 天),无法为登录日志配置更长的安全取证保留期~~ 新增独立 `loginLogRetentionDays` 配置项(`DEFAULT_LOGIN_LOG_RETENTION_DAYS = 365``AuditRetentionConfig` 类型新增 `loginLogRetentionDays` 字段;`getAuditRetentionConfig` / `saveAuditRetentionConfig` 读写新配置项;`purgeExpiredAuditLogs(retentionDays, loginLogRetentionDays?)` 支持为 login_logs 传入独立保留期(向后兼容,第二参数可选);`purgeAuditLogsAction` / `AuditService.purgeExpiredAuditLogs` 接口同步更新;`AuditRetentionSettings` 组件新增登录日志保留天数 Input新增 `vercel.json` 配置 cron schedule `0 3 * * *`(每日凌晨 3 点触发 `/api/cron/audit-cleanup``env.mjs` 新增 `CRON_SECRET` 可选环境变量校验
- ✅ P2-6 已修复:~~无单测~~ 新增 `retention.test.ts`9 个测试)+ `export.test.ts`13 个测试),全部通过
- ✅ P2-8 已修复:~~无 Service 接口抽象~~ 新增 `services/audit-service.tsx`(接口 + Context + hook+ `admin-audit-service.ts`(默认实现)+ `mock-audit-service.ts`(测试用)
- ✅ v2 P0-1 已修复:~~Service 抽象层定义但从未接线(死代码)~~ `admin-audit-service.ts` 改造为 Client-safe移除 `server-only` + data-access 依赖;查询方法抛错强制走 RSC props写操作委托 Server Action`overview/page.tsx` 注入 `AuditServiceProvider``AuditRetentionSettings` 与 `AuditLogExportButton` 改用 `useAuditService()` + `useAuditAnalytics()` hook
- ✅ v2 P0-2 已修复:~~`mock-audit-service.ts` 使用 9 处 `as` 类型断言违规~~ 为所有方法对象字面量添加显式返回类型标注 `Promise<T>`,消除全部 `as` 断言
- ✅ v2 P0-3 已修复:~~保留策略 Server Action 无 `revalidatePath`~~ `saveAuditRetentionConfigAction` 与 `purgeAuditLogsAction` 末尾新增 `revalidatePath("/admin/audit-logs/overview")` 精确刷新缓存
- ✅ v2 P1-1 已修复:~~`trackEvent` event 名称与业务不匹配(保留策略操作用了 `audit.exported`~~ 改为 `audit.retention`(同步更新 `actions.ts` + `api/cron/audit-cleanup/route.ts` + `shared/lib/track-event.ts` EventName 联合类型新增 `audit.retention`
- ✅ v2 P1-2 已修复:~~`AuditErrorBoundary` 使用 `namespace="common"` 不符合模块约定~~ 改为 `namespace="audit"` + 新增 `error.boundaryTitle` / `error.boundaryDescription` 翻译键
- ✅ v2 P1-3 已修复:~~`AuditLogExportButton` 与 `AuditRetentionSettings` 未接入埋点 hook~~ `AuditLogExportButton` 调用 `analytics.trackExport(exportType, 0)``AuditRetentionSettings` 调用 `analytics.trackRetentionConfigChange(config)` + `analytics.trackPurge(result)`
- ✅ v2 P1-4 已修复:~~`getAuditModuleOptions` / `getDataChangeTableOptions` catch 块吞错误返回空数组~~ 改为 `throw error`UI 通过 ErrorBoundary 捕获
- ✅ v2 P1-5 已修复:~~日期筛选器两个 Input 共用 `aria-label={t("table.time")}` 无法区分起止~~ 改为 `aria-label={t("filter.startDate")}` 和 `aria-label={t("filter.endDate")}`(同步更新三个 filters 组件)
- ✅ v2 P1-6 已修复:~~`AuditLogDetailDialog` 的 `DialogDescription` 与 DialogTitle 重复~~ 改为 `t("detail.description")` 并新增对应翻译键
- ✅ v2 P1-7 已修复:~~骨架屏缺少 `aria-hidden` 屏幕阅读器会朗读骨架~~ `audit-log-table-skeleton.tsx` 根 div 与 4 个 `loading.tsx` 添加 `aria-hidden="true" role="presentation"`
- ✅ data-access 职责清晰,无跨模块问题
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 210 | 7 个 Server Action编排层3 导出 + 3 保留策略 + 1 查询;✅ v2 新增 `revalidatePath` + `audit.retention` 埋点;✅ audit-P2-5 `purgeAuditLogsAction` 新增 `loginLogRetentionDays` 参数 + trackEvent 埋点) |
| `data-access.ts` | 387 | 日志查询12 个函数3 查询 + 3 导出 + 3 选项/统计 + 3 概览统计;✅ v2 P1-4 错误处理统一为 throw |
| `export.ts` | 144 | Excel 导出纯函数(列定义 + 行映射 + buildXxxExport |
| `retention.ts` | 181 | 保留策略(配置读写 + 清理 + 纯函数,✅ P2-5 新增;✅ audit-P2-5 新增 `DEFAULT_LOGIN_LOG_RETENTION_DAYS=365` + `loginLogRetentionDays` 配置 + `purgeExpiredAuditLogs` 第二可选参数) |
| `types.ts` | 167 | 类型定义(含概览统计 + 保留策略类型,✅ P2-4/P2-5 扩展;✅ audit-P2-5 `AuditRetentionConfig` 新增 `loginLogRetentionDays` 字段) |
| `hooks/use-log-pagination.ts` | 27 | 分页 URL 状态管理 hook |
| `services/audit-service.tsx` | 124 | Service 接口 + Context Provider + hook✅ P2-8 新增) |
| `services/admin-audit-service.ts` | 82 | 管理员默认实现(✅ v2 P0-1 改造为 Client-safe查询抛错 + 写操作委托 Server Action |
| `services/mock-audit-service.ts` | 70 | 测试用 Mock 实现(✅ v2 P0-2 消除 9 处 `as` 断言) |
| `retention.test.ts` | 85 | 保留策略纯函数单测9 个测试,✅ P2-6 新增) |
| `export.test.ts` | 175 | 导出映射纯函数单测13 个测试,✅ P2-6 新增) |
**组件清单**
| 组件 | 行数 | 职责 |
|------|------|------|
| `components/audit-error-boundary.tsx` | 19 | 审计模块错误边界class 组件,✅ v2 P1-2 namespace 改为 audit |
| `components/audit-log-table-skeleton.tsx` | 42 | 骨架屏(✅ v2 P1-7 添加 `aria-hidden` |
| `components/audit-log-table.tsx` | 115 | 操作日志表格i18n + a11y + 详情对话框) |
| `components/audit-log-filters.tsx` | 97 | 操作日志筛选器i18n + a11y + userId 筛选;✅ v2 P1-5 日期 aria-label 区分起止) |
| `components/audit-log-view.tsx` | 51 | 操作日志视图(筛选+表格+分页) |
| `components/audit-log-export-button.tsx` | 76 | 导出按钮i18n✅ v2 P1-3 接入 `useAuditAnalytics().trackExport` |
| `components/audit-log-detail-dialog.tsx` | 132 | 日志详情对话框discriminated union✅ P2-2 新增;✅ v2 P1-6 修复 DialogDescription 重复标题) |
| `components/login-log-table.tsx` | 109 | 登录日志表格i18n + a11y + 详情对话框) |
| `components/login-log-filters.tsx` | 83 | 登录日志筛选器i18n + a11y + userId 筛选;✅ v2 P1-5 日期 aria-label 区分起止) |
| `components/login-log-view.tsx` | 49 | 登录日志视图 |
| `components/data-change-log-table.tsx` | 150 | 数据变更日志表格i18n + a11y + 详情对话框) |
| `components/data-change-log-filters.tsx` | 139 | 数据变更日志筛选器i18n + a11y + userId 筛选;✅ v2 P1-5 日期 aria-label 区分起止) |
| `components/data-change-log-view.tsx` | 53 | 数据变更日志视图 |
| `components/audit-overview-stats-bar.tsx` | 52 | 概览统计卡片栏(✅ P2-4 新增) |
| `components/audit-activity-trend-chart.tsx` | 67 | 活动趋势折线图(✅ P2-4 新增) |
| `components/data-change-distribution-chart.tsx` | 72 | 数据变更分布柱状图(✅ P2-4 新增) |
| `components/audit-overview-view.tsx` | 113 | 概览页视图(统计+图表+快捷入口+保留配置,✅ P2-4 新增) |
| `components/audit-retention-settings.tsx` | 179 | 保留策略配置组件(✅ P2-5 新增;✅ v2 P0-1 + P1-3 改用 `useAuditService` + `useAuditAnalytics` |
**页面路由**
- `/admin/audit-logs` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹
- `/admin/audit-logs/login-logs` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹
- `/admin/audit-logs/data-changes` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹
- `/admin/audit-logs/overview` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹(✅ P2-4 新增)
- `/api/cron/audit-cleanup` → CRON_SECRET 鉴权(✅ P2-5 新增)
- ✅ 4 个页面均新增 `loading.tsx`(骨架屏)+ `error.tsx`(错误边界)
---
## 2.16 announcements公告模块
**职责**:公告 CRUD + 发布/归档 + 发布通知。
**导出函数**
- Actions`getAnnouncementsAction` / `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction`(✅ P1 已修复actions 层不再直接访问 DB全部下沉到 data-access✅ 发布公告时触发通知模块 `sendBatchNotifications`/ `toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`(✅ V2-P2-13d 新增:置顶切换、已读标记、批量已读状态查询;✅ Audit-P0-3`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` 新增资源所有权/可见性二次校验;✅ Audit-P1-1所有 Action 返回 i18n 化 message✅ Audit-P1-7`handleActionError` 集成 console.error + trackEvent("announcement.action_error") + 区分 PermissionDeniedError / FORBIDDEN_ANNOUNCEMENT
- Data-access`getAnnouncements`(✅ Audit-P0-2`audience` 字段从单值升级为数组 `{ gradeIds, classIds }`,过滤逻辑改用 `inArray`/ `getAnnouncementById` / `getAnnouncementByIdForUser`(✅ Audit-P0-1 新增:用户端详情页专用,结合 status=published + 受众过滤 + 已读状态 + readCount 一次返回,防止越权)/ `isAnnouncementVisibleToAudience`(✅ Audit-P0-1 新增:纯函数,判断公告对受众是否可见,可独立单测)/ `resolveUserAudience`(✅ Audit-P0-2从原 `resolveAudience` 重命名,遍历所有 childrenIds/classIds/gradeIds 而非仅首个)/ `resolveAnnouncementTargetUserIds`(✅ Audit-P1-5从 actions.ts 下沉到 data-access✅ P3-7school 分支改为分页循环遍历 `getAllUserIds(PAGE_SIZE, offset)`PAGE_SIZE=1000避免单次查询超大学校全量用户导致 OOM/ `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)/ `toggleAnnouncementPin`(✅ Audit-P1-6原子化 UPDATE ... SET isPinned = NOT isPinned/ `markAnnouncementAsRead`(✅ Audit-P1-6onDuplicateKeyUpdate 实现幂等 upsert/ `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`(✅ V2-P2-13d 新增:置顶切换 + 已读回执 CRUD/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`(✅ P1-5 新增:管理端列表页和编辑页编排函数)/ `getUserAnnouncementsPageData`(✅ V3-P0-2 新增:用户端列表页编排函数)/ `getAdminAnnouncementDetailPageData`(✅ Audit-P1-2 新增:管理端详情页编排函数,并行 getAnnouncementById + getAnnouncementReadCount + classes.getClassOptions
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`school`(获取年级列表)、`classes`(获取班级列表 + 解析受众)、`users`(获取目标用户 ID 列表)、`notifications`(发布公告时发送通知)、`shared/i18n`(✅ Audit-P1-1Actions 通过 `getTranslations('announcements')` 返回 i18n 化 message
- 被依赖:无
**已知问题**
- ✅ P1-2 已修复:~~所有写操作直接在 actions 层 `db.insert/update/delete`,未下沉到 data-access~~ 写操作已下沉到 data-access5 个新函数)
- ✅ P2 已修复:~~`getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission(ANNOUNCEMENT_READ)`~~ 改为 `requirePermission(Permissions.ANNOUNCEMENT_READ)`
- ✅ 已修复:用户端列表页传入 `audience` 受众过滤school/grade/class管理端返回所有公告
- ✅ 已修复:用户端新增公告详情页 `/announcements/[id]`(只读模式)
- ✅ 已修复:管理端列表页传递 `classes` 数据给 `AdminAnnouncementsView`
- ✅ 已修复:发布公告时(`publishAnnouncementAction` / `createAnnouncementAction` 直接发布 / `updateAnnouncementAction` 状态变为 published触发通知模块 `sendBatchNotifications`
- ✅ 已修复:新增 `loading.tsx` 骨架屏(用户端 + 管理端)
- ✅ P1 已修复:~~全模块零 i18n中英文案硬编码~~ 所有组件接入 next-intl`useTranslations("announcements")`),新增 `src/shared/i18n/messages/{zh-CN,en}/announcements.json` 翻译字典title/description/filter/status/type/form/actions/messages/meta/empty/error 共 11 个命名空间);所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata
- ✅ P1 已修复:~~缺 Error Boundary~~ 新增 4 个 `error.tsx` 错误边界(`/announcements`、`/announcements/[id]`、`/admin/announcements`、`/admin/announcements/[id]`),统一使用 `EmptyState` + i18n 错误文案 + 重试按钮
- ✅ P2 已修复a11y 改进,`announcement-card.tsx` / `announcement-detail.tsx` 添加 `aria-label`
- ✅ P1-5 已修复:~~页面层 `Promise.all` 编排 announcements/school/classes 三个模块的 data-access~~ 新增 `getAdminAnnouncementsPageData` 和 `getEditAnnouncementPageData` 编排函数,页面层仅调用单一函数
- ✅ P1-6 已修复:~~`targetGradeId` / `targetClassId` 为 optional未根据 `type` 做条件必填校验~~ `CreateAnnouncementSchema` 和 `UpdateAnnouncementSchema` 添加 `superRefine(refineAudience)`,年级公告强制 `targetGradeId`,班级公告强制 `targetClassId`
- ✅ P2-11 已修复:~~发布/归档/删除公告无埋点~~ `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction` 新增 `trackEvent` 埋点announcement.created / announcement.updated / announcement.published / announcement.deleted / announcement.archived
- ✅ V2-P0-2 已修复:~~通知标题硬编码~~ `createAnnouncementAction` / `updateAnnouncementAction` / `publishAnnouncementAction` 通过 `getTranslations('announcements')` 生成 i18n 通知标题(`notification.publishedTitle` / `publishedContent`
- ✅ V2-P1-1 已修复:~~AnnouncementList 客户端 useState/useMemo 过滤~~ 改为纯服务端过滤模式Select 切换仅更新 URL `?status=` 触发 RSC 重新渲染
- ✅ V2-P1-4 已修复:~~表单无服务端校验错误展示~~ `announcement-form.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示title/content/targetGradeId/targetClassId
- ✅ V2-P2-13d 已实现:公告置顶 + 已读回执。`announcements` 表新增 `isPinned` 字段(置顶优先排序,`getAnnouncements` orderBy 改为 `desc(isPinned), desc(createdAt)`);新增 `announcement_reads` 表(唯一索引 announcementId+userId 保证幂等);新增 3 个 Server Action`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`+ 5 个 data-access 函数(`toggleAnnouncementPin` / `markAnnouncementAsRead` / `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser``Announcement` 类型新增 `isPinned` / `isReadByCurrentUser?` / `readCount?` 字段;新增 `AnnouncementRead` 类型;埋点新增 `announcement.pin_toggled` / `announcement.marked_read`
- ✅ V3-P0-2 已修复:~~用户端公告列表页 `resolveAudience` 50 行业务逻辑在路由层~~ 新增 `getUserAnnouncementsPageData` 编排函数(含 `resolveAudience` 内部函数),将受众解析逻辑从 `page.tsx` 下沉到 `data-access.ts`,页面层仅调用单一函数
- ✅ Audit-P0-1 已修复:~~用户端详情页越权访问草稿/他人班级公告~~ 新增 `getAnnouncementByIdForUser`,结合 `status="published"` + 受众过滤(`isAnnouncementVisibleToAudience` 纯函数)+ 已读状态 + readCount 一次返回,`markAnnouncementAsReadAction` 调用此函数做资源可见性二次校验,不可见时抛 `FORBIDDEN_ANNOUNCEMENT`
- ✅ Audit-P0-2 已修复:~~`resolveAudience` 仅取首个 child/class/grade 导致家长多孩子/教师多班级时只看到首个受众公告~~ audience 类型升级为 `{ gradeIds: string[]; classIds: string[] }``resolveAudience` 重命名为 `resolveUserAudience` 遍历全部;`getAnnouncements` 过滤逻辑改用 `inArray` 支持数组
- ✅ Audit-P0-3 已修复:~~`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` 缺资源所有权二次校验~~ 前者新增资源存在性校验(不存在返回 404后者调用 `getAnnouncementByIdForUser` 做可见性二次校验
- ✅ Audit-P1-1 已修复:~~Actions 返回英文硬编码 message~~ 全部改用 `t("messages.created")` / `t("messages.updated")` / `t("messages.deleted")` / `t("messages.published")` / `t("messages.archived")` / `t("messages.pinned")` / `t("messages.unpinned")` / `t("messages.markedRead")` / `t("messages.invalidFormData")` / `t("messages.unexpectedError")` / `t("messages.permissionDenied")` / `t("messages.forbidden")`
- ✅ Audit-P1-2 已修复:~~管理端 `/admin/announcements/[id]` 原为编辑表单页,缺独立详情页~~ 拆分为详情页(`AnnouncementDetail canManage=true`,调用 `getAdminAnnouncementDetailPageData` 含 readCount 统计)+ `/edit` 子路由(编辑表单);用户端 `AnnouncementList` 通过 `service.getReadStatus` 批量获取已读状态传入卡片做已读/未读视觉区分
- ✅ Audit-P1-3 已修复:~~组件直接 import actions 耦合~~ 引入 `AnnouncementsServiceContext` 依赖注入(`AnnouncementsServiceProvider` + `useAnnouncementsService()` + `DefaultAnnouncementsService` 默认实现代理到真实 Server Actions所有交互型组件Card/Detail/Form/List通过 Hook 消费,便于测试 mock 和未来角色差异化实现
- ✅ Audit-P1-4 已修复:~~`AnnouncementForm` 硬编码 `router.push("/admin/announcements")` 路由~~ 新增 `onSuccess` / `onCancel` 回调优先于 `successHref` / `cancelHref` 兜底,便于在 Dialog 中复用
- ✅ Audit-P1-5 已修复:~~`resolveTargetUserIds` 业务逻辑位于 actions 层~~ 下沉到 data-access `resolveAnnouncementTargetUserIds`
- ✅ Audit-P1-6 已修复:~~`toggleAnnouncementPin` read-then-write 竞态~~ 原子化 `UPDATE ... SET isPinned = NOT isPinned`~~`markAnnouncementAsRead` select-then-insert 双查询~~ 改用 `onDuplicateKeyUpdate` 幂等 upsert
- ✅ Audit-P1-7 已修复:~~`handleActionError` 缺日志/埋点/i18n~~ 重写为接收 `t` 函数,集成 `console.error` + `trackEvent("announcement.action_error")` + 区分 `PermissionDeniedError` / `FORBIDDEN_ANNOUNCEMENT` / 其他错误
- ✅ Audit-P1-8 已修复:~~表单与 Dialog 行为耦合(保存后路由跳转关闭不了 Dialog~~ `AdminAnnouncementsView` Dialog 内表单通过 `onSuccess={handleFormSuccess}` 关闭弹窗
- ✅ Audit-P2-1 已修复:~~a11y置顶按钮嵌套在 Link 内导致键盘导航异常~~ 改为绝对定位 + `onKeyDown` 处理 Enter/Space脱离外层 Link 语义
- ✅ Audit-P2-2 已修复:~~`mapRow` 类型不安全~~ 改用 `typeof announcements.$inferSelect` 推导
- ✅ Audit-P2-3 已修复:~~i18n 死键与缺键~~ 清理 `description.detail` / `description.create` 等死键,补 `messages.invalidFormData` / `messages.unexpectedError` / `messages.permissionDenied` / `messages.forbidden`
- ✅ Audit-P2-4 已修复:~~`AnnouncementList.detailHrefBuilder` 死 prop~~ 删除(已由 V3 的 `detailHrefPrefix` 替代,本次彻底清理)
- ✅ Audit-P2-5 已修复:~~两处 loading.tsx 重复骨架屏 JSX~~ 抽取 `AnnouncementListSkeleton` 共享组件,支持 `showCreateButton` prop
- ✅ Audit-P2-6 已修复:~~用户端公告列表无分页 UI~~ 新增 `AnnouncementPagination` 组件URL `?page=N` 持久化、a11y、窗口策略、边界处理新增 `countAnnouncements` data-access 函数;`getUserAnnouncementsPageData` 升级为接收 `page`/`pageSize` 并返回 `PaginatedAnnouncements` 类型 `{ items, total, page, pageSize }`,内部并行执行列表查询与 count 查询;`/announcements/page.tsx` 读取 `?page=` 查询参数并传入分页数据;新增 i18n 键 `pagination.label` / `prev` / `next` / `page`
- ✅ Audit-P2-7 已修复:~~模块零测试覆盖~~ 新增 3 个测试文件:`schema.test.ts`18 个测试,覆盖 `refineAudience` 条件校验矩阵school/grade/class 类型与 target 必填组合 + transform 归一化);`is-announcement-visible.test.ts`15 个测试,覆盖 `isAnnouncementVisibleToAudience` 纯函数,含 P0-2 多孩子/多班级场景);`components/announcement-card.test.tsx`16 个测试覆盖基础渲染、isRead 视觉区分、isPinned 交互、a11y 键盘导航 Enter/Space、toggling 禁用、错误回滚)。同时修复项目级 `@testing-library/dom` peer dependency 缺失(已影响 `dashboard-section.test.tsx` 等现有测试)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~400 | 9 个 Server Action + 通知触发逻辑 + trackEvent 埋点P1-2 已修复,无直接 DB 操作V2-P2-13d 新增置顶/已读/批量已读状态 3 个 ActionAudit-P0-3 新增资源所有权二次校验Audit-P1-1 i18n 化 messageAudit-P1-7 `handleActionError` 集成日志/埋点/i18n |
| `data-access.ts` | ~580 | 公告 CRUD + 发布/归档 + 受众过滤 + `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData` / `getUserAnnouncementsPageData` 编排函数 + `getAdminAnnouncementDetailPageData`Audit-P1-2 新增)+ `countAnnouncements`Audit-P2-6 新增V2-P2-13d 置顶切换 + 已读回执 5 个函数Audit-P0-1 新增 `getAnnouncementByIdForUser` / `isAnnouncementVisibleToAudience`Audit-P0-2 `resolveAudience` → `resolveUserAudience` 遍历全部受众Audit-P1-5 `resolveAnnouncementTargetUserIds` 下沉Audit-P1-6 `toggleAnnouncementPin` / `markAnnouncementAsRead` 原子化Audit-P2-2 `mapRow` 改用 `$inferSelect` 推导 |
| `schema.ts` | ~95 | Zod 校验 + `refineAudience` 条件校验(✅ P1-6 新增 superRefine |
| `types.ts` | ~130 | 类型定义(`GetAnnouncementsParams.audience` 升级为数组 Audit-P0-2新增 `UserAudience` Audit-P0-2新增 `AnnouncementsService` 接口 Audit-P1-3新增 `PaginatedAnnouncements` Audit-P2-6 |
| `schema.test.ts` | ~200 | ✅ Audit-P2-7 新增18 个测试,覆盖 `refineAudience` 条件校验矩阵 + transform 归一化 |
| `is-announcement-visible.test.ts` | ~180 | ✅ Audit-P2-7 新增15 个测试,覆盖 `isAnnouncementVisibleToAudience` 纯函数(含 P0-2 多孩子/多班级场景) |
| `components/announcements-service-context.tsx` | ~40 | ✅ Audit-P1-3 新增:`AnnouncementsServiceProvider` + `useAnnouncementsService()` |
| `components/default-announcements-service.ts` | ~60 | ✅ Audit-P1-3 新增:`AnnouncementsService` 默认实现,代理到真实 Server Actions |
| `components/announcement-list-skeleton.tsx` | ~40 | ✅ Audit-P2-5 新增:共享骨架屏,支持 `showCreateButton` prop |
| `components/announcement-pagination.tsx` | ~100 | ✅ Audit-P2-6 新增分页组件URL `?page=N` 持久化a11y + 窗口策略 + 边界处理 |
| `components/announcement-card.test.tsx` | ~300 | ✅ Audit-P2-7 新增16 个测试覆盖基础渲染、isRead 视觉、isPinned 交互、a11y 键盘导航、toggling 禁用、错误回滚 |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/announcement-list.tsx` | 公告列表(用户端,支持状态筛选;✅ V2-P1-1纯服务端过滤Select 切换更新 URL ?status= 触发 RSC 重新渲染;✅ V3新增 `detailHrefPrefix` prop 替代 `detailHrefBuilder` 函数 prop✅ Audit-P1-2用户端通过 `service.getReadStatus` 批量获取已读状态传入卡片;✅ Audit-P2-4删除死 prop `detailHrefBuilder`;✅ Audit-P2-6新增 `pagination` prop传入时在列表底部渲染 `AnnouncementPagination` |
| `components/announcement-card.tsx` | 公告卡片(列表项;✅ Audit-P1-3通过 `useAnnouncementsService()` 消费 togglePin✅ Audit-P2-1置顶按钮绝对定位 + onKeyDown 处理;✅ Audit-P1-2新增 `isRead` prop 已读/未读视觉区分) |
| `components/announcement-detail.tsx` | 公告详情(只读;✅ Audit-P1-3所有 Action 调用改用 `useAnnouncementsService()` |
| `components/announcement-form.tsx` | 公告表单(创建/编辑,✅ P1-6条件校验由 schema superRefine 保证;✅ V2-P1-4fieldErrors + aria-invalid✅ Audit-P1-3通过 `useAnnouncementsService()` 消费 create/update✅ Audit-P1-4/P1-8新增 `onSuccess` / `onCancel` 回调优先于 `successHref` / `cancelHref` 兜底) |
| `components/admin-announcements-view.tsx` | 管理端公告视图(列表 + 筛选;✅ Audit-P1-3包裹 `AnnouncementsServiceProvider`;✅ Audit-P1-8Dialog 内表单通过 `onSuccess` 关闭弹窗) |
| `components/announcement-pagination.tsx` | ✅ Audit-P2-6 新增分页导航URL 持久化、a11y、窗口策略、边界处理 |
---
## 2.17 files文件模块
**职责**:文件附件 CRUD + 批量删除 + 统计 + 上传/预览 UI。
**导出函数**
- Actions✅ 2026-06-25 新增 `actions.ts` 编排层P0 修复):`uploadFileAction`FILE_UPLOAD 权限 + Zod 校验 + storageProvider.save + trackEvent + logAudit/ `getFileAction`FILE_READ 权限 + ownership 检查:非 FILE_DELETE 持有者只能读自己上传的)/ `deleteFileAction`FILE_DELETE 权限 + storageProvider.delete/ `batchDeleteFilesAction`FILE_DELETE 权限 + Zod 校验 max 100 ids/ `getFileListAction`FILE_READ 权限)/ `getFileStatsAction`FILE_READ 权限)
- Data-access`createFileAttachment` / `getFileAttachment`cache/ `getFileAttachmentsByTarget`cache/ `getFileAttachmentsByUploader`cache/ `getAllFileAttachments`cache/ `deleteFileAttachment` / `deleteFileAttachments`(批量)/ `getFileAttachmentsWithFilters`cache/ `getFileStats`cache/ `getFileByUrl`cache✅ 2026-06-25 新增:按 url 字段查询)/ `getFileAttachmentsByIds`cache✅ P2 已修复7 个读函数使用 `React.cache()` 包装实现请求级 memoization
- Types✅ 2026-06-25 更新):`FileTargetType` 新增 `"user_avatar"` 枚举值,现为 `"exam" | "textbook" | "question" | "announcement" | "homework" | "user_avatar"`用于用户头像上传场景targetId = users.id`FileUploadResult` / `FileAttachment` / `CreateFileAttachmentInput` / `FileAttachmentQueryParams` / `FileStats` / `BatchDeleteResult`
- Schema✅ 2026-06-25 新增 `schema.ts``UploadMetadataSchema` / `BatchDeleteSchema`max 100 ids/ `FileListQuerySchema` / `FileTargetTypeSchema` / `validateFileSize`
- Components`FileUpload` / `FilePreview` / `FilePreviewDialog`(✅ 2026-06-25 补录)/ `FileIcon` / `AdminFilesView`
- Hooks✅ 2026-06-25 新增 `hooks/` 目录):`useFileUpload`XHR 上传、进度、状态机、校验)/ `useFileBatchOperations`(客户端筛选、选择、批量删除)/ `useFilePreview` + `useImageZoom`(图片缩放)
**依赖关系**
- 依赖:`shared/*`、`@/auth`
- 被依赖:`app/api/upload` / `app/api/files/[id]` / `app/api/files/batch-delete`(✅ 2026-06-25 重构3 个 API 路由改为委托 `actions.ts`,仅保留速率限制和请求解析)/ `homework`(✅ 2026-06-24`homework/actions.deleteScanAction` 调用 `deleteFileAttachment``homework/components/scan-uploader` 调用 `/api/upload`targetType="homework"/ `settings`(✅ 2026-06-25 新增:`settings/actions-avatar.ts` 通过 `storageProvider` 抽象删除旧头像文件targetType="user_avatar"`settings/components/avatar-upload.tsx` 上传头像走 `/api/upload`
**已知问题**
- ✅ P0 已修复:~~无 `actions.ts`data-access 被路由直接调用~~ 新建 `actions.ts` 编排层,所有 API 路由upload/files/[id]/files/batch-delete改为委托 actions路由层仅保留速率限制
- ✅ P0 已修复:~~`app/api/upload/route.ts` 仅 `requireAuth()` 无权限校验~~ 改为 `requirePermission(FILE_UPLOAD)`
- ✅ P0 已修复:~~`app/api/files/[id]/route.ts` 横向越权(任意用户可读/删他人文件)~~ `getFileAction` 加入 ownership 检查(非 FILE_DELETE 持有者只能读自己上传的)
- ✅ P1 已修复:~~i18n 缺失,组件硬编码英文~~ 补全 `en/files.json` + `zh-CN/files.json`88 行,含 title/upload/list/preview/admin 等命名空间),所有组件接入 `useTranslations`
- ✅ P1 已修复:~~缺少 `loading.tsx` 骨架屏和 `error.tsx` 错误边界~~ 新增 `app/(dashboard)/admin/files/loading.tsx` + `error.tsx`
- ✅ P1 已修复:~~组件逻辑与 UI 耦合,不可单测~~ 抽取 3 个自定义 hooks`useFileUpload` / `useFileBatchOperations` / `useFilePreview`
- ✅ P1 已修复:~~`settings/actions-avatar.ts` 直接 import `fs/promises` 绕过 storageProvider 抽象~~ 改用 `storageProvider.delete`
- ✅ P1 已修复:~~`FileTargetType` 缺 `"user_avatar"` 枚举值导致 settings 头像上传 targetType 未注册~~ 已加入枚举
- ✅ P2 已修复:~~`file-list.tsx` 125 行死代码~~ 已删除
- ✅ P2 已修复:~~所有函数 try-catch 吞错误返回空数组/null~~ 所有 catch 块已添加 `console.error` 输出错误上下文
- ✅ P2 已修复:`getFileAttachmentsWithFilters` 中 `or(...)!` 非空断言清理为安全守卫
- ✅ P2 已修复:~~`getFileAttachmentsWithFilters` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注
- ✅ P2 已修复:~~`admin-files-view.tsx` 中 `t(`filter.${key}` as never)` 类型断言~~ 改为 `renderTypeLabel` 辅助函数 + 字面量联合类型 + switch
- ✅ P2 已修复:~~`file-preview-dialog.tsx` 中 `label` 变量赋值后未使用~~ 已移除
- ✅ P2 新增:监控埋点 — `actions.ts` 接入 `trackEvent`file.uploaded/upload_failed/deleted/batch_deleted/viewed/action_error+ `logAudit`
- ✅ P2 新增a11y — 所有图标加 `aria-hidden`,按钮加 `aria-label``AdminFilesView` 用三层 `WidgetBoundary` 包裹StatsSection / UploadSection / ListSection
- ✅ 职责单一,不跨模块查询
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 305 | 文件 CRUD + 批量删除 + 统计11 个函数 + mapRow |
| `actions.ts` | 330 | 6 个 Server Action编排层权限+Zod+storageProvider+trackEvent+logAudit |
| `schema.ts` | - | Zod 校验UploadMetadata/BatchDelete/FileListQuery/FileTargetType |
| `types.ts` | 70 | 类型定义FileTargetType 含 user_avatar |
| `components/*` | 5 文件 | 上传/预览/预览弹窗/图标/管理视图 |
| `hooks/*` | 3 文件 | 上传/批量操作/预览 hooks |
---
## 2.18 course-plans课程计划模块
**职责**:课程计划 CRUD + 周计划项 CRUD + 排序 + 批量操作 + 复制 + 年级进度统计 + 日历视图 + CSV 导出 + 模板库 + 拖拽排序 + 数据联动 + 组件级 Error Boundary。
**导出函数**
- Actions`getCoursePlansAction` / `getCoursePlanAction` / `createCoursePlanAction` / `updateCoursePlanAction` / `deleteCoursePlanAction` / `createCoursePlanItemAction` / `updateCoursePlanItemAction` / `deleteCoursePlanItemAction` / `toggleCoursePlanItemCompletedAction` / `getGradeCoursePlanProgressAction`年级仪表盘维度4/ `reorderCoursePlanItemsAction`(✅ P1-7 新增:拖拽排序周计划条目)/ `bulkToggleItemsAction`(✅ P2-4 新增:批量标记完成状态)/ `copyCoursePlanAction`(✅ P2-4 新增复制计划到其他班级P2-5 复用为"从模板创建"/ `getTemplateCandidatesAction`(✅ P2-5 新增:获取模板候选列表,复用现有计划作为模板)/ `trackCoursePlanEvent`(✅ P2-8 新增:监控埋点接口预留,已在 plan_deleted/items_reordered/plan_exported/plan_created_from_template 四处调用)
- Data-access与 actions 对应 + `getGradeCoursePlanProgress` + `reorderCoursePlanItems`(✅ P1-7 新增)+ `bulkUpdateItemCompleted`(✅ P2-4 新增)+ `copyCoursePlanToClasses`(✅ P2-4 新增)+ `enrichPlanRows`(✅ P1-1 新增:批量解析班级/科目/教师名称,通过动态 import 对方 data-access**不再直接 JOIN 其他模块表**+ `buildScopeCondition`(✅ P0-3 新增DataScope 权限过滤)
- Lib✅ P2 新增):`lib/export-utils.ts`(纯函数 `planToExportRows`/`buildExportColumns`/`exportCoursePlanReport`CSV 导出不依赖 i18n标签由调用方传入/ `lib/calendar-utils.ts`(纯函数 `planToCalendarEvents`/`buildMonthGrid`/`eventsOnDay`/`filterEventsInRange` + 日期工具 `addDays`/`addMonths`/`startOfWeek`/`startOfMonth`/`endOfMonth`/`isSameDay`/`isWithinRange`/`formatDateISO`/`parseISODate`
- Types✅ v4-P2-7 新增):`GradeCoursePlanProgressItem` / `GradeCoursePlanProgressResult` / `CoursePlanQueryScope`(✅ P0-3 新增:数据范围上下文,含 userId/isAdmin/classIds/teacherId/studentId/ `ReorderCoursePlanItemInput`(✅ P1-7 新增)/ 类型守卫 `isCoursePlanStatus` / `isCoursePlanSemester`(✅ P1-4 新增:替代 `as` 断言)/ 配置驱动 `ROLE_WIDGET_CONFIG`(✅ P2 新增admin/teacher/parent/student 四角色 Widget 配置)/ `CalendarEvent`(✅ P2-7 新增:日历事件类型)/ `CoursePlanExportColumnKey` + `CoursePlanColumnLabels`(✅ P2-6 新增:导出列类型与标签接口)
- Components`CoursePlanList`(✅ P1 修复:使用 `<Link>` + 类型守卫 + i18n/ `CoursePlanDetail`(✅ P1 修复:批量选择 + `successHref` prop + 埋点;✅ P2-1 增强:`SectionErrorBoundary` 包裹进度条 + 周计划表 + 日历视图;✅ P2-3 增强:`textbooksHref`/`homeworkHref` props 支持数据联动跳转;✅ P2-6 增强CSV 导出按钮;✅ P2-7 增强:嵌入 `CoursePlanCalendar`/ `CoursePlanForm`(✅ P1 修复:类型守卫 + `successHref` prop✅ P2-5 增强:嵌入 `TemplatePickerDialog`/ `CoursePlanItemEditor`(✅ P1 修复i18n/ `CoursePlanProgress`(✅ P1 修复a11y aria 属性 + i18n/ `SortableWeekRow`(✅ P1-7/P2-3 新增:@dnd-kit/sortable 拖拽行 + 数据联动跳转链接 + 复选框)/ `CoursePlanCalendar`(✅ P2-7 新增:月历视图 6×7 网格 + 上一月/下一月/今天导航 + 事件列表)/ `TemplatePickerDialog`(✅ P2-5 新增:模板选择器 + 搜索过滤 + 克隆到目标班级)
**依赖关系**
- 依赖:`shared/*`(含 ✅ P2-6 新增 `shared/lib/export-utils.ts` 通用 CSV 导出工具)、`@/auth`、`classes`(✅ P1-1 已修复:通过动态 import `getClassNamesByIds` 批量解析,不再直接 JOIN classes 表)、`school`(✅ P1-1 已修复:通过动态 import `getSubjectNameMapByIds` 批量解析;`getSubjectOptions` 已移至 school 模块)、`users`(✅ P1-1 已修复:通过动态 import `getUserNamesByIds` 批量解析)、`@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`(✅ P1-7 新增:拖拽排序依赖)
- 被依赖parent/student 路由通过 page.tsx 调用 data-access✅ P2-2 已新增)
**已知问题**
- ✅ P0-1 已修复:教师详情页信息泄露 — `getCoursePlanById` 增加 `scope` 参数,非 admin 仅能查看自己负责的计划
- ✅ P0-2 已修复admin 页面无 requirePermission — 所有 admin 页面补充 `requirePermission(COURSE_PLAN_READ/MANAGE)`
- ✅ P0-3 已修复data-access 无 DataScope 过滤 — `getCoursePlans`/`getCoursePlanById`/`getGradeCoursePlanProgress` 增加可选 `scope` 参数
- ✅ P0-4 已修复i18n 严重缺失 — 翻译键从 5 个扩展至 ~230 个,覆盖状态/筛选/列表/详情/表单/条目/进度/Toast/教师/家长/学生/错误/日历/导出/批量/模板/加载 + Error Boundary 文案
- ✅ P1-1 已修复:跨模块直接 JOIN — 移除 `buildPlanSelect` 的 `leftJoin(classes/subjects/users)`,改用 `enrichPlanRows` 动态 import 对方 data-access 批量查询
- ✅ P1-2 已修复:缺 loading.tsx/error.tsx — 12 个路由(含 P2-2 新增 parent/student均补充骨架屏 + 错误边界(使用 `EmptyState` + i18n
- ✅ P1-3 已修复:原生 `<a>` 标签 — 替换为 Next.js `<Link>` 组件
- ✅ P1-4 已修复:`as` 断言 — 替换为类型守卫函数 `isCoursePlanStatus`/`isCoursePlanSemester`
- ✅ P1-5 已修复URL 路径推断角色 — 改为 `successHref` prop 显式传入
- ✅ P1-6 已修复Action 入参未验证 — `getCoursePlansAction`/`getGradeCoursePlanProgressAction`/`reorderCoursePlanItemsAction`/`bulkToggleItemsAction`/`copyCoursePlanAction` 增加 Zod schema 验证
- ✅ P1-7 已修复:死代码 `reorderCoursePlanItems` — 已补充对应 Action + UI 拖拽排序(`SortableWeekRow` + `DndContext` + `SortableContext`PointerSensor + KeyboardSensor 支持 a11y
- ✅ P2-1 已修复:无 Error Boundary — `SectionErrorBoundary` 包裹进度条 + 周计划表 + 日历视图三个数据区块
- ✅ P2-2 已修复parent/student 无路由 — 新增 parent/student 课程计划只读路由(按子女班级/学生班级 scope 过滤)
- ✅ P2-3 已修复:无数据联动 — 周计划项章节文本渲染为教材跳转链接,行尾新增作业跳转按钮(通过 `textbooksHref`/`homeworkHref` props 按角色注入)
- ✅ P2-4 已修复:无批量操作 — 批量标记完成 + 批量复制计划到其他班级
- ✅ P2-5 已修复:无模板复用 — `TemplatePickerDialog` 复用现有计划作为模板,搜索 + 选择 + 克隆到目标班级
- ✅ P2-6 已修复:无导出 — CSV 导出UTF-8 BOM 兼容 Excel通用工具迁移至 `shared/lib/export-utils.ts`
- ✅ P2-7 已修复:无日历视图 — `CoursePlanCalendar` 月历视图,周计划项按 `plan.startDate` 映射到日期范围
- ✅ P2-8 已修复:无监控埋点 — `trackCoursePlanEvent` 预留接口,已在 plan_deleted/items_reordered/plan_exported/plan_created_from_template 四处调用
- ✅ actions 层使用 `handleActionError`/`revalidatePlanPaths` 辅助函数(良好范例)
- ✅ `resolveScope` 函数正确处理 DataScope union 类型admin/teacher/parent/student 四角色范围解析)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 507 | 课程计划 + 周计划项 CRUD + 批量 + 复制 + 名称解析解耦 |
| `actions.ts` | 520 | 14 个 Server Action + 埋点接口 + scope 解析 + 模板候选 |
| `schema.ts` | 213 | Zod 校验(含 Action 入参验证 schema |
| `types.ts` | 161 | 类型定义 + 类型守卫 + 角色配置驱动 + CalendarEvent + 导出列类型 |
| `lib/export-utils.ts` | 91 | CSV 导出纯函数planToExportRows + buildExportColumns + exportCoursePlanReport |
| `lib/calendar-utils.ts` | 175 | 日历视图纯函数(事件映射 + 月历网格 + 日期工具) |
| `components/course-plan-list.tsx` | 162 | 列表组件i18n + Link + 类型守卫) |
| `components/course-plan-detail.tsx` | 384 | 详情组件(批量选择 + 埋点 + successHref + Error Boundary + 拖拽 + 导出 + 日历 + 数据联动) |
| `components/course-plan-form.tsx` | 326 | 表单组件(类型守卫 + successHref + 模板选择器) |
| `components/course-plan-item-editor.tsx` | 250 | 周计划编辑器i18n |
| `components/course-plan-progress.tsx` | 42 | 进度条a11y + i18n |
| `components/sortable-week-row.tsx` | 176 | 拖拽行(@dnd-kit + 数据联动跳转 + 复选框 + a11y |
| `components/course-plan-calendar.tsx` | 200 | 月历视图6×7 网格 + 导航 + 事件列表 + a11y |
| `components/template-picker-dialog.tsx` | 195 | 模板选择器(搜索 + 列表 + 克隆 + a11y |
| `i18n/zh-CN/course-plans.json` | 233 | 中文翻译(~230 键) |
| `i18n/en/course-plans.json` | 233 | 英文翻译(~230 键) |
**页面路由**(✅ P1-2 全部补充 loading.tsx + error.tsx✅ P2-2 新增 parent/student 路由):
| 路由 | 角色 | 权限 | 说明 |
|------|------|------|------|
| `admin/course-plans` | admin | COURSE_PLAN_READ | 列表(全局视图) |
| `admin/course-plans/[id]` | admin | COURSE_PLAN_READ | 详情(含日历/导出/拖拽) |
| `admin/course-plans/[id]/edit` | admin | COURSE_PLAN_MANAGE | 编辑 |
| `admin/course-plans/create` | admin | COURSE_PLAN_MANAGE | 创建(含从模板创建) |
| `teacher/course-plans` | teacher | COURSE_PLAN_READ | 列表(按 teacherId 过滤) |
| `teacher/course-plans/[id]` | teacher | COURSE_PLAN_READ | 详情scope 过滤防越权 + 数据联动) |
| `parent/course-plans` | parent | COURSE_PLAN_READ | 列表(按子女 classIds 过滤) |
| `parent/course-plans/[id]` | parent | COURSE_PLAN_READ | 详情(按子女 classIds scope 过滤) |
| `student/course-plans` | student | COURSE_PLAN_READ | 列表(按 class_members scope 过滤) |
| `student/course-plans/[id]` | student | COURSE_PLAN_READ | 详情(按 class_members scope 过滤 + 教材联动) |
---
## 2.19 parent家长模块
**职责**:家长视角的子女数据聚合与展示。
**导出函数**
- Data-access`getChildren` / `getChildBasicInfo` / `getChildDashboardData`V3-11 增强:并行调用 `homework/data-access.getStudentExamResults` 获取考试结果)/ `getParentDashboardData` / `verifyParentChildRelation` / `getChildNameList`(✅ v4 新增:用于详情页头部多子女切换器,一次批量查询避免 N+1
- TypesV3-11 新增):`ChildExamResultItem`单条考试结果submissionId/examId/examTitle/assignmentId/assignmentTitle/score/maxScore/submittedAt/status`ChildDashboardData` 扩展 `examResults: ChildExamResultItem[]` 字段
- Components`ParentDashboard` / `ChildCard` / `ChildDetailHeader` / `ChildDetailPanel`V3-11 增强:新增 exams Tab/ `SiblingSwitcher` / `ChildHomeworkSummary` / `ChildHomeworkDetail`v4 新增)/ `ChildGradeSummary` / `ChildGradeDetail`v4 新增)/ `ChildScheduleCard` / `ChildExamDetail`V3-11 新增:子女考试详情视图,汇总卡片+考试列表)/ `ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentAttentionBanner`v4 新增)/ `ParentAttendanceWarning`v4 新增)/ `ParentAttendanceRateCard`v4 新增)/ `ParentAttendanceCalendar`v4 新增)/ `ParentExportButton`v4 新增)
**v4 修复(产品/UX 维度)**
- ✅ FEAT-G01新增 `/parent/leave` 请假申请占位页(含 loading.tsx
- ✅ FEAT-G02详情页 Schedule Tab 支持完整周课表(新增 `weeklySchedule` 字段 + `ChildWeeklyScheduleItem` 类型 + `buildWeeklySchedule` 函数)
- ✅ FEAT-G03详情页 Grades Tab 新增 `ChildGradeDetail` 按科目分组展示(平均分、趋势、最近成绩)
- ✅ FEAT-G04详情页 Homework Tab 新增 `ChildHomeworkDetail` 展示完整作业信息(状态、截止、提交时间、尝试次数)
- ✅ FEAT-G05考勤页新增 `ParentAttendanceWarning` 异常预警横幅(聚合缺勤/迟到/低出勤率)
- ✅ FEAT-G06详情页底部新增"Contact Teacher"快捷入口
- ✅ FEAT-G07详情页头部新增 `SiblingSwitcher` 多子女切换器
- ✅ LAYOUT-P01仪表盘新增 `ParentAttentionBanner` 待办事项横幅
- ✅ LAYOUT-P02`ChildCard` 突出 Overdue 异常(红色边框 + AlertTriangle 图标)
- ✅ LAYOUT-P03仪表盘快捷入口改为 4 宫格大图标卡片
- ✅ LAYOUT-P04详情页改为 Tab 布局Overview/Homework/Grades/Schedule/Attendance/Diagnostic
- ✅ LAYOUT-P05详情页新增面包屑导航
- ✅ LAYOUT-P07成绩趋势图 X 轴改用序号,避免日期重叠
- ✅ LAYOUT-P08成绩页新增 `ParentExportButton` 导出按钮(占位)
- ✅ LAYOUT-P09考勤页新增 `ParentAttendanceCalendar` 月历视图(按状态着色,支持按月切换)
- ✅ LAYOUT-P10考勤异常高亮与 FEAT-G05 同步实现)
- ✅ NAV-P02Grades/Attendance 页面描述明确职责(多子女对比 vs 单子女详情)
- ✅ NAV-P03详情页实现 `?tab=` 参数支持
- ✅ NAV-P04所有 parent 路由新增 `loading.tsx` 骨架屏 + `error.tsx` 错误边界
- ✅ DATA-P02成绩卡片新增 TrendIcon 进步/退步/持平标识
- ✅ DATA-P03排名展示新增"Top X%"百分比
- ✅ DATA-P04作业列表新增科目标识 Badge
- ✅ DATA-P05作业分数显示新增"pts"单位
- ✅ DATA-P06考勤页新增 `ParentAttendanceRateCard` 出勤率汇总卡片
- ✅ HABIT-P01仪表盘"一眼定位异常"能力AttentionBanner 聚合)
- ✅ HABIT-P02待办横幅作业项直接跳转详情页 homework tab1 次点击到达)
- ✅ HABIT-P03多子女切换无需返回仪表盘
- ✅ HABIT-P06仪表盘展示未读/待办数量
- ✅ A11Y-P02Overdue 状态增加 AlertTriangle 图标辅助
- ✅ A11Y-P04成绩图表容器新增 aria-label
- ✅ PERF-P01/P02骨架屏 + 错误边界
- ✅ PERF-P03空状态新增"Contact support"引导按钮
- ✅ PERF-P04`ChildCard` Link 添加 `prefetch`
- ✅ MOBILE-P03移动端子女卡片改为水平滑动 Carouselsnap-x
- ✅ MOBILE-P04作业/成绩列表项 `min-h-[44px]` 触摸区域
**V3-11 修复(家长端考试详情)**
- ✅ V3-11`parent/types.ts` 新增 `ChildExamResultItem` 类型;`ChildDashboardData` 扩展 `examResults` 字段
- ✅ V3-11`parent/data-access.ts` `getChildDashboardData` 并行调用 `homework/data-access.getStudentExamResults` 获取子女考试结果(加入现有 `Promise.all`
- ✅ V3-11新增组件 `ChildExamDetail`(考试成绩汇总卡片:已参加考试数/平均分/最高分 + 考试成绩列表:考试标题/分数/得分率 Progress/提交时间)
- ✅ V3-11`child-detail-panel.tsx` 新增 exams Tab对标智学网家长端考试详情
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`classes`(合理)、`homework`合理V3-11 新增 `getStudentExamResults` 调用)、`grades`(合理)、`users`(合理)、`school`(合理)、`attendance`v4 新增:考勤页复用 `StudentAttendanceView`;⚠️ 跨模块 UI 类型依赖3 个组件直接 import `@/modules/attendance/types`
- 被依赖:无
**已知问题**
- ⚠️ P13 个 parent 组件(`parent-attendance-warning.tsx`/`parent-attendance-rate-card.tsx`/`parent-attendance-calendar.tsx`)直接 import `@/modules/attendance/types`,违反模块解耦原则(应通过 data-access 接口或 shared 类型抽象)
- ⚠️ P1parent-attendance-calendar.tsx 重新定义 `STATUS_DOT`/`STATUS_LABEL` 常量,与 attendance 模块重复
- ⚠️ P13 个 parent 组件的纯函数(`buildWarnings`/`aggregate`/`rateTone`/`formatDateKey`/`parseDateKey`/`buildCalendarDays`/`isSameDay`)未导出,无法单测
- ✅ P1 已修复:~~`app/(dashboard)/parent/children/[studentId]/page.tsx` 直接访问 DB违反三层架构~~ 改为调用 `verifyParentChildRelation` data-access 函数
- ✅ P1 已修复:~~权限校验未加 parentId 条件,存在信息泄露风险~~ `verifyParentChildRelation` 同时按 parentId + studentId 过滤
- ✅ P2 已修复:~~`getChildBasicInfo` 多次串行查询~~ 改为 `Promise.all` 并行化,并使用 `getStudentActiveClass` 一次 JOIN
- ✅ P2 已修复:~~`getGradeOptions` 全量查询效率低~~ 改为 `getGradeNameById` 按 ID 查询
- ✅ P2 已修复:~~`buildHomeworkSummary` 中 `[...assignments].sort()` 不必要拷贝~~ 改为 `toSorted()`
- ✅ P2 已修复:~~`in7Days` 死代码~~ 已删除
- ⚠️ v4 保留:`/parent/leave` 为占位页,待后端实现请假审批流后接入
- ✅ v4-P1-5 改进2026-06-23新增 `getParentIdsByStudentIds` data-access 函数,批量查询多个学生的家长 userId去重供 diagnostic/grades 模块通知场景调用
- ✅ v4-P1-9 改进2026-06-23parent/diagnostic/page.tsx 新增错误卡片展示子女诊断查询失败原因
- ✅ v4-P1-12 改进2026-06-23`ParentExportButton` 接入 `exportGradesAction`,支持按 studentId 导出单个子女成绩parent/grades/page.tsx 传 studentId 到 ParentExportButton
- ⚠️ v4 保留:详情页 Attendance/Diagnostic Tab 为占位提示,待对应功能实现后填充
- ✅ v3-P2 改进2026-06-23parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`,渲染 GradeTrendCardparent/diagnostic/page.tsx 传入 `practiceHrefBase={null}` 隐藏练习按钮
- ✅ 职责单一,正确复用其他模块 data-access
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 243+ | 子女关系 + 仪表盘数据聚合 + 关系校验 + 子女姓名列表v4 新增 `getChildNameList` + `buildWeeklySchedule`V3-11 `getChildDashboardData` 并行调用 `getStudentExamResults` |
| `types.ts` | 79+ | 类型定义(含 JSDocv4 新增 `ChildWeeklyScheduleItem`V3-11 新增 `ChildExamResultItem` + `ChildDashboardData.examResults` |
| `components/parent-dashboard.tsx` | 110 | 仪表盘v4 重构:待办横幅 + 宫格快捷入口 + 移动端水平滑动) |
| `components/parent-attention-banner.tsx` | 128 | v4 新增:待办事项/异常聚合横幅(作业项直接跳转详情页 homework tab |
| `components/parent-attendance-warning.tsx` | 89 | v4 新增:考勤异常预警 |
| `components/parent-attendance-rate-card.tsx` | 105 | v4 新增:考勤出勤率汇总卡片 |
| `components/parent-attendance-calendar.tsx` | 175 | v4 新增考勤月历视图use client |
| `components/parent-export-button.tsx` | 50 | v4 新增成绩导出按钮v4-P1-12 已接入 exportGradesAction支持按 studentId 导出单个子女成绩) |
| `components/child-card.tsx` | 148 | 子女卡片v4 增强:异常突出 + 趋势图标) |
| `components/child-detail-header.tsx` | 78 | 详情页头部v4 增强:面包屑) |
| `components/child-detail-panel.tsx` | 200+ | 详情页 Tab 面板 + SiblingSwitcherv4 重写,集成 Homework/Grade DetailV3-11 新增 exams Tab✅ i18n硬编码英文提取为 `parent.childDetail.*` 翻译键,新增 parent.json修复 P2-4`as` 断言改用 `isTab` 类型守卫) |
| `components/child-homework-summary.tsx` | 147 | 作业摘要v4 增强:科目标识 + 触摸区域 + pts 单位) |
| `components/child-homework-detail.tsx` | 145 | v4 新增:作业详情视图(完整作业信息) |
| `components/child-grade-summary.tsx` | 159 | 成绩趋势v4 增强:趋势图标 + aria-label |
| `components/child-grade-detail.tsx` | 165 | v4 新增:成绩详情视图(按科目分组分析) |
| `components/child-exam-detail.tsx` | 165 | V3-11 新增 / ✅ P3-4 改进:子女考试详情视图(汇总卡片+考试列表对标智学网家长端P3-4所有硬编码英文文案改为 i18n 翻译键 `examHomework.homework.parentExam.*`,新增 ChevronRight 导航图标ul/li 语义化标签 + ARIA 属性,触控目标 ≥ 44px |
| `components/child-schedule-card.tsx` | 119 | 课表卡片v4 增强:周课表视图) |
| `components/parent-children-data-page.tsx` | 92 | 共享数据页v4 增强headerExtra |
**路由清单**
| 路由 | 文件 | 说明 |
|------|------|------|
| `/parent/dashboard` | `dashboard/page.tsx` + `loading.tsx` | 家长仪表盘 |
| `/parent/grades` | `grades/page.tsx` + `loading.tsx` | 多子女成绩聚合v3-P2并行查询 getClassAverageTrend + GradeTrendCard |
| `/parent/diagnostic` | `diagnostic/page.tsx` + `loading.tsx` + `error.tsx` | P2-5 新增多子女学情诊断聚合v3-P2practiceHrefBase={null} 隐藏练习按钮) |
| `/parent/attendance` | `attendance/page.tsx` + `loading.tsx` | 多子女考勤聚合v4 新增预警横幅) |
| `/parent/leave` | `leave/page.tsx` + `loading.tsx` | v4 新增:请假申请(占位) |
| `/parent/children/[studentId]` | `children/[studentId]/page.tsx` + `loading.tsx` | 子女详情页v4 重构Tab 布局 + 多子女切换) |
| `error.tsx` | `error.tsx` | v4 新增:错误边界 |
---
## 2.20 elective选课模块
**职责**:选修课程管理 + 学生选课 + 抽签。
**导出函数**
- Actions11 个):`getElectiveCoursesAction` / `createElectiveCourseAction` / `updateElectiveCourseAction` / `deleteElectiveCourseAction` / `getStudentSelectionsAction` / `selectCourseAction` / `dropCourseAction` / `runLotteryAction` / `getAvailableCoursesForStudentAction` / `openSelectionAction` / `closeSelectionAction`(✅ P2-28 个写 Action 错误消息改用 `getTranslations("elective")` 国际化)
- Data-access`getElectiveCourses` / `getElectiveCourseById` / `createElectiveCourse` / `updateElectiveCourse` / `deleteElectiveCourse` / `openSelection` / `closeSelection` / `buildCourseSelect` / `mapCourseRow` / `resolveCourseDisplayNames`(✅ P2-7使用 React `cache()` 包装 subject/grade options 请求级去重;✅ P2-8通过 `resolvers.ts` 接口抽象跨模块依赖)/ `CourseCoreRow`
- Data-access-operations`selectCourse`(✅ P2-9新增 `checkScheduleConflict` 时间冲突检测;✅ P2-10新增 `checkCreditLimit` 学分上限校验;✅ P2-4学分上限改用 `getElectiveCreditLimit` 配置化,按年级覆盖;✅ P2-4FCFS 录取后触发 `notifyCapacityThresholdIfNeeded` 容量阈值通知)/ `dropCourse`(✅ P2-4新增 `dropReason` 参数 + `dropDeadline` 校验)/ `runLottery` / `buildLotteryRankCase`(⚠️ 未导出,无法单测)/ `checkScheduleConflict`P2-9 新增)/ `checkCreditLimit`P2-10 新增P2-4 重构为配置化)
- Data-access-selections`getCourseSelections` / `getStudentSelections` / `getStudentGradeId`(✅ P2-8通过 `resolvers.ts` StudentGradeResolver 接口抽象;✅ P2-4被 `selectCourse` 调用以获取年级用于学分上限)/ `getAvailableCoursesForStudent` / `resolveStudentDisplayNames`(✅ P2-8通过 `resolvers.ts` CourseDisplayResolver 接口抽象)
- Data-access-settings✅ P2-4 新增):`getElectiveCreditLimit(gradeId?)`按年级覆盖的学期学分上限fallback 到全局,默认 10/ `getCapacityNotifyThreshold()`(容量阈值通知比例,默认 0.9/ `ELECTIVE_DEFAULTS`
- Resolvers✅ P2-8 新增):`CourseDisplayResolver` / `StudentGradeResolver`(接口)/ `getCourseDisplayResolver` / `getStudentGradeResolver` / `setCourseDisplayResolver` / `setStudentGradeResolver` / `resetResolvers`(可注入测试 mock
- Lib 纯函数(✅ P2-9 新增):`parseSchedule`(解析时间段字符串)/ `isScheduleConflict`(判断两个时间段是否冲突)
- Import-export✅ P2-11 新增):`exportElectiveCoursesToExcel`(课程列表导出)/ `exportCourseSelectionsToExcel`(选课名单导出)
- Components`ElectiveCourseList`(课程卡片网格 + 管理操作)/ `ElectiveCourseForm`(课程创建/编辑表单)/ `ElectiveFilters`nuqs 筛选栏)/ `StudentMySelectionsSection` + `StudentAvailableCoursesSection`(✅ P2-3由原 `StudentSelectionView` 拆分为两个独立客户端组件,支持 Suspense 流式渲染)/ `ElectivePageLayout`(✅ P2-4 新增页面布局骨架admin/teacher 选课页复用)/ `ElectiveStatsCards`(✅ P1-13 新增)/ `ElectiveCourseDetail`(✅ P2-2 新增:课程详情复用组件)/ `ParentSelectionView`(✅ P0-2 新增:家长只读视图)
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`school`(✅ P3 已修复:通过 school data-access.getSubjectOptions/getGradeOptions 获取科目/年级名称;✅ P2-8通过 resolvers.ts 接口抽象)、`users`(✅ P3 已修复:通过 users data-access.getUserNamesByIds✅ P2-8通过 resolvers.ts 接口抽象)、`classes`(通过 classes data-access.getStudentActiveGradeId✅ P2-8通过 resolvers.ts 接口抽象)、`next-intl`(✅ P2-2Server Action 错误消息 i18n✅ P2-4容量阈值通知文案 i18n、`notifications`(✅ P2-4 新增:通过 `sendNotification` 发送容量阈值通知给教师/管理员)、`systemSettings` 表(✅ P2-4读取 elective 学分上限与容量阈值配置)
- 被依赖:`app/api/export`(✅ P2-11通过 `exportElectiveCoursesToExcel` / `exportCourseSelectionsToExcel`
**已知问题**(详见 `docs/architecture/audit/elective-audit-report.md` 与 `attendance-elective-audit-report.md`
- ✅ P2-2 已修复8 个写 Action 错误消息改用 `getTranslations("elective")` 国际化
- ✅ P2-4 已修复:新增 `ElectivePageLayout` 组件admin/teacher 选课页复用布局
- ✅ P2-7 已修复:`resolveCourseDisplayNames` 使用 React `cache()` 包装 subject/grade options 请求级去重
- ✅ P2-8 已修复:新增 `resolvers.ts`跨模块依赖users/school/classes通过接口抽象支持测试注入 mock
- ✅ P2-9 已修复:`selectCourse` 新增时间冲突检测(`parseSchedule` + `isScheduleConflict` 纯函数 + `checkScheduleConflict` 异步查询)
- ✅ P2-10 已修复:`selectCourse` 新增学分上限校验(`checkCreditLimit`MAX_CREDIT_PER_TERM=10
- ✅ P2-11 已修复:新增 `export.ts`,课程列表/选课名单可导出 Excel
- ✅ P0-1 已修复2026-06-25教师页面跳转 admin 路由修复,新增 `teacher/elective/create` 与 `teacher/elective/[id]/edit` 路由(含权限校验、教师越权防护)
- ✅ P0-2 已修复2026-06-25新增 `parent/elective/page.tsx`(含 loading.tsx + error.tsx使用 `ParentChildrenDataPage` 模式 + `parentId+studentId` 双重校验
- ✅ P0-3 已修复2026-06-25`admin/elective/page.tsx` 与 `student/elective/page.tsx` 入口添加 `requirePermission(Permissions.ELECTIVE_READ)` 纵深防御
- ✅ P0-4 已修复2026-06-25`data-access-operations.ts` 引入 `ElectiveBusinessError`(带 i18n code + params由 `actions.ts` 通过 `translateBusinessError` 翻译后再返回
- ✅ P0-5 已修复2026-06-25`export.ts` 移除 `as` 类型断言,改用 `ElectiveCourseStatusEnum.safeParse()` 类型守卫
- ✅ P1-6 已修复2026-06-25`elective-course-form.tsx` 全量 i18n 化,所有硬编码英文替换为 `t("form.*")` 键
- ✅ P1-7 已修复2026-06-25`elective-course-list.tsx` 中 `<a>` 替换为 Next.js `<Link>`
- ✅ P1-8 已修复2026-06-253 个 `error.tsx` 文案修正为 `t("errors.title")/t("errors.description")/t("actions.retry")`
- ✅ P1-9 已修复2026-06-25`export.ts` 表头全量 i18n 化statusHeader/priorityHeader/selectedAtHeader/enrolledAtHeader/indexHeader
- ✅ P1-10 已修复2026-06-25`getElectiveCourses` / `getElectiveCourseById` 移除 try-catch 静默吞错,让异常冒泡到 RSC 触发 error.tsx
- ✅ P1-11 已修复2026-06-25`parseSchedule` 重写支持完整英文星期Monday/Tuesday 等)+ 中文(周一/星期一)+ 多时段(逗号/分号分隔)
- ✅ P1-12 已修复2026-06-25`runLottery` 不再自动将课程置为 `closed`,保留 `status="open"` 支持重抽
- ✅ P1-13 已修复2026-06-25新增 `data-access-stats.ts` 与 `ElectiveStatsCards` 组件,集成到 admin 列表页顶部展示 4 项关键指标
- ✅ P2-1 已修复2026-06-25新增 `tests/integration/elective/elective-pure-functions.test.ts`35 个单测,覆盖 `parseSchedule` / `isScheduleConflict` / `normalizeDay` / `buildLotteryRankCase` / `mapCourseRow`
- ✅ P2-2 已修复2026-06-25新增 `ElectiveCourseDetail` 复用组件 + admin/student 课程详情页(含 loading.tsx
- ✅ P2-3 已修复2026-06-25student 页面拆分"我的选课"与"可选课程"为两个独立 Suspense 边界admin 列表与统计卡片分离 Suspense`StudentSelectionView` 拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection`;新增 4 个异步 RSC loader`MySelectionsLoader` / `AvailableCoursesLoader` / `StatsCardsLoader` / `CourseListLoader`+ 分段骨架屏
- ✅ P2-4 已修复2026-06-25配置化学分上限`data-access-settings.ts` 读取 systemSettings按年级覆盖默认 10退课截止时间 `dropDeadline` 字段 + 校验;退课理由 `dropReason` 字段(可选填写,最长 255 字符容量阈值通知FCFS 录取后跨过 `capacityNotifyThreshold` 阈值时通知教师fire-and-forget新增 `ElectiveErrorCode.dropDeadlinePassed`;通知文案 i18n 化
- ✅ P1 已修复:~~`buildCourseSelect` 跨模块 join users/subjects/grades 表~~ 改为只查 electiveCourses 表,通过 `resolveCourseDisplayNames` 调用 school/users data-access 获取显示名称
- ✅ P1 已修复:~~`getSubjectOptions` 本地直查 subjects 表且与 school 模块重复~~ 删除本地实现,改用 `school/data-access.getSubjectOptions`
- ✅ P1 已修复:~~`selectCourse`/`dropCourse` 缺事务包裹~~ 改为 `db.transaction` 包裹FCFS 模式下使用 `FOR UPDATE` 行锁防止并发超卖
- ✅ P2 已修复:~~`mapCourseRow` 在 data-access.ts 与 data-access-selections.ts 重复定义~~ 抽取到 data-access.ts 统一导出data-access-selections.ts 复用
- ✅ P2 已修复:~~`runLottery` 使用 `sort(() => Math.random() - 0.5)` 有偏 shuffle~~ 改为 Fisher-Yates 无偏洗牌算法
- ✅ P2 已修复:~~`selectCourse` FCFS 并发超卖风险~~ 使用 `db.transaction` + `.for("update")` 行锁
- ✅ 权限校验完整ELECTIVE_MANAGE/SELECT/READ所有页面入口均调用 `requirePermission`
- ⚠️ 无 P2 待办:所有审计报告 P0/P1/P2 项均已实施完毕
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 348 | 11 个 Server ActionP0-4translateBusinessError 翻译 ElectiveBusinessError |
| `data-access.ts` | 258 | 课程 CRUD + scope 过滤 + 共享映射函数P1-10移除静默吞错 |
| `data-access-operations.ts` | 374 | 选课操作P0-4ElectiveBusinessError + i18n codeP1-11parseSchedule 支持完整星期+多时段P1-12抽签可重跑 |
| `data-access-selections.ts` | 147 | 选课记录查询 + 学生可选课程 |
| `data-access-stats.ts` | 88 | 管理员概览统计P1-13 新增getElectiveOverviewStats |
| `resolvers.ts` | 83 | 跨模块依赖接口抽象 |
| `export.ts` | 102 | Excel 导出P0-5移除 as 断言P1-9表头全量 i18n |
| `schema.ts` | 179 | Zod 校验 |
| `types.ts` | 73 | 类型定义 |
| `constants.ts` | 55 | i18n key 映射 + Badge variant + 类型守卫 |
| `components/elective-course-list.tsx` | 236 | 课程卡片网格 + 管理操作P1-7a 转 Link |
| `components/elective-course-form.tsx` | 301 | 课程创建/编辑表单P1-6全量 i18n 化) |
| `components/elective-filters.tsx` | 51 | nuqs 筛选栏(搜索 + 模式) |
| `components/student-selection-view.tsx` | 278 | 学生选课视图(✅ P2-3拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection` 两个独立客户端组件,支持 Suspense 流式渲染) |
| `components/elective-page-layout.tsx` | 30 | 页面布局骨架 |
| `components/elective-stats-cards.tsx` | 73 | 管理员统计卡片网格P1-13 新增) |
| `components/parent-selection-view.tsx` | 80 | 家长查看子女选课只读视图P0-2 新增) |
| `components/elective-course-detail.tsx` | 130 | 课程详情复用组件P2-2 新增admin/student 共用) |
| `app/(dashboard)/student/elective/_components/my-selections-loader.tsx` | 17 | 异步 RSC loader我的选课P2-3 新增) |
| `app/(dashboard)/student/elective/_components/available-courses-loader.tsx` | 45 | 异步 RSC loader可选课程 + 过滤逻辑P2-3 新增) |
| `app/(dashboard)/admin/elective/_components/stats-cards-loader.tsx` | 12 | 异步 RSC loader统计卡片P2-3 新增) |
| `app/(dashboard)/admin/elective/_components/course-list-loader.tsx` | 30 | 异步 RSC loader课程列表 + 状态过滤P2-3 新增) |
---
## 2.21 proctoring监考模块
**职责**:考试监考事件记录 + 防作弊监控 + 监考面板。
**导出函数**
- Actions`recordProctoringEventAction` / `getProctoringDashboardAction`
- Data-access`recordProctoringEvent` / `getExamSubmissionForProctoring` / `getExamForProctoring` / `getExamProctoringSummary` / `getStudentProctoringStatuses` / `getRecentProctoringEvents`(✅ P2 已修复:`getExamProctoringSummary` 使用 `Promise.all` 并行化考试信息与提交记录查询、事件类型统计与学生事件统计查询;合并两次 filter 为单次循环统计 started/submitted
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionsForExam/getExamTitleById、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds
- 被依赖:无
**已知问题**
- ✅ P1-2 已修复:`exam-mode-config.tsx` 已集成到考试表单(`exam-form.tsx` 调用 `<ExamModeConfig />`),移除全部 10 处 `as` 类型断言,改用 `useFormContext` 替代 `Control` prop 避免 `Control<T>` 不变型问题
- ✅ P1-3 已修复:`proctoring-dashboard.tsx` 用类型守卫函数 `isProctoringEventType` + `toProctoringEventTypes` 替代 `Object.keys(...) as ProctoringEventType[]` 断言
- ✅ P0-6 已修复:~~事件上报存在 Server Action 与 REST API 双通道重复~~ 删除 `/api/proctoring/event` REST 路由(移至 deletes/Server Action `recordProctoringEventAction` 为唯一规范路径
- ✅ P1-1 已修复:~~跨模块直查 `exams`/`examSubmissions`/`users`~~ 改为通过 exams/users data-access 函数获取数据
- ✅ P2 已修复:`actions.ts` 不再直接 import `db` 和 `examSubmissions`submission 归属校验已下沉到 data-access`recordProctoringEventAction` 改用 `requirePermission(EXAM_SUBMIT)` 并增加 `revalidatePath`
- ✅ P2 已修复:~~`getStudentProctoringStatuses` 串行查询getUserNamesByIds 后再查事件)~~ 改为 `Promise.all` 并行拉取学生姓名与事件记录
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 409 | 事件记录 + 查询 + 摘要统计 + submission 归属校验 |
| `actions.ts` | 139 | 2 个 Server Action |
| `types.ts` | 136 | 类型定义 + 标签常量 + 阈值常量 |
| `components/anti-cheat-monitor.tsx` | - | 学生端防作弊监控 |
| `components/exam-mode-config.tsx` | - | 考试模式配置(✅ P1-2 已修复:已集成到 exam-formuseFormContext 替代 Control prop无 as 断言) |
| `components/proctoring-dashboard.tsx` | - | 教师监考面板(✅ P1-3 已修复:类型守卫函数替代 as 断言) |
---
## 2.22 diagnostic学情诊断模块
**职责**:知识点掌握度查询 + 诊断报告生成。
**导出函数**
- Actions`generateStudentReportAction` / `generateClassReportAction` / `generateGradeReportAction`v4-P2-3 新增:生成年级诊断报告)/ `publishReportAction`v4-P1-4 + v4-P1-5 增强:班级报告发布时通知全班学生 + 全班学生家长,个人报告通知学生本人 + 其家长,调用 `parent/data-access.getParentIdsByStudentIds` 批量查询家长 ID/ `deleteReportAction`v2-P2-3 修复:删除死代码 `getDiagnosticReportsAction` / `getDiagnosticReportByIdAction`,页面直接调用 data-access 并自行权限校验)/ `exportDiagnosticReportAction`v3-P2-4 新增:导出 Excel/ `getClassStudentsByKnowledgePointAction`v3-P2-5 新增:按知识点筛选学生)
- Data-access`updateMasteryFromSubmission`v2-P1-8 修复累积模式v2-P2-5 修复db.transaction 包裹)/ `getStudentMastery`P3-19 修复:移除 export改为模块内部函数/ `getStudentMasterySummary`P3-18 修复getUserNamesByIds 与 getStudentMastery 并行查询)/ `getClassMasterySummary`v2-P2-4 修复totalStudents 语义 + 班级平均掌握度按学生平均)/ `getGradeMasterySummary`v4-P2-3 新增:年级掌握度摘要)/ `getKnowledgePointStats`v2-P1-7 修复:页面先查班级再传参)/ `getClassStudentsByKnowledgePoint`v3-P2-5 新增)
- Data-access-reports`generateDiagnosticReport` / `generateClassDiagnosticReport`v2-P2-6 修复校验掌握度数据P3-27 修复:使用 DiagnosticReportError 结构化错误码)/ `generateGradeDiagnosticReport`v4-P2-3 新增:生成年级诊断报告)/ `getDiagnosticReports`P3-15 修复:支持分页 limit/offset返回 { reports, total } 结构v4-P1-1 增强:新增可选 `scope?: DataScope` 参数,支持 children/class_taught 行级权限过滤class_taught 通过 `getStudentIdsByClassIds` 解析所教班级学生 ID/ `getDiagnosticReportById` / `publishDiagnosticReport` / `deleteDiagnosticReport`(✅ P2 已修复:使用 `React.cache()` 包装实现请求级 memoizationP3-1 修复toNumber 从 grades 模块导入)/ `DiagnosticReportError`P3-27 新增结构化错误码类v4-P2-3 新增 GRADE_NOT_FOUND/GRADE_NO_MASTERY_DATA 错误码)
- Stats-service✅ v2-P1-6 新增):`serializeMasteryWithKp` / `computeAverageMastery` / `classifyStrengthsWeaknesses`P3-16 修复:弱项阈值从 <60 改为 <80消除 60-79 盲区)/ `buildStudentMasterySummary` / `aggregateClassMastery` / `computeKpStats` / `computeClassAverageMastery` / `buildStudentsNeedingAttention` / `buildClassMasterySummary` / `buildGradeMasterySummary`v4-P2-3 新增)/ `buildStudentReportContent` / `buildClassReportContent` / `buildGradeReportContent`v4-P2-3 新增)/ `computeMasteryLevel` / `serializeMastery`(从 data-access / data-access-reports 抽取的纯函数)
- Exportv3-P2-4 新增):`exportDiagnosticReportToExcel`v4-P2-1 增强:班级报告导出新增知识点统计+需关注学生 Sheeti18n 全覆盖)/ `buildDiagnosticReportFilename`v4-P2 改为 async使用 i18n 翻译文件名)
- Role-configv4-P2-2 新增):`DIAGNOSTIC_ROLE_CONFIG` / `getDiagnosticRoleConfig` / `DiagnosticRole` 类型(配置驱动角色差异,新增角色只需添加配置项)
- Schema`GenerateStudentReportSchema` / `GenerateClassReportSchema` / `GenerateGradeReportSchema`v4-P2-3 新增)/ `PublishReportSchema` / `DeleteReportSchema`v2-P2-3 修复:删除死代码 `GetDiagnosticReportsSchema` / `GetDiagnosticReportByIdSchema`
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamSubmissionWithAnswers、`questions`(✅ P1-1 已修复:通过 questions data-access.getKnowledgePointsForQuestions、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getActiveStudentIdsByClassIdv2-P1-7 新增 getStudentActiveClassId、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds/getUserIdsByGradeId、`grades`P3-1 修复:通过 grades/lib/grade-utils.toNumber 复用工具函数)、`school`v4-P2-3 新增:通过 school data-access.getGradeNameById 获取年级名称)
- 被依赖:无
**已知问题**
- ✅ P1-1 已修复:~~`updateMasteryFromSubmission` 跨模块直查 4 张表~~ 改为调用 `exams/data-access.getExamSubmissionWithAnswers` 和 `questions/data-access.getKnowledgePointsForQuestions`
- ✅ P2 已修复:~~`data-access-reports.ts` 有未使用代码(`round2` + `void round2`~~ 已删除死代码
- ✅ P2 已修复:~~`updateMasteryFromSubmission` 循环内串行 await upsert~~ 改为 `Promise.all` 并行执行所有 upsert
- ✅ P2 已修复:~~`getClassMasterySummary` 串行查询~~ 改为两组 `Promise.all` 并行
- ✅ P2 已修复:~~`getDiagnosticReports` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注
- ✅ P0-2 已修复:~~`data-access-reports.ts` 直查 `users` 表获取姓名~~ 改为通过 `users/data-access.getUserNamesByIds`
- ✅ P2-2 已修复:~~Tailwind 任意值~~ 改用标准 Tailwind 类
- ✅ P2-1 已修复:~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 5 个图表添加 `role="img"` + `aria-label`2 个表格添加 `<caption>`3 个列表添加 `role="list"`3 个图标按钮添加 `aria-label`
- ✅ P2-3 已修复:~~班级报告将生成者 ID 存入 `studentId` 字段~~ schema 改为可空,班级报告 `studentId` 置空
- ✅ v2-P1-6 已修复:~~统计逻辑混在 data-access 层~~ 抽取 `stats-service.ts`352 行12 个纯函数 + 2 个常量 + 4 个接口)
- ✅ v2-P1-7 已修复:~~`getKnowledgePointStats` 无参调用~~ 页面先查 `getStudentActiveClassId` 再传参
- ✅ v2-P1-8 已修复:~~`updateMasteryFromSubmission` 覆盖模式~~ 改为累积计算(读取已有记录后累加)
- ✅ v2-P2-3 已修复:~~死代码 `getDiagnosticReportsAction` / `getDiagnosticReportByIdAction` 全局零调用~~ 已删除,页面直接调用 data-access
- ✅ v2-P2-4 已修复:~~`totalStudents` 语义错误 + 班级平均掌握度计算偏差~~ 改为实际有掌握度记录的学生数;先算学生个人平均再取平均
- ✅ v2-P2-5 已修复:~~多 upsert 无事务包裹~~ 使用 `db.transaction()` 保证原子性
- ✅ v2-P2-6 已修复:~~生成报告未校验掌握度数据~~ 添加 `totalKnowledgePoints === 0` 和 `studentCount === 0` 校验
- ✅ v2-P1-4 已修复:~~4 个组件 i18n 完全未接入~~ 全部接入 `useTranslations("diagnostic")`
- ✅ v2-P2-7 已修复:~~`report-list.tsx` 过滤器 Label 缺少 `htmlFor`~~ 添加 `htmlFor` 和 `id`
- ✅ 与 grades 模块无职责重叠
- ✅ v3-P2 改进2026-06-23`StudentDiagnosticView` 新增 `practiceHrefBase` propstring | nullnull 时隐藏练习按钮teacher/diagnostic/student/[studentId] 传入 `practiceHrefBase="/teacher/questions"`parent/diagnostic 传入 `practiceHrefBase={null}` 隐藏练习按钮
- ✅ P3-1 已修复2026-06-23~~`data-access-reports.ts` 本地定义 `toNumber` 与 `grades/lib/grade-utils.ts` 重复~~ 改为从 grades 模块导入
- ✅ P3-15 已修复2026-06-23~~`getDiagnosticReports` 无分页,可能返回大量数据~~ 添加 limit/offset 分页支持,返回 `{ reports, total }` 结构Promise.all 并行查询总数和数据
- ✅ P3-16 已修复2026-06-23~~强弱项分类存在 60-79 盲区~~ 弱项阈值从 <60 改为 <80确保所有知识点都被分类
- ✅ P3-17 已修复:~~班级报告 strengths 无数量上限~~ `buildClassReportContent` 中 strengths 已限制为前 5 个(按掌握度降序)
- ✅ P3-18 已修复2026-06-23~~`getStudentMasterySummary` 串行查询用户名和掌握度~~ 改为 Promise.all 并行查询
- ✅ P3-19 已修复2026-06-23~~`getStudentMastery` 使用 export 但仅内部使用~~ 移除 export改为模块内部函数
- ✅ P3-27 已修复2026-06-23~~`generateDiagnosticReport` / `generateClassDiagnosticReport` 中 `throw new Error("...")` 直接暴露给用户~~ 改为使用 `DiagnosticReportError` 结构化错误码(继承 BusinessError
- ✅ P3-21 已修复2026-06-23`ClassDiagnosticView` View 按钮已有描述性 aria-label`classDiagnostic.viewAriaLabel`,含学生名)
- ✅ P3-22 已修复2026-06-23`StudentDiagnosticView` Practice 按钮添加 aria-label`studentDiagnostic.practiceAriaLabel`,含知识点名)
- ✅ v4-P3-6 已修复2026-06-23`MasteryRadarChart` 雷达图添加 `tabIndex={0}` 键盘可达性 + `focus-visible` 焦点样式(原有 `role="img"` + `aria-label` 保留)
- ✅ v4-P1-1 改进2026-06-23`getDiagnosticReports` 新增可选 `scope?: DataScope` 参数,支持 children/class_taught 行级权限过滤teacher/diagnostic/page.tsx 传入 dataScopestudent/diagnostic/page.tsx 传 status:"published" + dataScopeparent/diagnostic/page.tsx 传 status:"published" + dataScope + 错误卡片
- ✅ v4-P1-3 改进2026-06-23`student-diagnostic-view.tsx` 移除草稿回退逻辑(不再展示 draft 状态报告)
- ✅ v4-P1-4 + v4-P1-5 改进2026-06-23`publishReportAction` 班级报告发布时通知全班学生 + 全班学生家长(通过 `getStudentIdsByClassId` 获取全班学生,通过 `parent/data-access.getParentIdsByStudentIds` 获取家长 ID个人报告通知学生本人 + 其家长
- ✅ v4-P1-8 + v4-P1-11 改进2026-06-23`class-diagnostic-view.tsx` 热力图添加图例 + 表格 `overflow-x-auto` 水平滚动
- ✅ v4-P1-9 改进2026-06-23parent/diagnostic/page.tsx 新增错误卡片Error Card展示子女诊断查询失败原因
- ✅ v4-P1 数据库变更2026-06-23`learningDiagnosticReports` 表新增 `classId` 字段varchar(128), references classes.id, onDelete: set null+ `diagnostic_class_idx` 索引;`DiagnosticReport` 接口新增 `classId: string | null` 字段(班级报告关联班级 ID个人报告为 null
- ✅ v4-P1 师生关系校验2026-06-23teacher/diagnostic/student/[studentId]/page.tsx 新增 class_taught 师生关系校验
- ✅ v4-P2 审计修复2026-06-24P0-1 跨模块 import WidgetBoundary → 提升到 `shared/components/widget-boundary.tsx`diagnostic 和 grades 模块统一从 shared 引用
- ✅ v4-P2 审计修复2026-06-24P0-2/P0-3 教师页面标题 + error.tsx 硬编码 → 接入 `getTranslations("diagnostic")` / `useTranslations("diagnostic")`
- ✅ v4-P2 审计修复2026-06-24P1-1 parent 错误卡片中英文混用 → 提取完整 i18n 键 `error.childLoadFailed` / `error.childLoadFailedDesc`
- ✅ v4-P2 审计修复2026-06-24P1-2 `as` 类型断言 → 改用类型守卫函数 `isReportType` / `isReportStatus` 返回值收窄
- ✅ v4-P2 审计修复2026-06-24P1-3 分享链接指向不存在路由 → 移除分享功能(无报告详情页需求)
- ✅ v4-P2 审计修复2026-06-24P1-4 报告内容硬编码中文 → stats-service 接收 `ReportContentTranslations` 接口参数actions 层从 next-intl 构建翻译注入
- ✅ v4-P2 审计修复2026-06-24P1-5 Excel 导出表头硬编码 → export.ts 使用 `getTranslations("diagnostic.exportContent")``buildDiagnosticReportFilename` 改为 async
- ✅ v4-P2 审计修复2026-06-24P2-1 班级报告导出缺少明细 → 利用 `classId` 字段查询 `getClassMasterySummary`,导出知识点统计 Sheet + 需关注学生 Sheet
- ✅ v4-P2 审计修复2026-06-24P2-2 角色差异通过 props 硬编码 → 新增 `role-config.ts``DIAGNOSTIC_ROLE_CONFIG` 配置对象驱动 `practiceHrefBase`,组件接受 `role` prop
- ✅ v4-P2 审计修复2026-06-24P2-3 无年级诊断报告入口 → 新增 `gradeId` 字段schema + 迁移 0012、`GradeMasterySummary` 类型、`getGradeMasterySummary` / `buildGradeMasterySummary` / `buildGradeReportContent` / `generateGradeDiagnosticReport` / `generateGradeReportAction` / `GenerateGradeReportSchema`i18n 新增 `gradeSummary` / `gradeRecommendation` / `gradeNoWeakness`
- ✅ v4-P2 审计修复2026-06-24P2-4 热力图色块缺少键盘导航 → 添加 `tabIndex={0}` + `focus-visible:ring-2` 样式,外层容器 `role` 从 `img` 改为 `group`
- ✅ v2-P0-1 审计修复2026-06-253 个子路由 error.tsx 硬编码中文 → teacher/diagnostic/class/[classId]/error.tsx、teacher/diagnostic/student/[studentId]/error.tsx、parent/diagnostic/error.tsx 全部接入 `useTranslations("diagnostic")`,新增 `error.classLoadFailed/Desc`、`error.studentLoadFailed/Desc`、`error.parentLoadFailed/Desc` i18n 键
- ✅ v2-P1-1 审计修复2026-06-25~~`getDiagnosticReports` 未过滤 `grade_managed` DataScope~~ 在 data-access-reports.ts 中新增 `grade_managed` 分支,调用 `users/data-access.getUserIdsByGradeId` 解析年级学生 ID 后用 `inArray` 过滤,修复年级主任越权查看全校报告的安全漏洞
- ✅ v2-P1-2 审计修复2026-06-25i18n 弱项标签"弱项(<60%"与代码逻辑(<80%)不一致 → 修正为"弱项(<80%"
- ✅ v2-P1-3 审计修复2026-06-25热力图 aria-label 硬编码中文标点 → 使用 i18n 模板键 `classDiagnostic.heatmapCellAriaLabel: "{name}{level}%{label}{mastered}/{total}"`
- ✅ v2-P1-4 审计修复2026-06-25组件直接 import actions 无接口抽象 → 新增 `services/diagnostic-service.ts`DiagnosticService 接口)、`services/diagnostic-service-context.tsx`DiagnosticServiceProvider + useDiagnosticService、`services/default-diagnostic-service.ts`(默认实现绑定 Server Actionsreport-list.tsx 和 class-diagnostic-view.tsx 改用 Context 注入
- ✅ v2-P1-5 审计修复2026-06-25置信度计算过于简化仅 null/非 null 两级)→ 改为基于知识点数量的多级判断0 insufficient / 1-3 low / 4-8 medium / >8 high
- ✅ v2-P1-6 审计修复2026-06-25无 Error Boundary 包裹独立数据区块 → student-diagnostic-view雷达图/强弱项/最新报告/历史报告)和 class-diagnostic-view概览/热力图/筛选/排名/关注列表/生成报告)各数据区块均用 WidgetBoundary 包裹
- ✅ v2-P2-1 审计修复2026-06-25死 i18n 键未清理 → 移除 9 个 share 相关 i18n 键share/shareAriaLabel/shareTitle 等)
- ✅ v2-P2-2 审计修复2026-06-25教师报告列表页客户端过滤 class_members → 移除客户端 filterDataScope 过滤已在 data-access 层完成
- ✅ v2-P2-3 审计修复2026-06-25export.ts 错误消息硬编码英文 → 新增 `DiagnosticExportError` 结构化错误码(继承 BusinessError`throw new Error("Report not found")` 改为 `throw new DiagnosticExportError("REPORT_NOT_FOUND", ...)`
- ✅ v2-P2-5 审计修复2026-06-25雷达图名称截断无 tooltip → 改用完整 `knowledgePoint` 作为 angleKey 使 Tooltip 显示完整名称,通过 `angleTickFormatter` 截断轴上显示文本8 字符+省略号)
- ✅ v2-P2-6 审计修复2026-06-25通知类型复用 "grade" → NotificationType 新增 "diagnostic" 类型,更新 notification-list/notification-dropdown TYPE_ICON 和 TYPE_KEYSdiagnostic/actions.ts 两处 createNotification 改用 `type: "diagnostic"`
- ✅ v2-P2-7 审计修复2026-06-25无监控埋点接口 → 新增 `services/diagnostic-monitor.ts`DiagnosticMonitor 接口 + noopDiagnosticMonitor 实现)、`services/diagnostic-monitor-context.tsx`DiagnosticMonitorProvider + useDiagnosticMonitor、`services/monitored-diagnostic-service.ts`createMonitoredDiagnosticService 工厂函数,组合模式包装服务添加埋点),两个 teacher 页面接入 DiagnosticMonitorProvider
- ✅ v2-P2-9 审计修复2026-06-25parent/diagnostic noRecordsTitle 与 noRecordsDescription 重复 → 新增 `parent.noReportsTitle` / `parent.noReportsDescription` 独立 i18n 键
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | 477 | 知识点掌握度查询 + 更新v2-P1-8 累积模式v2-P2-5 事务v2-P2-4 语义修正v2-P1-6 改用 stats-service 纯函数v4-P2-3 新增 getGradeMasterySummary |
| `data-access-reports.ts` | 256+ | 诊断报告 CRUDv2-P2-6 校验v2-P1-6 改用 stats-service 纯函数v4-P1-1getDiagnosticReports 新增 scope 参数支持行级权限过滤v4-P2-3 新增 generateGradeDiagnosticReport |
| `stats-service.ts` | 388+ | 统计计算纯函数v2-P1-6 新增12 个纯函数 + 2 个常量 + 4 个接口v4-P2-3 新增 buildGradeMasterySummary/buildGradeReportContent |
| `actions.ts` | 274+ | 5 个 Server Actionv2-P2-3 删除 2 个死代码读 Actionv4-P1-4/v4-P1-5publishReportAction 新增全班学生 + 家长通知v4-P2-3 新增 generateGradeReportAction |
| `schema.ts` | 31+ | Zod 校验4 个 schemav2-P2-3 删除 2 个死代码 schemav4-P2-3 新增 GenerateGradeReportSchema |
| `types.ts` | 109+ | 类型定义v4-P2-3 新增 GradeMasterySummary 接口) |
| `export.ts` | 165+ | Excel 导出v3-P2-4 新增v4-P2-1 班级报告导出新增知识点统计+需关注学生 Sheeti18n 全覆盖) |
| `role-config.ts` | 40+ | 角色配置驱动v4-P2-2 新增DIAGNOSTIC_ROLE_CONFIG 配置对象,通过 role prop 驱动 practiceHrefBase 等角色差异) |
| `components/class-diagnostic-view.tsx` | 448+ | 班级诊断视图v2-P1-6 热力图 a11yv2-P1-4 i18nv4-P1-8 热力图图例v4-P1-11 表格 overflow-x-autov4-P2-4 热力图色块 tabIndex+focus-visible 键盘导航) |
| `components/student-diagnostic-view.tsx` | 293+ | 学生诊断视图v2-P1-4 i18nv3-P2 新增practiceHrefBase propv4-P1-3 移除草稿回退逻辑v4-P2-2 改为 role prop 配置驱动) |
| `components/mastery-radar-chart.tsx` | 85 | 雷达图v2-P1-4 i18n |
| `components/report-list.tsx` | 445 | 报告列表v2-P2-7 Label htmlForv2-P1-4 i18nv4-P1-3 移除分享功能避免 404 |
| `components/confidence-utils.ts` | 31 | 置信度计算v4-P3-7 新增v2-P1-5 改为基于知识点数量的多级判断) |
| `services/diagnostic-service.ts` | 58 | DiagnosticService 接口v2-P1-4 新增:抽象客户端可调用的诊断操作) |
| `services/diagnostic-service-context.tsx` | 46 | DiagnosticServiceProvider + useDiagnosticServicev2-P1-4 新增Context 注入) |
| `services/default-diagnostic-service.ts` | 86 | 默认服务实现绑定 Server Actionsv2-P1-4 新增) |
| `services/diagnostic-monitor.ts` | 95 | DiagnosticMonitor 接口 + noopDiagnosticMonitorv2-P2-7 新增:监控埋点扩展点) |
| `services/diagnostic-monitor-context.tsx` | 56 | DiagnosticMonitorProvider + useDiagnosticMonitorv2-P2-7 新增) |
| `services/monitored-diagnostic-service.ts` | 159 | createMonitoredDiagnosticService 工厂v2-P2-7 新增:组合模式包装服务添加埋点) |
---
## 2.23 settings设置模块— V3 AI 配置统一入口
**职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页(学生/教师概览)+ 学校品牌配置audit-P2-6 新增:登录/注册页 logo/标语/作者。V3 将 AI Provider 配置统一到 `/admin/ai-settings` 独立页面,移除 `/settings?tab=ai` 标签页和考试页面内嵌弹窗。V3.1 新增 public/private 可见性模型:管理员发布 public Provider 全员可用,普通用户可创建 private Provider 仅本人可见。audit-P2-6 新增品牌配置:`brand-config.ts` 纯类型 + 默认值(无 `server-only`,可被 Client Component 导入)+ `data-access-brand.ts` 数据访问(`server-only`,从 `system_settings` 表 `brand` 分类读取 4 个键)+ `actions-brand.ts` Server Action + `BrandConfigCard` 管理员配置组件。
**导出函数**
- Actions`getAiProviderSummaries`V3.1 增强:按用户身份过滤 public + own private/ `upsertAiProviderAction`V3.1 增强visibility 字段,非管理员强制 private/ `testAiProviderAction` / `deleteAiProviderAction`V3 新增删除能力)/ `canConfigurePublicAiProvider`V3.1 新增:检查当前用户是否拥有 AI_CONFIGURE 权限)
- Actions-password`changePasswordAction`(✅ P1 已修复:使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access
- Actions-avatar`updateUserAvatarAction` / `removeUserAvatarAction`(✅ P2-8 新增:头像上传/删除,复用 `/api/upload` 路由)
- Actions-notifications`sendTestNotificationAction`(✅ P2-10 新增:发送测试通知,占位实现待接入真实通知服务)
- Actions-system-settings`getAdminSystemSettingsAction` / `saveAdminSystemSettingsAction`(✅ P0-3 新增:管理员系统设置 CRUD4 分类 Zod 校验)
- Actions-brand`getBrandConfigAction` / `saveBrandConfigAction`(✅ audit-P2-6 新增:品牌配置 CRUD`requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath` 刷新 `/(auth)/login` / `/(auth)/register` / `/admin/settings`
- Actions-security`getSecurityCenterAction` / `toggleTwoFactorAction`(✅ P2-9 新增2FA 状态查询/切换 + 最近登录历史)
- Data-access`getAiProviderSummaries`(管理员视图,返回全部)/ `getAiProviderSummariesForUser`V3.1 新增:用户视图,返回 public + own private/ `countDefaultAiProviders` / `getAiProviderForUpdate`V3.1 增强:可选 userId 做所有权校验)/ `updateAiProvider`V3.1 增强visibility 字段)/ `createAiProvider`V3.1 增强visibility 字段)/ `deleteAiProvider`V3 新增:事务删除 + 自动转移默认V3.1 增强:可选 userId 做所有权校验)/ `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`P1 新增,从 actions 下沉)
- Data-access-system-settings`getSystemSettingsByCategory` / `getAllSystemSettings` / `getSystemSetting` / `upsertSystemSetting` / `upsertSystemSettings`(✅ P0-3 新增system_settings 表 CRUD键值对存储模式✅ audit-P2-6 `SystemSettingCategory` 联合类型新增 `"brand"`
- Data-access-brand`getBrandConfig` / `saveBrandConfig`(✅ audit-P2-6 新增:`server-only` 数据访问,从 `system_settings` 表 `brand` 分类并行读取 4 个键 schoolName/logoUrl/testimonialQuote/testimonialAuthor未配置项使用 `DEFAULT_BRAND_CONFIG` 默认值)
- Brand-config纯类型无 `server-only``BrandConfig`接口schoolName / logoUrl: string|null / testimonialQuote / testimonialAuthor/ `DEFAULT_BRAND_CONFIG`默认值schoolName="Next_Edu" / logoUrl=null / 默认英文标语)
- Components`SettingsView`统一设置页布局V3 移除 AI 标签页后为 4 标签页 General/Notifications/Appearance/Security角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏)、`SettingsServiceProvider` / `useSettingsService`Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary局部失败不影响整页、`QuickLinksCard`快捷链接卡片i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`(✅ P0-3 已修复:从 mock 改为真实数据层;✅ v3 已拆分:编排 4 个子 Card、`SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard`(✅ v3 新增:从 AdminSettingsView 拆分的 4 个独立 Card 组件,接收 values + onChange props、`AvatarUpload`(✅ P2-8 新增:头像上传/预览/删除客户端组件,文件验证 + i18n、`SecurityCenterCard`(✅ P2-9 新增;✅ v3 已拆分:改为编排器,加载数据 + 渲染两个子区块)、`SecurityTwoFactorSection`(✅ v3 新增2FA 启用/关闭/重新生成备份码,从 SecurityCenterCard 拆分)、`SecurityRecentLoginsSection`(✅ v3 新增:最近登录历史 + 远程登出,从 SecurityCenterCard 拆分)、`ThemePreferencesCard`(✅ P2-11 已增强:集成 `LocaleSwitcher` 语言切换)、`AiProviderSettingsCard`V3 增强:新增删除按钮 + AlertDialog 确认V3.1 增强visibility 选择器 + 可见性/归属 Badge✅ v3 已拆分:提取 Selector + DeleteDialog 子组件)、`AiProviderSelector`(✅ v3 新增:服务商选择器 + 密钥状态展示,从 AiProviderSettingsCard 拆分)、`AiProviderDeleteDialog`(✅ v3 新增:删除确认对话框,从 AiProviderSettingsCard 拆分)、`BrandConfigCard`(✅ audit-P2-6 新增:管理员品牌配置卡片,独立表单 + 独立保存,通过 `getBrandConfigAction`/`saveBrandConfigAction` 调用,挂载在 `AdminSettingsView` 主表单之后以避免嵌套 `<form>`
- Config`ROLE_SETTINGS_CONFIG` / `resolveRoleSettingsConfig`(配置驱动角色 → 设置视图映射,新增角色只需添加条目)、`ProfileOverviewType` / `resolveProfileOverviewType`(✅ v3 新增:配置驱动角色 → 概览类型映射,消除 roles.includes 硬编码,优先级 student > teacher > none
- Lib`buildStudentOverviewData` / `computeStudentStats` / `sortUpcomingAssignments` / `filterTodaySchedule` / `toWeekday`(纯数据计算函数,与 UI 分离,便于单元测试)、`toSettingItem` / `SettingItem`(✅ v3 新增:从 actions-system-settings.ts 提取的纯函数,将表单值序列化为数据库存储格式,便于单元测试)
- Types`AiProviderSummary`V3.1 增强:新增 visibility/createdBy 字段)/ `AiProviderName`V3.2 增强:新增 'ollama' 本地部署支持)/ `AiProviderVisibility`V3.1 新增:'public' | 'private'/ `AiProviderExisting`V3.1 增强:新增 visibility/createdBy 字段)/ `SettingsService` / `ProfileService` / `NotificationPreferenceService`(服务接口定义,用于依赖注入解耦)
**依赖关系**
- 依赖:`shared/*`(含 `shared/lib/bcrypt-utils`)、`@/auth`、`messaging`(页面层通过 `SettingsService` 接口注入,组件层不直接 import、`users`(页面层通过 `SettingsService` 接口注入)、`classes` / `homework` / `dashboard`ProfileStudentOverview 异步组件获取学生概览数据)、`notifications`(页面层获取通知偏好)
- 被依赖:无
**已知问题**
- ⚠️ P2混合 5 类职责AI Provider + 密码 + 资料 + 主题 + 通知偏好)
- ✅ P1 已修复:~~无 `data-access.ts``actions.ts` 直接使用 `db`~~ 新建 `data-access.ts`,所有 DB 操作已下沉
- ✅ P1 已修复:~~`changePasswordAction` 使用 `requireAuth()` 无 Zod 校验~~ 改为 `requirePermission(USER_PROFILE_UPDATE)` + `ChangePasswordSchema` Zod 校验 + 并行查询优化
- ✅ audit-P2-4 已修复:~~`changePasswordAction` 未检测新密码是否在已知数据泄露中,用户可将弱密码(如 "Pass1234")改回使用~~ 在 `validatePassword` 校验通过后、DB 查询前调用 `checkBreachedPassword(newPassword)`命中则拒绝改密并提示用户更换密码fail-open 策略HIBP API 不可用时跳过检查不阻断改密流程
- ✅ audit-P2-6 已修复:~~AuthLayout 学校品牌信息(学校名/logo/标语/作者)硬编码在组件内,无法由管理员动态配置~~ 新增品牌配置完整链路:`brand-config.ts`(纯类型 + `DEFAULT_BRAND_CONFIG` 默认值,无 `server-only` 可被 Client Component 导入)+ `data-access-brand.ts``server-only` 数据访问,从 `system_settings` 表 `brand` 分类读取 4 个键)+ `actions-brand.ts``requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath`+ `BrandConfigCard`(管理员配置卡片,独立表单 + 独立保存,挂载在 `AdminSettingsView` 主表单之后以避免嵌套 `<form>`)。`SystemSettingCategory` 联合类型新增 `"brand"`。i18n 键 `settings.brand.*`11 个键)已添加到 zh-CN + en
- ✅ P2 已修复:`actions-password.ts` 删除本地 `normalizeBcryptHash`,统一复用 `shared/lib/bcrypt-utils.normalizeBcryptHash`,消除重复代码
- ✅ P2-a 已修复:~~admin/teacher/student/parent 四个设置视图重复布局~~ 新增 `SettingsView` 统一设置页布局 + `resolveRoleSettingsConfig` 配置驱动角色路由,删除 `parent-settings-view.tsx` / `student-settings-view.tsx` / `teacher-settings-view.tsx`
- ✅ parent 角色路由已修复:通过 `ROLE_SETTINGS_CONFIG` 配置驱动parent 用户正确渲染对应配置
- ✅ Tab URL 持久化已修复:`SettingsView` 改为受控模式,通过 `useSearchParams` 读取 `tab` 参数,`router.push` 更新 URL
- ✅ 登出二次确认已修复:`SettingsView` 的 Log out 按钮使用 `AlertDialog` 包裹,点击时弹出确认对话框
- ✅ AiProviderSettingsCard 已集成:`SettingsView` 新增 AI 标签页,条件渲染需 `AI_CONFIGURE` 权限
- ✅ password-change-form 任意值 Tailwind 类已修复:~~`[&>div]:bg-red-500` 等任意值类~~ Progress 组件新增 `indicatorClassName` prop使用标准颜色类
- ✅ P0 已修复:~~`notification-preferences-form.tsx` 跨模块直接 import messaging/actions~~ 改为通过 `useSettingsService().notifications.updatePreferences` 调用,页面层注入实现
- ✅ P0 已修复:~~`profile-settings-form.tsx` 跨模块直接 import users/actions~~ 改为通过 `useSettingsService().profile.updateProfile` 调用,页面层注入实现
- ✅ P0 已修复:~~i18n 完全缺失~~ 新增 `settings.json` 翻译文件zh-CN + en所有组件改用 `useTranslations` / `getTranslations`
- ✅ P1 已修复:~~缺少 Error Boundary~~ 新增 `SettingsSectionErrorBoundary`,每个 TabsContent + profile 页面角色概览区块均包裹
- ✅ P1 已修复:~~缺少 Suspense 骨架屏~~ 每个 TabsContent 包裹 `Suspense` + `SettingsSectionSkeleton`profile 页面包裹 `ProfileStudentOverviewSkeleton` / `ProfileTeacherOverviewSkeleton`
- ✅ P1 已修复:~~profile/page.tsx 业务逻辑与 UI 混合~~ 抽取 `buildStudentOverviewData` 等纯函数到 `lib/student-overview-data.ts`;拆分 `ProfileStudentOverview` / `ProfileTeacherOverview` 异步组件
- ✅ 密码修改有速率限制
- ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验
- ✅ P0-3 已修复:~~AdminSettingsView 为 mock 实现,无数据持久化~~ 新增 `system_settings` 表(键值对存储)+ `data-access-system-settings.ts` + `actions-system-settings.ts`AdminSettingsView 改为真实数据层
- ✅ P2-8 已修复:~~无头像上传~~ 新增 `updateUserAvatar` data-access + `actions-avatar.ts` + `AvatarUpload` 组件,复用 `/api/upload` 路由
- ✅ P2-9 已修复:~~无 2FA / 会话管理~~ 新增 `actions-security.ts` + `SecurityCenterCard` 组件2FA 开关占位 + 最近登录历史来自 login_logs 表)
- ✅ P2-10 已修复:~~通知偏好表单无测试通知按钮~~ 新增 `sendTestNotificationAction`,每个已启用渠道旁显示测试按钮
- ✅ P2-11 已修复:~~语言切换未集成到设置页~~ `ThemePreferencesCard` 集成 `LocaleSwitcher` 到 Appearance 标签页
- ✅ v1.1 已修复:~~`/settings` 页面 ErrorBoundary 触发Functions cannot be passed directly to Client Components~~ 新增 `actions-service.ts`"use server" 文件),导出 `updateProfileAction` + `updateNotificationPreferencesAction` 两个 Server Action wrapper`page.tsx` 直接传递 Server Action 引用;`SettingsService` 接口的 `getProfile`/`getPreferences` 改为可选
- ✅ v1.1 已修复:~~i18n 键双重 `settings.` 前缀MISSING_MESSAGE~~ `role-settings-config.tsx` 中 `descriptionKey` 去掉 `settings.` 前缀
- ✅ v1.1 已修复:~~AI 标签页 FormLabel 在 Form 上下文外使用getFieldState null~~ `ai-provider-settings-card.tsx` 中两处 `<FormLabel>` 改为 `<Label>`
- ✅ v1.1 已修复:~~非管理员访问 `/settings?tab=ai` 显示空白~~ `settings-view.tsx` 新增 `resolveTab()` 函数,对无权限的 tab 回退到 `general`
- ✅ v1.1 已修复:~~数据库 `notification_preferences` 表缺少 `quiet_hours_*` 字段~~ 通过 ALTER TABLE 添加缺失字段
- ✅ v1.1 已修复:~~数据库 `system_settings` 表不存在~~ 通过 CREATE TABLE 创建
- ✅ v3 已修复:~~ProfileStudentOverview / ProfileTeacherOverview 直接 import classes/homework data-access~~ 新增 `data-access-profile-overview.ts` 包装层,组件通过本模块 data-access 调用,消除跨模块直接依赖
- ✅ v3 已修复:~~profile/page.tsx 使用 `roles.includes("student")` 角色硬编码~~ 新增 `config/profile-overview-config.ts` 配置驱动映射,消除硬编码
- ✅ v3 已修复:~~i18n 命名空间不一致profilePage vs profile.page~~ 统一合并到 `profile.page`,删除顶层 `profilePage` 节点zh-CN + en
- ✅ v3 已修复:~~SecurityCenterCard 645 行超过 500 行限制~~ 拆分为 `SecurityTwoFactorSection`2FA 流程)+ `SecurityRecentLoginsSection`(登录历史),主文件改为 ~115 行编排器
- ✅ v3 已修复:~~AiProviderSettingsCard 529 行超过 500 行限制~~ 拆分出 `AiProviderSelector`(选择器)+ `AiProviderDeleteDialog`(删除对话框),主文件降至 ~443 行
- ✅ v3 已修复:~~AdminSettingsView 444 行内联 4 个 Card~~ 拆分为 `SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard` 4 个独立组件,主文件降至 ~191 行
- ✅ v3 已修复:~~settings/page.tsx metadata 为静态 "Settings" 字符串~~ 改为 `generateMetadata` + `getTranslations("settings")`,支持 i18n
- ✅ v3 已修复:~~toSettingItem 为 actions 文件内私有函数,无法单元测试~~ 提取到 `lib/system-settings-utils.ts` 并导出,新增 `system-settings-utils.test.ts` 单元测试覆盖 string/number/boolean/json 4 种类型 + 边界情况
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 160 | AI Provider CRUD + 测试P1 已修复,无直接 DB 操作) |
| `actions-password.ts` | 101 | 修改密码P1 已修复requirePermission + Zod + data-access✅ audit-P2-4 新增 `checkBreachedPassword()` HIBP 检测) |
| `actions-avatar.ts` | 56 | 头像上传/删除P2-8 新增requirePermission + revalidatePath |
| `actions-notifications.ts` | 46 | 发送测试通知P2-10 新增:占位实现待接入真实通知服务) |
| `actions-system-settings.ts` | 186 | 管理员系统设置 CRUDP0-3 新增4 分类 Zod 校验 + upsert |
| `actions-security.ts` | 165 | 2FA 状态查询/切换 + 最近登录历史P2-9 新增) |
| `actions-service.ts` | 60 | 设置页 Server Action wrapperv1.1 新增updateProfileAction + updateNotificationPreferencesAction作为 Server Action 引用传递给 Client Component避免 Server→Client 函数传递违规) |
| `data-access.ts` | 158 | AI Provider CRUD + 密码修改 DB 操作P1 新增) |
| `data-access-system-settings.ts` | 112 | system_settings 表 CRUDP0-3 新增:键值对存储模式;✅ audit-P2-6 `SystemSettingCategory` 联合类型新增 `"brand"` |
| `data-access-brand.ts` | 72 | 品牌配置数据访问audit-P2-6 新增:`server-only`,从 `system_settings` 表 `brand` 分类并行读取 4 个键,未配置项使用 `DEFAULT_BRAND_CONFIG` 默认值re-exports `BrandConfig` + `DEFAULT_BRAND_CONFIG` 类型) |
| `brand-config.ts` | 25 | 品牌配置类型 + 默认值audit-P2-6 新增:纯类型无 `server-only`,可被 Server / Client Component 安全导入;`BrandConfig` 接口 + `DEFAULT_BRAND_CONFIG` 默认值) |
| `actions-brand.ts` | 70 | 品牌配置 Server Actionaudit-P2-6 新增:`getBrandConfigAction` / `saveBrandConfigAction``requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath` 刷新 `/(auth)/login` / `/(auth)/register` / `/admin/settings` |
| `types.ts` | 60 | 类型定义AiProviderSummary + SettingsService/ProfileService/NotificationPreferenceService 接口) |
| `config/role-settings-config.tsx` | 85 | 角色设置页配置驱动映射ROLE_SETTINGS_CONFIG + resolveRoleSettingsConfig |
| `lib/student-overview-data.ts` | 150 | 学生概览纯数据计算buildStudentOverviewData + computeStudentStats 等,便于单测) |
| `components/settings-view.tsx` | 236 | SettingsView 统一设置页布局5 标签页 + Error Boundary + Suspense + i18n + SecurityCenterCard 集成) |
| `components/settings-service-context.tsx` | 39 | SettingsServiceProvider + useSettingsServiceContext 注入服务接口) |
| `components/settings-section-error-boundary.tsx` | 64 | 分区 Error Boundary局部失败不影响整页 |
| `components/quick-links-card.tsx` | 42 | 快捷链接卡片i18n 键驱动) |
| `components/profile-student-overview.tsx` | 91 | 学生概览异步 Server Component + 骨架屏 |
| `components/profile-teacher-overview.tsx` | 115 | 教师概览异步 Server Component + 骨架屏 |
| `components/admin-settings-view.tsx` | 176 | AdminSettingsView 系统设置视图P0-3 已修复:真实数据层 + 4 个 Card + i18n✅ audit-P2-6 在主表单后挂载 `BrandConfigCard`,独立表单避免嵌套 `<form>` |
| `components/brand-config-card.tsx` | 148 | 品牌配置卡片audit-P2-6 新增:管理员配置学校品牌信息 schoolName/logoUrl/testimonialQuote/testimonialAuthor独立表单 + 独立保存;通过 `getBrandConfigAction`/`saveBrandConfigAction` 调用i18n `settings.brand.*` |
| `components/avatar-upload.tsx` | ~150 | 头像上传/预览/删除客户端组件P2-8 新增:文件验证 + i18n |
| `components/security-center-card.tsx` | ~240 | 安全中心卡片P2-9 新增2FA 开关 + 最近登录历史) |
| `components/profile-settings-form.tsx` | 158 | 个人资料表单(通过 SettingsService 注入i18n |
| `components/notification-preferences-form.tsx` | ~160 | 通知偏好表单(通过 SettingsService 注入i18n + P2-10 测试按钮) |
| `components/password-change-form.tsx` | ~130 | 密码修改表单i18n + a11y |
| `components/theme-preferences-card.tsx` | ~80 | 主题偏好卡片i18n + P2-11 LocaleSwitcher 集成) |
| `components/ai-provider-settings-card.tsx` | ~200 | AI 服务商配置卡片i18n |
---
## 2.24 auth认证模块
**职责**:认证页面 UI登录/注册/布局)+ 注册 Server Action + 注册 data-access + 登录 2FA 服务编排。
> **架构变更2026-06-25audit-P0 重构)**:模块从"纯 UI"升级为完整三层结构。原 `app/(auth)/register/page.tsx` 内联的 167 行 `registerAction` 下沉至 `modules/auth/actions.ts`;原 `auth.ts` 跨模块依赖 `modules/settings/actions-security` 的 2FA 校验函数,改为同模块 `modules/auth/services/two-factor-service.ts` 编排(仍通过 `modules/settings/data-access-two-factor` 与 `modules/settings/lib/totp` 获取数据,属合理跨模块 data-access 调用)。
**导出函数**
- UI 组件:`LoginForm` / `RegisterForm` / `AuthLayout`(✅ audit-P2-6 增强:接受 `brand?: BrandConfig` prop由 `app/(auth)/layout.tsx` Server Component 调用 `getBrandConfig()` 注入;未配置时使用 `DEFAULT_BRAND_CONFIG` 默认值;渲染 schoolName / logoUrlimg 或 GraduationCap 图标)/ testimonialQuote / testimonialAuthor
- Server Actions`actions.ts``registerAction` / `checkEmailAvailabilityAction`
- Data-access`data-access.ts``createUser` / `isEmailAvailable` / `buildRegisterInput`
- Services`services/two-factor-service.ts``verifyTwoFactorForLogin` / `preflightTwoFactorByEmail`
- Schema`schema.ts``RegisterSchema`Zod含 `superRefine` 未成年人 guardian 字段条件校验、`calcAge`、`isMinorByBirthDate` 纯函数)
- Types`types.ts``RegisterResult` / `RegisterInput` / `EmailAvailabilityResult` / `RegisterErrorCode`
**依赖关系**
- 依赖:`shared/*`(含 `shared/lib/rate-limit`、`shared/lib/login-logger`、`shared/lib/bcrypt-utils`、`shared/lib/http-utils`)、`modules/settings/data-access-two-factor`、`modules/settings/lib/totp`(✅ 合理跨模块 data-access 调用)
- 被依赖:`app/(auth)/*`、`src/auth.ts`(动态 import `services/two-factor-service`
**已知问题**
- ✅ audit-P0-1 已修复:~~`app/(auth)/register/page.tsx` 内联 167 行 `registerAction`~~ 下沉至 `modules/auth/actions.ts`,页面降至 13 行
- ✅ audit-P0-2 已修复:~~`auth.ts` 跨模块依赖 `modules/settings/actions-security`~~ 改为同模块 `services/two-factor-service.ts` 编排
- ✅ audit-P0-5 已修复:~~`auth.ts` 3 处 `as` 断言~~ 通过 `User` 接口扩展(`next-auth.d.ts`)与 `"token" in message` 类型收窄消除
- ✅ audit-P1-1 已修复:~~`actions.ts` 的 `ERROR_MESSAGES` 硬编码中文字符串~~ 替换为 next-intl `getTranslations("auth.register")` 翻译键(`messageForError()` 按 `RegisterErrorCode` 查表 `errors.${code}``register-form.tsx` 与 `login-form.tsx` 同步迁移至 `useTranslations`,含 `t.rich()` 嵌入隐私政策/用户协议链接;新增 `preflightTwoFactorAction` Server Action 消除 login-form 对 `modules/settings/actions-security` 的跨模块依赖
- ✅ audit-P1-3 已修复:~~`auth.ts` authorize 回调混合 7 类职责107 行)~~ 抽取至 `modules/auth/services/login-service.ts` 的 `authenticateUser()` 函数(输入解析/速率限制/用户查询/账户锁定/密码校验/2FA/角色加载 7 步auth.ts authorize 降至 3 行薄包装(动态 import + 委托调用),可独立测试
- ✅ audit-P1-6 已修复:~~`shared/lib/rate-limit.ts` 单实例内存实现~~ 重构为 `shared/lib/rate-limit/` 目录:抽象 `RateLimiter` 接口(`limit`/`reset` 均 Promise 返回);默认 `MemoryRateLimiter` 进程级实现;可选 `RedisRateLimiter` 基于 `@upstash/ratelimit` + `@upstash/redis`(懒加载,按 `${limit}:${windowMs}` 缓存 Ratelimit 实例);通过 `env.RATE_LIMIT_DRIVER=memory|redis` 切换;`rateLimit()`/`resetRateLimit()` 改为 Promise 返回以支持异步后端,调用方全部更新为 `await``rateLimitKey`/`RATE_LIMIT_RULES`/`rateLimitHeaders` 保持同步纯函数
- ✅ audit-P1-7 已修复:~~JWT 中存储完整 `permissions[]` 数组67 项约 1.1KB)导致 cookie 体积过大~~ 新增 `shared/lib/permission-bitmap.ts`,将 67 个权限点编码为 BigInt → base36 字符串(约 13 字符,体积减少 99%)。`auth.ts` JWT callback 写入 `token.permissionsBitmap`(不再写 `token.permissions` 数组);`session` callback 通过 `decodePermissionsBitmap()` 还原为 `Permission[]` 填充 `session.user.permissions`,业务代码(`getAuthContext()` / `usePermission()`)无需感知位图存在;`proxy.ts` 直接调用 `hasPermissionInBitmap()` 检查单个权限,无需完整解码(性能优化:边缘运行时每请求都经过)。`PERMISSION_BITMAP_ORDER` 数组顺序固定不可变,新增权限只能追加末尾。`next-auth.d.ts` JWT 接口新增 `permissionsBitmap: string``permissions` 标记为 `@deprecated`
- ✅ audit-P1-9 已修复:~~auth 模块未定义任何 trackEvent 事件,登录成功/失败/登出/注册/2FA 启用/禁用/账户锁定/限流触发均无业务埋点~~ `shared/lib/track-event.ts` EventName 新增 8 个 auth 事件(`auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.2fa_disabled`/`auth.account_locked`/`auth.rate_limited`+ `trackAuthEvent` 便捷函数;`auth.ts` events.signIn/signOut 调用 `trackAuthEvent``login-service.ts` 在限流/账户锁定/密码错误/2FA 失败 4 个失败分支调用 `trackAuthEvent``modules/auth/actions.ts` `registerAction` 成功后调用 `trackAuthEvent("auth.signup")``modules/settings/actions-security.ts` `verifyTwoFactorAction`/`disableTwoFactorAction` 调用 `trackAuthEvent("auth.2fa_enabled/disabled")`
- ✅ audit-P1-10 已修复:~~proxy.ts `SPECIFIC_ROUTE_PERMISSIONS` 仅覆盖 3 个 `/admin/*` 子路径ai-settings/roles/permissions其他 `/admin/announcements`/`/admin/audit-logs`/`/admin/users` 等子路径全部走 `/admin` 前缀匹配 SCHOOL_MANAGE 权限,导致拥有 ANNOUNCEMENT_MANAGE/AUDIT_LOG_READ/USER_MANAGE 等细粒度权限的非管理员角色(如教师、年级主任)无法访问这些页面~~ 扩展 `SPECIFIC_ROUTE_PERMISSIONS` 至 15 个条目,新增 `/admin/announcements`(ANNOUNCEMENT_MANAGE)、`/admin/audit-logs` + 3 子路径(AUDIT_LOG_READ)、`/admin/elective`(ELECTIVE_MANAGE)、`/admin/questions`(QUESTION_READ)、`/admin/users`(USER_MANAGE)、`/admin/error-book`(ERROR_BOOK_ANALYTICS_READ)、`/admin/lesson-plans`(LESSON_PLAN_READ)、`/admin/course-plans`(COURSE_PLAN_READ)。精确路由优先级高于前缀匹配,使非管理员角色可直接访问对应权限的 `/admin/*` 页面
- ✅ audit-P1-12 已修复:~~proxy.ts 内联 4 个权限映射常量SPECIFIC_ROUTE_PERMISSIONS / ROUTE_PERMISSIONS / DASHBOARD_ROUTE_PERMISSIONS / API_PERMISSIONS新增路由或权限点时必须修改 proxy.ts 逻辑代码,违反配置驱动设计~~ 抽取 `shared/lib/route-permissions.ts` 配置文件,导出 4 个 `Record<string, Permission>` 类型常量(重命名 `ROUTE_PERMISSIONS`→`ROUTE_PREFIX_PERMISSIONS`、`API_PERMISSIONS`→`API_ROUTE_PERMISSIONS` 以更准确表达语义。proxy.ts 从此文件 import新增路由权限只需修改配置文件无需改 proxy.ts 逻辑;所有映射 value 类型从 `string` 收紧为 `Permission`,消除 `as Permission` 断言
- ✅ audit-P2-3 已实施:~~系统开放注册缺少预分配角色/班级的机制,管理员无法批量引导特定用户(如新教师/家长)注册~~ 新增 `invitation_codes` 表 + `modules/invitation-codes` 模块types/schema/lib/code-generator/data-access/actions8 字符安全字母表(剔除 I/O/0/1+ Set 去重 + 批量插入admin 可批量生成邀请码1-100 张/批)并指定角色/限定邮箱/绑定班级/设置过期/备注,生成结果仅此一次返回明文供复制;注册流程集成 `invitationCode` 字段(可选),`registerAction` 调用 `validateInvitationCode()` → `consumeInvitationCode()`(乐观锁 `UPDATE ... WHERE usedBy IS NULL`),邀请码角色覆盖默认 student 角色admin 管理页 `/admin/invitation-codes`USER_MANAGE 权限)含统计卡片/表格/生成对话框/删除确认/复制i18n `invitationCodes` 命名空间已注册zh-CN + en
- ✅ audit-P2-4 已修复:~~注册流程未检测密码是否在已知数据泄露中出现,弱密码(如 "Pass1234" 等热门泄露密码)可通过 Zod 最小长度校验~~ 新增 `shared/lib/breached-password.ts` 接入 Have I Been Pwned APIk-anonymity 范围查询:仅发送 SHA-1 前 5 字符,本地比对完整哈希后缀,不泄露用户密码);`registerAction` 在邮箱可用性检查后、`createUser` 前调用 `checkBreachedPassword(data.password)`,命中则返回新错误码 `BREACHED_PASSWORD` + 写审计日志i18n 键 `auth.register.errors.BREACHED_PASSWORD` 已添加zh-CN + en。fail-open 策略HIBP API 不可用时(超时/网络错误)返回 `checkSkipped=true` 跳过检查,不阻断注册流程
- ✅ audit-P2-6 已修复:~~`AuthLayout` 学校品牌信息(学校名/logo/标语/作者)硬编码在组件内,无法由管理员动态配置~~ `AuthLayout` 接受 `brand?: BrandConfig` prop`app/(auth)/layout.tsx` 改为 async Server Component调用 `modules/settings/data-access-brand.getBrandConfig()` 读取品牌配置并注入;数据库不可用时 try/catch fail-open 使用 `DEFAULT_BRAND_CONFIG` 默认值,不阻断认证页面渲染。品牌配置由 `modules/settings` 提供完整 CRUD 链路(详见 settings 模块)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 161 | 注册 Server Action`registerAction` + `checkEmailAvailabilityAction` + `preflightTwoFactorAction`Zod 校验 + 独立 rate limit key `register:{ip}` + 审计日志 + 结构化错误码 `REGISTER_ERROR_CODES` + `messageForError()` i18n 翻译;✅ audit-P1-1 已迁移 next-intl✅ audit-P1-9 注册成功埋点 `trackAuthEvent("auth.signup")`;✅ audit-P2-4 邮箱可用性检查后调用 `checkBreachedPassword()` HIBP k-anonymity 检测) |
| `data-access.ts` | 90 | 注册数据访问(`createUser` bcrypt cost=12 不自动创建角色;`isEmailAvailable``buildRegisterInput` 从 FormData 构造 RegisterInput |
| `schema.ts` | 95 | Zod `RegisterSchema`(含 `superRefine` 未成年人 guardian 字段条件校验、`calcAge`、`isMinorByBirthDate` 纯函数) |
| `types.ts` | 35 | 类型定义(`RegisterResult` / `RegisterInput` / `EmailAvailabilityResult` / `RegisterErrorCode`,纯类型无 `server-only` |
| `services/two-factor-service.ts` | 75 | 登录 2FA 编排(`verifyTwoFactorForLogin` + `preflightTwoFactorByEmail`,依赖 `modules/settings/data-access-two-factor` + `modules/settings/lib/totp`,✅ audit-P0-2 新增) |
| `services/login-service.ts` | 177 | audit-P1-3 新增:登录认证服务(`authenticateUser` 函数,从 auth.ts authorize 回调提取 7 类职责:输入解析/速率限制/用户查询/账户锁定/密码校验/2FA/角色加载;导出 `AuthenticateUserInput` / `AuthenticatedUser` 类型audit-P1-6 后 `rateLimit()`/`resetRateLimit()` 调用改为 `await` |
| `components/auth-layout.tsx` | 46 | 认证页面布局audit-P2-6 增强:接受 `brand?: BrandConfig` prop渲染 schoolName / logoUrlimg 或 GraduationCap 图标 fallback/ testimonialQuote / testimonialAuthor未配置时使用 `DEFAULT_BRAND_CONFIG` |
| `components/login-form.tsx` | - | 登录表单 |
| `components/register-form.tsx` | - | 注册表单(`registerAction` 通过 prop 注入,含 `agreedTerms`/`agreedGuardian` hidden input 确保 Zod 收到 boolean |
---
## 2.24b onboarding首次登录引导模块
**职责**:首次登录引导流程(独立路由 `/onboarding` + Server Action + proxy 重定向v3 对标 PowerSchool/Veracross/Auth0
> **架构变更2026-06-25audit-P1-2 拆分)**:原 `onboarding-stepper.tsx` 单文件 451 行(接近 500 上限)拆分为三层结构:
> - 状态与提交逻辑 → `hooks/use-onboarding-form.ts`208 行,含所有 React state / 导航 / `completeOnboardingAction` 调用)
> - 家长子女子表单 → `components/parent-children-form.tsx`122 行纯受控组件props: `childRows`/`isSubmitting`/`onUpdate`/`onAdd`/`onRemove`
> - 主文件 `onboarding-stepper.tsx` 降至 259 行,成为纯展示编排器
> - `ChildRow` / `EMPTY_CHILD_ROW` 类型与常量从组件内提取至 `types.ts`,便于跨文件复用
**导出函数**
- UI 组件:`OnboardingStepper` / `ParentChildrenForm`
- Hook`useOnboardingForm` / `UseOnboardingFormReturn`(类型)
- Server Actions`actions.ts``getOnboardingStatusAction` / `completeOnboardingAction`
- Data-access`data-access.ts``getOnboardingStatus` / `updateUserProfile` / `bindParentToChild`
- Schema`schema.ts``OnboardingSchema`Zodv3 重构为 children 数组)
- Types`types.ts``OnboardingRoleInfo` / `OnboardingStatus` / `OnboardingCompleteData` / `OnboardingFailureItem` / `BindParentToChildParams` / `ChildRow` / `EMPTY_CHILD_ROW`
**依赖关系**
- 依赖:`shared/*`(含 `shared/lib/auth-guard`)、`modules/classes`(班级码绑定 data-access、`modules/users`(用户表读写)
- 被依赖:`app/onboarding/page.tsx`、`proxy.ts`(读 `token.onboarded`
**已知问题**
- ✅ audit-P1-2 已修复:~~`onboarding-stepper.tsx` 451 行接近 500 上限~~ 拆分为 Hook + 子组件 + 编排器三层,主文件降至 259 行
- ✅ audit-P1-8 已修复:~~`bindParentToChild` 未做独立速率限制,单次 Action 内可循环调用 10 次,三因子(生日 × 手机后4枚举攻击窗口~~ 在 `completeOnboardingAction` 中针对家长角色且 `children.length > 0` 时添加独立速率限制 key `onboarding:bind:{parentId}`,限制每小时 5 次5 × 10 = 50 次尝试/小时,远低于 3.65M 组合枚举所需量级);触发限流时写审计日志 `onboarding.bind_rate_limited` 并返回中文提示i18n 待后续迁移)
- ✅ audit-P1-13 已修复:~~步骤列表与角色过滤逻辑硬编码在 `use-onboarding-form.ts``STEPS_KEYS` 常量 + `isAdmin ? filter : identity` 三元表达式),新增/调整步骤必须修改 Hook 逻辑~~ 抽取配置驱动设计:在 `types.ts` 新增 `OnboardingStepId` / `OnboardingStepConfig` 类型 + `ONBOARDING_STEPS` 配置数组 + `getOnboardingStepsForRole(role)` 纯函数过滤步骤;`use-onboarding-form.ts` 消费配置派生 `stepConfigs`/`stepKeys`/`currentStepId`/`canSkip`,不再内联步骤定义;`onboarding-stepper.tsx` 按 `currentStepId` 渲染内容(替代数字下标 `step === N`),跳过按钮可见性由配置 `canSkip` 驱动。**附带修复**:原 `grade_head`/`teaching_head` 在 roleInfo 步骤无内容却无法跳过(`canSkipStep2 = isAdmin || isStudent || isTeacher` 未包含他们),现配置 `applicableRoles: ["teacher","student","parent"]` 使他们直接跳过该步骤
- ✅ P2-4 已修复:~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ 已删除,迁移至 `modules/onboarding/`
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | - | Server Actions`getOnboardingStatusAction` + `completeOnboardingAction`,含幂等检查/局部错误收集/教师多科目循环绑定/审计日志/audit-P1-8 家长绑定子女独立速率限制 `onboarding:bind:{userId}` 5次/小时) |
| `data-access.ts` | - | 数据访问(`getOnboardingStatus` + `updateUserProfile` + `bindParentToChild` v3 三因子验证) |
| `schema.ts` | - | Zod `OnboardingSchema`(含 children 数组,支持多子女) |
| `types.ts` | - | 类型定义(含 `ChildRow` / `EMPTY_CHILD_ROW`audit-P1-2 新增) |
| `hooks/use-onboarding-form.ts` | 208 | audit-P1-2 新增:表单状态与处理逻辑 Hookstep/name/phone/children 状态 + 导航 + URL query 持久化 + completeOnboardingAction 调用) |
| `components/onboarding-stepper.tsx` | 259 | 客户端 stepper 编排器4 步audit-P1-2 重构后纯展示) |
| `components/parent-children-form.tsx` | 122 | audit-P1-2 新增:家长子女绑定子表单(纯受控组件) |
---
## 2.25 layout布局模块
**职责**:应用骨架(侧边栏 + 顶部导航 + 导航配置 + 多角色切换)。
**导出函数**`AppSidebar` / `SidebarProvider` / `SiteHeader` + `navigation` 配置
**依赖关系**
- 依赖:`shared/hooks/use-permission`、`@/auth`useSession、`messaging`(通知下拉)、`shared/components/ui/select`(角色切换下拉)
- 被依赖:`app/(dashboard)/layout.tsx`
**已知问题**
- ✅ P2 已修复:~~用权限反推角色~~ `app-sidebar.tsx` 改用 `hasRole()` 判断角色,并新增多角色切换机制(`SidebarContext.currentRole`/`setCurrentRole`null 表示自动检测;当用户拥有多个角色时在侧边栏底部显示 `Select` 下拉切换)
- ✅ navigation.ts 无幽灵路由13 个已修复)
**文件清单**
| 文件 | 职责 |
|------|------|
| `components/app-sidebar.tsx` | 侧边栏(根据权限渲染 + 多角色切换下拉) |
| `components/sidebar-provider.tsx` | 侧边栏状态 Context含 `currentRole`/`setCurrentRole` |
| `components/site-header.tsx` | 顶部导航(含通知下拉) |
| `config/navigation.ts` | 导航配置4 个角色) |
---
## 2.26 student学生 UI 模块)
**职责**:学生端 UI 组件(课程视图 + 课表筛选/视图)。
**导出函数**`StudentCoursesView` / `StudentScheduleFilters` / `StudentScheduleView`
**依赖关系**
- 依赖:`shared/*`
- 被依赖:`app/(dashboard)/student/*`
**已知问题**
- ⚠️ P2与 classes 模块的 `schedule-view.tsx`/`schedule-filters.tsx` 可能功能重叠
- ✅ 纯 UI 模块,数据由页面通过 classes data-access 获取
- ✅ 认证模式已统一:所有 student 页面使用 `getCurrentStudentUser()`users 模块)或 `getAuthContext()`shared 模块),不再直接调用 `auth()` 或 `getDemoStudentUser()`
**文件清单**
| 文件 | 职责 |
|------|------|
| `components/student-courses-view.tsx` | 学生课程视图(含 `ClassCard` memo 组件 + 加入班级表单,使用 `useTransition` |
| `components/student-schedule-filters.tsx` | 课表筛选器 |
| `components/student-schedule-view.tsx` | 学生课表视图 |
**路由文件清单**`app/(dashboard)/student/`
| 文件 | 职责 |
|------|------|
| `dashboard/page.tsx` + `loading.tsx` | 学生仪表盘 + 骨架屏 |
| `attendance/page.tsx` + `loading.tsx` | 学生考勤 + 骨架屏 |
| `diagnostic/page.tsx` + `loading.tsx` | 学情诊断 + 骨架屏 |
| `elective/page.tsx` + `loading.tsx` | 选课中心 + 骨架屏 |
| `grades/page.tsx` + `loading.tsx` | 我的成绩 + 骨架屏 |
| `learning/page.tsx` | 学习中心入口(课程/作业/教材 3 个卡片;✅ 2026-06-26 审计修复:权限点改为 CLASS_READ |
| `learning/assignments/page.tsx` + `loading.tsx` | 作业列表(含 `AssignmentCard` 组件)+ 骨架屏 |
| `learning/assignments/[assignmentId]/page.tsx` + `loading.tsx` | 作业作答/复习 + 骨架屏 |
| `learning/courses/page.tsx` + `loading.tsx` | 课程列表 + 骨架屏(✅ 2026-06-26 审计修复:权限点改为 CLASS_READ |
| `learning/courses/[classId]/page.tsx` | 课程详情(✅ 2026-06-26 审计修复新增路由:权限点 CLASS_READ |
| `learning/textbooks/page.tsx` + `loading.tsx` | 教材列表 + 骨架屏 |
| `learning/textbooks/[id]/page.tsx` + `loading.tsx` | 教材阅读 + 骨架屏 |
| `schedule/page.tsx` + `loading.tsx` | 课表 + 骨架屏(✅ 2026-06-26 审计修复:权限点改为 CLASS_READ |
| `error.tsx` | 路由组错误边界(提供"重试"按钮) |
| `learning/error.tsx` + `loading.tsx` | ✅ 2026-06-26 审计修复新增学习中心错误边界shared `ErrorState`+ 加载骨架 |
| `learning/courses/error.tsx` | ✅ 2026-06-26 审计修复新增课程列表错误边界shared `ErrorState` |
| `learning/courses/[classId]/error.tsx` | ✅ 2026-06-26 审计修复新增课程详情错误边界shared `ErrorState` |
| `schedule/error.tsx` | ✅ 2026-06-26 审计修复新增学生课表错误边界shared `ErrorState` |
---
## 2.27 lesson-preparation备课模块
**职责**:教师备课,基于教材章节创建课案(**V4 纸感重构:三栏布局 StructureTree + PaperEditor + DetailPanel**),正文采用 Tiptap 富文本编辑器AnchorMark/AnchorPoint 内联锚点),支持模板、版本管理、知识点标注、题目创建/拉取、作业发布、撤销/重做、AI 反馈/差异化、一致性校验、课时安排。
> 架构变更2026-06-21编辑器从列表式BlockRenderer + @dnd-kit升级为节点图式NodeEditor + @xyflow/react。数据结构从 v1blocks 数组)升级到 v2nodes + edges 节点图),旧数据通过 `migrateV1ToV2()` 自动迁移。
> 架构变更2026-06-23数据结构从 v2 升级到 v3课文锚点画布格式。正文节点textbook_content作为画布中央核心教学节点围绕正文组织。新增锚点系统range/point 两种锚定方式、占位符注入算法、11 种定制节点类型。旧数据通过 `migrateV2ToV3()` 自动迁移。
> 架构变更2026-07-04V4 纸感重构):抛弃 React Flow 画布改用三栏布局260px 结构树 + 1fr 纸感编辑器 + 380px 详情面板)。正文采用 Tiptap 富文本编辑器,锚点从字符串偏移升级为 Tiptap MarkAnchorMark 区间锚/AnchorPoint 点锚v3 旧锚点通过 AnchorMigrationBanner 提示失效。doc.version=4editor-slice 移除 updateNodePosition/connect/setEdges/autoLayout。新增 interaction block师生交互对话回合。结构树支持节点展开到正文流expanded-slice。PrintView 改为按 nodes.order 排序渲染。删除 8 个 React Flow 画布和字符串锚点相关文件node-editor/nodes/*/anchor-injector/rf-mappers/auto-layout
**数据结构**
- v1已废弃仅向后兼容读取`{ version: 1, blocks: Block[] }`
- v2已废弃仅向后兼容读取`{ version: 2, nodes: LessonPlanNode[]; edges: LessonPlanEdge[] }`
- v3已废弃仅向后兼容读取`{ version: 3, textbookContentNodeId: string; nodes: AnyLessonPlanNode[]; edges: AnyLessonPlanEdge[]; anchors: NodeAnchor[] }`
- `NodeAnchor`v3 字符串偏移锚点(`{ id, nodeId, type: "range"|"point", start, end?, textPreview?, invalid? }`V4 中通过 `AnchorMigrationBanner` 提示失效
- v4当前`{ version: 4, textbookContentNodeId: string; nodes: AnyLessonPlanNode[]; edges: AnyLessonPlanEdge[]; anchors: NodeAnchor[] }`
- 锚点改为 Tiptap Mark`AnchorMark`(区间锚)+ `AnchorPoint`(点锚),存储在正文富文本 doc 中
- `nodes` 新增 `order` 字段,按顺序渲染(替代画布 position
- `InteractionBlockData`:师生交互对话回合(`{ designIntent, turns: QATurn[], expectedAnswer? }`
**导出函数**
- Data-access`data-access.ts``getLessonPlans`V4查询后按 textbookId+chapterId+creatorId 聚合版本,返回代表项 + versionCount + versions 摘要数组)/ `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent` / `migrateV1ToV2` / `normalizeDocument`v4 规范化,兼容 v1/v2/v3 旧数据)/ `buildDefaultSkeleton`(默认骨架)/ `getTextbooksForPicker` / `getChaptersForPicker` / `findChapterById` / `publishLessonPlan` / `unpublishLessonPlan`
- Lib`lib/document-migration.ts``defaultDataForType` / `migrateV1ToV2` / `migrateV2ToV3` / `normalizeDocument` / `buildInitialContent` / `buildDefaultSkeleton` / `isTextbookContentNode` / `isAnchorEdge` / `getAnchorsForNode` / `getActiveAnchorIds` / `getAnchorEdges`
- Lib`lib/anchor-mark.ts`V4 新增)Tiptap `AnchorMark` 扩展(区间锚)+ `AnchorPoint` 扩展(点锚),替代 v3 字符串偏移锚点
- Lib`lib/node-summary.ts``getNodeSummary` / `getTextbookContentSummary` / `getNodeColor` / `NODE_COLORS`
- Lib`lib/export.ts`V5-4 新增 + V4 增强 + i18n 注入):`flattenLessonPlanForPrint` / `flattenBlockData`V4新增 `interaction` case + `flattenInteraction` 函数)/ `PrintableLessonPlan` 类型 / `TranslationFn` 类型V5-4 i18n所有 label 通过翻译函数注入,调用方 print-view.tsx 注入 `useTranslations`
- Data-access-versions`data-access-versions.ts``getLessonPlanVersions` / `createLessonPlanVersion` / `getVersionContent` / `revertToVersion` / `pruneAutoVersions`
- Data-access-templates`data-access-templates.ts``getLessonPlanTemplates` / `saveAsTemplate` / `deletePersonalTemplate`
- Data-access-knowledge`data-access-knowledge.ts``getLessonPlansByKnowledgePoint` / `getLessonPlansByQuestion`
- Publish-service`publish-service.ts``publishLessonPlanHomework`
- AI-suggest`ai-suggest.ts``suggestKnowledgePoints`
- Actions`getLessonPlansAction` / `getLessonPlanByIdAction` / `createLessonPlanAction` / `updateLessonPlanAction` / `saveLessonPlanVersionAction` / `getLessonPlanVersionsAction` / `revertLessonPlanVersionAction` / `deleteLessonPlanAction` / `duplicateLessonPlanAction` / `getLessonPlanTemplatesAction` / `saveAsTemplateAction` / `deleteTemplateAction` / `suggestKnowledgePointsAction` / `publishLessonPlanHomeworkAction` / `getKnowledgePointOptionsAction` / `getTextbooksForPickerAction` / `getChaptersForPickerAction` / `publishLessonPlanAction` / `unpublishLessonPlanAction`
**依赖关系**
- 依赖:`shared/*`、`@/auth`、`shared/lib/ai`、`@tiptap/react`、`@tiptap/starter-kit`、`@tiptap/pm`、`@tiptap/extension-image`V4Tiptap 富文本编辑器)、`textbooks`(只读章节/知识点树)、`questions`(创建/查询题目)、`exams`(创建 exam 草稿)、`homework`(创建作业下发)、`classes`(查询教师班级)、`files`(附件)
- 被依赖:无
**已知问题**
- ✅ 通过对方 data-access 调用跨模块数据,无直查跨模块表
- ✅ data-access 按职责拆分为 4 个文件data-access/data-access-versions/data-access-templates/data-access-knowledge
- ✅ actions 按职责拆分为 4 个文件actions/actions-publish/actions-ai/actions-kp
- ✅ V4 纸感重构:三栏布局 StructureTree + PaperEditor + DetailPanel正文采用 Tiptap 富文本编辑器,锚点升级为 Tiptap MarkAnchorMark/AnchorPoint
- ✅ V4删除 8 个 React Flow 画布和字符串锚点相关文件node-editor/nodes/*/anchor-injector/rf-mappers/auto-layout无残留引用
- ✅ V4`block-renderer.tsx`@deprecated已删除无残留引用
> 架构变更2026-06-22本次审计修复
> - **P0-1 跨模块直查修复**`publish-service.ts` 不再直接 `db.insert(examQuestions)` 和本地实现 `getStudentIdsByClassIds`,改为调用 `exams/data-access.addExamQuestions` 和 `classes/data-access.getStudentIdsByClassIds`,恢复三层架构约束
> - **P0-2 i18n 接入**:新增 `shared/i18n/messages/zh-CN/lesson-preparation.json` 和 `shared/i18n/messages/en/lesson-preparation.json`,注册 `lessonPreparation` 命名空间到 `src/i18n/request.ts`17 个组件改造为 `useTranslations`/`getTranslations`
> - **P0-3 DataScope 过滤修复**`data-access.ts` 的 `buildScopeCondition` 按 scope 类型精确过滤——`class_taught` 增加 `subjectId IN teacher.subjects AND gradeId IN teacher.grades``grade_managed` 限制 `gradeId IN managedGrades``class_members`/`children` 仅允许查看 `published` 课案
> - **P1-1 类型安全修复**`as never` 断言全部替换为类型守卫函数(`isQuestionType`/`isV1Document`/`isV2Document`+ `validTypes` 数组;`block-renderer.tsx` 使用 `as ExerciseBlockData`/`as TextStudyBlockData` 精确断言;`constants.ts` 的 `BLOCK_TYPE_LABELS`/`LESSON_PLAN_STATUS_LABELS` 改为 i18n 键(`BLOCK_TYPE_KEYS`/`LESSON_PLAN_STATUS_KEYS`
> - **P1 纯函数抽取**:新增 `lib/document-migration.ts`migrateV1ToV2/normalizeDocument/buildInitialContent使用类型守卫替代 as 断言)、`lib/node-summary.ts`getNodeSummary + NODE_COLORS + getNodeColor接受翻译函数注入、`lib/rf-mappers.ts`toRfNodes/toRfEdges/fromRfEdgesdata-access.ts 改为从 lib/ 导入并 re-export 保持向后兼容
> - **P1-2/P1-3 错误边界 + 骨架屏**:新增 `components/lesson-plan-error-boundary.tsx`LessonPlanErrorBoundary 类组件)和 `components/lesson-plan-skeleton.tsx`VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton
> - **P1-4 阻塞式 UI 修复**`alert()` 全部替换为 `sonner` toast`confirm()` 全部替换为 `AlertDialog`shadcn`window.location.reload` 替换为 `router.refresh()`;涉及 lesson-plan-card/version-history-drawer/inline-question-editor/text-study-block/exercise-block
> - **P1-5/P1-7 多实例 + 角色配置驱动**:新增 `providers/lesson-plan-provider.tsx`LessonPlanProvider + Context + 4 个角色配置 TEACHER/ADMIN/STUDENT/PARENT + LessonPlanDataService 接口 + LessonPlanTracker 埋点接口 + useLessonPlanContextSafe/useRoleConfig 等 hooks和 `services/default-data-service.ts`(包装 Server Actions 为 DataService 实现lesson-plan-list/lesson-plan-card 通过 Context 注入数据服务useRoleConfig 控制按钮可见性
> - **P1-8 Block 注册表**:新增 `config/block-registry.tsx`BLOCK_REGISTRY 配置表 + getBlockComponent/isRichTextBlock`node-edit-panel.tsx` 重构为配置驱动渲染,移除 if/else 链
> - **P1-4 window.location.reload 修复**`exercise-block.tsx` 改用 `router.refresh()` 精确刷新缓存
> - **P2-1 a11y 修复**5 个组件question-bank-picker/publish-homework-dialog/knowledge-point-picker/exercise-block/text-study-block添加 `role="dialog"`/`aria-modal`/`aria-label`inline-question-editor 添加 `role="dialog"`/`aria-modal`/`aria-label`
> - **P2-4 监控埋点预留**`providers/lesson-plan-provider.tsx` 定义 `LessonPlanTracker` 接口 + `noopTracker` 默认空实现,生产环境可替换为真实埋点
> 架构变更2026-06-23本次审计修复
> - **Zod schema 补全**`schema.ts` 新增 `publishLessonPlanHomeworkSchema`planId/blockId 必填classIds 至少 1 个availableAt/dueAt 日期格式校验),导出类型 `PublishLessonPlanHomeworkInput`3 个 action 文件actions/actions-publish/actions-ai补全 Zod 校验
> - **data-access-versions.ts 事务修复**`revertToVersion` 包裹 `db.transaction` 确保原子性(回滚版本时同步更新 lessonPlans.content 与版本记录)
> - **统一错误处理**:所有 Server Action catch 块改用 `handleActionError``JSON.parse` 改用 `safeJsonParse`
> - **block-renderer 拖拽 BUG 修复**:修复拖拽时节点位置计算错误
> 架构变更2026-06-23V3 多角色课案查看):
> - **V3-1 课案发布/撤回**:新增 `publishLessonPlan(planId, userId)` / `unpublishLessonPlan(planId, userId)` data-access 函数 + `publishLessonPlanAction(planId)` / `unpublishLessonPlanAction(planId)` Server Actions均 requirePermission(LESSON_PLAN_PUBLISH)),支持课案 status=published 供学生/家长/教研组长只读查看
> - **V3-2 只读画布组件**:新增 `LessonPlanReadonlyView`,复用 React FlownodesDraggable=false, nodesConnectable=false供学生/家长/管理员/教研组长查看已发布课案
> - **V3-3 多角色视图**`LessonPlanCard` 新增 viewMode propteacher/student/parent/admin/gradeHead动态跳转链接 + 发布/撤回按钮;`LessonPlanEditor` 新增 initialStatus prop + 发布/撤回按钮AlertDialog 确认);`LessonPlanList` 新增 viewMode prop
> - **V3-4 锚点选择器重写**`TextbookContentNode` 新增 props: `anchorableNodes`, `onCreateNewNode`AnchorNodeSelector 重写为节点列表+创建新节点选项;`NodeEditPanel` 选中 textbook_content 时显示操作提示 + 锚点列表(含删除功能)
> - **V3-5 模板分区显示**`TemplatePicker` 加载并显示个人模板(调用 getLessonPlanTemplatesAction分区显示系统/个人模板
> - **V3-6 rf-mappers 增强**`toRfNodes` ctx 新增 `anchorableNodes` 和 `onCreateNewNode` 字段;`toRfEdges` 锚点边颜色使用 `getNodeColor(anchor.nodeId)` 替代硬编码anchorId 存入 edge.data`fromRfEdges` 从 `e.data.anchorId` 读取 anchorId回退到 className 判断
> - **V3-7 权限扩展**student/parent/grade_head/teaching_head 角色新增 `LESSON_PLAN_READ` 权限,可查看已发布课案
> - **V3-8 DataScope 扩展**`class_members` 和 `children` 新增可选 `gradeIds?: string[]` 字段auth-guard `resolveDataScope` 中 student 通过 `classEnrollments.innerJoin(classes)` 预解析 gradeIdsparent 通过孩子的 classEnrollments.innerJoin(classes) 预解析 gradeIds
> - **V3-9 新增路由**`/student/lesson-plans`、`/student/lesson-plans/[planId]/view`、`/parent/lesson-plans`、`/parent/lesson-plans/[planId]/view`、`/admin/lesson-plans`、`/admin/lesson-plans/[planId]/view`
> - **V3-10 导航变更**admin 导航新增「课案管理」(/admin/lesson-plansstudent 导航新增「我的课案」(/student/lesson-plansparent 导航新增「孩子课案」(/parent/lesson-plans
> 架构变更2026-06-24V3 深度审计修复):
> - **P0-1 admin 直查 DB 修复**`admin/lesson-plans/page.tsx` 移除 `import { db }` 和 `import { lessonPlans }`,改用 data-access 新增的 `getLessonPlanStats()` 函数(返回 total/published/draft/archived 统计)
> - **P0-2 publish-service 硬编码中文 + as 断言修复**`publish-service.ts` 移除 `as ExerciseBlockData`/`as typeof validTypes[number]` 断言,改用 `lib/type-guards.ts` 的 `isExerciseBlockData`/`isValidQuestionType` 类型守卫;移除硬编码中文标题/描述,改为接受 `homeworkTitle`/`homeworkDescription` 参数由 actions 层 i18n 翻译后传入
> - **P0-3 duplicateLessonPlan 硬编码修复**`duplicateLessonPlan` 接受 `duplicateSuffix` 参数(默认 " - Copy"),由 actions 层传入 `t("error.duplicateSuffix")`
> - **P0-4 schema Zod 错误消息 i18n 化**`schema.ts` 所有错误消息从硬编码中文改为 i18n 键(如 `error.titleRequired`);新增 `lib/i18n-errors.ts``translateFieldErrors`/`safeParseWithI18n`)在 actions 层翻译 Zod 错误
> - **P0-5 loading/error 边界补全**:为 6 个路由新增 12 个 `loading.tsx` 和 `error.tsx` 文件teacher/lesson-plans、teacher/lesson-plans/new、teacher/lesson-plans/[planId]/edit、admin/lesson-plans、admin/lesson-plans/[planId]/view、student/lesson-plans、student/lesson-plans/[planId]/view、parent/lesson-plans、parent/lesson-plans/[planId]/view
> - **P1-2 组件完全通过 service 调用**`lesson-plan-card.tsx`/`lesson-plan-list.tsx`/`lesson-plan-editor.tsx` 移除所有直接 `import { xxxAction } from "../actions"`,改为通过 `useLessonPlanContextSafe()` 获取 service 调用;`LessonPlanDataService` 接口扩展 5 个方法getLessonPlanById/updateLessonPlan/saveLessonPlanVersion/publishLessonPlan/unpublishLessonPlan`default-data-service.ts` 实现扩展
> - **P1-3 as 断言修复**:新增 `lib/type-guards.ts` 集中类型守卫11 种 BlockData 类型守卫 + 节点/题目类型守卫);`block-registry.tsx` 所有 `as XxxBlockData` 替换为类型守卫;`node-edit-panel.tsx` 移除冗余 `as LessonPlanNode`TypeScript 判别联合自动收窄);`node-editor.tsx` MiniMap `nodeColor` 使用类型守卫替代 `as { node?: AnyLessonPlanNode }``rf-mappers.ts` 移除冗余 `as TextbookContentNode`/`as LessonPlanNode``use-lesson-plan-editor.ts` `updateNode` patch 类型改为 `Omit<Partial<Block>, "type">` 防止类型变更
> - **P1-4 MiniMap 颜色硬编码修复**`lesson-plan-readonly-view.tsx` MiniMap 移除硬编码 `#455a64`/`#1976d2`,改用 `getNodeColor(nodeData.type)` 与编辑器保持一致
> - **i18n 错误码扩展**`zh-CN/lesson-preparation.json` 和 `en/lesson-preparation.json` 新增 `publish.homeworkTitle`/`publish.homeworkDescription`/`error.invalidQuestionType`/`error.duplicateSuffix`/`error.titleRequired`/`error.titleTooLong`/`error.templateRequired`/`error.invalidDate`/`error.classRequired` 等键
> 架构变更2026-06-24V3 深度审计修复续):
> - **P0 硬编码中文修复6处**`actions.ts` 的 `getLessonPlanByIdAction`/`revertLessonPlanVersionAction` 错误消息改为 i18n 键(`t("error.notFound")`/`t("error.versionNotFound")``data-access-versions.ts` 的 `revertToVersion` 接受 `revertLabel` 参数替代硬编码 `` `回退到 v${versionNo}` ```lesson-plan-error-boundary.tsx` 重写为包装组件模式(内部类组件接受 `errorText`/`retryText` props外部函数组件通过 `useTranslations` 注入 i18n 文案);`lesson-plan-provider.tsx` 的 `useLessonPlanContext` 错误消息改为英文
> - **P1 组件完全通过 service 调用5个组件迁移**`version-history-drawer.tsx`/`template-picker.tsx`/`knowledge-point-picker.tsx`/`publish-homework-dialog.tsx`/`question-bank-picker.tsx` 移除所有直接 `import { xxxAction } from "../actions"`,改用 `useLessonPlanContextSafe()` 获取 service 调用;`LessonPlanDataService` 接口扩展 7 个方法createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions+ 6 个导出类型TextbookPickerOption/ChapterPickerOption/KnowledgePointOption/PublishHomeworkInput/QuestionPickerParams/QuestionPickerItem`default-data-service.ts` 实现扩展;新增 `providers/lesson-plan-provider-setup.tsx`(页面层 Provider 设置包装组件自动注入默认数据服务和角色配置3 个页面teacher/lesson-plans/page、teacher/lesson-plans/new/page、teacher/lesson-plans/[planId]/edit/page用 `LessonPlanProviderSetup` 包裹;`question-bank-picker.tsx` 新增 `isQuestionType` 类型守卫替代 `as QuestionType` 断言
> - **P2 文件拆分**`textbook-content-node.tsx` 从 578 行拆分为 3 个文件——主文件471行+ `anchor-node-selector.tsx`AnchorNodeSelector 组件62行+ `textbook-segments.tsx`renderSegments 函数75行
> - **P3 as 断言修复8处**`textbook-content-node.tsx` 的 `as unknown as TextbookContentNodeProps` 替换为 `isTextbookContentNodePropsData` 类型守卫7 个 block 组件blackboard/import/exercise/objective/key-point/homework/reflection的 select onChange `as` 断言替换为类型守卫——`lib/type-guards.ts` 新增 7 个字段值类型守卫isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect
> 架构变更2026-06-25V3 续审计 P1/P2 修复):
> - **P1-1a node-summary.ts as 断言修复**:移除 `node.data as { html?: string; ... }` 断言,改用 `isRecord`/`getArrayLength`/`getString`/`getNumber` 安全字段提取辅助函数从 `unknown` 收窄
> - **P1-1b ai-suggest.ts as 断言修复**:移除 `as Record<string, unknown>` 断言,改用 `isRecord` 类型守卫;`extractNodeText` 从 `unknown` 节点安全提取 `html`/`sourceText` 字段
> - **P1-1c data-access-templates.ts + data-access.ts as 断言修复**:新增 `normalizeTemplateBlocks` 规范化函数(`lib/type-guards.ts`),替代 `as LessonPlanTemplate["blocks"]` 和 `as unknown as LessonPlanTemplate["blocks"]` 断言从 DB `unknown` 安全转换
> - **P1-1d inline-question-editor.tsx as 断言修复**:移除 `as typeof QUESTION_TYPES[number]` 断言,改用 `isInlineQuestionType` 类型守卫
> - **P1-1e data-access-knowledge.ts as 断言修复**:移除两处 `as unknown as` 断言,改用 `getKnowledgePointIds`/`getQuestionIds` 安全字段提取辅助函数
> - **P1-2/P1-3 actions-ai.ts + actions-kp.ts 隐式 any 修复**:显式类型标注 `const nodes: unknown[]` 和 `let kps: KnowledgePointOption[]` 替代隐式 any统一使用 `handleActionError(e)` + `translateFieldErrors` 处理错误
> - **P1-6 findChapterById 提取到共享工具**:新增 `textbooks/utils.ts` 的 `findChapterById` 递归函数,消除 4 个页面文件admin/student/parent/teacher lesson-plans view/edit page+ `data-access.ts` 共 5 处重复本地递归实现
> - **P1-7 inline-question-editor.tsx FocusTrap**:包裹 `FocusTrap``shared/components/a11y/focus-trap.tsx`)实现焦点陷阱
> - **P1-8 a11y + 类型守卫修复**:补全 aria-labelinline-question-editor 选项/删除/知识点按钮)+ 新增 `optionLabel` i18n 键;`lesson-node.tsx` 使用 `isLessonNodeData` 类型守卫替代 `as { node: LessonPlanNode }` 断言 + 空状态回退;`template-picker.tsx` 通过 `ChapterPickerOption.children` 递归类型消除 `as ChapterPickerOption[]` 断言
> - **P2-2 lesson-plan-card.tsx useCallback 性能优化**4 个异步处理函数handleArchive/handleDuplicate/handlePublish/handleUnpublish用 `useCallback` 包裹
> - **P2-4 lesson-plan-list.tsx 筛选加载状态**:增加 `isLoading` 状态,传递给 `LessonPlanFilters``lesson-plan-filters.tsx` 加载时禁用输入并设置 `aria-busy`
> - **P2-6 ai-suggest.ts AI prompt 提取为常量**:新增 `AI_SUGGEST_PROMPT_TEMPLATE` 常量,便于维护和未来国际化
> 架构变更2026-06-25V4 深度审计修复):
> - **P0-1 跨模块越权修复**`parent/lesson-plans/[planId]/view/page.tsx` 和 `student/lesson-plans/[planId]/view/page.tsx` 新增 `assertPlanInScope(planId, userId)` 调用,防止越权访问非本人/子女范围课案
> - **P0-2 权限校验补全**9 个 `lesson-plans` 路由 `page.tsx`teacher/admin/student/parent新增 `requirePermission(LESSON_PLAN_READ)` 调用
> - **P0-3 锚点 a11y 修复**`textbook-segments.tsx` 锚点 span 补全 `role="button"`/`tabIndex={0}`/`aria-label`/键盘事件处理
> - **P0-4 跨模块解耦**:新增 `default-data-service.ts` 的 `getQuestions` 方法 + `QuestionService` 接口;`question-bank-picker.tsx` 移除直接 `import` `@/modules/questions/actions`,改为通过 `useQuestionService()` 注入;`lesson-plan-provider-setup.tsx` 自动注入默认 QuestionService
> - **P1-1 FocusTrap + ESC**4 个 dialog/drawerpublish-homework-dialog/knowledge-point-picker/version-history-drawer/question-bank-picker包裹 `FocusTrap``shared/components/a11y/focus-trap.tsx`+ 添加 ESC 键监听关闭
> - **P1-2 骨架屏替换加载文本**3 个 picker/drawer 的 `{loading ? <p>{t("loading")}</p>}` 替换为 `<VersionListSkeleton/>`/`<QuestionBankSkeleton/>`/`<KnowledgePointSkeleton/>`
> - **P1-3 ErrorBoundary 独立包裹**9 处子组件渲染NodeEditor/VersionHistoryDrawer/QuestionBankPicker/InlineQuestionEditor/PublishHomeworkDialog/KnowledgePointPicker独立用 `<LessonPlanErrorBoundary>` 包裹,避免单个区块错误导致整个编辑器崩溃
> - **P2-1 并行查询**3 个 view 页teacher/admin/student/parent `lesson-plans/[planId]/view`)的 `getTextbookById` + `getChaptersByTextbookId` 改为 `Promise.all` 并行
> - **P2-2 notFound() 统一**3 个 view 页 `if (!plan) return <div>...</div>` 改为 `notFound()`,与 Next.js 约定一致
> - **P2-3 async IIFE 替代 Promise 链**3 个组件version-history-drawer/knowledge-point-picker/question-bank-picker的 `Promise.resolve().then()`/`queueMicrotask` 改为 `useEffect` + async IIFE + `cancelled` flag避免竞态
> - **P2-4 废弃文件删除**`components/block-renderer.tsx`@deprecated无源码引用删除
> - **P2-5 anchor-injector regex 一致性**`markdownToPlainText` 的 `/^>\s+/gm` 改为 `/^\s*>\s+/gm``skipPatterns` 5 个 regex 统一添加 `g` 标志,与 `markdownToPlainText` 保持一致
> - **P2-6 Zustand store 拆 slice**`hooks/use-lesson-plan-editor.ts` 从 303 行单体 store 拆分为 3 个独立 slice——`hooks/editor-slice.ts`(文档结构 + 文档操作)/`hooks/selection-slice.ts`(选中状态)/`hooks/version-slice.ts`(版本与保存状态);主文件重写为 25 行薄层,组合 3 个 slice 并导出 `EditorState` 类型
> - **type-guards 导出补全**`lib/type-guards.ts` 的 `VALID_QUESTION_TYPES` 常量从模块私有改为 `export`(供 `inline-question-editor.tsx` 复用,消除重复定义)
**文件清单**
| 文件 | 职责 |
|------|------|
| `types.ts` | 类型定义(含 v1/v2/v3 文档类型、TextbookContentNode、LessonPlanNode、NodeAnchor、AnchorEdge、FlowEdge、11 种 BlockData 接口) |
| `constants.ts` | 常量定义 |
| `schema.ts` | Zod 验证V3错误消息改为 i18n 键,如 `error.titleRequired` |
| `lib/type-guards.ts` | **集中类型守卫V3 新增)**11 种 BlockData 类型守卫isRichTextBlockData/isTextStudyBlockData/isExerciseBlockData/isObjectiveBlockData/isKeyPointBlockData/isImportBlockData/isNewTeachingBlockData/isSummaryBlockData/isHomeworkBlockData/isBlackboardBlockData/isReflectionBlockData+ 节点类型守卫isTextbookContentNode/isLessonPlanNode+ 题目类型守卫isValidQuestionType + **V4 导出 `VALID_QUESTION_TYPES` 常量 + `ValidQuestionType` 类型**+ 基础类型守卫isLessonPlanStatus/isTemplateType/isTemplateScope/isBlockType/isReviewDecision+ **Block 字段值类型守卫V3 续新增)**isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect用于 select onChange 替代 `as` 断言)+ **normalizeTemplateBlocks 规范化函数V3 续审计新增)**:从 DB `unknown` 安全转换为 `TemplateBlockSkeleton[]`,替代 `as LessonPlanTemplate["blocks"]` 断言 + **类型守卫专项重构2026-07-04**:所有 `isXxxBlockData` 守卫参数从 `BlockData` 拓宽为 `unknown`(守卫内部已用 `isObject` 检查,安全;使守卫可从 `unknown` 直接收窄,支持 `AnyLessonPlanNode.data` 联合类型);新增 `mergeBlockDataPatch(node, patch)` 辅助函数 + `BLOCK_DATA_GUARDS` 注册表BlockType → 守卫映射),替代 AI patch 合并中的 `{ ...node.data, ...patch } as BlockData` 断言,校验失败回退原 data 保证编辑器完整性 |
| `lib/i18n-errors.ts` | **Zod 错误 i18n 翻译辅助V3 新增)**`translateFieldErrors`(将 Zod fieldErrors 中的 i18n 键翻译为实际消息)/ `safeParseWithI18n`(安全解析 Zod 结果并返回带翻译的 ActionState 错误格式) |
| `lib/document-migration.ts` | **纯函数**v1→v2migrateV1ToV2/ v2→v3migrateV2ToV3/ 规范化normalizeDocument兼容 v1/v2/v3/v4/ 初始内容buildInitialContent/ 默认骨架buildDefaultSkeleton/ defaultDataForType / 工具函数isTextbookContentNode/isAnchorEdge/getAnchorsForNode/getActiveAnchorIds/getAnchorEdges |
| `lib/anchor-mark.ts` | **V4 新增**Tiptap `AnchorMark` 扩展(区间锚)+ `AnchorPoint` 扩展(点锚),替代 v3 字符串偏移锚点;存储在正文富文本 doc 中 |
| `lib/export.ts` | **V5-4 新增 + V4 增强**`flattenLessonPlanForPrint`(按 `nodes.order` 排序扁平化为 `PrintableLessonPlan`+ `flattenBlockData`V4新增 `interaction` case + `flattenInteraction` 函数)+ `PrintableLessonPlan` 类型;支持 detailed/concise 两种 variant |
| `lib/node-summary.ts` | **纯函数**getNodeSummary支持 12 种节点类型,含 V4 interaction+ getTextbookContentSummary + NODE_COLORS + getNodeColorV3 续审计:移除 `as { html?: string; ... }` 断言,改用 isRecord/getArrayLength/getString/getNumber 安全字段提取从 `unknown` 收窄) |
| `config/block-registry.tsx` | **配置驱动**BLOCK_REGISTRY 注册表 + BlockRendererV3使用 `lib/type-guards.ts` 类型守卫替代所有 `as XxxBlockData` 断言) |
| `providers/lesson-plan-provider.tsx` | **Provider + Context**LessonPlanProvider 注入数据服务/角色配置/埋点;定义 LessonPlanDataService 接口V3扩展 5 个方法 getLessonPlanById/updateLessonPlan/saveLessonPlanVersion/publishLessonPlan/unpublishLessonPlan**V3 续扩展 7 个方法 createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions + 6 个导出类型**、4 个角色配置、ROLE_CONFIGS 注册表、LessonPlanTracker 接口 + noopTrackerhooksuseLessonPlanContextSafe/useLessonPlanContext/useRoleConfig/useLessonPlanService/useLessonPlanTracker/useLessonPlanTrackerSafe |
| `providers/lesson-plan-provider-setup.tsx` | **页面层 Provider 设置包装组件V3 续新增)**LessonPlanProviderSetup 自动注入默认数据服务createDefaultDataService和角色配置TEACHER_ROLE_CONFIG3 个 teacher 页面用此组件包裹 |
| `services/default-data-service.ts` | **默认数据服务实现**createDefaultDataService() 包装 Server Actions 为 LessonPlanDataService 实现V3实现扩展的 5 个方法;**V3 续扩展 7 个方法 createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions**),测试可替换为 mock |
| `data-access.ts` | 课案 CRUD + 模板查询V3新增 `getLessonPlanStats()` 统计函数 + `LessonPlanStats` 接口;`duplicateLessonPlan` 接受 `duplicateSuffix` 参数消除硬编码V3 续审计:使用 `normalizeTemplateBlocks` 替代 `as unknown as LessonPlanTemplate["blocks"]` 断言;从 `textbooks/data-access` 导入 `findChapterById` 消除本地重复实现) |
| `data-access-versions.ts` | 版本管理(创建/查询/回滚/清理) |
| `data-access-templates.ts` | 个人模板 CRUDV3 续审计:使用 `normalizeTemplateBlocks` 替代 `as LessonPlanTemplate["blocks"]` 断言) |
| `data-access-knowledge.ts` | 按知识点/题目反查课案V3 续审计:移除两处 `as unknown as` 断言,改用 `getKnowledgePointIds`/`getQuestionIds` 安全字段提取辅助函数) |
| `actions.ts` | 课案 CRUD/版本/模板 Server ActionsV3所有 action 使用 `translateFieldErrors` 翻译 Zod 错误;`duplicateLessonPlanAction` 传入 i18n 翻译的副本后缀;`getLessonPlanByIdAction` 返回类型改为 `ActionState<{ plan: LessonPlan }>` |
| `actions-publish.ts` | 发布作业 Server ActionV3actions 层注入 i18n 翻译的作业标题/描述/日期标签;新增 `INVALID_QUESTION_TYPE` 错误码映射) |
| `actions-ai.ts` | AI 知识点建议 Server ActionV3 续审计:显式 `unknown[]` 类型标注替代隐式 any统一使用 `handleActionError` + `translateFieldErrors` 处理错误) |
| `actions-kp.ts` | 知识点选项 Server ActionV3 续审计:显式 `KnowledgePointOption[]` 类型标注替代隐式 any统一使用 `handleActionError` + `translateFieldErrors` 处理错误) |
| `publish-service.ts` | 发布作业服务V3移除 `as` 断言改用类型守卫;移除硬编码中文,接受 `homeworkTitle`/`homeworkDescription` 参数;新增 `INVALID_QUESTION_TYPE` 错误码) |
| `ai-suggest.ts` | AI 知识点建议服务V3 续审计:移除 `as Record<string, unknown>` 断言改用 `isRecord` 类型守卫AI prompt 提取为 `AI_SUGGEST_PROMPT_TEMPLATE` 模块常量) |
| `seed-templates.ts` | 模板种子数据 |
| `hooks/use-lesson-plan-editor.ts` | **课案编辑器 Zustand store 主文件V4从 303 行单体 store 重写为 25 行薄层,组合 3 个 slice 并导出 `EditorState` 类型)**V3`updateNode` patch 类型改为 `Omit<Partial<Block>, "type">` 防止类型变更) |
| `hooks/editor-slice.ts` | **编辑器 sliceV4 P2-6 新增 + V4 纸感重构)**Zustand `StateCreator`,包含 planId/title/doc 状态及所有文档操作方法addNode/updateNode/removeNode/updateTextbookContent/getTextbookContentNode/addAnchor/removeAnchor/updateAnchor/setTitle/setPlanId**V4 移除**updateNodePosition/connect/disconnect/setEdges/autoLayout画布相关方法 |
| `hooks/selection-slice.ts` | **选中状态 sliceV4 P2-6 新增)**Zustand `StateCreator`,包含 selectedNodeId 状态 + selectNode 方法 |
| `hooks/version-slice.ts` | **版本与保存状态 sliceV4 P2-6 新增)**Zustand `StateCreator`,包含 isDirty/isSaving/lastSavedAt/saveError/isOnline 状态 + hydrate/markSaved/setSaving/replaceDoc/setSaveError/setOnline 方法 |
| `hooks/history-slice.ts` | **撤销/重做 sliceV5-2 新增)**Zustand `StateCreator`past/future 栈50 步上限)+ canUndo/canRedo/pushHistory/undo/redo/clearHistory 方法 |
| `hooks/expanded-slice.ts` | **V4 纸感重构新增**Zustand `StateCreator`节点展开到正文流状态expandedNodeIds/toggleExpand/setExpanded |
| `components/lesson-plan-list.tsx` | 课案列表V3完全通过 service 调用,移除直接 import `getLessonPlansAction`V3 续审计:新增 `isLoading` 状态,加载时显示加载文本,传递给 `LessonPlanFilters` |
| `components/lesson-plan-card.tsx` | 课案卡片V3完全通过 service 调用,移除直接 import actionsV3 续审计4 个异步处理函数用 `useCallback` 包裹性能优化) |
| `components/lesson-plan-filters.tsx` | 课案筛选器V3 续审计:新增 `isLoading` prop加载时禁用输入并设置 `aria-busy` |
| `components/lesson-plan-editor.tsx` | 课案编辑器V3完全通过 service 调用;**V4 纸感重构**:移除 NodeEditor/NodeEditPanel改用三栏布局 StructureTree + PaperEditor + DetailPanel保留顶部工具栏 + 5 个对话框) |
| `components/lesson-plan-readonly-view.tsx` | **只读视图**V3MiniMap 使用 `getNodeColor` 替代硬编码颜色;**V4**:移除 React Flow 依赖,委托 `LessonPlanMobileView` 线性卡片视图渲染所有视口) |
| `components/anchor-migration-banner.tsx` | **V4 新增**v3 锚点失效提示 banner检测 `doc.anchors` 中 `invalid:true` 或含 `start` 字段的旧锚点;通过 `key={planId}` 重新挂载 + lazy initial state 读取 localStorage 避免 setState in effect |
| `components/print-view.tsx` | **V5-4 新增 + V4 适配**:打印视图组件,按 `nodes.order` 排序渲染,详细版/简洁版切换 + `window.print()` |
| `components/schedule-dialog.tsx` | **V5-7 新增**:课案绑定课时对话框 |
| `components/consistency-check-dialog.tsx` | **V5-19 新增**:目标-评价一致性校验对话框 |
| `components/ai-feedback-dialog.tsx` | **V5-17 新增**AI 反馈闭环对话框4 维度 strengths/improvements/alignment/differentiation |
| `components/ai-differentiation-dialog.tsx` | **V5-21 新增**AI 差异化教学对话框3 Tab差异化建议/课标核对/可解释评估) |
| `components/curriculum-heatmap.tsx` | **V5-20 新增**:课标覆盖热力图组件 |
| `components/version-diff-view.tsx` | **V5-16 新增**:版本对比视图组件 |
| `components/lesson-plan-mobile-view.tsx` | **V5-13 新增**:移动端只读视图(按教学阶段分组线性卡片视图);**V4**:被 `lesson-plan-readonly-view.tsx` 委托渲染所有视口 |
| `components/attachment-picker.tsx` | **V5-5 新增**:素材库 picker复用 use-file-upload + M6 附件表 data-access |
| `components/paper-editor/paper-editor.tsx` | **V4 新增**:纸感编辑器容器(中栏),承载 Tiptap 富文本编辑器 |
| `components/paper-editor/paper-toolbar.tsx` | **V4 新增**:纸感编辑器工具栏 |
| `components/paper-editor/paper-context-menu.tsx` | **V4 新增**:纸感编辑器右键菜单 |
| `components/paper-editor/textbook-tiptap-editor.tsx` | **V4 新增**:教材正文 Tiptap 编辑器(集成 AnchorMark/AnchorPoint 扩展) |
| `components/paper-editor/inline-node.tsx` | **V4 新增**:内联节点渲染组件 |
| `components/paper-editor/inline-qa-dialog.tsx` | **V4 新增**:内联问答对话框 |
| `components/structure-tree/structure-tree.tsx` | **V4 新增**:结构树容器(左栏 260px支持节点展开到正文流 |
| `components/structure-tree/tree-node-row.tsx` | **V4 新增**:结构树节点行组件 |
| `components/detail-panel/detail-panel.tsx` | **V4 新增**:详情面板容器(右栏 380px |
| `components/detail-panel/detail-head.tsx` | **V4 新增**:详情面板头部 |
| `components/detail-panel/detail-props.tsx` | **V4 新增**:详情面板属性编辑 |
| `components/detail-panel/qa-editor.tsx` | **V4 新增**师生问答编辑器interaction block 配套) |
| `components/blocks/interaction-block.tsx` | **V4 新增**:师生交互 BlockdesignIntent + turns[] + expectedAnswer |
| `components/lesson-plan-error-boundary.tsx` | **错误边界**V3 续:重写为包装组件模式——内部类组件接受 `errorText`/`retryText` props外部函数组件通过 `useTranslations` 注入 i18n 文案) |
| `components/lesson-plan-skeleton.tsx` | **骨架屏**VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton |
| `components/template-picker.tsx` | 模板选择器V3 续:完全通过 service 调用,移除直接 import 4 个 actionsV3 续审计:通过 `ChapterPickerOption.children` 递归类型消除 `as ChapterPickerOption[]` 断言) |
| `components/version-history-drawer.tsx` | 版本历史抽屉V3 续:完全通过 service 调用,移除直接 import 2 个 actions |
| `components/knowledge-point-picker.tsx` | 知识点选择器V3 续:完全通过 service 调用,移除直接 import actions-kp |
| `components/question-bank-picker.tsx` | 题库选择器V3 续:完全通过 service 调用,移除跨模块直接 import `@/modules/questions/actions`;新增 `isQuestionType` 类型守卫替代 `as QuestionType` |
| `components/inline-question-editor.tsx` | 内联题目编辑器V3 续审计:移除 `as typeof QUESTION_TYPES[number]` 断言改用 `isInlineQuestionType` 类型守卫;包裹 `FocusTrap` 实现焦点陷阱;补全 aria-label + 新增 `optionLabel` i18n 键) |
| `components/publish-homework-dialog.tsx` | 发布作业对话框V3 续:完全通过 service 调用,移除直接 import actions-publish |
| `components/blocks/rich-text-block.tsx` | 富文本 Block |
| `components/blocks/text-study-block.tsx` | 课文研读 Block |
| `components/blocks/objective-block.tsx` | 教学目标 BlockV3 续select onChange 使用 `isObjectiveDimension` 类型守卫替代 `as` 断言) |
| `components/blocks/key-point-block.tsx` | 重难点 BlockV3 续select onChange 使用 `isKeyPointType` 类型守卫替代 `as` 断言) |
| `components/blocks/import-block.tsx` | 导入 BlockV3 续select onChange 使用 `isImportMethod` 类型守卫替代 `as` 断言) |
| `components/blocks/new-teaching-block.tsx` | 新授 Block |
| `components/blocks/summary-block.tsx` | 总结 Block |
| `components/blocks/homework-block.tsx` | 作业 BlockV3 续select onChange 使用 `isHomeworkType` 类型守卫替代 `as` 断言) |
| `components/blocks/blackboard-block.tsx` | 板书 BlockV3 续select onChange 使用 `isBlackboardLayout` 类型守卫替代 `as` 断言) |
| `components/blocks/exercise-block.tsx` | 练习 BlockV3 续select onChange 使用 `isExercisePurpose` 类型守卫替代 `as` 断言) |
| `components/blocks/reflection-block.tsx` | 反思 BlockV3 续select onChange 使用 `isReflectionAspect` 类型守卫替代 `as` 断言) |
| `components/calendar-view.tsx` | **M9 日历视图组件2026-07-01 新增)**:客户端组件,支持周历/月历切换6 种事件类型颜色映射 |
| `components/curriculum-map-view.tsx` | **M4 课程地图热力图组件2026-07-01 新增)**:学科×年级矩阵表格,颜色编码覆盖率 |
| `data-access-calendar.ts` | **M9 日历事件数据访问层2026-07-01 新增)** |
| `data-access-review.ts` | **M3 审核工作流数据访问层2026-07-01 新增)** |
| `data-access-comments.ts` | **M2 协同备课评论数据访问层2026-07-01 新增)** |
| `data-access-formative.ts` | **M5 形成性评价数据访问层2026-07-01 新增)** |
| `data-access-attachments.ts` | **M6 资源附件库数据访问层2026-07-01 新增)** |
| `data-access-substitutes.ts` | **M12 代课教师数据访问层2026-07-01 新增)** |
| `data-access-ai-evaluation.ts` | **M8 AI 评估数据访问层2026-07-01 新增)** |
| `data-access-analytics.ts` | **M10 备课分析仪表盘数据访问层2026-07-01 新增)** |
| `actions-calendar.ts` | **M9 日历 Server Actions2026-07-01 新增)** |
| `actions-review.ts` | **M3 审核工作流 Server Actions2026-07-01 新增)** |
| `actions-comments.ts` | **M2 评论 Server Actions2026-07-01 新增)** |
| `actions-formative.ts` | **M5 形成性评价 Server Actions2026-07-01 新增)** |
| `actions-attachments.ts` | **M6 附件库 Server Actions2026-07-01 新增)** |
| `actions-substitutes.ts` | **M12 代课教师 Server Actions2026-07-01 新增)** |
| `actions-ai-evaluation.ts` | **M8 AI 评估 Server Actions2026-07-01 新增)** |
| `actions-analytics.ts` | **M10 分析仪表盘 Server Actions2026-07-01 新增)** |
> 架构变更2026-07-01M1-M12 中长期计划完整实施):
> - **M1 课标对标体系**:新增 modules/standards/ 模块;新增 3 个 STANDARD_* 权限点;新增路由 /admin/curriculum-map
> - **M2 协同备课评论**:新增 data-access-comments.ts + actions-comments.ts支持 Block 级评论、resolved 状态
> - **M3 审核工作流**:新增 data-access-review.ts + actions-review.ts状态机 draft → submitted → approved/rejected → published
> - **M4 课程地图**:新增 admin/curriculum-map 路由 + curriculum-map-view.tsx 热力图组件
> - **M5 形成性评价闭环**:新增 data-access-formative.ts + actions-formative.ts4 种互动类型
> - **M6 资源附件库**:新增 data-access-attachments.ts + actions-attachments.ts
> - **M7 移动端 PWA**:见 1.1.1 章节
> - **M8 AI 差异化生成 + 评估**:新增 data-access-ai-evaluation.ts + actions-ai-evaluation.ts
> - **M9 日历视图**:新增 data-access-calendar.ts + actions-calendar.ts + calendar-view.tsx + teacher/lesson-plans/calendar 路由
> - **M10 备课分析仪表盘**:新增 data-access-analytics.ts + actions-analytics.ts3 个分析维度
> - **M11 版本 diff 预览**data-access-versions.ts 的 getVersionContent 支持读取历史版本内容
> - **M12 代课教师**:新增 data-access-substitutes.ts + actions-substitutes.ts
> - **权限扩展**:新增 STANDARD_READ/MANAGE/LINK + LESSON_PLAN_SUBMIT/REVIEW/ANALYTICS_READ/SUBSTITUTE_MANAGE
> - **i18n 扩展**zh-CN 新增 9 个命名空间status 扩展为 6 状态
> - **数据库表**:新增 6 张表lessonPlanComments/lessonPlanReviewRecords/lessonPlanFormativeItems/lessonPlanAttachments/lessonPlanAiEvaluations/lessonPlanSubstituteAssignments
>
> 架构变更2026-07-03V5 场景缺口修复第一阶段):
> - **V5-1 自动保存失败 UI 兜底**version-slice.ts 新增 saveError/isOnline 状态lesson-plan-editor.tsx 监听 online/offline 事件 + toast 提示 + 重试按钮
> - **V5-2 撤销/重做**:新增 hooks/history-slice.tspast/future 栈50 步上限editor-slice.ts 所有 mutation 包装 pushHistory编辑器顶部 Undo/Redo 按钮 + Ctrl/Cmd+Z/Shift+Z 快捷键
> - **V5-3 发布前作业单预览**publish-homework-dialog.tsx 改为 3 步流程(选班级 → 预览题目+总分+学生数 → 确认发布exercise-block.tsx 传入 items
> - **V5-4 导出/打印**:新增 lib/export.ts文档扁平化详细版/简洁版)+ components/print-view.tsx打印视图编辑器顶部新增"导出"按钮
> - **V5-5 多媒体嵌入 + 素材库**types.ts RichTextBlockData 新增 attachments 字段rich-text-block.tsx 集成 @tiptap/extension-image新增 components/attachment-picker.tsx复用 use-file-upload + M6 附件表 data-accessLessonPlanDataService 接口新增 getLessonPlanAttachments/createLessonPlanAttachment/deleteLessonPlanAttachment 三个方法;新增依赖 @tiptap/extension-imageFileTargetType 新增 "lesson_plan"
> - **i18n 扩展**zh-CN/en 同步新增 action.undo/redo、status.retrySave/saveFailed/recovered/offline、publish.step1-3/preview*/confirm*、questionBank.type 多题型、export.*、attachment.*、editor.textbookContent
> - **审计报告**:新增 docs/architecture/audit/lesson-preparation-audit-report-v5.md场景缺口维度对照 v4 架构合规维度)
>
> 架构变更2026-07-03V5 场景缺口修复第二阶段 V5-6~V5-21
> - **V5-6 W1 题库组卷预览**exercise-block.tsx 增加组卷预览模式(题目列表 + 总分统计)
> - **V5-7 W3/W4 课案绑定课时**:新增 data-access-schedules.ts + actions-schedules.ts + components/schedule-dialog.tsx编辑器新增「安排课时」按钮数据库新增 lesson_plan_schedules 表
> - **V5-8 U2 画布自动布局**:新增 lib/auto-layout.ts@dagrejs/dagre DAG 自动布局项目已安装依赖无需新增editor-slice.ts 新增 autoLayout action
> - **V5-9 F5 教材模糊搜索 + 最近使用**template-picker.tsx 增加客户端模糊搜索(标题/学科/年级/出版社)+ localStorage 最近使用 5 个教材
> - **V5-10 P1 画布性能优化**node-editor.tsx ReactFlow 新增 onlyRenderVisibleElements 虚拟化 + minZoom={0.2}/maxZoom={2.5}/elevateNodesOnSelect={false}
> - **V5-12 R4 校内课案库**:新增 teacher/lesson-plans/library 路由 + loading.tsx + error.tsxactions.ts 新增 duplicateLessonPlanFormActionFormData wrapper 适配 form action
> - **V5-13 P3 移动端只读视图**:新增 components/lesson-plan-mobile-view.tsx按教学阶段分组线性卡片视图lesson-plan-readonly-view.tsx 集成 useMediaQuery("(max-width: 768px)") 小屏切换
> - **V5-14 F4 板书可视化工具**blackboard-block.tsx 增加轻量级可视化预览(不引入新库)——编辑/预览模式切换、structure 层级树、mindmap 中心+分支、text 等宽展示;新增 BlackboardPreview 子组件 + parseContent 纯函数(按缩进识别层级)
> - **V5-15 T1 教学阶段分组**types.ts 新增 TeachingStage 类型 + TEACHING_STAGE_KEYS 常量Block 接口新增 stage? 字段node-edit-panel.tsx 添加教学阶段选择器 UI
> - **V5-16 T2 反思闭环(版本对比)**:新增 lib/version-diff.ts纯函数 diffDocuments 对比两个 LessonPlanDocument 节点级差异added/removed/modified/unchanged + changedFields新增 components/version-diff-view.tsxversion-history-drawer.tsx 集成版本对比currentDoc prop + 对比按钮)
> - **V5-17 A1/A2 AI 反馈闭环**:新增 lib/ai-feedback.tsgenerateLessonPlanFeedback 纯服务端函数4 维度 strengths/improvements/alignment/differentiation含解释性 reason 字段actions-ai.ts 新增 generateLessonPlanFeedbackAction新增 components/ai-feedback-dialog.tsx编辑器工具栏新增「AI 反馈」按钮
> - **V5-18 W6 差异化教学标记**types.ts 新增 DifferentiationLevel 类型 + DIFFERENTIATION_LEVEL_KEYS 常量Block 接口新增 differentiation? 字段node-edit-panel.tsx 添加差异化标记选择器 UI
> - **V5-19 T3 目标-评价一致性校验**:新增 lib/consistency-check.ts纯函数 checkConsistency 输出 ConsistencyResult校验规则缺少 objective/exercise 警告、exercise 无知识点警告、objective 未被评价覆盖警告);新增 components/consistency-check-dialog.tsx编辑器工具栏新增「一致性校验」按钮
> - **V5-20 T4 课标覆盖热力图**:新增 lib/curriculum-coverage.ts纯函数 computeCurriculumCoverage 按 chapterId 分组统计覆盖率,识别 isBlindSpot 教学盲点getHeatLevel 0-4 热力等级);新增 components/curriculum-heatmap.tsx新增 teacher/lesson-plans/heatmap 路由 + loading.tsx + error.tsx
> - **V5-21 A3/A4/A5 AI 差异化模块**:新增 lib/ai-differentiation.ts3 个纯服务端函数generateDifferentiationSuggestions/checkCurriculumAlignment/generateExplainableAssessment复用 ai-feedback.ts 的 createAiChatCompletion + Zod + JSON 提取模式actions-ai.ts 新增 4 个 server actionsgenerateDifferentiationSuggestionsAction/checkCurriculumAlignmentAction/generateExplainableAssessmentAction/getKnowledgePointsForAlignmentAction新增 components/ai-differentiation-dialog.tsx3 Tab 视图:差异化建议/课标核对/可解释评估A4 按教材 ID 按需加载知识点编辑器工具栏新增「AI 差异化」按钮
> - **i18n 扩展**zh-CN/en 同步新增 editor.stage*/editor.differentiation*/editor.consistency*/consistency.*/diff.*/library.*/feedback.*/heatmap.*/aiDifferentiation.*/blackboard.editMode/previewMode/editHint/previewEmpty/untitled
> - **V5-11 R3 协同编辑**需引入新依赖Yjs/Liveblocks或采用轻量协作方案评论+@提醒+版本对比),暂标记为待规划,待用户决策
> 架构变更2026-07-04V4 纸感重构 — 抛弃 React Flow 画布,改用三栏布局 + Tiptap 富文本编辑器):
> - **H1 interaction block 注册**`config/block-registry.tsx` 新增 `interaction` case导入 `InteractionBlock` 组件 + `isInteractionBlockData` 类型守卫BLOCK_REGISTRY 支持 12 种 Block 类型(含 V4 interaction
> - **H2 三栏布局重构**`lesson-plan-editor.tsx` 移除 `NodeEditor`/`NodeEditPanel` 导入与 JSX改用 `StructureTree + PaperEditor + DetailPanel` 三栏布局(`grid-template-columns: 260px 1fr 380px`),保留顶部工具栏(标题/保存/撤销重做/发布/导出/安排课时/一致性校验/AI 反馈/AI 差异化)+ 5 个对话框VersionHistoryDrawer/ConsistencyCheckDialog/AiFeedbackDialog/AiDifferentiationDialog/PrintView/ScheduleDialog删除 `components/node-edit-panel.tsx` + `app/(dashboard)/teacher/lesson-plans/[planId]/edit/ai-content-generator-slot.tsx`(依赖 NodeEditPanel 类型)
> - **I1 v3 锚点失效 banner**:新增 `components/anchor-migration-banner.tsx`,检测 `doc.anchors` 中 `invalid:true` 或含 `start` 字段的旧锚点并提示用户;通过 `key={planId}` 实现 planId 变化时重新挂载内部组件,使用 lazy initial state 读取 localStoragedismiss 状态按 planId 隔离),避免 useEffect 中 setState符合 V4 `react-hooks/set-state-in-effect` lint 规则)
> - **J1 删除废弃文件**:删除 8 个 React Flow 画布和字符串锚点相关文件——`components/node-editor.tsx`、`components/nodes/lesson-node.tsx`、`components/nodes/textbook-content-node.tsx`、`components/nodes/textbook-segments.tsx`、`components/nodes/anchor-node-selector.tsx`、`lib/anchor-injector.ts`、`lib/rf-mappers.ts`、`lib/auto-layout.ts``lesson-plan-readonly-view.tsx` 重写为委托 `LessonPlanMobileView`(线性卡片视图)渲染所有视口,移除 React Flow 依赖
> - **J2 PrintView 适配 V4**`lib/export.ts` 的 `flattenBlockData` switch 新增 `interaction` case + `flattenInteraction` 函数(输出设计意图 + 师生对话回合 + 预期回答export.ts 已使用 `nodes.sort((a, b) => a.order - b.order)` 按顺序扁平化无画布依赖docstring 更新为 12 种 Block含 V4 interaction
> - **J3 lint + tsc 零错误验证**tsc --noEmit 0 错误lint 中 V4 引入的 `react-hooks/set-state-in-effect` 错误已通过 `anchor-migration-banner.tsx` 的 key-based remount + lazy initial state 模式修复;剩余 6 个 lint 错误为预存无关模块问题exams/use-exam-preview-tasks react-hooks/refs 3 个、template-picker V5-9 set-state-in-effect 1 个),按计划要求记录不修复
> - **J4 架构图同步**004_architecture_impact_map.md 新增 V4 纸感重构章节005_architecture_data.json 更新 lesson_preparation 节点——description 改为 V4 描述、dependencies 移除 `@xyflow/react` 改为 `@tiptap/*` 包、files 数组移除 9 个废弃文件并新增 V4 组件、auditFixes 新增 V4-PAPER-1~8 + V4-PAPER-DOC 条目
> - **J5 known-issues V4 规则**`docs/troubleshooting/known-issues.md` 新增 V4 纸感重构规则表,记录 8 条规则——React Flow 弃用、Tiptap Mark 锚点替代字符串偏移、setState in effect 禁用、doc.version=4、editor-slice 移除画布相关方法、interaction block 注册、PrintView 按 order 排序、AnchorMigrationBanner 失效提示
> - **数据模型 v4**`types.ts` `LessonPlanDocument.version = 4``lib/document-migration.ts` `normalizeDocument` 兼容 v1/v2/v3/v4 链式迁移;`editor-slice.ts` 移除 `updateNodePosition`/`connect`/`setEdges`/`autoLayout` 画布相关方法;`hooks/expanded-slice.ts` 新增 `expandedNodeIds`/`toggleExpand`/`setExpanded` 状态(节点展开到正文流);`lib/anchor-mark.ts` 新增 Tiptap `AnchorMark` + `AnchorPoint` 扩展(替代 v3 字符串偏移锚点)
> - **V2-1 复制节点**`hooks/editor-slice.ts` 新增 `duplicateNode(id, copySuffix)` 方法(深拷贝 `BlockData` + 新 id + 标题加 `copySuffix` + order 置末interaction 节点的 turns 重新生成 id 避免冲突;`copySuffix` 由调用方注入 i18n 文案);`components/paper-editor/paper-context-menu.tsx` 的 `copyNode` 从 toast 占位改为调用 `duplicateNode` 并 toast 成功提示i18n 新增 `v4.contextMenu.copied`/`copyFailed`/`copySuffix` 键zh-CN + en
> - **V2-2 节点级 AI 协助 4 项**`lib/ai-node-assist.ts` 新增 4 个服务函数(`generateNodeContent`/`optimizeNodeExpression`/`suggestNodeDifferentiation`/`generateLayeredQuestions`全部纯服务端、Zod 校验、失败返回 null`actions-ai.ts` 新增 4 个 Server Action每个 action 校验 `LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT` 三个权限点;`hooks/use-node-ai-assist.ts` 新增客户端 hook`components/paper-editor/paper-editor.tsx` 注入 `onAiAction` 回调;`components/detail-panel/detail-panel.tsx` 的 4 个 `AiButton` 接入真实 actioni18n 新增 `v4.contextMenu.aiRunning`/`aiSuccess`/`aiFailed`/`aiComingSoon` 键zh-CN + en
> - **V4-I18N-1 i18n 完整审查与修复**:对 lesson-preparation 模块 i18n 配置进行全量审查并修复——P0 修复运行时 MISSING_MESSAGE`version.diff.*` 3 键title/summary/noChanges+ `schema.ts` 引用的 8 个 `error.*` 键labelTooLong/versionNoInvalid/nameRequired/nameTooLong/queryTooLong/blockIdRequired/commentTooLong/reviewerRequired+ `v4.contextMenu` 6 键copied/copyFailed/aiComingSoon/aiRunning/aiSuccess/aiFailedzh-CN + en 同步P1 修复 en locale 下大面积报错en `calendar` 节键名对齐 zh-CNweekView/monthView/loadFailed + prev/next/eventMeta/weekDays 子节en `analytics` 节补齐 totalPublished/totalSubmitted/totalStandardsLinked/loadFailedP2 修复 7 处硬编码中文detail-props.tsx 的 stageLabel/differentiationLabel、inline-node.tsx 的 exerciseCount、inline-qa-dialog.tsx 的 expectedAnswer、qa-editor.tsx 的 turnContentPlaceholder、curriculum-map-view.tsx 的 legendTitle/noData并新增对应 6 个 i18n 键(`v4.detail.stageLabel`/`differentiationLabel`/`exerciseCount`/`turnContentPlaceholder`、`v4.heatmap.legendTitle`/`noData`);清理 7 个 dead 节review/comment/formative/substitute/evaluation/standards/gradeHeadanalytics 因被 `app/(dashboard)/admin/curriculum-map` 引用而保留);`lib/i18n-errors.ts` 新增 `t.has(msg)` 运行时守卫,避免 `as` 断言绕过类型检查(键不存在时返回原消息而非抛出 MISSING_MESSAGEzh-CN 与 en 顶层键集一致性验证通过43 = 43005_architecture_data.json 同步追加 `V4-I18N-1` auditFixes 条目known-issues.md 追加 3 个 i18n 规则表(键缺失与中英文不同步 / dead 节清理规则 / 动态键翻译守卫)
> 架构变更2026-07-05Phase 4.2 — Zustand selector 细粒度拆分,避免整体订阅级联 re-render
> - **lesson-plan-editor.tsx**`const editor = useLessonPlanEditor()` 整体订阅拆分为 9 个细粒度 selector`isDirty`/`isOnline`/`doc`/`title`/`isSaving`/`saveError`/`lastSavedAt`/`setTitle` + `canUndo`/`canRedo` 派生 boolean`canUndo`/`canRedo` 改为 `useLessonPlanEditor((s) => s.canUndo())` 订阅派生 boolean移除每次渲染时调用 `editor.canUndo()` 函数;`useEffect` 依赖与 `printablePlan` useMemo 同步更新使用拆分后的变量
> - **text-study-block.tsx**`const { updateNode } = useLessonPlanEditor()` → `useLessonPlanEditor((s) => s.updateNode)`
> - **exercise-block.tsx**:拆为 `useLessonPlanEditor((s) => s.updateNode)` + `useLessonPlanEditor((s) => s.planId)`
> - **paper-context-menu.tsx**:拆为 7 个细粒度 selector`toggleExpand`/`expandedNodeIds`/`updateNode`/`removeNode`/`addAnchor`/`duplicateNode`/`nodes`);特别地将 `doc` 改为 `s.doc.nodes` 更精细订阅(`addAnchor` 等仅修改 anchors 不会触发本组件 re-render
> - **关键决策**:不使用 `useShallow` 包装多字段 selector因为单字段 selector 已经够细,且避免了 `useShallow` 自身的浅比较开销;函数引用(`setTitle`/`updateNode` 等)天然稳定,无需 `useShallow`
> - **验证**tsc 零新增错误lint 零新增错误4 个 modified 文件均未出现在 lint 输出中)
---
## 2.27b standards课标模块2026-07-01 新增)
**职责**K12 课标管理,支持课标树、按学科+年级筛选、课案与课标多对多关联。
**导出函数**
- Data-accessgetStandards / getStandardsTree / getStandardById / createStandard / updateStandard / deactivateStandard / searchStandards / getStandardsByPlanId / linkPlanToStandard / unlinkPlanFromStandard
- Actions10 个 Server Actions全部使用 requirePermission() + ActionState<T>
- SchemacreateStandardSchema / updateStandardSchema / getStandardsParamsSchema / linkPlanToStandardSchema
- TypesStandard / StandardTreeNode / LessonPlanStandardLink
**权限点**
- STANDARD_READstandard:read
- STANDARD_MANAGEstandard:manage
- STANDARD_LINKstandard:link
**依赖关系**
- 依赖shared/*、modules/lesson-preparation/lib/i18n-errors
- 被依赖modules/lesson-preparation、app/(dashboard)/admin/curriculum-map
**文件清单**
| 文件 | 职责 |
|------|------|
| types.ts | 类型定义 |
| schema.ts | Zod 验证 |
| data-access.ts | 课标 CRUD + 树形查询 + 课案关联 |
| actions.ts | 10 个 Server Actions |
---
## 2.28 error-book错题本模块
**职责**:自动采集考试/作业错题,基于 SM-2 间隔重复算法科学复习,提供知识点薄弱度分析与多角色视图(学生/教师/家长/管理员)。
**导出函数**
- Actions`getErrorBookItemsAction` / `getErrorBookItemDetailAction` / `getErrorBookStatsAction` / `createErrorBookItemAction` / `updateErrorBookNoteAction` / `reviewErrorBookItemAction` / `deleteErrorBookItemAction` / `archiveErrorBookItemAction` / `collectFromSubmissionAction`
- Data-access`data-access.ts`,学生端 CRUD`getErrorBookItems` / `getErrorBookItemById` / `getErrorBookStats`SQL 聚合优化)/ `createErrorBookItem` / `updateErrorBookNote` / `recordReview` / `deleteErrorBookItem` / `archiveErrorBookItem` / `buildStudentErrorWhereClause`(共享)/ `toStatus`(共享)/ `toStringArray`(共享)
- Data-access`data-access-analytics.ts`,教师/管理员分析查询):`getStudentErrorBookSummaries`SQL 聚合)/ `getTopWrongQuestionsByStudentIds`SQL 聚合)/ `getKnowledgePointWeakness` / `getSubjectErrorDistribution`SQL 聚合)/ `getChapterWeakness` / `getClassErrorOverviews`SQL 聚合)/ `getSubjectErrorOverviews`SQL 聚合)/ `getStudentNameMap` / `getStudentIdsByClassIdList` / `getAllStudentIds`
- Data-access`data-access-collection.ts`,自动采集):`collectFromExamSubmission` / `collectFromHomeworkSubmission`(通过 `data-access.ts` re-export
- Schema`CreateErrorBookItemSchema` / `UpdateErrorBookNoteSchema` / `ReviewErrorBookItemSchema` / `CollectFromSubmissionSchema`
- Types`ErrorBookItem` / `ErrorBookItemDetail` / `ErrorBookReviewRecord` / `ErrorBookStats` / `StudentErrorBookSummary` / `KnowledgePointWeakness` / `SubjectErrorDistribution`
- Components`ErrorBookStatsCards` / `ErrorBookFilters` / `ErrorBookItemCard` / `ReviewButtons` / `ErrorBookDetailDialog` / `ErrorBookList` / `AddErrorBookDialog` / `TopWrongQuestions`
**权限点**
- `ERROR_BOOK_READ``error_book:read`查看错题本student 自己、parent 子女)
- `ERROR_BOOK_MANAGE``error_book:manage`):管理错题(添加/复习/删除/归档student
- `ERROR_BOOK_ANALYTICS_READ``error_book:analytics_read`查看错题分析teacher/admin/grade_head/teaching_head
**依赖关系**
- 依赖:`shared/*`db、auth-guard、types、utils、`modules/classes`getStudentIdsByClassIds
- 被依赖:`app/(dashboard)/student/error-book`、`app/(dashboard)/teacher/error-book`、`app/(dashboard)/parent/error-book`、`app/(dashboard)/admin/error-book`、`modules/homework`collectFromHomeworkSubmission作业批改后自动采集、`modules/grades`collectFromExamSubmission✅ 2026-06-25 新增:教师录入考试成绩后自动采集考试错题)
- ✅ 2026-06-24 i18n + 解耦改造:`error-book-detail-dialog` 移除直接 import `AiErrorBookAnalysis`modules/ai和 `createPracticeSessionAction`modules/adaptive-practice改为通过 `aiAnalysisSlot` 和 `onStartVariantPractice` props 注入;`add-error-book-dialog` 移除直接 import `getQuestionsAction`modules/questions改为通过 `questionOptions` 和 `onLoadQuestions` props 注入。跨模块依赖上移至 `app/(dashboard)/student/error-book/page.tsx`
**DataScope 行级权限**
| 角色 | DataScope | 说明 |
|------|-----------|------|
| student | `owned` | 仅查看/管理自己的错题 |
| parent | `children` | 查看子女的错题统计 |
| teacher | `class_taught` | 查看所教班级学生的错题分析 |
| admin | `all` | 查看全校错题分析 |
| grade_head | `grade_managed` | 查看所管年级学生的错题分析 |
| teaching_head | `grade_managed` | 查看所管年级学生的错题分析 |
**核心算法SM-2 间隔重复(简化版)**
- 独立纯函数模块:[sm2-algorithm.ts](file:///e:/Desktop/CICD/src/modules/error-book/sm2-algorithm.ts)(可替换为其他算法如 FSRS
- 4 级评级:`again`(重来)/ `hard`(困难)/ `good`(良好)/ `easy`(简单)
- 初始间隔1/2/4/7 天
- 间隔增长:`again` 重置为 1 天;`hard` ×1.2`good` ×1.5`easy` ×2
- 掌握度0-5 级,`again` -1`hard` ±0`good` +1`easy` +2
- 已掌握判定:掌握度 ≥5 或连续答对 ≥3 次
- 复习时间:次日早上 9 点
- 单元测试:[sm2-algorithm.test.ts](file:///e:/Desktop/CICD/src/modules/error-book/sm2-algorithm.test.ts)39 个测试用例,全部通过)
- 导出函数:`calculateNewInterval` / `calculateNewMastery` / `deriveStatus` / `calculateNextReviewAt` / `calculateNewCorrectStreak` / `calculateSm2Result`
**自动采集机制**
- `collectFromExamSubmission`:从考试提交记录中筛选得分 < 满分的题目,去重后批量插入
- `collectFromHomeworkSubmission`:从作业提交记录中筛选错题,去重后批量插入
- 自动关联知识点(通过 `questionsToKnowledgePoints` 表)
- subjectId 来源:`questions` 表无 subjectId 字段,考试错题从 `exams.subjectId` 获取;作业错题从 `homeworkAssignments.sourceExamId` 关联到源试卷的 `exams.subjectId` 获取(独立作业无学科归属则为 null
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~180 | 9 个 Server Actions全部使用 `requirePermission()` + `ActionState<T>` |
| `data-access.ts` | ~480 | 学生端 CRUD10 个函数)+ 共享工具函数buildStudentErrorWhereClause/toStatus/toStringArray+ re-export 采集函数。✅ 2026-06-24 拆分:分析函数迁移至 data-access-analytics.ts修复 as 断言/or()! 非空断言/JSON 列类型安全getErrorBookStats 优化为 SQL GROUP BY 聚合 |
| `data-access-analytics.ts` | ~430 | ✅ 2026-06-24 新增:教师/管理员分析查询9 个函数SQL 聚合优化getStudentErrorBookSummaries/getTopWrongQuestionsByStudentIds/getSubjectErrorDistribution/getClassErrorOverviews/getSubjectErrorOverviews 使用 GROUP BY + 条件聚合) |
| `data-access-collection.ts` | ~170 | 自动采集逻辑(从考试/作业提交采集错题,通过跨模块接口避免直查多表) |
| `sm2-algorithm.ts` | ~180 | SM-2 间隔重复算法(独立纯函数模块,可替换,支持时间注入测试) |
| `sm2-algorithm.test.ts` | ~280 | SM-2 算法单元测试39 个测试用例,覆盖所有函数和边界条件) |
| `schema.ts` | ~60 | 4 个 Zod 验证 schema |
| `types.ts` | ~245 | 11 个类型定义 + 状态映射常量 + 错误标签常量(新增 ChapterWeakness/ClassErrorOverview/SubjectErrorOverview |
| `components/error-book-stats-cards.tsx` | ~80 | 5 个统计卡片(总数/待学习/学习中/已掌握/待复习) |
| `components/analytics-stats-cards.tsx` | ~110 | 教师/管理员分析视图 5 个统计卡片(覆盖学生/错题总数/平均掌握率/待复习/知识点数) |
| `components/error-book-filters.tsx` | ~100 | 筛选栏(搜索/状态/来源/待复习),使用 nuqs |
| `components/error-book-item-card.tsx` | ~150 | 错题卡片(预览/标签/笔记/掌握度/操作) |
| `components/review-buttons.tsx` | ~80 | 4 按钮复习面板again/hard/good/easy |
| `components/error-book-detail-dialog.tsx` | ~340 | 详情对话框(题目/答案/复习/笔记/历史)。✅ 2026-06-24 i18n + 解耦:移除直接 import `AiErrorBookAnalysis`/`createPracticeSessionAction`,改为 `aiAnalysisSlot`/`onStartVariantPractice` props 注入;移除 `as` 断言 |
| `components/error-book-list.tsx` | ~60 | 网格列表。✅ 2026-06-24 i18n + 解耦:新增 `aiAnalysisSlot?: (item: ErrorBookItem) => React.ReactNode` 和 `onStartVariantPractice?: (item: ErrorBookItem) => void` props透传至 `ErrorBookItemCard`/`ErrorBookDetailDialog` |
| `components/add-error-book-dialog.tsx` | ~170 | 手动添加对话框(题库选择 + 标签)。✅ 2026-06-24 i18n + 解耦:移除直接 import `getQuestionsAction`,改为 `questionOptions`/`onLoadQuestions` props 注入;移除 `as` 断言 |
| `components/subject-tabs.tsx` | ~95 | 学科切换 Tab显示每个学科错题数概览URL 参数持久化) |
| `components/class-filter.tsx` | ~85 | 班级筛选器显示每个班级错题数和待复习数URL 参数持久化) |
| `components/class-error-bar-chart.tsx` | ~115 | 班级错题数对比柱状图rechartstooltip 显示学生数/人均/掌握率) |
| `components/subject-distribution-chart.tsx` | ~110 | 学科错题分布柱状图管理员视图recharts |
| `components/knowledge-point-weakness-chart.tsx` | ~150 | 知识点薄弱度横向柱状图(按掌握率红/黄/绿着色,显示章节归属) |
| `components/chapter-weakness-chart.tsx` | ~165 | 章节错题分布横向柱状图(哪些课在错,含 Top 3 薄弱知识点) |
| `components/grouped-student-error-table.tsx` | ~180 | 按班级分组的学生错题表格(可展开/折叠,显示每个学生的错题详情) |
| `components/top-wrong-questions.tsx` | ~80 | 高频错题列表Top 10 |
**路由清单**
| 路由 | 文件 | 说明 |
|------|------|------|
| `/student/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` + `student-error-book-list-client.tsx` + `add-error-book-dialog-with-questions.tsx` | 学生错题本(统计/筛选/列表/手动添加/详情复习)。✅ 2026-06-24 解耦app 层新增两个包装组件承接跨模块依赖:`StudentErrorBookListClient` 注入 `AiErrorBookAnalysis`modules/ai+ `createPracticeSessionAction`modules/adaptive-practice到 `ErrorBookList` 的 `aiAnalysisSlot`/`onStartVariantPractice` props`AddErrorBookDialogWithQuestions` 注入 `getQuestionsAction`modules/questions+ `extractQuestionPreview`shared/lib到 `AddErrorBookDialog` 的 `onLoadQuestions` prop |
| `/teacher/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 教师错题分析学科Tab/班级筛选/统计卡片/班级对比图/章节错题图/知识点薄弱度图/按班级分组学生表/高频错题) |
| `/parent/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 家长错题本(子女错题统计/薄弱知识点/高频错题) |
| `/admin/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 管理员错题分析学科Tab/统计卡片/学科分布图/章节错题图/知识点薄弱度图/按班级分组学生Top50/高频错题) |
**数据库表**
| 表 | 说明 |
|------|------|
| `errorBookItems` | 错题条目主表18 列/4 索引/2 外键studentId、questionId、sourceType、sourceId、studentAnswer、correctAnswer、subjectId、knowledgePointIds、status、masteryLevel、nextReviewAt、reviewInterval、reviewCount、correctStreak、note、errorTags、createdAt、updatedAt |
| `errorBookReviews` | 复习记录表8 列/2 索引/2 外键itemId、studentId、result、reviewedAt、newInterval、newMasteryLevel |
**i18n**
- `src/shared/i18n/messages/zh-CN/error-book.json`
- `src/shared/i18n/messages/en/error-book.json`
**导航配置**
- 6 个角色admin/teacher/student/parent/grade_head/teaching_head均添加「错题分析」导航项
- 图标:`BookX`lucide-react
---
## 2.29 aiAI 模块)— ✅ 新增 / V2 增强 / V3 安全加固+竞品对标
**职责**:统一 AI 能力封装,为备课、错题集、试卷、改题等业务模块提供 AI 服务。V2 增加流式响应、Markdown 渲染、全局助手、内容安全过滤、家长学情摘要、管理员使用统计、学生学习路径推荐。V3 安全加固(原子配额/苏格拉底校验/重试机制)+ 竞品对标(知识图谱集成/苏格拉底式辅导强化)。
**架构定位**
- 位于 `modules/` 层,通过 `shared/lib/ai` 调用底层 AI SDK
- 通过 `AiClientProvider`React Context向客户端组件注入 Server Action 引用
- 业务模块不直接 import `ai/actions`,仅通过 Context 消费
- V2在 dashboard layout 全局注入 `AiClientProvider`,所有页面均可使用 AI 助手
**核心导出**
| 类型 | 名称 | 文件 | 说明 |
|------|------|------|------|
| **Server Actions** | `aiChatAction` | `modules/ai/actions.ts` | AI 对话权限AI_CHAT |
| **Server Actions** | `suggestSimilarQuestionsAction` | `modules/ai/actions.ts` | 相似题推荐权限AI_CHAT + ERROR_BOOK_READ |
| **Server Actions** | `suggestGradingAction` | `modules/ai/actions.ts` | AI 辅助批改权限AI_CHAT + HOMEWORK_GRADE |
| **Server Actions** | `generateLessonContentAction` | `modules/ai/actions.ts` | 备课内容生成权限AI_CHAT + LESSON_PLAN_READ |
| **Server Actions** | `generateQuestionVariantAction` | `modules/ai/actions.ts` | 题目变体生成权限AI_CHAT + EXAM_AI_GENERATE |
| **Server Actions** | `analyzeWeaknessAction` | `modules/ai/actions.ts` | 薄弱点分析权限AI_CHAT + ERROR_BOOK_READ |
| **Server Actions** | `generateChildSummaryAction` | `modules/ai/actions.ts` | 家长学情摘要权限AI_CHAT— V2 新增 |
| **Server Actions** | `recommendStudyPathAction` | `modules/ai/actions.ts` | 学习路径推荐权限AI_CHAT— V2 新增 |
| **Server Actions** | `getAiUsageStatsAction` | `modules/ai/actions.ts` | AI 使用统计权限AI_CONFIGURE— V2 新增 |
| **Server Actions** | `explainErrorAction` | `modules/ai/actions.ts` | 错题 AI 解释权限AI_CHAT + ERROR_BOOK_READ— V3 P2-1 新增 |
| **SSE Route** | `POST /api/ai/chat/stream` | `app/api/ai/chat/stream/route.ts` | 流式 AI 对话SSE— V2 新增 |
| **SSE Route** | `GET /api/notifications/stream` | `app/api/notifications/stream/route.ts` | 通知实时推送SSE15s 心跳 + 5min 超时)— V2-P3 新增 |
| **Service** | `AiService` | `modules/ai/types.ts` | 服务端 AI 服务接口(含 8 个方法) |
| **Service** | `AiClientService` | `modules/ai/types.ts` | 客户端 AI 服务接口Server Action 引用集合) |
| **Provider** | `AiClientProvider` | `modules/ai/context/ai-client-provider.tsx` | React Context Provider注入 AiClientService |
| **Hook** | `useAiClient` | `modules/ai/context/ai-client-provider.tsx` | 消费 AiClientService必须在 Provider 内使用) |
| **Hook** | `useAiClientOptional` | `modules/ai/context/ai-client-provider.tsx` | 可选消费 AiClientService未注入返回 null |
| **Hook** | `useAiChatStream` | `modules/ai/hooks/use-ai-chat-stream.ts` | 流式 AI 对话 HookSSE + AbortController— V2 新增 |
| **Hook** | `useAiSuggestion` | `modules/ai/hooks/use-ai-suggestion.ts` | AI 建议 Hook |
| **Hook** | `useFloatingBall` | `modules/ai/hooks/use-floating-ball.ts` | 可拖拽悬浮球 Hook边缘吸附+半隐藏+位置持久化)— V3 拆分 |
| **Hook** | `usePositionPersistence` | `modules/ai/hooks/use-position-persistence.ts` | 位置持久化 HooklocalStorage + resize 校正)— V3 新增 |
| **Hook** | `useDragPosition` | `modules/ai/hooks/use-drag-position.ts` | 拖拽位置 Hookpointer 事件 + 拖拽状态)— V3 新增 |
| **Context** | `createFullAiClientService` | `modules/ai/context/create-ai-client-service.ts` | 创建完整 AI 客户端服务工厂(含全部 9 个 Action— V3 新增 |
| **Context** | `createCoreAiClientService` | `modules/ai/context/create-ai-client-service.ts` | 创建核心 AI 客户端服务工厂(仅 6 个常用 Action— V3 新增 |
| **Component** | `AiAssistantWidget` | `modules/ai/components/ai-assistant-widget.tsx` | 全局 AI 助手悬浮按钮(上下文感知)— V2 新增 / Phase 4.7 改造为懒加载 wrapper`next/dynamic` ssr:false仅渲染 Skeleton 占位 + LazyInner |
| **Component** | `AiAssistantWidgetInner` | `modules/ai/components/ai-assistant-widget-inner.tsx` | 全局 AI 助手悬浮球内部实现(悬浮球 + 抽屉 + AiChatPanel + 上下文推断)— Phase 4.7 从 ai-assistant-widget.tsx 拆出,通过 dynamic 懒加载,使 AI SDK / Markdown 渲染等重型依赖打入独立 chunk |
| **Component** | `AiChatPanel` | `modules/ai/components/ai-chat-panel.tsx` | AI 对话面板(流式 + Markdown + 复制 + 停止 + 建议)— V2 增强 |
| **Component** | `AiMarkdownRenderer` | `modules/ai/components/ai-markdown-renderer.tsx` | Markdown 渲染器GFM + 复制按钮)— V2 新增 |
| **Component** | `AiGradingAssist` | `modules/ai/components/ai-grading-assist.tsx` | AI 批改辅助(主观题预评分 + 反馈建议) |
| **Component** | `AiErrorBookAnalysis` | `modules/ai/components/ai-error-book-analysis.tsx` | 错题本 AI 分析(相似题 + 薄弱点) |
| **Component** | `AiLessonContentGenerator` | `modules/ai/components/ai-lesson-content-generator.tsx` | 备课内容生成器(活动/评估/讨论题/素材) |
| **Component** | `AiQuestionVariantGenerator` | `modules/ai/components/ai-question-variant-generator.tsx` | 题目变体生成器(同知识点/不同难度/不同题型) |
| **Component** | `AiChildSummary` | `modules/ai/components/ai-child-summary.tsx` | 家长 AI 学情摘要 — V2 新增 |
| **Component** | `AiUsageDashboard` | `modules/ai/components/ai-usage-dashboard.tsx` | 管理员 AI 使用统计仪表盘 — V2 新增 |
| **Component** | `AiStudyPath` | `modules/ai/components/ai-study-path.tsx` | 学生学习路径推荐 — V2 新增 |
| **Component** | `AiErrorBoundary` | `modules/ai/components/ai-error-boundary.tsx` | AI 功能错误边界 |
| **Component** | `AiSuggestionSkeleton` | `modules/ai/components/ai-skeleton.tsx` | AI 建议加载骨架屏 |
| **Component** | `AiChartRenderer` | `modules/ai/components/ai-chart-renderer.tsx` | AI 图表渲染器bar/line/pie/radar— V3 新增 |
| **Component** | `AiProviderSelector` | `modules/ai/components/ai-provider-selector.tsx` | AI 服务商选择器 |
| **Safety** | `filterUserInput` | `modules/ai/services/content-safety.ts` | 输入安全过滤 — V2 新增 |
| **Safety** | `filterAiOutput` | `modules/ai/services/content-safety.ts` | 输出安全过滤 — V2 新增 |
| **Safety** | `checkDailyLimit` | `modules/ai/services/content-safety.ts` | 每日交互限制 — V2 新增 |
| **Safety** | `tryConsumeDailyQuota` | `modules/ai/services/content-safety.ts` | 原子化每日限额(防 TOCTOU 竞态)— V3 新增 |
| **Safety** | `refundDailyQuota` | `modules/ai/services/content-safety.ts` | 配额回退(过滤/失败时不扣配额)— V3 新增 |
| **Safety** | `validateSocraticOutput` | `modules/ai/services/content-safety.ts` | 苏格拉底式辅导输出校验(问号结尾/连续陈述句限制)— V3 新增 |
| **Data Access** | `recordAiEvent` | `modules/ai/data-access.ts` | AI 事件记录(内存存储,生产环境替换为 DB— V3 新增 |
| **Data Access** | `getAiUsageStats` | `modules/ai/data-access.ts` | AI 使用统计聚合(真实聚合,替代硬编码零)— V3 新增 |
| **Prompt** | `SOCRATIC_TUTOR_SYSTEM_PROMPT` | `modules/ai/services/prompt-templates.ts` | 苏格拉底式辅导专用提示词3 级提示升级/禁止直接给答案)— V3 新增 |
| **Prompt** | `EXPLAIN_ERROR_SYSTEM_PROMPT` | `modules/ai/services/prompt-templates.ts` | 错题解释专用提示词(错误分析/正确思路/关键知识点/防范建议)— V3 P2-1 新增 |
| **Util** | `consumeSseStream` / `getStreamErrorKey` / `removeTrailingEmptyAssistant` | `modules/ai/hooks/stream-utils.ts` | SSE 流解析工具(从 hook 抽取)— V3 新增 |
**集成点**
| 业务模块 | 集成组件 | 页面 | 说明 |
|---------|---------|------|------|
| homework | `AiGradingAssist` | `teacher/homework/submissions/[submissionId]` | 主观题辅助评分 |
| error-book | `AiErrorBookAnalysis` | `student/error-book` | 相似题推荐 + 薄弱点分析 |
| lesson-preparation | `AiLessonContentGenerator` | `teacher/lesson-plans/[planId]/edit` | 备课内容生成 |
| exams | `AiQuestionVariantGenerator` | `teacher/exams/[id]/build` | 题目变体生成 |
| **全局** | `AiAssistantWidget` | `app/(dashboard)/layout.tsx` | 全局 AI 助手悬浮按钮(所有页面)— V2 新增 |
| **parent** | `AiChildSummary` | `parent/dashboard` | 家长学情 AI 摘要 — V2 新增V4 实际接入到 ParentDashboard 的 aiSummarySlot按子女渲染 |
| **admin** | `AiUsageDashboard` | `admin/ai-settings` | AI 使用统计仪表盘 — V2 新增V4 校正:原误记为 `admin/ai-usage`,实际位于 `/admin/ai-settings` |
| **student** | `AiStudyPath` | `student/learning/study-path` | 学习路径推荐 — V2 新增V4 实际接入页面(含 loading.tsx + error.tsx |
**依赖关系**
- `modules/ai` → `shared/lib/ai`AI SDK 封装,含流式 `createAiChatCompletionStream`V3 增加 `withRetry` 重试机制)
- `modules/ai` → `shared/lib/auth-guard`(权限校验)
- `modules/ai` → `shared/lib/track-event`(使用量埋点)
- `modules/ai` → `shared/types/permissions`(权限常量)
- `modules/ai` → `shared/types/action-state`(返回值类型)
- `modules/ai` → `modules/textbooks/data-access-graph`(知识图谱+掌握度查询)— V3 新增
- `app/(dashboard)/layout` → `modules/ai`(全局 Provider + Widget— V2 新增
- `app/api/ai/chat/stream` → `modules/ai/services/content-safety`SSE 端点安全过滤)— V3 加固
- 业务模块 → `modules/ai/context/ai-client-provider`(通过 Context 注入)
- 业务模块 → `modules/ai/components/*`(组合 AI 组件)
**安全机制V2 新增 / V3 加固)**
- 输入过滤:`filterUserInput` 拦截暴力/自残/色情/PII
- 输出过滤:`filterAiOutput` 扫描 AI 回复
- 每日限制:学生 50 次/天,教师 200 次/天,家长 30 次/天
- 学生 Socratic 模式system prompt 强制不直接给答案
- SSE 端点权限校验:`requirePermission(AI_CHAT)`
- V3 原子配额:`tryConsumeDailyQuota` 解决 TOCTOU 竞态,`refundDailyQuota` 过滤/失败时回退
- V3 苏格拉底校验:`validateSocraticOutput` 检查问号结尾+连续陈述句限制,`SOCRATIC_TUTOR_SYSTEM_PROMPT` 3 级提示升级
- V3 SSE 端点加固Zod 校验 + rate limit + 服务端强制 systemPrompt忽略客户端
- V3 重试机制:`withRetry` 指数退避429/5xx2 次重试1s/2s/4s 延迟)
- V3 知识图谱集成:`recommendStudyPathAction` 自动从 textbooks 模块获取图谱+掌握度注入 prompt
- V4 超时优化:`shared/lib/ai/client.ts` 按场景分离超时(普通 30s / 测试连接 120s / 流式 180s解决添加 Provider 时检测超时过短导致测试失败
- V4 规范修复:`ai-service.ts` 消除 `as unknown as Record` 双重断言(改用 `in` 类型缩小);`actions.ts` 消除非空断言 `!`(改用 `flatMap``ai-assistant-widget.tsx` 硬编码英文提示词迁移至 i18n新增 `chat.suggestedPrompts.context.*` 翻译键)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `modules/ai/types.ts` | ~290 | 类型定义8 个业务场景类型 + AiService/AiClientServiceV3 新增 knowledgeGraph/textbookId |
| `modules/ai/schema.ts` | ~230 | Zod 验证 schema8 个输入 + 8 个输出V3 新增 knowledgeGraph/textbookId |
| `modules/ai/actions.ts` | ~400 | 9 个 Server Actions含权限校验V3 新增知识图谱获取V4 消除非空断言) |
| `modules/ai/data-access.ts` | ~138 | AI 事件存储+统计聚合 — V3 新增 |
| `modules/ai/services/ai-service.ts` | ~430 | DefaultAiService 实现8 个方法V3 新增知识图谱上下文注入V4 消除 as 断言) |
| `modules/ai/services/prompt-templates.ts` | ~250 | 9 个系统提示词模板V3 新增 SOCRATIC_TUTOR_SYSTEM_PROMPT |
| `modules/ai/services/usage-tracker.ts` | ~90 | AI 使用量埋点V3 新增 child_summary/study_path 能力) |
| `modules/ai/services/content-safety.ts` | ~290 | 内容安全过滤(输入/输出/每日限制/原子配额/苏格拉底校验)— V2 新增 / V3 加固 |
| `modules/ai/context/ai-client-provider.tsx` | ~62 | React Context Provider + Hooks |
| `modules/ai/components/ai-assistant-widget.tsx` | ~25 | 懒加载 wrappernext/dynamic ssr:false + Skeleton 占位)— Phase 4.7 改造 / V2 新增 / V4 i18n 修复 |
| `modules/ai/components/ai-assistant-widget-inner.tsx` | ~330 | 全局 AI 助手悬浮球内部实现(悬浮球 + 抽屉 + AiChatPanel + 上下文推断)— Phase 4.7 新增(从 ai-assistant-widget.tsx 拆出) |
| `modules/ai/components/ai-chat-panel.tsx` | ~305 | AI 对话面板(流式 + Markdown— V2 增强 |
| `modules/ai/components/ai-markdown-renderer.tsx` | ~100 | Markdown 渲染器 — V2 新增 |
| `modules/ai/components/ai-child-summary.tsx` | ~170 | 家长学情摘要 — V2 新增 |
| `modules/ai/components/ai-usage-dashboard.tsx` | ~180 | 管理员使用统计 — V2 新增 |
| `modules/ai/components/ai-study-path.tsx` | ~170 | 学生学习路径 — V2 新增 |
| `modules/ai/components/ai-grading-assist.tsx` | 173 | AI 批改辅助组件 |
| `modules/ai/components/ai-error-book-analysis.tsx` | 246 | 错题本 AI 分析组件 |
| `modules/ai/components/ai-lesson-content-generator.tsx` | ~187 | 备课内容生成器 |
| `modules/ai/components/ai-question-variant-generator.tsx` | ~208 | 题目变体生成器 |
| `modules/ai/components/ai-error-boundary.tsx` | ~88 | AI 错误边界 |
| `modules/ai/components/ai-skeleton.tsx` | ~47 | AI 骨架屏 |
| `modules/ai/components/ai-provider-selector.tsx` | ~129 | 服务商选择器 |
| `modules/ai/hooks/use-ai-chat-stream.ts` | ~107 | 流式 AI 对话 HookV3 拆分至 ≤80 行函数体)— V2 新增 / V3 重构 |
| `modules/ai/hooks/stream-utils.ts` | ~120 | SSE 流解析工具consumeSseStream/getStreamErrorKey 等)— V3 新增 |
| `modules/ai/hooks/use-ai-suggestion.ts` | ~72 | AI 建议 Hook |
| `app/api/ai/chat/stream/route.ts` | ~160 | SSE 流式端点 — V2 新增 |
| `app/api/notifications/stream/route.ts` | ~120 | 通知实时推送 SSE 端点 — V2-P3 新增 |
**i18n**
- 翻译文件:`shared/i18n/messages/{locale}/ai.json`
- 命名空间:`ai`
- V2 新增键:`chat.streaming/stopGeneration/copy/clearConfirm/suggestedPrompts`、`grading.description/batch*`、`lessonPrep.description/additionalContext/insertContent`、`exam.variantType.*/targetDifficulty/addVariant`、`parent.*`、`admin.*`、`studyPath.*`、`widget.*`、`safety.*`
- V4 新增键:`chat.suggestedPrompts.context.{teacherGrading,teacherLesson,teacherExam,studentHomework}`(上下文感知提示词 i18n 化)
---
## 2.30 adaptive-practice专项练习模块— ✅ 新增(核心教学链路闭环)
**职责**:实现"错题集 → 知识点 → 专项出题 → 学生答题 → 自动判分 → 教师/年级主任宏观数据分析"的完整闭环。支持四种出题策略错题变式、知识点专项、薄弱章节、AI 推荐。提供教师端与年级主任端的宏观数据分析工作台。
**架构定位**
- 位于 `modules/` 层,严格遵循三层架构
- 通过 `data-access.ts` 提供 CRUD 操作,通过 `data-access-strategy.ts` 实现出题策略,通过 `data-access-analytics.ts` 提供教师/年级分析查询
- 跨模块通信:通过 `modules/error-book/data-access` 获取错题数据,通过 `modules/questions/data-access` 获取题目数据,通过 `modules/classes/data-access` 获取班级/学生数据,通过 `modules/users/data-access` 获取用户数据
- Server Actions 通过 `requirePermission()` 校验 `ADAPTIVE_PRACTICE_READ` / `ADAPTIVE_PRACTICE_MANAGE` 权限
**核心导出**
| 类型 | 名称 | 文件 | 说明 |
|------|------|------|------|
| **DB Schema** | `practiceSessions` | `shared/db/schema.ts` | 练习会话表(学生 ID/类型/状态/统计) |
| **DB Schema** | `practiceAnswers` | `shared/db/schema.ts` | 练习答题记录表(含变式题支持) |
| **Server Actions** | `getPracticeSessionsAction` | `modules/adaptive-practice/actions.ts` | 获取练习列表权限ADAPTIVE_PRACTICE_READ |
| **Server Actions** | `getPracticeSessionDetailAction` | `modules/adaptive-practice/actions.ts` | 获取练习详情权限ADAPTIVE_PRACTICE_READ |
| **Server Actions** | `getPracticeStatsAction` | `modules/adaptive-practice/actions.ts` | 获取练习统计权限ADAPTIVE_PRACTICE_READ |
| **Server Actions** | `createPracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 创建练习会话权限ADAPTIVE_PRACTICE_MANAGE |
| **Server Actions** | `submitPracticeAnswerAction` | `modules/adaptive-practice/actions.ts` | 提交答案+自动判分权限ADAPTIVE_PRACTICE_MANAGE |
| **Server Actions** | `completePracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 完成练习权限ADAPTIVE_PRACTICE_MANAGE |
| **Server Actions** | `abandonPracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 放弃练习权限ADAPTIVE_PRACTICE_MANAGE |
| **Data Access** | `getPracticeSessions` / `getPracticeSessionById` | `data-access.ts` | 学生端 CRUD |
| **Data Access** | `createPracticeSession` | `data-access.ts` | 创建会话(事务:会话+答题记录) |
| **Data Access** | `submitPracticeAnswer` | `data-access.ts` | 提交答案+自动判分 |
| **Data Access** | `autoGradeAnswer` | `data-access.ts` | 自动判分逻辑(选择题/判断题自动,填空题返回 null |
| **Strategy** | `selectQuestionsForPractice` | `data-access-strategy.ts` | 出题策略主入口 |
| **Strategy** | `selectForErrorVariant` | `data-access-strategy.ts` | 错题变式策略 |
| **Strategy** | `selectForKnowledgePoint` | `data-access-strategy.ts` | 知识点专项策略 |
| **Strategy** | `selectForWeakChapter` | `data-access-strategy.ts` | 薄弱章节策略 |
| **Strategy** | `selectForAiRecommended` | `data-access-strategy.ts` | AI 推荐策略 |
| **Analytics** | `getClassPracticeStats` | `data-access-analytics.ts` | 班级练习统计 |
| **Analytics** | `getClassStudentPracticeSummaries` | `data-access-analytics.ts` | 班级学生练习摘要 |
| **Analytics** | `getGradePracticeStats` | `data-access-analytics.ts` | 年级练习统计 |
| **Analytics** | `getPracticeTypeBreakdown` | `data-access-analytics.ts` | 练习类型分布 |
| **Analytics** | `getStudentsWithoutPractice` | `data-access-analytics.ts` | 识别未参与练习学生 |
| **Analytics** | `getTeacherClassPracticeOverviews` | `data-access-analytics.ts` | 教师所教班级练习概览 |
| **Analytics** | `getClassKnowledgePointWeakness` | `data-access-analytics.ts` | 班级知识点薄弱度(基于练习答题) |
| **Analytics** | `getGradeClassPracticeComparison` | `data-access-analytics.ts` | 年级各班级练习对比 |
| **Analytics** | `getClassLearningProfile` | `data-access-analytics.ts` | 班级综合学习画像(跨模块整合) |
| **Analytics** | `getGradePracticeOverview` | `data-access-analytics.ts` | 年级综合练习统计 |
| **Component** | `PracticeStarter` | `components/practice-starter.tsx` | 练习发起器四种模式选择V4 重构:通过 `usePracticeService()` 调用 actionsa11y 增强,原生 input → shadcn Checkboxreason 改为枚举值) |
| **Component** | `PracticeStarterWithNav` | `components/practice-starter-with-nav.tsx` | V4 新增:客户端路由导航包装器,注入 `onSessionCreated` 回调,使 Server Component 页面可直接渲染 PracticeStarter |
| **Component** | `PracticeSessionView` | `components/practice-session-view.tsx` | 答题界面(逐题作答+自动判分V4 重构:通过 `usePracticeService()` 调用 actions埋点接入 `usePracticeAnalytics()``onAbandoned` 回调注入路由跳转) |
| **Component** | `PracticeHistory` | `components/practice-history.tsx` | 练习历史列表V4新增 `routePrefix?` 可选 prop未传时渲染只读卡片供家长端复用 |
| **Component** | `PracticeStatsCards` | `components/practice-stats-cards.tsx` | 学生端统计卡片 |
| **Component** | `PracticeOverviewStatsCards` | `components/practice-overview-stats-cards.tsx` | 教师/年级端统计卡片 — 新增 |
| **Component** | `ClassPracticeComparisonTable` | `components/class-practice-comparison-table.tsx` | 班级练习对比表格 — 新增 |
| **Component** | `PracticeTypeBreakdownChart` | `components/practice-type-breakdown-chart.tsx` | 练习类型分布柱状图 — 新增 |
| **Component** | `ClassKnowledgePointWeaknessChart` | `components/class-knowledge-point-weakness-chart.tsx` | 知识点薄弱度柱状图 — 新增 |
| **Component** | `StudentPracticeRankingTable` | `components/student-practice-ranking-table.tsx` | 学生练习排名表格 — 新增 |
| **Component** | `InactiveStudentsAlert` | `components/inactive-students-alert.tsx` | 未参与练习学生提醒 — 新增 |
| **Service (DI)** | `PracticeService` / `PracticeServiceProvider` / `usePracticeService` / `usePracticeAnalytics` | `services/practice-service.tsx` | V4 新增:依赖注入基础设施(接口 + 默认实现代理 actions + Provider + hooks参考 dashboard-service.tsx 模式;未包裹 Provider 时回退默认实现 |
| **Lib** | `grading` / `source-meta` / `type-guards` | `lib/grading.ts` / `lib/source-meta.ts` / `lib/type-guards.ts` | V4 提取纯函数库判分逻辑、sourceMeta 类型守卫、PracticeType 类型守卫),便于单测 |
**集成点**
| 业务模块 | 集成组件 | 页面 | 说明 |
|---------|---------|------|------|
| error-book | `createPracticeSessionAction` | `student/error-book` 详情弹窗 | 错题详情页"发起变式练习"按钮(通过 `onStartVariantPractice` props 注入,已解耦) |
| student | `PracticeStarterWithNav` / `PracticeSessionView` | `student/practice` / `student/practice/[sessionId]` | 学生端练习入口与答题V4通过 `PracticeServiceProvider` 注入 service |
| teacher | `PracticeOverviewStatsCards` 等 | `teacher/practice` | 教师端专项练习分析V4班级筛选改用 `shared/components/class-filter`,移除对 error-book 的跨模块依赖) |
| management | `PracticeOverviewStatsCards` 等 | `management/grade/practice` | 年级主任宏观数据分析 |
| parent | `PracticeStatsCards` / `PracticeHistory``routePrefix=undefined` | `parent/practice` | V4 新增2026-06-25 P0-修复-11家长查看子女练习统计与历史只读视图PracticeServiceProvider 注入 |
**依赖关系**
- `modules/adaptive-practice` → `shared/db`schema: practiceSessions/practiceAnswers/questions/knowledgePoints 等)
- `modules/adaptive-practice` → `shared/lib/auth-guard`(权限校验)
- `modules/adaptive-practice` → `shared/types/permissions`(权限常量)
- `modules/adaptive-practice` → `shared/types/action-state`(返回值类型)
- `modules/adaptive-practice` → `modules/classes/data-access`getActiveStudentIdsByClassId/getClassNameById/getClassesByGradeId/getClassIdsByGradeIds/getStudentIdsByClassIds
- `modules/adaptive-practice` → `modules/users/data-access`getUserIdsByGradeId/getUserNamesByIds
- `modules/adaptive-practice` → `modules/questions/data-access`(题目查询,出题策略使用)
- `modules/adaptive-practice` → `modules/error-book/data-access`(错题数据,错题变式策略使用)
- `app/(dashboard)/student/practice` → `modules/adaptive-practice`学生端页面V4通过 `PracticeServiceProvider` 注入 PracticeService
- `app/(dashboard)/teacher/practice` → `modules/adaptive-practice`(教师端分析页面)— 新增V4班级筛选改用 `shared/components/class-filter`(通用组件,`ClassFilterItem` 接口 + 预翻译文本 props**不再依赖** `modules/error-book/components/class-filter`
- `app/(dashboard)/management/grade/practice` → `modules/adaptive-practice`(年级主任分析页面)— 新增
- `app/(dashboard)/parent/practice` → `modules/adaptive-practice`(家长端查看子女练习)— V4 新增2026-06-25 P0-修复-11
- `modules/error-book/components/error-book-detail-dialog` → `modules/adaptive-practice/actions`(变式练习入口,通过 `onStartVariantPractice` props 注入,已解耦)
**权限点**
- `ADAPTIVE_PRACTICE_READ`查看练习数据admin/teacher/student/parent/grade_head/teaching_head 均有)
- `ADAPTIVE_PRACTICE_MANAGE`:创建/提交/完成/放弃练习(仅 student 有)
**自动判分逻辑**
- `single_choice`:学生答案 ID 与正确答案 ID 完全匹配
- `multiple_choice`:学生答案 ID 集合与正确答案 ID 集合完全匹配(集合比对)
- `judgment`:学生布尔答案与正确布尔答案一致
- `text`(填空题):不自动判分,返回 `null`,待教师批阅
**出题策略**
- `error_variant`:按难度升序选取原题(确保 AI 不可用时也能练习)
- `knowledge_point`:按知识点+难度筛选,随机抽取
- `weak_chapter`:排除已答题目,避免重复
- `ai_recommended`:从 AI 推荐知识点抽题
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `modules/adaptive-practice/types.ts` | ~143 | 类型定义PracticeType/PracticeSourceMeta 联合类型等V4WeakChapterSourceMeta.chapterId 改为可选) |
| `modules/adaptive-practice/schema.ts` | ~75 | Zod 验证 schemaV4discriminatedUnion |
| `modules/adaptive-practice/data-access.ts` | ~620 | CRUD 操作+自动判分逻辑V4类型守卫替换 as、submitPracticeAnswer 事务化、completePracticeSession 校验答题完整性、抛 PracticeError |
| `modules/adaptive-practice/data-access-strategy.ts` | ~343 | 四种出题策略实现V4移除 as WeakChapterSourceMeta 断言) |
| `modules/adaptive-practice/data-access-analytics.ts` | ~634 | 教师/年级宏观数据分析查询V4N+1 优化,单 SQL GROUP BY |
| `modules/adaptive-practice/actions.ts` | ~267 | 7 个 Server ActionsV4i18n 错误码返回 + dataScope 下 studentId 归属校验) |
| `modules/adaptive-practice/lib/grading.ts` | ~40 | V4 新增:纯函数判分逻辑 |
| `modules/adaptive-practice/lib/source-meta.ts` | ~60 | V4 新增sourceMeta 类型守卫 + parseSourceMeta |
| `modules/adaptive-practice/lib/type-guards.ts` | ~30 | V4 新增isPracticeType / asPracticeType / asPracticeSourceMeta |
| `modules/adaptive-practice/services/practice-service.tsx` | ~150 | V4 新增依赖注入基础设施PracticeService 接口 + 默认实现 + Provider + hooks + PracticeAnalytics 接口) |
| `modules/adaptive-practice/components/practice-starter.tsx` | ~330 | 练习发起器V4usePracticeService 注入 + Checkbox + a11y + 枚举 reason + onSessionCreated 回调) |
| `modules/adaptive-practice/components/practice-starter-with-nav.tsx` | ~25 | V4 新增:路由导航包装器(注入 routePrefix 跳转) |
| `modules/adaptive-practice/components/practice-session-view.tsx` | ~550 | 答题界面V4拆分为 5 个子组件 + usePracticeService 注入 + 埋点) |
| `modules/adaptive-practice/components/practice-history.tsx` | ~110 | 练习历史列表V4新增 routePrefix 可选 prop |
| `modules/adaptive-practice/components/practice-stats-cards.tsx` | ~73 | 学生端统计卡片 |
| `modules/adaptive-practice/components/practice-overview-stats-cards.tsx` | ~99 | 教师/年级端统计卡片 — 新增 |
| `modules/adaptive-practice/components/class-practice-comparison-table.tsx` | ~88 | 班级练习对比表格 — 新增 |
| `modules/adaptive-practice/components/practice-type-breakdown-chart.tsx` | ~123 | 练习类型分布柱状图 — 新增 |
| `modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx` | ~144 | 知识点薄弱度柱状图 — 新增 |
| `modules/adaptive-practice/components/student-practice-ranking-table.tsx` | ~112 | 学生练习排名表格 — 新增 |
| `modules/adaptive-practice/components/inactive-students-alert.tsx` | ~64 | 未参与练习学生提醒 — 新增 |
| `app/(dashboard)/student/practice/page.tsx` | - | 学生端练习列表页V4注入 PracticeServiceProvider + routePrefix |
| `app/(dashboard)/student/practice/[sessionId]/page.tsx` | - | 学生端答题页V4注入 PracticeServiceProvider |
| `app/(dashboard)/teacher/practice/page.tsx` | ~250 | 教师端专项练习分析页V4改用 shared/components/class-filter移除 error-book 依赖) |
| `app/(dashboard)/teacher/practice/loading.tsx` | - | 教师端加载骨架 |
| `app/(dashboard)/teacher/practice/error.tsx` | - | 教师端错误边界V4i18n 化) |
| `app/(dashboard)/management/grade/practice/page.tsx` | ~140 | 年级主任宏观数据分析页 |
| `app/(dashboard)/management/grade/practice/loading.tsx` | - | 年级端加载骨架 |
| `app/(dashboard)/management/grade/practice/error.tsx` | - | 年级端错误边界V4i18n 化) |
| `app/(dashboard)/parent/practice/page.tsx` | ~135 | V4 新增2026-06-25家长查看子女练习统计与历史 |
| `app/(dashboard)/parent/practice/loading.tsx` | - | V4 新增:家长端加载骨架 |
| `app/(dashboard)/parent/practice/error.tsx` | - | V4 新增家长端错误边界i18n |
| `shared/components/class-filter.tsx` | ~50 | V4 新增:通用班级筛选器(`ClassFilterItem` 接口 + 预翻译文本 props消除 teacher/practice 对 error-book 的跨模块依赖 |
**i18n**
- 翻译文件:`shared/i18n/messages/{locale}/practice.json`
- 命名空间:`practice`
- 键:
- `starter.*`发起器V4a11y aria-label
- `session.*`、`result.*`、`history.*`、`stats.*`、`types.*`、`status.*`
- `teacher.*`(含 overview/classComparison/typeBreakdown/knowledgePointWeakness/studentRanking/inactiveStudents
- `grade.*`(含 overview/classComparison
- V4 新增:`parent.*`家长端title/description/noChild/noChildDescription/stats/history/noChildSelected
- V4 新增:`errors.*`结构化错误码session_not_found/session_ended/answer_not_found/answer_already_submitted/question_not_found/session_not_complete/no_questions_found/forbidden_scope/invalid_input/validation_error/invalid_source_meta页面错误pageErrorPractice/pageErrorGrade/pageErrorParent + pageErrorTitle* + loadFailed/loadFailedDescription/retry
- V4 新增:`messages.*`用户可见操作反馈sessionCreated/answerSubmitted/answerCorrect/answerIncorrect/answerSkipped/sessionCompleted/sessionAbandoned
- V4 新增:`reasons.*`AI 推荐理由枚举翻译student_initiated/teacher_assigned/parent_suggested
- V4 新增:`classFilter.*`班级筛选器徽章sessionCount plural 规则)
**数据库表**
- `practice_sessions`练习会话表id/studentId/subjectId/practiceType/sourceMeta/status/totalQuestions/answeredQuestions/correctCount/startedAt/completedAt
- `practice_answers`练习答题记录表id/sessionId/studentId/questionId/variantContent/isVariant/orderIndex/status/studentAnswer/isCorrect/score/maxScore
---
## 2.31 rbac角色与权限管理模块— ✅ 新增
**职责**角色与权限管理RBAC。提供角色 CRUD、角色启停、角色-权限分配、用户-角色分配、权限目录查询能力,支撑管理员对系统权限体系的动态化管理。
**架构定位**
- 位于 `modules/` 层,严格遵循三层架构
- 通过 `data-access.ts` 提供角色 CRUD通过 `data-access-assignments.ts` 提供用户-角色分配查询,通过 `lib/permission-catalog.ts` 提供权限元数据目录
- Server Actions 通过 `requirePermission()` 校验 `ROLE_CREATE` / `ROLE_READ` / `ROLE_UPDATE` / `ROLE_DELETE` / `ROLE_ASSIGN` / `PERMISSION_READ` 权限
- 与 `shared/types/permissions.ts` 协作:`Role` 类型从固定联合类型改为 `string`,支持动态角色;`BUILTIN_ROLES` 常量 + `isBuiltinRole()` 类型守卫区分内置角色与自定义角色
- 与 `shared/lib/permissions.ts` 协作:`resolvePermissions` 改为 async 从 DB 读取角色权限;`ROLE_PERMISSIONS` 重命名为 `ROLE_PERMISSIONS_SEED`(保留旧名 re-export 兼容)
**导出函数**
- Actions`createRoleAction` / `updateRoleAction` / `deleteRoleAction` / `toggleRoleEnabledAction` / `setRolePermissionsAction` / `assignUserRolesAction` / `getRoleDetailAction` / `isAdminRole`(✅ 每个 Action 调用 `requirePermission()` 进行权限校验;内置角色不可删除/改名的校验在 data-access 层P2-7 新增 `setRolePermissionsAction` / `assignUserRolesAction` 调用 `trackPermissionChange` 埋点)
- Data-access`data-access.ts``getRoles` / `getRoleById`(✅ P2-3 新增返回 `userCount` 字段)/ `getRoleByName` / `createRole` / `updateRole` / `deleteRole` / `setRoleEnabled` / `getRolePermissions` / `setRolePermissions` / `ADMIN_ROLE_NAME`
- Data-access`data-access-assignments.ts``getUserRoleNames` / `assignRolesToUser` / `getUserRoleAssignments`
- Lib`lib/permission-catalog.ts``PERMISSION_CATALOG`(权限目录常量,按模块分组)/ `getAllPermissionMetas`(获取所有权限元数据)/ `getPermissionMeta`(按权限点获取元数据)
- Lib`lib/track.ts``trackPermissionChange`(✅ P2-7 新增RBAC 关键操作埋点接口no-op 实现,预留未来 analytics 接入)/ `TrackPermissionChangeParams` 接口
**依赖关系**
- 依赖:`shared/db`schema: roles/usersToRoles/rolePermissions、`shared/types/permissions`(权限常量与 Role 类型)、`shared/lib/auth-guard`(权限校验)、`shared/lib/audit-logger`(操作日志)、`shared/lib/change-logger`(数据变更日志)
- 被依赖:`app/(dashboard)/admin/roles`、`app/(dashboard)/admin/permissions`、`app/(dashboard)/admin/users`(用户-角色分配)、`modules/users`(通过 UserRoleAssignDialog 组件)
**已知问题**
- ✅ 职责清晰data-access 按职责拆分为 `data-access.ts`(角色 CRUD与 `data-access-assignments.ts`(用户-角色分配)
- ✅ 内置角色保护:`ADMIN_ROLE_NAME` 常量标识管理员角色,`isSystem` 字段标记系统内置角色,禁止删除/改名
- ✅ 角色启停:`isEnabled` 字段控制角色启用/停用,停用角色不参与权限解析
**文件清单**
| 文件 | 职责 |
|------|------|
| `modules/rbac/actions.ts` | 8 个 Server Action含权限校验 + 审计日志 + 变更日志 + P2-7 埋点) |
| `modules/rbac/data-access.ts` | 角色 CRUD + 角色权限分配getRoles/getRoleById[含userCount]/createRole/updateRole/deleteRole/setRoleEnabled/getRolePermissions/setRolePermissions |
| `modules/rbac/data-access-assignments.ts` | 用户-角色分配getUserRoleNames/assignRolesToUser/getUserRoleAssignments |
| `modules/rbac/lib/permission-catalog.ts` | 权限目录常量 + 元数据查询PERMISSION_CATALOG/getAllPermissionMetas/getPermissionMeta |
| `modules/rbac/lib/track.ts` | ✅ P2-7 新增RBAC 关键操作埋点接口trackPermissionChangeno-op 实现) |
**组件清单**
| 组件 | 职责 |
|------|------|
| `components/role-list.tsx` | 角色列表(含启停切换、删除;✅ P2-5 新增 caption/aria-label |
| `components/role-form-dialog.tsx` | 角色创建/编辑表单对话框 |
| `components/role-permission-matrix.tsx` | 角色-权限分配矩阵(按模块分组的权限勾选;✅ P2-1 搜索+折叠P2-3 userCount 影响提示P2-5 fieldset/legend |
| `components/permission-catalog-view.tsx` | 权限目录浏览视图(只读,展示所有权限点元数据;✅ P2-5 aria-label |
| `components/user-role-assign-dialog.tsx` | 用户-角色分配对话框(导出 `UserRoleAssignDialog` + `AssignableRole` 类型;✅ P2-5 aria-label |
| `components/role-management-view.tsx` | 角色管理主视图(列表 + 创建按钮) |
| `components/role-detail-edit-button.tsx` | 角色详情页编辑按钮 |
**路由清单**
| 路由 | 文件 | 说明 |
|------|------|------|
| `/admin/roles` | `app/(dashboard)/admin/roles/page.tsx` | 角色管理列表页权限ROLE_READ |
| `/admin/roles/[id]` | `app/(dashboard)/admin/roles/[id]/page.tsx` | 角色详情页权限ROLE_READ含权限矩阵 |
| `/admin/permissions` | `app/(dashboard)/admin/permissions/page.tsx` | 权限目录浏览页权限PERMISSION_READ |
**权限点**
- `ROLE_CREATE`:创建角色(`role:create`
- `ROLE_READ`:查看角色(`role:read`
- `ROLE_UPDATE`:更新角色(`role:update`
- `ROLE_DELETE`:删除角色(`role:delete`
- `ROLE_ASSIGN`:分配用户角色(`role:assign`
- `PERMISSION_READ`:查看权限目录(`permission:read`
**数据库表**
- `roles`(✅ 字段更新id / name / description / createdAt / updatedAt / **isSystem**boolean DEFAULT FALSE标记内置角色/ **isEnabled**boolean DEFAULT TRUE角色启停开关
- 迁移文件:`drizzle/0011_rbac_role_flags_and_seed.sql`
**i18n**
- 翻译文件:`shared/i18n/messages/{zh-CN,en}/rbac.json`
- 命名空间:`rbac`
---
## 2.32 invitation-codes邀请码模块— ✅ audit-P2-3 新增
**职责**:批量生成邀请码替代开放注册,支持预分配角色/班级/限定邮箱/过期时间;注册流程集成邀请码校验与消费(乐观锁),管理员可生成、列表、删除邀请码。
**导出函数**
- Actions`actions.ts`
- `generateInvitationCodesAction(input)` — 批量生成邀请码USER_MANAGE 权限Zod 校验 1-100 张/批trackEvent `invitation_codes.generated`,返回明文 codes 仅此一次)
- `listInvitationCodesAction()` — 列出邀请码USER_MANAGE 权限,含已使用+未使用,按 createdAt desc最多 200 条)
- `deleteInvitationCodeAction(codeId)` — 删除邀请码USER_MANAGE 权限,仅允许删除未使用,已使用保留作审计记录)
- `validateInvitationCodeAction(code, email)` — 校验邀请码(公开接口,注册流程预检用,不返回明文)
- Data-access`data-access.ts`
- `generateInvitationCodes(input, createdBy)` — 批量插入 + 查询回记录
- `listInvitationCodes(limit, includeUsed)` — 列表查询
- `validateInvitationCode(code, email)` — 校验(存在/已用/过期/邮箱匹配 4 项)
- `consumeInvitationCode(input)` — 乐观锁消费(`UPDATE ... WHERE usedBy IS NULL` + `affectedRows` 检查)
- `deleteInvitationCode(codeId)` — 删除(仅未使用)
- `purgeExpiredInvitationCodes()` — 清理已过期且未使用cron 预留)
- Lib`lib/code-generator.ts`
- `generateInvitationCode()` — 8 字符邀请码(`node:crypto.randomBytes` + 安全字母表 `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`,剔除 I/O/0/1
- `generateUniqueInvitationCodes(count)` — 批量生成Set 去重 + 最多 3× 重试)
- `generateBatchId()` — 批次 ID`batch_<timestamp36>_<random6hex>`
- Schema`schema.ts``GenerateInvitationCodesSchema`count 1-100、role enum、可选 email/classId/expiresAt/notes、`InvitationCodeFieldSchema`(嵌入 RegisterSchema8-64 字符大写字母数字)、`INVITATION_ROLE_VALUES`
- Types`types.ts``InvitationRole` / `InvitationCodeRecord` / `GenerateInvitationCodesInput` / `GenerateInvitationCodesResult` / `ValidateInvitationCodeResult` / `ConsumeInvitationCodeInput`
- Components
- `InvitationCodesView` — 管理视图(统计卡片 + 表格 + 删除确认 + 生成对话框入口,接受 `initialCodes` + `now` props 由 Server Component 注入避免 `Date.now()` 在 render 调用)
- `GenerateInvitationCodesDialog` — 生成对话框form/result 两态切换result 视图仅此一次展示明文供复制)
**依赖关系**
- 依赖 `shared/db`drizzle 客户端)、`shared/db/schema`invitationCodes 表)、`shared/lib/auth-guard`requirePermission、`shared/lib/track-event`、`shared/types/action-state`、`shared/types/permissions`
- 被依赖:`modules/auth/actions.ts`registerAction 调用 validateInvitationCode + consumeInvitationCode、`app/(dashboard)/admin/invitation-codes/page.tsx`Server Component 调用 listInvitationCodes + 渲染 InvitationCodesView
**已知问题**
- ✅ audit-P2-3 已实施:~~系统开放注册缺少预分配角色/班级的机制~~ 完整邀请码模块 + 注册流程集成 + admin 管理页
**数据库表**
- `invitation_codes`(✅ 新增id / code(varchar 64, unique) / email(varchar 255, nullable) / role(varchar 50) / class_id(varchar 128, nullable) / notes(varchar 500, nullable) / created_at(timestamp) / created_by(varchar 128, nullable) / expires_at(timestamp, nullable) / used_by(varchar 128, nullable) / used_at(timestamp, nullable)
- 索引:`invitation_codes_code_idx`(code) / `invitation_codes_email_idx`(email) / `invitation_codes_used_by_idx`(used_by)
- 迁移文件:`drizzle/0016_invitation_codes.sql`
**i18n**
- 翻译文件:`shared/i18n/messages/{zh-CN,en}/invitation-codes.json`
- 命名空间:`invitationCodes`
- 注册位置:`src/i18n/request.ts`Promise.all 数组 + messages 对象)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 135 | Server Actionsgenerate/list/delete/validateUSER_MANAGE 权限trackEvent 埋点) |
| `data-access.ts` | 247 | 数据访问层generate/list/validate/consume/delete/purge乐观锁消费模式 |
| `schema.ts` | 45 | Zod 验证GenerateInvitationCodesSchema + InvitationCodeFieldSchema + INVITATION_ROLE_VALUES |
| `types.ts` | 65 | 类型定义InvitationRole / InvitationCodeRecord / Generate/Validate/Consume 接口) |
| `lib/code-generator.ts` | 67 | 邀请码生成器8 字符安全字母表 + Set 去重 + 批次 ID |
| `components/invitation-codes-view.tsx` | 302 | 管理视图(统计卡片 + 表格 + 删除确认 + 生成对话框入口) |
| `components/generate-invitation-codes-dialog.tsx` | 387 | 生成对话框form/result 两态,结果可复制全部/单个) |
**路由**
- `/admin/invitation-codes` — 管理员邀请码管理页USER_MANAGE 权限Server Component → InvitationCodesView 客户端组件)
---
## 2.33 leave-requests在线请假模块— ✅ L-5 新增
**职责**:学生/家长代子女提交请假申请 → 教师/年级主任/教务主任审批 → 审批通过自动同步 excused 考勤记录(幂等)→ 申请人 pending 状态可撤销。
**状态机**`pending → approved/rejected``pending/approved → cancelled`(仅 requester 本人可撤销 pending
**导出函数**
- Actions`actions.ts`
- `createLeaveRequestAction(prevState, formData)` — 创建请假申请LEAVE_REQUEST_CREATE 权限Zod 校验;二次校验 requester-student 关系:家长代子女需在 `parentStudentRelations` 中、学生本人 `requesterId === studentId`;校验 classId 确为学生当前活跃班级;`hasOverlappingLeave` 防止日期重叠trackEvent `leave_request.created`
- `reviewLeaveRequestAction(prevState, formData)` — 审批请假申请LEAVE_REQUEST_REVIEW 权限pending → approved/rejectedrejected 必填 reviewComment审批通过后调用 `attendance/data-access.syncExcusedFromLeaveRequest` 同步考勤try/catch 不阻断;`markAttendanceSynced` 标志位防重复trackEvent `leave_request.reviewed`
- `cancelLeaveRequestAction(id)` — 撤销请假LEAVE_REQUEST_CREATE 权限;仅 requester 本人的 pending 状态可撤销trackEvent `leave_request.cancelled`
- Data-access`data-access.ts`
- `getLeaveRequests(params)` — 分页查询(按 DataScope 过滤all/class_taught/children/class_members/ownedclass_members 由 currentUserId 过滤本人提交;批量查询学生/班级/提交人/审批人名称避免 N+1
- `getLeaveRequest(id, scope, currentUserId?)` — 单条详情(按 DataScope 校验访问权限)
- `hasOverlappingLeave(studentId, startDate, endDate, excludeId?)` — 日期重叠校验SQL `startDate <= endDate AND endDate >= startDate`,仅查 pending/approved
- `createLeaveRequest(data, requesterId)` — 创建(`new Date(startDate)` 转换字符串为 Date状态默认 pending
- `reviewLeaveRequest(id, review, reviewerId)` — 审批Drizzle `db.update()` 返回 ResultSetHeader需重新 `db.select()` 获取更新行)
- `cancelLeaveRequest(id, requesterId)` — 撤销(仅 requester 本人的 pending 状态)
- `markAttendanceSynced(id)` — 标记 attendanceSynced=true防止重复触发同步
- `getLeaveRequestsByIds(ids)` — 按 ID 批量查询(按 startDate 升序)
- `countPendingForClasses(classIds)` — 教师审批待办计数
- Schema`schema.ts``CreateLeaveRequestSchema`(含 `.refine()` 跨字段校验 endDate >= startDate、`ReviewLeaveRequestSchema`rejected 时 reviewComment 必填)
- Types`types.ts``LeaveType` / `LeaveStatus` / `LeaveRequest` / `LeaveRequestListItem` / `LeaveQueryParams` / `PaginatedLeaveResult` / `LeaveReviewInput`
- Components
- `LeaveRequestForm` — 请假表单(`useActionState` + `useFormStatus`);家长模式 `childOptions` 下拉选择子女,学生模式 hidden input 写入 studentId/classId。**注意**prop 名为 `childOptions` 而非 `children`,避免与 React 特殊 children prop 冲突触发 `react/no-children-prop` ESLint 规则
- `LeaveRequestList` — 只读列表展示STATUS_STYLES + STATUS_ICONS 状态徽章)
- `LeaveReviewDialog` — 审批对话框(批准/拒绝按钮切换,拒绝必填 reviewComment
- `LeaveReviewList` — 教师审批列表pending 项显示审批按钮,打开 LeaveReviewDialog
**依赖关系**
- 依赖:`shared/*`db/auth-guard/track-event/types/permissions、`@/auth`、`classes``getStudentActiveClass` 校验当前班级、`getClassNamesByIds` 列表展示)、`users``getUserNamesByIds` 列表展示学生/提交人/审批人姓名)、`parent``getParentStudentMapByStudentIds` 校验家长-子女关系)、`attendance``syncExcusedFromLeaveRequest` 审批通过后同步考勤)、`next-intl`
- 被依赖:`app/(dashboard)/{parent,student,teacher}/leave/page.tsx`Server Component 直接调用 data-access 与渲染组件)
**数据库表**
- `leave_requests`(✅ L-5 新增id / student_id / requester_id / class_id / leave_type(enum: sick/personal/family/other) / start_date(date) / end_date(date) / reason(text) / status(enum: pending/approved/rejected/cancelled, default pending) / reviewer_id(varchar, nullable) / review_comment(text, nullable) / reviewed_at(timestamp, nullable) / attendance_synced(boolean, default false) / created_at / updated_at
- 索引:`leave_requests_student_idx` / `leave_requests_requester_idx` / `leave_requests_class_idx` / `leave_requests_status_idx` / `leave_requests_reviewer_idx` / `leave_requests_class_status_idx`(复合)/ `leave_requests_student_status_idx`(复合)
- 外键:`lr_c_fk`(class_id → classes.id ON DELETE CASCADE) / `lr_s_fk`(student_id → users.id ON DELETE CASCADE) / `lr_rq_fk`(requester_id → users.id ON DELETE CASCADE) / `lr_rv_fk`(reviewer_id → users.id ON DELETE SET NULL)
- 迁移文件:`drizzle/0020_leave_requests.sql`(含 INSERT IGNORE role_permissions 为 admin/teacher/parent/student/grade_head/teaching_head 注入权限)
**权限**
- `LEAVE_REQUEST_CREATE`leave_request:create— admin/parent/student
- `LEAVE_REQUEST_READ`leave_request:read— admin/teacher/parent/student/grade_head/teaching_head
- `LEAVE_REQUEST_REVIEW`leave_request:review— admin/teacher/grade_head/teaching_head
**i18n**
- 翻译文件:`shared/i18n/messages/{zh-CN,en}/leave.json`
- 命名空间:`leave`
- 注册位置:`src/i18n/request.ts`Promise.all 数组 + messages 对象)
**埋点事件**
- `leave_request.created` / `leave_request.reviewed` / `leave_request.cancelled`(在 `shared/lib/track-event.ts` 的 EventName 联合类型中扩展)
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~180 | 3 个 Server Actioncreate/review/cancel权限校验 + Zod + 跨模块校验 + trackEvent |
| `data-access.ts` | ~330 | 数据访问层CRUD + 分页 + DataScope 过滤 + 日期重叠校验 + 批量名称查询) |
| `schema.ts` | ~50 | Zod 校验CreateLeaveRequestSchema + ReviewLeaveRequestSchema |
| `types.ts` | ~70 | 类型定义LeaveType/Status/Request/ListItem/Query/Result/ReviewInput |
| `components/leave-request-form.tsx` | ~190 | 请假表单useActionState + 家长/学生双模式) |
| `components/leave-request-list.tsx` | ~120 | 只读列表 + 状态徽章 |
| `components/leave-review-dialog.tsx` | ~150 | 审批对话框 |
| `components/leave-review-list.tsx` | ~100 | 教师审批列表 |
**路由**
- `/parent/leave` — 家长请假页LEAVE_REQUEST_CREATE + LEAVE_REQUEST_READDataScope.children表单 childOptions 模式 + 列表)
- `/student/leave` — 学生请假页LEAVE_REQUEST_CREATE + LEAVE_REQUEST_READDataScope.class_members 由 currentUserId 过滤;表单 defaultStudentId/classId 模式 + 列表)
- `/teacher/leave` — 教师审批页LEAVE_REQUEST_REVIEWDataScope.class_taughtLeaveReviewList + LeaveReviewDialog
**设计要点**
- ✅ **幂等考勤同步**`syncExcusedFromLeaveRequest` 遍历日期区间,仅插入不存在的 excused 记录,避免重复同步;`attendanceSynced` 标志位防止审批重复触发
- ✅ **跨模块联动容错**:审批通过后调用考勤同步使用 try/catch 包裹,同步失败不阻断审批结果(审批已成功,考勤可由管理员后续手动补录)
- ✅ **日期重叠校验**`hasOverlappingLeave` 使用 SQL 条件检测区间重叠,防止重复请假
- ✅ **DataScope 行级过滤**`buildScopeFilter` 支持 5 种 scopeall/class_taught/children/class_members/ownedclass_members 由 currentUserId 过滤本人提交
- ✅ **Drizzle update 返回值处理**`db.update()` 返回 ResultSetHeader 而非行数据,需重新 `db.select()` 查询更新后的行
- ✅ **React children prop 冲突规避**LeaveRequestForm 的 prop 名为 `childOptions` 而非 `children`,避免触发 `react/no-children-prop` ESLint 规则
---
## 2.34 search全文检索模块— ✅ 2026-07-04 API 规范化重构新增
**职责**:封装跨 4 张表questions / textbooks / exams / announcements的全文检索数据访问层供 `app/api/search/route.ts` 调用。本次 API 规范化重构从 `app/api/search/route.ts` 下沉而来,消除 `app/` 层直接查询 DB 的架构违规违反「app → modules → shared」三层架构单向依赖
**导出函数**`data-access.ts`server-only
- `searchQuestions(q, limit?)` — 题库检索P3-6 改用 `MATCH(questions.contentText) AGAINST(? IN BOOLEAN MODE)` 全文检索(基于 `content_text` STORED 生成列的 FULLTEXT 索引);`toBooleanModeQuery` 工具函数将原始查询转为 `+word1* +word2*` 格式;返回 `SearchResultItem[]`href 指向 `/admin/questions?id=`
- `searchTextbooks(kw, limit?)` — 教材检索title/subject/publisher 三字段 OR 模糊匹配
- `searchExams(kw, limit?)` — 试卷检索title/description 模糊匹配
- `searchAnnouncements(kw, limit?)` — 公告检索:仅返回 `status=published` 的公告title/content 模糊匹配HTML 内容通过 `stripHtml` 提取纯文本摘要
- `DEFAULT_SEARCH_PAGE_SIZE`= 10— 默认分页大小常量
**类型定义**`types.ts`
- `SearchType = "all" | "question" | "textbook" | "exam" | "announcement"`
- `SearchResultItem` — `{ id, title, snippet, type, href, createdAt }`
- `SearchResponse` — `{ success, query, type, results, total, page, pageSize }`
- `isSearchType(v): v is SearchType` — 类型守卫
**设计要点**
- ✅ **容错降级**:每个查询函数 try/catch 包裹,单表查询失败返回空数组而非抛错,避免拖垮整体搜索
- ✅ **职责分离**:本模块仅负责 DB 查询;角色过滤(学生不能搜题目/考试)由路由层基于 `getAuthContext()` 处理
- ✅ **摘要生成**`extractTextFromJson`JSON content → string、`stripHtml`(去 HTML 标签)、`truncate`(截断+省略号)三个内部纯函数
- ✅ **`as` 断言清理**:所有 `as const` 与 `as SearchResultItem` 类型收窄均使用类型守卫或字面量标注,符合项目 TypeScript 严格模式
**依赖关系**
- 依赖:`shared/db`db 客户端)、`shared/db/schema`questions/textbooks/exams/announcements 表)、`drizzle-orm`and/or/eq/like/desc/sql
- 被依赖:`app/api/search/route.ts`
**数据库表**(只读):
- `questions`content JSON 字段、P3-6 新增 `content_text` STORED 生成列 `CAST(content AS CHAR)` 用于 FULLTEXT 索引、type、createdAt
- `textbooks`title、subject、grade、publisher、createdAt
- `exams`title、description、status、createdAt
- `announcements`title、content、type、status、createdAt
**文件清单**
| 文件 | 行数 | 职责 |
|------|------|------|
| `data-access.ts` | ~230 | 4 个 search 函数P3-6: searchQuestions 改 MATCH AGAINST+ 4 个内部纯函数extractTextFromJson/stripHtml/truncate/toBooleanModeQuery+ DEFAULT_SEARCH_PAGE_SIZE 常量 |
| `types.ts` | 31 | SearchType/SearchResultItem/SearchResponse 类型 + isSearchType 类型守卫 |
---
# 第三部分:已知架构问题和技术债
## 3.1 P0 严重问题(必须立即修复)
### P0-1文件超 1000 行硬上限3 个文件)
| 文件 | 行数 | 问题 | 拆分建议 |
|------|------|------|---------|
| `classes/data-access.ts` | ~~2104~~ → 548 | ~~混入 homework/scheduling/grades 逻辑~~ ✅ 已拆分 | 已拆为 5 个文件data-access.ts(548行) + data-access-stats.ts(531行) + data-access-schedule.ts(194行) + data-access-students.ts(244行) + data-access-admin.ts(406行),通过 re-export 保持向后兼容 |
| `homework/data-access.ts` | ~~1038~~ → 626 | ~~混入排名计算业务逻辑~~ ✅ 已拆分 | 已拆为 data-access.ts(626行2026-06-25 删除 re-export) + stats-service.ts(414行) + data-access-student.ts(293行) + data-access-scans.ts(44行),统计函数迁移至 stats-service.ts |
| `shared/db/schema.ts` | 1111 | 54 张表混合 | 按业务域拆分为 schema/auth.ts + schema/academic.ts + schema/exam.ts + ...,通过 index.ts 聚合 |
### P0-2shared/lib ↔ auth 循环依赖 ✅ 已修复
```
shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/*
```
**影响**shared 层无法独立测试/复用;架构上基础设施不应反向依赖应用层。
**修复方案**(已实施):
- ✅ 创建 `shared/lib/session.ts` 封装 session 获取(`getSession()`server-only
- ✅ `session.ts` 内部使用 `dynamic import("@/auth")` 打破模块级静态循环
- ✅ `audit-logger.ts` / `change-logger.ts` / `auth-guard.ts` 改为 `import { getSession } from "@/shared/lib/session"`,不再直接依赖 `@/auth`
- ✅ 运行时调用链保持不变,模块加载图无环
### P0-3dashboard 跨模块直接查询 11 张表 ✅ 已修复
`dashboard/data-access.ts` 的 `getAdminDashboardData` 原直查 sessions/users/usersToRoles/roles/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions。
**修复方案**(已实施):
- 各模块新增 dashboard stats 函数:
- `users/data-access.ts` → `getUsersDashboardStats()`userCount/activeSessionsCount/userRoleCounts/recentUsers
- `classes/data-access.ts` → `getClassesDashboardStats()`classCount
- `textbooks/data-access.ts` → `getTextbooksDashboardStats()`textbookCount/chapterCount
- `questions/data-access.ts` → `getQuestionsDashboardStats()`questionCount
- `exams/data-access.ts` → `getExamsDashboardStats(scope?)`examCount支持 scope 过滤)
- `homework/stats-service.ts` → `getHomeworkDashboardStats(scope?)`4 个计数,支持 scope 过滤)
- dashboard 改为并行调用:`Promise.all([getUsersDashboardStats(), getClassesDashboardStats(), ...])`
- 返回值结构保持不变,调用方无需修改
### P0-4messaging 绕过 notifications 直接写通知 ✅ 已修复
`messaging/actions.ts` 原直接调用 `createNotification` 写 `messageNotifications` 表,导致用户通知偏好失效、多渠道通知无效。`notifications/data-access.ts` 和 `in-app-channel.ts` 反向依赖 messaging 模块。
**修复方案**(已实施):
- 将 `messageNotifications` 表的 CRUD 函数(`createNotification` / `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount`)从 `messaging/data-access.ts` 迁移到 `notifications/data-access.ts`
- 将 `notificationPreferences` 表的 CRUD 函数(`getNotificationPreferences` / `upsertNotificationPreferences`)从 `messaging/notification-preferences.ts` 迁移到新文件 `notifications/preferences.ts`
- 将 `NotificationType` / `Notification` / `NotificationPreferences` / `UpdateNotificationPreferencesInput` / `CreateNotificationInput` / `GetNotificationsParams` / `PaginatedResult` 类型从 `messaging/types.ts` 迁移到 `notifications/types.ts`
- `notifications/channels/in-app-channel.ts` 改为静态导入本地 `createNotification`(不再动态 import messaging
- `notifications/dispatcher.ts` 改为从 `notifications/preferences.ts` 导入 `getNotificationPreferences`(不再通过 `getUserNotificationPreferences` 包装器反向依赖 messaging
- messaging 模块通过 re-export 保持向后兼容:`messaging/data-access.ts` re-export 通知 CRUD`messaging/notification-preferences.ts` 转为 re-export shim`messaging/types.ts` re-export 通知类型
- 依赖方向变为单向messaging → notificationsmessaging 调用 `sendNotification` dispatcher 发送通知)
### P0-5classSchedule 表三处写入口 ✅ 已修复
- ~~`classes/data-access.ts`createClassScheduleItem 等)~~
- ~~`scheduling/actions.ts`applyAutoScheduleAction 直接 transaction 写入)~~
- ~~`scheduling/data-access.ts`(间接)~~
**影响**:数据完整性高风险。
**修复方案**(已实施):
- 将 classSchedule 写函数(`createClassScheduleItem` / `updateClassScheduleItem` / `deleteClassScheduleItem`)从 `classes/data-access-schedule.ts` 迁移到新文件 `scheduling/data-access-class-schedule.ts`
- classes 模块仅保留 READ 函数(`getStudentSchedule` / `getClassSchedule`),不再有任何 classSchedule 写入口
- scheduling 模块通过 `classes/data-access.verifyTeacherOwnsClass` 跨模块校验教师班级归属(合理依赖)
- `classes/actions.ts` 改为从 `@/modules/scheduling/data-access-class-schedule` 导入写函数
- 类型 `CreateClassScheduleItemInput` / `UpdateClassScheduleItemInput` 从 `classes/types.ts` 迁移到 `scheduling/types.ts`
- 所有 classSchedule DB 写入统一由 scheduling 模块管理(`insertClassScheduleItem` / `updateClassScheduleItemById` / `deleteClassScheduleItemById` / `replaceClassSchedule`
### P0-6proctoring 死代码与重复实现
- `exam-mode-config.tsx` 未集成到考试表单(监考功能无法启用)
- ~~事件上报存在 Server Action 与 REST API 双通道重复~~ ✅ 已修复
**修复方案**(已实施):
- 将 `src/app/api/proctoring/event/route.ts` 移至 `deletes/api/proctoring/event/route.ts`,消除事件上报的 REST API 重复通道
- Server Action `recordProctoringEventAction``proctoring/actions.ts`)为唯一规范路径
- `exam-mode-config.tsx` 暂保留原位(集成到考试表单属于功能新增,不在本次修复范围)
### P0-7proxy.ts 路由权限跨角色访问漏洞 ✅ 已修复webapp-testing 发现)
**问题**`proxy.ts` 中 `/teacher` 和 `/parent` 路由前缀都使用 `EXAM_READ` 权限校验,但 `student` 和 `parent` 角色也拥有 `EXAM_READ` 权限,导致:
- 学生可访问 `/teacher/*` 所有页面
- 教师可访问 `/parent/*` 所有页面
**修复方案**(已实施):
- `/teacher` 改为 `Permissions.EXAM_CREATE`(仅 teacher/admin 拥有)
- `/parent` 改为 `Permissions.DASHBOARD_PARENT_READ`(仅 parent 拥有)
- 新增 `DASHBOARD_ROUTE_PERMISSIONS` 细粒度仪表盘权限表,覆盖各角色 dashboard 路由
- 跨角色访问测试验证teacher/student/parent 访问其他角色路由均被重定向回各自 dashboard
### P0-8list-pagination.tsx 客户端/服务端边界错误 ✅ 已修复webapp-testing 发现)
**问题**`shared/components/ui/list-pagination.tsx` 文件顶部声明 `"use client"`,但导出的 `computePagination()` 和 `paginate()` 纯工具函数被 4 个服务端组件页面直接调用:
- `teacher/attendance/page.tsx`
- `teacher/homework/assignments/page.tsx`
- `teacher/homework/submissions/page.tsx`
- `teacher/grades/page.tsx`
导致 Next.js 16 / React 19 抛出错误:`Attempted to call computePagination() from the server but computePagination is on the client`,页面渲染时触发 ErrorBoundary。
**修复方案**(已实施):
- 移除 `list-pagination.tsx` 的 `"use client"` 指令
- `ListPagination` 组件仅使用 `Link`、`Button` 和图标,无需客户端交互,可作为服务端组件渲染
- `computePagination` 和 `paginate` 恢复为服务端可调用的纯函数
---
## 3.2 P1 较严重问题(短期执行)
### P1-1跨模块直接 DB 查询普遍存在 ✅ 已修复
| 被访问表 | 访问次数 | 应归属模块 | 主要违规者 |
|---------|---------|-----------|-----------|
| `classes` | 8+ | classes | ✅ exams/homework/grades/dashboard 已改为通过 classes data-access |
| `classEnrollments` | 6+ | classes | ✅ homework/grades/attendance/users 已改为通过 classes data-access |
| `users` | 6+ | users | ✅ 多个模块已改为通过 users data-access |
| `subjects` | 6+ | school | ✅ exams/homework/questions/grades 已改为通过 school data-access |
| `exams` | 5+ | exams | ✅ homework/grades/dashboard/classes 已改为通过 exams data-access |
| `homeworkAssignments` | 5+ | homework | ✅ classes反向直查已改为通过 homework/data-access-classes |
**修复方案**(已实施):
- ✅ 各模块 data-access 暴露查询接口:
- `classes/data-access`~~`getClassGradeIdsByClassIds`~~ ✅ 已实现 / `getClassStudentsByClassId` / `getActiveClassStudents` / `getClassExists` / `getClassNameById` / `getClassNamesByIds` / `getActiveStudentIdsByClassId` / `getStudentActiveClassId` / `getClassesByGradeId` / `verifyTeacherOwnsClass` / `getTeacherIdForMutations`
- `exams/data-access`~~`getExamForHomeworkCreation`~~ ✅ 已实现(`getExamIdsByGradeIds` / `getExamSubjectIdMap` / `getExamWithQuestionsForHomework` / `getExamSubmissionWithAnswers` / `getExamForProctoringCrossModule` / `getExamSubmissionForProctoringCrossModule` / `getExamSubmissionsForExam` / `getExamTitleById`
- `school/data-access`~~`getSubjectOptions` / `getGradeOptions`~~ ✅ 已实现(含 `getSubjectNameMapByIds` 批量科目名称映射P1-1 新增供 homework/data-access-classes 调用)
- `users/data-access`~~`getUserNameByIds` / `getStudentInfo`~~ ✅ 已实现(`getUserNamesByIds` / `getUserWithRole` / `getUserBasicInfo` / `getUserIdsByGradeId` / `getCurrentStudentUser`
- `textbooks/data-access`~~`getKnowledgePointOptions`~~ ✅ 已实现
- `questions/data-access`~~`insertQuestionWithRelations`~~ ✅ 已通过 `createQuestionWithRelations` 供 exams 调用 / `getKnowledgePointsForQuestions`
- `homework/data-access-classes`:✅ 新增 7 个函数供 classes 模块跨模块调用
### P1-2actions 层混入数据访问逻辑 ✅ 已修复
**已完成修复**2026-06-17commit 84d66364 个模块的 actions 层 DB 操作全部下沉到 data-access
| 模块 | 问题 Action | 修复内容 |
|------|------------|---------|
| exams | `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` | ✅ 新增 7 个 data-access 函数getExamCreatorId/updateExamWithQuestions/deleteExamById/duplicateExam/getExamPreview/getExamSubjects/getExamGradesactions.ts 831→691 行data-access.ts 374→471 行。✅ 2026-06-24 进一步拆分actions.ts 1525→633 行(拆出 actions-helpers.ts/actions-rich-editor.ts/ai-pipeline/auto-mark.tsP0-4 修复:新增 updateExamWithRichEditorData 下沉富文本编辑器更新事务 |
| homework | `createHomeworkAssignmentAction`157 行)/ `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` | ✅ 新建 `data-access-write.ts`285 行10 个写函数actions.ts 387→239 行 |
| questions | `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction` | ✅ 新增 4 个 data-access 函数createQuestionWithRelations/updateQuestionById/deleteQuestionByIdRecursive/getKnowledgePointOptionsactions.ts 294→149 行data-access.ts 138→260 行;✅ 审计新增 3 个级联 data-access 函数getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter+ 3 个对应 Server Action + 批量删除/导入导出 data-access 与 Actionactions.ts 149→380+ 行data-access.ts 260→600+ 行 |
| announcements | 所有写操作 Action | ✅ 新增 5 个 data-access 函数insertAnnouncement/updateAnnouncementById/deleteAnnouncementById/publishAnnouncementById/archiveAnnouncementByIdactions.ts 242→197 行data-access.ts 120→171 行 |
**剩余未修复模块**(不在本次 P1-2 范围):
- ✅ users~~`updateUserProfileAction` 直接 db.update~~ 已下沉到 data-accessP1-1 修复)
- ✅ scheduling~~`applyAutoScheduleAction` / `autoScheduleAction` 直接 db.transaction + db.select~~ 已改为调用 `replaceClassSchedule` 统一写入口;`autoScheduleAction` 直查 users 表已改为通过 users data-accessP0-5 / P1-1 修复)
### P1-3auth.ts 混合 5 类职责 ✅ 已完成
`src/auth.ts` 原 293 行混合NextAuth 配置 + 密码安全 DB 操作 + 角色规范化 + IP 解析 + 回调函数。
**已完成拆分**auth.ts 现 193 行,仅保留 NextAuth 配置):
- ✅ 密码安全 DB 操作 → `shared/lib/password-security-service.ts`getOrCreatePasswordSecurity / recordFailedLogin / resetFailedLoginserver-only
- ✅ 角色规范化 → `shared/lib/role-utils.ts`normalizeRole / resolvePrimaryRole纯函数
- ✅ bcrypt 哈希规范化 → `shared/lib/bcrypt-utils.ts`normalizeBcryptHash纯函数
- ✅ IP 解析 → `shared/lib/http-utils.ts`resolveClientIpserver-only
**后续可选优化**audit-P1-3 已完成 ✅):
- ✅ ~~`authorize` 回调可进一步拆分为 `checkRateLimit` / `checkAccountLockout` / `verifyPassword` / `loadUserRoles`,使 auth.ts 降至 ≤150 行~~ 已抽取至 `modules/auth/services/login-service.ts` 的 `authenticateUser()` 函数7 步:输入解析/速率限制/用户查询/账户锁定/密码校验/2FA/角色加载auth.ts authorize 降至 3 行薄包装,整体文件 132 行
> **audit-P0 重构2026-06-25**:在 P1-3 拆分基础上进一步审计 auth.ts识别并修复以下问题
> - audit-P0-2~~`auth.ts` 跨模块依赖 `modules/settings/actions-security.verifyTwoFactorForLogin`~~ 改为同模块 `modules/auth/services/two-factor-service.ts` 编排2FA 仍通过 `modules/settings/data-access-two-factor` + `modules/settings/lib/totp` 获取数据(合理跨模块 data-access
> - audit-P0-5~~`auth.ts` 3 处 `as` 断言~~ 通过 `next-auth.d.ts` 扩展 `User` 接口(添加 `role?`/`roles?` 字段)+ `"token" in message` 类型收窄消除
> - audit-P0-6~~`proxy.ts` 与 `onboarding/data-access.resolveDefaultPathByRoles` 重复实现角色路径解析~~ 提取 `shared/lib/route-resolver.ts` 纯函数 `resolveDefaultPath(roles)`proxy.ts 与 onboarding/actions.ts 共用
> - audit-P0-7~~e2e smoke-auth 测试选择器与实际中文 UI 不符~~ 修正为 `getByLabel("姓名")` / `getByLabel("邮箱")` / `getByLabel("密码")` / `getByRole("button", { name: "创建账户" })`
### P1-4users/import-export.ts 四重职责 ✅ 已完成
`users/import-export.ts` 原 291 行混合:导入解析 + 导出 + 用户创建(含密码哈希)+ 班级注册(跨模块写 classEnrollments
**已完成拆分**import-export.ts 现 157 行,仅保留文件解析/生成):
- ✅ 用户创建(含密码哈希、角色分配)→ `user-service.ts``batchImportUsers`82 行server-only
- ✅ 班级注册 → `class-registration.ts``registerStudentByInvitationCode`21 行server-only
- ✅ `batchImportUsers` 不再直写 `classEnrollments`,改为调用 `classes/data-access.enrollStudentByInvitationCode`
- ✅ `import-export.ts` 通过 re-export `batchImportUsers` / `UserImportResult` 保持向后兼容(`actions.ts` 和 `app/api/export/route.ts` 无需修改)
### P1-5notifications 反向依赖 messaging ✅ 已完成
`notifications/data-access.ts` 和 `in-app-channel.ts` 原反向依赖 messaging 模块的偏好和 createNotification。
**已完成修复**(与 P0-4 一并解决):
- ✅ 将 `messageNotifications` 和 `notificationPreferences` 表所有权移交 notifications 模块
- ✅ `notifications/data-access.ts` 不再 import messaging 模块
- ✅ `notifications/channels/in-app-channel.ts` 改为静态导入本地 `createNotification`(不再动态 import messaging
- ✅ `notifications/dispatcher.ts` 改为从本地 `preferences.ts` 导入偏好函数
- ✅ messaging 模块通过 re-export 保持向后兼容
### P1-6三个 logger 重复实现 IP/Header 提取 ✅ 已修复
`audit-logger.ts` / `change-logger.ts` / `login-logger.ts` / `auth.ts` 四处重复实现 IP/User-Agent 提取逻辑,且实现略有差异。
**修复方案**(已实施):
- ✅ `shared/lib/http-utils.ts` 新增 `getUserAgent()` 函数(与已有 `resolveClientIp()` 配套)
- ✅ `audit-logger.ts` / `change-logger.ts` / `login-logger.ts` 改为从 `@/shared/lib/http-utils` 导入 `resolveClientIp` 和 `getUserAgent`,删除本地重复实现
- ✅ `auth.ts` 已在 P1-3 中改用 `resolveClientIp`
- ✅ 四处实现统一,消除不一致风险(`resolveClientIp` 取 `x-forwarded-for` 第一段,更准确)
---
## 3.3 P2 代码质量问题(机会修复)
| 序号 | 问题 | 模块 |
|------|------|------|
| P2-1 | `exams/ai-pipeline.ts` 857 行,混合 4 类职责 | exams |
| ~~P2-2~~ | ~~`exams/actions.ts` 832 行(超 800 建议)~~ ✅ 已修复P1-2 后降至 691 行) | exams |
| ~~P2-3~~ | ~~`shared/lib/ai.ts` 218 行,混合 5 类职责~~ ✅ 已修复P2-2 已拆分为 `ai/` 目录) | shared |
| ~~P2-4~~ | ~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ ✅ 已修复(迁移至 `modules/onboarding/`,改用独立路由 + Server Action + middleware 重定向) | shared/onboarding |
| P2-5 | `global-search.tsx` 业务类型硬编码在 shared | shared |
| ~~P2-6~~ | ~~`proxy.ts` 硬编码权限字符串,未复用 Permissions 常量~~ ✅ 已修复(改用 `Permissions` 常量) | proxy |
| ~~P2-7~~ | ~~`useA11yId` Hook 错放在 lib/ 而非 hooks/~~ ✅ 已修复(文件已不存在;`use-aria-live.ts` 已在 `hooks/` 目录) | shared |
| ~~P2-8~~ | ~~`schema.ts` 分节编号混乱section 12 出现在 14b 之后)~~ ✅ 已修复(重新编号为连续 1-24 | shared/db |
| P2-9 | `audit/actions.ts` Excel 导出逻辑内联 | audit |
| P2-10 | school 模块审计日志不一致(仅 school 实体记录) | school |
| ~~P2-11~~ | ~~`announcements` 死代码 `void wasPublished`~~ ✅ 已修复(代码中已不存在) | announcements |
| ~~P2-12~~ | ~~`announcements` 权限模式不一致requireAuth vs requirePermission~~ ✅ 已修复 | announcements |
| P2-13 | ~~`files` try-catch 吞错误~~ ✅ 已修复(所有 catch 块已添加 console.errorconditions 隐式 any[] 改为 SQL[] | files |
| ~~P2-14~~ | ~~`elective` runLottery 使用 Math.random~~ ✅ 已修复(改为 Fisher-Yates 无偏洗牌) | elective |
| ~~P2-15~~ | ~~`elective` selectCourse FCFS 并发超卖风险~~ ✅ 已修复db.transaction + FOR UPDATE 行锁) | elective |
| P2-16 | `diagnostic` 班级报告 studentId 字段复用 | diagnostic |
| ~~P2-17~~ | ~~`layout` 用权限反推角色~~ ✅ 已修复(`app-sidebar.tsx` 改用 `hasRole()` 判断角色N3 新增多角色切换机制:`SidebarContext.currentRole`/`setCurrentRole` + 侧边栏底部 Select 下拉) | layout |
| ~~P2-18~~ | ~~`scheduling/actions.ts` 末尾 re-export data-access~~ ✅ 已修复(移除 re-export4 个页面改为从 `data-access` 导入) | scheduling |
| P2-19 | `ExamAssembly` / `ExamPreviewQuestionEditor` 10 个 props | exams |
| P2-20 | ~~`homework/data-access.getDemoStudentUser` 使用 `auth()` 而非 auth-guard~~ ✅ 已修复(已迁移至 `users/data-access.getCurrentStudentUser`6 个 student 页面改用 users 模块;`elective` 页面改用 `getAuthContext()`homework 保留 re-export 向后兼容) | homework |
---
## 3.4 解耦优先级路线图
### 立即执行P0
1. ~~拆分 `classes/data-access.ts`2104 行 → 按职责拆 3-4 个文件)~~ ✅ 已完成(拆为 5 个文件data-access.ts 548行 + data-access-stats.ts 531行 + data-access-schedule.ts 194行 + data-access-students.ts 244行 + data-access-admin.ts 406行
2. ~~拆分 `homework/data-access.ts`1038 行 → 分离排名逻辑)~~ ✅ 已完成(拆为 data-access.ts 626行 + stats-service.ts 414行 + data-access-student.ts 293行 + data-access-scans.ts 44行
3. ~~修复 `shared/lib` ↔ `auth` 循环依赖~~ ✅ 已完成(新增 `shared/lib/session.ts` 单一入口3 个 shared/lib 文件改为通过 getSession 获取 sessiondynamic import 打破静态循环)
4. ~~dashboard 改为通过各模块 data-access 获取数据~~ ✅ 已完成P0-3 修复:并行调用各模块 dashboard stats 函数)
5. ~~messaging 写通知改为通过 notifications dispatcher~~ ✅ 已完成P0-4 / P1-5 修复:通知 CRUD 和偏好迁移至 notifications 模块messaging 通过 dispatcher 发送通知)
6. ~~统一 classSchedule 写入口到 scheduling 模块~~ ✅ 已完成P0-5 修复classSchedule 写函数从 classes/data-access-schedule.ts 迁移至 scheduling/data-access-class-schedule.tsclasses 模块仅保留读函数)
7. ~~集成 proctoring/exam-mode-config 到考试表单~~ 部分完成P0-6 修复:删除重复的 /api/proctoring/event REST 路由Server Action 为唯一规范路径exam-mode-config.tsx 集成属于功能新增,暂保留原位)
### 短期执行P1
8. ~~actions 层移除直接 DB 操作exams/homework/questions/announcements/users/scheduling~~ ✅ 已完成(全部 6 个模块均已修复)
9. ~~拆分 `auth.ts`~~ ✅ 已完成4 个辅助函数组迁移至 shared/libauth.ts 保留 NextAuth 配置)
10. ~~拆分 `users/import-export.ts`~~ ✅ 已完成(拆为 import-export.ts 157行 + user-service.ts 82行 + class-registration.ts 21行班级注册改为调用 classes/data-access
11. ~~消除 notifications → messaging 反向依赖~~ ✅ 已完成P0-4 / P1-5 修复:通知表所有权迁移至 notificationsin-app-channel 改为静态导入)
12. ~~提取 `shared/lib/http-utils.ts` 统一 IP 提取~~ ✅ 已完成(新增 `getUserAgent`,三个 logger 统一复用 `resolveClientIp` / `getUserAgent`
13. ~~各模块暴露跨模块查询接口(见 P1-1~~ ✅ 已完成(所有跨模块直查已改为通过对方 data-access 接口)
### 中期执行P2
14. ~~建立模块间数据访问规范(通过对方 data-access 或导出查询函数)~~ ✅ 已完成P1-1 修复)
15. ~~`schema.ts` 按业务域分节~~ ✅ 已完成P2-8 修复:重新编号为连续 1-24消除 8b/14b/乱序问题)
16. 拆分 `exams/ai-pipeline.ts`
17. ~~拆分 `shared/lib/ai.ts`~~ ✅ 已完成P2-2commit 6588f74拆分为 `ai/` 目录 6 个文件,原 ai.ts 保留为重导出)
18. shared 层业务逻辑下沉到 modules 层
19. 代码质量问题逐项修复(✅ 大部分已修复React.cache 包装、Promise.all 并行化、错误吞没清理、非空断言清理、函数返回类型补齐、重复代码提取、合并 filter 遍历、Set/Map 优化、P2-6 proxy.ts 权限常量复用、P2-11 announcements 死代码清理、P2-17 layout 角色判断、P2-18 scheduling re-export 移除)
---
## 3.5 标杆实践(建议推广)
| 实践 | 模块 | 说明 |
|------|------|------|
| 算法纯函数化 | `scheduling/auto-scheduler.ts` | 无 DB 依赖,可独立测试,应作为算法抽取模板 |
| stats 文件拆分 | `attendance/data-access-stats.ts` | 统计逻辑独立成文件classes 应效仿 |
| data-access 多文件拆分 | `grades/data-access*.ts` | 按职责拆分为 3 个文件CRUD/分析/排名) |
| actions 辅助函数 | `course-plans/actions.ts` | handleError / revalidatePlanPaths 消除重复 |
| actions 编排模式 | `textbooks/actions.ts` | 权限校验 → 调用 data-access → revalidatePath标杆 |
| DataScope 接入 | `attendance/actions.ts` | 6 种数据范围完整支持 |
| 权限统一接入 | school / attendance / course-plans | 全部 Action 使用 requirePermission |
| 跨模块解耦 | `grades` | 通过外键引用 exams/homework不直接访问其表 |
| 渠道抽象 | `notifications/channels/` | 接口 + 工厂 + Mock 实现 |
---
## 3.6 教师端全模块审计修复2026-06-23
> 本次审计修复覆盖教师端 13 个模块组、约 80+ 个文件,聚焦越权访问漏洞、错误处理一致性、类型安全与 React 性能。以下仅记录涉及架构图同步的变更(函数签名/导出/新增文件),其余纯实现修复(如 try/catch 补齐、useMemo/useCallback、key prop、as 断言清理)不在此列出。
### 3.6.1 新增共享文件 `src/shared/lib/action-utils.ts`
统一 Server Action 错误处理与客户端 Action 调用模式,避免内部错误消息暴露给客户端。
| 导出 | 类型 | 签名 | 用途 |
|------|------|------|------|
| `handleActionError` | function | `(e: unknown) => ActionState<never>` | 统一 Server Action catch 块错误处理PermissionDeniedError/BusinessError 返回其 message其他 Error 返回通用消息并 console.error 记录 |
| `safeActionCall` | function | `<T>(action: () => Promise<ActionState<T>>, options?: { onError?, onFinally? }) => Promise<ActionState<T> \| null>` | 客户端调用 Server Action 的 try/catch/finally 包装器,防止 UI 永久卡 loading |
| `safeJsonParse` | function | `<T>(json: string, errorMessage: string) => T` | 安全 JSON.parse失败抛 ValidationError替代裸 JSON.parse |
| `safeParseDate` | function | `(value: string, fieldName: string) => Date` | 校验日期字符串有效性,无效抛 ValidationError |
| `safeParseNumber` | function | `(value: string, fieldName: string) => number` | 校验数字字符串,无效抛 ValidationError |
| `escapeLikePattern` | function | `(input: string) => string` | 转义 SQL LIKE 通配符(% _ \\),防止用户输入干扰模糊查询 |
| `BusinessError` | class | `extends Error` | 已知业务错误基类message 可安全返回客户端(含可选 code |
| `NotFoundError` | class | `extends BusinessError` | 资源不存在错误(自动生成 `${resource} 不存在` 消息code: not_found |
| `ValidationError` | class | `extends BusinessError` | 输入校验错误code: validation_error |
### 3.6.2 grades 模块签名变更
**data-access 层新增 scope 参数**P3 修复:所有查询函数应用 `buildScopeClassFilter` 进行行级 scope 过滤):
| 函数 | 文件 | 旧签名 | 新签名 |
|------|------|--------|--------|
| `getGradeRecords` | `data-access.ts` | `(params: GradeQueryParams) => Promise<GradeRecordListItem[]>` | `(params: GradeQueryParams & { scope: DataScope; currentUserId?: string; limit?: number; offset?: number }) => Promise<PaginatedGradeRecords>` |
| `getClassGradeStats` | `data-access.ts` | `(classId, subjectId?, examId?) => Promise<GradeStats>` | `(classId, subjectId?, examId?, scope?: DataScope, currentUserId?: string) => Promise<GradeStats \| null>` |
| `getStudentGradeSummary` | `data-access.ts` | `(studentId) => Promise<StudentGradeSummary>` | `(studentId, scope?: DataScope) => Promise<StudentGradeSummary \| null>`class_taught scope 校验学生归属) |
| `getClassRanking` | `data-access.ts` | `(classId, subjectId?, examId?) => Promise<ClassRankingItem[]>` | `(classId, subjectId?, examId?, scope?: DataScope, currentUserId?: string) => Promise<ClassRankingItem[]>`(含并列排名处理) |
| `getClassStudentsForEntry` | `data-access.ts` | `(classId) => Promise<{id,name}[]>` | `(classId, scope?: DataScope) => Promise<{id,name,email}[]>`class_taught scope 校验 classId |
| `getRankingTrend` | `data-access-ranking.ts` | `(studentId, subjectId?, semester?) => Promise<RankingTrendResult \| null>` | `(studentId, subjectId?, semester?, scope?: DataScope) => Promise<RankingTrendResult \| null>`class_taught scope 校验) |
**lib 层签名变更**
- `buildScopeClassFilter(scope: DataScope, currentUserId?: string): SQL | null`(新增 `currentUserId` 参数,`class_members` scope 内置 `eq(gradeRecords.studentId, currentUserId)` 过滤P3-26从 `lib/grade-utils.ts` 迁移至 `lib/scope-filter.ts`
**新增导出**
- `assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 `actions.ts` 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因actions.ts 是 "use server" 文件要求所有 export 为 async而 assertClassInScope 是同步函数)
- `PaginatedGradeRecords` 接口(`data-access.ts``{ records: GradeRecordListItem[]; total: number }`,配合 DB 层分页)
### 3.6.3 homework 模块签名变更
**data-access 层新增 scope 参数**P3 修复:教师仅查看自己创建的作业,学生/家长仅查看相关作业):
| 函数 | 文件 | 新签名 |
|------|------|--------|
| `getHomeworkAssignments` | `data-access.ts` | `(params?: { creatorId?, ids?, classId?, scope?: DataScope }) => Promise<HomeworkAssignmentListItem[]>` |
| `getHomeworkAssignmentById` | `data-access.ts` | `(id: string, scope?: DataScope) => Promise<HomeworkAssignmentListItem \| null>` |
| `getHomeworkSubmissions` | `data-access.ts` | `(params?: { assignmentId?, classId?, creatorId?, scope?: DataScope }) => Promise<HomeworkSubmissionListItem[]>` |
| `getHomeworkAssignmentReviewList` | `data-access.ts` | `(params: { creatorId: string; scope?: DataScope }) => Promise<HomeworkReviewListItem[]>` |
### 3.6.4 lesson-preparation 模块变更
**新增 Zod schema**`schema.ts`
- `publishLessonPlanHomeworkSchema`发布作业输入校验planId/blockId 必填classIds 至少 1 个availableAt/dueAt 日期格式校验)
- 导出类型 `PublishLessonPlanHomeworkInput = z.infer<typeof publishLessonPlanHomeworkSchema>`
**data-access-versions.ts 事务变更**
- `revertToVersion` 包裹 `db.transaction` 确保原子性(回滚版本时同步更新 lessonPlans.content 与版本记录)
- `createLessonPlanVersion` 已使用 `db.transaction`(版本号自增 + 插入版本记录原子化)
### 3.6.5 其他修复(不涉及签名变更,仅记录范围)
- 越权访问修复15+ 处页面添加 `requirePermission`data-access 层添加 scope 过滤
- 功能性 BUG 修复question-actions 删除字段名、question-columns 时间显示、textbook-card 删除按钮、block-renderer 拖拽
- 统一错误处理:所有 Server Action catch 块改用 `handleActionError`
- 客户端 Action 调用添加 try/catch/finally20+ 处,使用 `safeActionCall`
- `JSON.parse` 改用 `safeJsonParse`8+ 处)
- Date 解析添加校验15+ 处,使用 `safeParseDate`
- 数据库事务添加:`batchCreateGradeRecords`、`deleteChapter`、`revertToVersion`
- 性能优化:`Promise.all` 并行化、DB 层分页grades `getGradeRecords`
- TypeScript `as` 断言清理30+ 处,改用类型守卫)
- React 性能修复:`useMemo`、`useCallback`、`key` prop、渲染期间副作用移除
---
## 3.7 API 规范化专项重构2026-07-04
> 本次重构统一 `app/api/**/route.ts` 的响应信封、错误处理与状态码映射,消除 11 个路由的重复模板代码与脆弱字符串匹配模式。新增 2 个共享工具文件 + 1 个 search 模块。
### 3.7.1 新增共享工具
| 文件 | 用途 |
|------|------|
| `shared/lib/api-response.ts` | API 路由层统一响应工具:`apiSuccess` / `apiError` / `apiFromAction` / `handleApiError` / `withApiErrorHandler` HOF / `parseJsonBody` / `errorToStatus` |
| `shared/lib/sse.ts` | SSE 共享工具:`formatSseEvent` / `formatSseError` / `formatSseDone` / `createSseResponse` / `createSseError` |
### 3.7.2 新增模块
**`modules/search/`**2 个文件):从 `app/api/search/route.ts` 下沉 4 个查询函数searchQuestions/searchTextbooks/searchExams/searchAnnouncements消除 `app/` 直查 DB 的架构违规。
### 3.7.3 重构路由清单11 个)
| 路由 | 重构内容 |
|------|---------|
| `api/search` | 改用 `modules/search/data-access`(消除直查 DB`withApiErrorHandler` + `apiSuccess` |
| `api/proctoring/event` | 改用 `getExamSubmissionForProctoring`(消除与 data-access 重复实现);`parseJsonBody` + Zod + `ValidationError`;修复 401→403 |
| `api/upload` | `apiFromAction(result, { headers })` 替代字符串匹配415 等状态码改为 errorCode 驱动 |
| `api/files/[id]` | GET/DELETE 用 `apiFromAction`;路由从 80+ 行简化到 38 行 |
| `api/files/batch-delete` | 使用 `apiFromAction` |
| `api/export` | `EXPORT_TYPES: Record<ExportType, Permission \| null>` 映射表替代 4 处嵌套 try/catch统一 `requirePermission` |
| `api/import` | `ValidationError` + `apiError`;修复 401→403 |
| `api/ai/chat` | `apiSuccess` / `apiError` + `withApiErrorHandler` |
| `api/ai/chat/stream` | 改用 `createSseError` / `createSseResponse` / `formatSseEvent` / `formatSseDone`;补充 `dynamic = "force-dynamic"` |
| `api/notifications/stream` | 改用 SSE 共享 helpers补充 `dynamic = "force-dynamic"``PermissionDeniedError` → 403其他 → 401 |
| `api/cron/audit-cleanup` | `withApiErrorHandler`;字段名 `error` → `message`、`ok` → `success` |
| `api/onboarding/complete` | 410 响应改用 `apiError(..., 410, "deprecated")` |
| `api/onboarding/status` | 同上 |
| `api/rate-limit-test` | `apiSuccess` / `apiError` + `withApiErrorHandler` |
### 3.7.4 模块改动
**`modules/files/actions.ts`**:移除本地 `handleActionError`,改用 `@/shared/lib/action-utils` 统一版本;为所有失败分支添加 `errorCode``validation_error` / `not_found` / `permission_denied` / `unexpected`),驱动 `apiFromAction` 状态码映射。
### 3.7.5 客户端适配6 个文件)
所有客户端 fetch 调用适配新响应信封 `{ success, message?, errorCode?, data? }`
| 文件 | 改动 |
|------|------|
| `modules/files/hooks/use-file-upload.ts` | 响应体类型改为 `{ success, data: Partial<FileUploadResult> }` |
| `modules/exams/editor/selection-toolbar.tsx` | `data.url` → `data.data.url``data.id` → `data.data.id` |
| `modules/homework/components/scan-uploader.tsx` | 同上 |
| `modules/settings/components/avatar-upload.tsx` | 修复原双重 `.json()` bug适配 `responseJson.data.url` |
| `modules/files/hooks/use-file-batch-operations.ts` | `body.deletedCount` → `body.data?.deletedCount` |
| `modules/users/components/user-import-dialog.tsx` | `data.sheets?.[0]?.rows` → `data.data?.sheets?.[0]?.rows` |
### 3.7.6 设计决策
| 问题 | 决策 | 理由 |
|------|------|------|
| 搜索 API 架构违规 | 新建 `modules/search/` 模块 | 遵循三层架构 `app → modules → shared`,下沉 DB 查询到 data-access 层 |
| deprecated onboarding 路由 | 保留 410 响应 + 统一信封 | 兼容旧客户端,`apiError(..., 410, "deprecated")` 表明废弃原因 |
| rate-limit-test 测试端点 | 保留并规范化 | 测试基础设施,使用统一信封便于客户端解析 |
| 错误→状态码映射 | errorCode 驱动 | 替代 `result.message?.includes("not found")` 字符串匹配脆弱模式 |
| 异常处理 | `withApiErrorHandler` HOF | 消除每个 route 重复 try/catch统一 `PermissionDeniedError`→403 映射 |
### 3.7.7 响应信封规范
```typescript
type ApiResponse<T = unknown> = {
success: boolean
message?: string
errorCode?: string
data?: T
}
```
错误→状态码映射规则:
- `PermissionDeniedError` → 403已认证但无权限
- `NotFoundError` → 404
- `ValidationError` / `BusinessError` → 400
- 未认证 → 401
- 限流 → 429
- 其他 → 500
`ActionState.errorCode` → 状态码映射:
- `not_found` → 404
- `validation_error` → 400
- `auth_required` → 401
- `permission_denied` → 403
- 其他 → 500
---
# 附录 A模块间依赖矩阵
> 行表示使用方,列表示被使用方。`` 合理依赖,`` 违规直查,`` 循环依赖。
> ✅ P1-1 已修复:所有跨模块直查已改为通过对方 data-access 接口。
| ↓ 使用 → | shared | auth | exams | homework | questions | textbooks | classes | school | dashboard | users | grades | messaging | notifications | lesson-prep | error-book | 其他 |
|----------|--------|------|-------|----------|-----------|-----------|---------|--------|-----------|-------|--------|-----------|---------------|-------------|------------|------|
| **shared** | - | ⟳✅已修复 | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **auth(root)** | ✅ db/lib | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **exams** | ✅ | ✅ | - | - | ✅data-access | - | ✅data-access | ✅data-access | - | - | - | - | - | - | - | - |
| **homework** | ✅ | ✅ | ✅data-access | - | ✅关系 | - | ✅data-access | ✅data-access | - | ✅data-access | - | - | - | - | - | - |
| **questions** | ✅ | ✅ | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | - | - |
| **textbooks** | ✅ | ✅ | - | - | ✅UI | - | - | - | - | - | - | - | - | - | - | - |
| **classes** | ✅ | ✅ | ✅data-access | ✅data-access | - | - | - | ✅data-access | - | - | - | - | - | - | - | ✅schedulingP0-5data-access-class-schedule 写函数) |
| **school** | ✅ | ✅ | - | - | - | - | - | - | - | ⚠️可接受 | - | - | - | - | - | - |
| **grades** | ✅ | ✅ | ✅外键 | ✅外键 | - | - | ✅data-access | ✅data-access | - | ✅data-access | - | - | - | - | - | - |
| **dashboard** | ✅ | ✅ | ✅data-access | ✅data-access | ✅data-access | ✅data-access | ✅data-access | - | - | ✅data-access | - | - | - | - | - | - |
| **users** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | ✅rbacUserRoleAssignDialog 组件) |
| **messaging** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | ✅dispatcher | - | - | - |
| **notifications** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | - |
| **attendance** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - | - |
| **scheduling** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | ✅data-access | - | - | - | - | - | - |
| **proctoring** | ✅ | ✅ | ✅data-access | - | - | - | - | - | - | ✅data-access | - | - | - | - | - | - |
| **diagnostic** | ✅ | ✅ | ✅data-access | - | ✅data-access | - | ✅data-access | - | - | ✅data-access | - | - | ✅data-access | - | - | ✅parent/notificationsv2-P2-6/v4-P1-5 |
| **parent** | ✅ | ✅ | - | ✅data-access | - | - | ✅data-access | ✅data-access | - | ✅data-access | ✅data-access | - | - | - | - | - |
| **elective** | ✅ | ✅ | - | - | - | - | ✅data-access | ✅data-access | - | ✅data-access | - | - | - | - | - | - |
| **course-plans** | ✅ | ✅ | - | - | - | - | ✅ | ✅ | - | ✅ | - | - | - | - | - | - |
| **audit** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **announcements** | ✅ | ✅ | - | - | - | - | - | ✅ | - | - | - | - | - | - | - | - |
| **files** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **settings** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | ✅ | - | - | - | - |
| **layout** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | ✅ | - | - | - | - |
| **lesson-preparation** | ✅ | ✅ | ✅data-access | ✅data-access | ✅data-access | ✅data-access | ✅data-access | - | - | - | - | - | - | - | - | ✅files/ai |
| **error-book** | ✅ | ✅ | - | - | ✅actions | - | ✅data-access | - | - | - | - | - | - | - | - | - |
| **rbac** | ✅ db/types/lib | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
---
# 附录 B关键参数影响链
### `userId`
1. 由 `auth.ts` JWT callback 从 `users` 表查询产生,存入 JWT
2. 通过 `session.user.id` 传递到所有 Server/Client Components
3. 通过 `getAuthContext().userId` 传递到所有 Server Actions
4. 在 `auth-guard.ts` 中用于查询 `usersToRoles`(角色)和 `classSubjectTeachers`/`grades`DataScope
5. 在 `exams/actions.ts` 中作为 `creatorId` 写入 `exams` 表
6. 在 `homework/actions.ts` 中作为 `creatorId` 写入 `homeworkAssignments` 表
7. 在 `classes/data-access.ts` 中查询 `getTeacherClasses(teacherId)` / `getGradeManagedClasses(userId)`
8. 在 `elective/actions.ts` 中作为 `teacherId`/`studentId` 用于选课过滤
### `examId`
1. 由 `exams/actions.createExamAction` 产生CUID2写入 `exams` 表
2. 被 `exams/data-access.getExamById(id)` 读取
3. 被 `exams/actions` 的 `updateExamAction`/`deleteExamAction`/`duplicateExamAction` 用于定位考试
4. 传入 `homework/actions.createHomeworkAssignmentAction` 的 `sourceExamId` 参数
5. 在 `homeworkAssignments` 表中作为外键关联到源考试
6. 被 `homework/data-access.getHomeworkAssignmentAnalytics` 用于追溯作业来源
### `classId`
1. 由 `classes/actions` 的 `createTeacherClassAction`/`createAdminClassAction` 产生
2. 被 `classes/data-access.getClassStudents(classId)` 读取学生列表
3. 被 `classes/data-access.getClassSchedule(classId)` 读取课表
4. 被 `classes/data-access.getClassHomeworkInsights(classId)` 读取作业洞察(✅ P0-7 已修复:通过 `homework/data-access-classes` 获取数据)
5. 被 `homework/data-access.getHomeworkAssignments({ classId })` 过滤作业列表
6. 在 `auth-guard.ts` 中通过 `classSubjectTeachers` 查询教师关联的 classIds构建 `DataScope.class_taught`
### `permission`
1. 由 `shared/types/permissions.ts` 的 `Permissions` 常量定义61 个权限点)
2. 在 `shared/lib/permissions.ts` 中通过 `ROLE_PERMISSIONS` 映射角色到权限列表
3. 在 `auth.ts` JWT callback 中通过 `resolvePermissions(roleNames)` 合并多角色权限,存入 JWT
4. 在 `proxy.ts` middleware 中通过 `token.permissions` 检查路由访问权限
5. 在 `shared/lib/auth-guard.ts` 中通过 `requirePermission(permission)` 在 Server Action 层断言权限
6. 在 `shared/hooks/use-permission.ts` 中通过 `hasPermission(permission)` 在客户端组件中条件渲染
7. 在 `layout/config/navigation.ts` 中作为 `NavItem.permission` 字段过滤侧边栏菜单
### `DataScope`
1. 由 `auth-guard.ts` 的 `resolveDataScope(userId, roles)` 根据用户角色和 DB 关系动态计算
2. 支持类型:`all` / `grade_managed` / `class_taught` / `class_members` / `children` / `owned`
3. 传递到 `exams`/`homework`/`grades`/`attendance`/`elective`/`dashboard` 的 data-access 进行行级过滤
4. 对 parent 角色,查询 `parentStudentRelations` 表构建 `{ type: "children", childrenIds: string[] }`
5. 在 `parent/children/[studentId]/page.tsx` 中通过 `ctx.dataScope.childrenIds.includes(studentId)` 二次校验
---
# 附录 C核心函数签名索引
> 完整函数签名见 `005_architecture_data.json`。本附录仅列出关键函数。
### shared 层核心函数
```typescript
// shared/lib/auth-guard.ts
getAuthContext(): Promise<AuthContext>
requirePermission(permission: Permission): Promise<AuthContext>
requireAuth(): Promise<AuthContext>
checkPermission(permission: Permission): Promise<{ allowed: boolean; ctx: AuthContext }>
resolveDataScope(userId: string, roleNames: string[]): Promise<DataScope>
// shared/lib/permissions.ts
resolvePermissions(roleNames: string[]): Promise<Permission[]>
// shared/lib/audit-logger.ts
logAudit(params: LogAuditParams): Promise<void>
// shared/lib/login-logger.ts
logLoginEvent(params: LogLoginEventParams): Promise<void>
// shared/lib/change-logger.ts
logDataChange(params: LogDataChangeParams): Promise<void>
// shared/lib/session.ts (P0-2 新增session 获取单一入口)
getSession(): Promise<AppSession>
// shared/lib/http-utils.ts (P1-6 完成,统一 IP/UA 提取)
resolveClientIp(): Promise<string>
getUserAgent(): Promise<string>
// shared/lib/ai/ (P2-2 已拆分,原 ai.ts 保留为重导出)
// ai/client.ts
createAiChatCompletion(input: AiChatRequest): Promise<{ content, usage }>
// ai/payload-parser.ts
parseAiChatPayload(body: unknown): AiChatRequest
// ai/api-key-crypto.ts
encryptAiApiKey(value: string): string
decryptAiApiKey(value: string): string
// shared/lib/password-policy.ts
validatePassword(password: string): { valid: boolean; errors: string[] }
getPasswordStrength(password: string): "weak" | "medium" | "strong"
isAccountLocked(failedAttempts: number, lastFailedAt: Date | null): boolean
// shared/lib/rate-limit/ (audit-P1-6 重构:单文件拆分为目录,抽象 RateLimiter 接口)
// index.ts —— 门面getRateLimiter() 按 env.RATE_LIMIT_DRIVER 选择实现rateLimit()/resetRateLimit() 返回 Promise
// types.ts —— RateLimiter 接口 + RateLimitParams/RateLimitResult 类型
// rules.ts —— RATE_LIMIT_RULES / rateLimitKey / rateLimitHeaders 纯函数(同步)
// memory-limiter.ts —— MemoryRateLimiter 类(默认,进程级 Map
// redis-limiter.ts —— RedisRateLimiter 类(基于 @upstash/ratelimit + @upstash/redis懒加载
// upstash-modules.d.ts —— @upstash/* 可选依赖类型声明(仅 RATE_LIMIT_DRIVER=redis 时安装)
rateLimit(params: RateLimitParams): Promise<RateLimitResult> // audit-P1-6改为 Promise 返回
resetRateLimit(key: string): Promise<void> // audit-P1-6改为 Promise 返回
rateLimitKey(prefix: string, identifier: string): string // 同步纯函数
rateLimitHeaders(result: RateLimitResult): Record<string, string> // 同步纯函数
getRateLimiter(): Promise<RateLimiter> // audit-P1-6 新增返回当前进程的限流器实例redis 驱动下动态 import redis-limiter 故为异步
RATE_LIMIT_RULES // 同步常量(含 LOGIN/API/UPLOAD/AI_CHAT/PASSWORD_CHANGE/ONBOARDING_BINDaudit-P1-8 新增 ONBOARDING_BIND: 5次/小时)
// RateLimiter interface (audit-P1-6 新增)
// limit(params: RateLimitParams): Promise<RateLimitResult>
// reset(key: string): Promise<void>
// shared/lib/permission-bitmap.ts (audit-P1-7 新增:权限位图编解码)
// 纯函数无 server-only可在 edge runtime 使用proxy.ts 直接调用)
// PERMISSION_BITMAP_ORDER: readonly Permission[] —— 67 个权限点的固定顺序(不可变,新增权限只能追加末尾)
// 编码:每个权限按位图中位置分配 bit (0-66)67 bit 打包为 BigInt → base36 字符串(约 13 字符 vs JSON 数组 1.1KB,减少 99%
// BigInt 精度安全:手工按位累加 base36 → BigIntparseBase36ToBigInt避免 parseInt(bitmap, 36) 的 Number 53 bit 精度丢失
encodePermissionsBitmap(permissions: Permission[]): string // 编码Permission[] → base36 字符串
decodePermissionsBitmap(bitmap: string): Permission[] // 解码base36 字符串 → Permission[](用于 session callback 还原数组)
hasPermissionInBitmap(bitmap: string, permission: Permission): boolean // 单权限检查不解码全部proxy.ts 性能优化)
// shared/lib/excel.ts
exportToExcel(params: { sheets: ExcelSheet[] }): Promise<Buffer>
parseExcel(buffer: Buffer): Promise<ParsedSheet[]>
generateTemplate(params: { sheets: TemplateSheet[] }): Promise<Buffer>
// shared/lib/file-storage.ts
isAllowedMimeType(mimeType: string): boolean
generateStoragePath(originalName: string): string
formatFileSize(bytes: number): string
// shared/lib/utils.ts
cn(...inputs: ClassValue[]): string
formatDate(date: string | Date, locale?: string): string
getSearchParam(params: SearchParams, key: string): string | undefined
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 重构新增)
// 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
// cache-fn.ts —— cacheFn 双层包装器:外层 react.cache 请求级 memoization内层 cacheStore.getOrSet 跨请求缓存(按 tag 失效TTL 可选)
// 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
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
```typescript
// exams/actions.ts
createExamAction(prevState: ActionState, formData: FormData): Promise<ActionState<Exam>>
createAiExamAction(prevState: ActionState, formData: FormData): Promise<ActionState<{ examId: string }>>
updateExamAction(prevState: ActionState, formData: FormData): Promise<ActionState>
deleteExamAction(prevState: ActionState, formData: FormData): Promise<ActionState>
duplicateExamAction(prevState: ActionState, formData: FormData): Promise<ActionState<{ examId: string }>>
// homework/actions.ts
createHomeworkAssignmentAction(prevState: ActionState, formData: FormData): Promise<ActionState>
startHomeworkSubmissionAction(prevState: ActionState, formData: FormData): Promise<ActionState<{ submissionId: string }>>
saveHomeworkAnswerAction(prevState: ActionState, formData: FormData): Promise<ActionState>
submitHomeworkAction(prevState: ActionState, formData: FormData): Promise<ActionState>
gradeHomeworkSubmissionAction(prevState: ActionState, formData: FormData): Promise<ActionState>
// classes/actions.ts
createTeacherClassAction(prevState: ActionState, formData: FormData): Promise<ActionState>
createAdminClassAction(prevState: ActionState, formData: FormData): Promise<ActionState>
createGradeClassAction(prevState: ActionState, formData: FormData): Promise<ActionState>
// + update/delete 各 3 个,共 9 个
// grades/actions.ts
getGradeRecordsAction(params: GetGradeRecordsParams): Promise<ActionState<PaginatedGradeRecords>>
createGradeRecordAction(prevState: ActionState, formData: FormData): Promise<ActionState>
exportGradesAction(params: ExportGradesParams): Promise<ActionState<Buffer>>
// scheduling/actions.ts
autoScheduleAction(prevState: ActionState, formData: FormData): Promise<ActionState>
applyAutoScheduleAction(prevState: ActionState, formData: FormData): Promise<ActionState>
```
### 业务模块核心 Data-access
```typescript
// exams/data-access.ts
getExams(params: GetExamsParams & { scope: DataScope }): Promise<{ items: Exam[]; total: number }>
getExamById(id: string, scope: DataScope): Promise<Exam | null>
persistExamDraft(input: ExamDraftInput): Promise<{ examId: string }>
persistAiGeneratedExamDraft(input: AiExamDraftInput): Promise<{ examId: string }>
// homework/data-access.ts
getHomeworkAssignments(params: GetHomeworkAssignmentsParams & { scope: DataScope }): Promise<{ items, total }>
getStudentHomeworkAssignments(studentId: string): Promise<StudentHomeworkAssignment[]>
getStudentDashboardGrades(studentId: string): Promise<StudentDashboardGrades>
getHomeworkAssignmentAnalytics(assignmentId: string): Promise<HomeworkAnalytics>
// classes/data-access.ts
getAdminClasses(scope: DataScope): Promise<Class[]>
getTeacherClasses(teacherId: string): Promise<Class[]>
getStudentClasses(studentId: string): Promise<Class[]>
getClassStudents(classId: string): Promise<Student[]>
getClassHomeworkInsights(classId: string): Promise<ClassHomeworkInsights> // ✅ P0-7 已修复:通过 homework/data-access-classes 获取数据
// grades/data-access.ts
getGradeRecords(params: GetGradeRecordsParams & { scope: DataScope; currentUserId?: string; limit?: number; offset?: number }): Promise<PaginatedGradeRecords>
getStudentGradeSummary(studentId: string, scope?: DataScope): Promise<StudentGradeSummary | null>
getClassRanking(classId: string, subjectId?: string, examId?: string, scope?: DataScope, currentUserId?: string): Promise<ClassRankingItem[]>
// scheduling/auto-scheduler.ts纯函数标杆
findOptimalSlot(input: FindOptimalSlotInput): ScheduleSlot | null
validateSchedule(schedule: ScheduleItem[]): ValidationResult
autoSchedule(input: AutoScheduleInput): AutoScheduleResult
buildDefaultTimeSlots(): TimeSlot[]
```
---
## 相关文档
| 文档 | 说明 |
|------|------|
| [005_architecture_data.json](./005_architecture_data.json) | AI 友好格式的结构化架构数据 |
| [006_k12_feature_checklist.md](./006_k12_feature_checklist.md) | 企业级 K12 标准功能模块清单 |
| [007_gap_audit_report.md](./007_gap_audit_report.md) | 差距审计报告 |
| [008_module_role_mapping.md](./008_module_role_mapping.md) | 模块-角色-功能映射总览 |
---
## 文档维护说明
- **修改源码后**:同步更新本文档对应模块章节 + `005_architecture_data.json`
- **新增模块**:在第二部分添加模块清单 + 更新 1.1 分层架构图 + 更新附录 A 依赖矩阵
- **新增/删除导出函数**:更新对应模块的"导出函数"清单 + 附录 C
- **修改依赖关系**:更新 1.2 模块依赖关系图 + 附录 A 依赖矩阵
- **新增路由**:更新 `005_architecture_data.json` 的 `routes` 节点
- **新增数据库表**:更新 `shared/db/schema.ts` 分节 + `005` 的 `dbTables` 节点
> 完整路由表、DevOps 脚本、E2E 测试等信息见 `005_architecture_data.json`。