Primitive + Semantic 双层令牌架构,HEX->HSL,明暗双份,@theme inline 暴露为 Tailwind 类。 - 新建 src/app/styles/tokens/ 6 个令牌文件(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index) - globals.css 改为 @import 引入,477->258 行 - 清理 91 处 #hex 硬编码颜色 -> hsl(var(--*)) - 清理 10 处硬编码字体 -> var(--font-family-*) - 清理 100 文件 Tailwind 任意值(Tier 1 映射/Tier 3 注释豁免) - 清理 M3 Surface 死代码,升级 --lp-* 令牌(HEX->HSL + 暗色补全) - 新建 ESLint 自定义规则 no-hardcoded-design-tokens(单词边界正则) - eslint.config.mjs 新增 no-restricted-syntax 禁止 #hex + 自定义规则加载(pathToFileURL) - 项目规则新增设计令牌规范强制章节 - 架构图 004/005 同步设计令牌体系节点 - known-issues.md 追加设计令牌问题分类(7 个规则表) 验证: tsc --noEmit 0 errors, npm run lint 0 errors/12 warnings(均为既有问题)
4286 lines
547 KiB
Markdown
4286 lines
547 KiB
Markdown
# 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 + Semantic),Tailwind 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.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` | 通用分页 UI(Showing 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 }` | 通用 Server Action mutation Hook,替代 50+ 文件中重复的 setIsWorking + try/catch/finally + toast 模式 | 1 个示范(P1-4: schools-view),潜在影响 50+ 文件 |
|
||
| `useActionQuery` | `hooks/use-action-query.ts` | `useActionQuery<T>(action, options?): { data, loading, error, refetch }` | 通用 Server Action 查询 Hook,替代 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式,内置竞态防护 | 1 个示范(P1-4: create-question-dialog),潜在影响 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 store(zustand),集中管理 modal registry。命令式 API + Hook 双入口。与 sonner 的分工:toast 走 notify,modal 走本 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 拿首屏 props,useQuery 负责交互数据) |
|
||
| 客户端业务状态 | 模块级状态机(如 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-CN,weekday=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.x(without 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 获取单一入口(getSession,server-only,dynamic 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 获取 session,http-utils 获取 IP/UA) |
|
||
| `lib/change-logger.ts` | - | 数据变更日志(通过 session.ts 获取 session,http-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 秒超时 + AbortController;fail-open 策略:API 不可用时返回 `checkSkipped=true` 不阻断注册/改密;server-only) |
|
||
| `lib/http-utils.ts` | 44 | 请求头解析(resolveClientIp / getUserAgent,server-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 + FilterResetButton(P3-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-only,no-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 新增:年级仪表盘维度3,exams 表有直接 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/children,group 节点含 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`(状态子 Hook,67 行,10 个 useState + 3 个更新函数)/ `usePreviewBackgroundTasks`(后台任务子 Hook,80 行,PQueue 队列 + localStorage 持久化 + 入队/打开任务)/ `usePreviewRewrite`(AI 重写子 Hook,79 行,handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction)
|
||
- Stats-service(V3-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 attrs,isolating: 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/children,group 含 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` 聚合导出上述所有组件、扩展、类型与转换函数
|
||
- Components(V3-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-access,P0-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-access,actions.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 Action(createExamFromRichEditorAction/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 新增:editorDocToStructure(Tiptap JSONContent → EditorDoc) |
|
||
| `editor/structure-to-editor.ts` | - | 2026-06-24 新增:structureToEditorDoc(EditorDoc → 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 新增:预览状态子 Hook(10 个 useState + updatePreviewQuestionNode/updateSelectedQuestionFromAi/applyPreviewResult 三个更新函数) |
|
||
| `hooks/use-exam-preview-tasks.ts` | 80 | ✅ 2026-06-24 新增:预览后台任务子 Hook(PQueue 并发队列 + localStorage 持久化恢复 + handleBackgroundPreview/handleOpenPreviewTask) |
|
||
| `hooks/use-exam-preview-rewrite.ts` | 79 | ✅ 2026-06-24 新增:预览 AI 重写子 Hook(handleRewriteSelectedQuestion 调用 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-4):ExamServicePort 的 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-access,actions.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-write:11 个写操作函数(P1-2 新增 10 个从 actions 下沉 + V3-7 新增 `batchAutoGradeSubmissions`)
|
||
- Stats-service:`getTeacherGradeTrends` / `getHomeworkAssignmentAnalytics` / `getStudentDashboardGrades`(从 data-access.ts re-export 以保持向后兼容)
|
||
- Components(V3-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-100,isLate 标注迟交但质量优秀样例)/ `ExcellentSubmissionQuery`(优秀作业查询参数 { assignmentId, minPercentage?, limit? },minPercentage 默认 80,limit 默认 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 审计修复 P0:13 个缺失的 `loading.tsx`/`error.tsx` 路由文件全部补全(teacher/homework 根 + assignments + submissions + submissions/[submissionId] + scan-grading;student/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 Action(P1-2 已修复,无直接 DB 操作;✅ 2026-06-25:`ScanAttachment` 类型迁移至 types.ts;schema.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 | 答案防抖自动保存 Hook(localStorage 离线缓存) |
|
||
| `components/homework-take-view.tsx` | 396 | ✅ 2026-06-25 重构:从 520 行拆分为 3 个组件(主容器 + Sidebar + ConfirmDialog);V3-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/upload,targetType="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 i18n:Fallback 接入 `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、知识点关联、题型支持、教材→章节→知识点级联筛选)。
|
||
|
||
**导出函数**:
|
||
- Actions(11 个,全部调用 `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)/ `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` | 155+ | 数据表格(i18n + 集成 BatchOperations) |
|
||
| `components/question-columns.tsx` | 100+ | 列定义 Hook(useQuestionColumns) |
|
||
| `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 内容编辑、知识图谱)。
|
||
|
||
**导出函数**:
|
||
- Actions(14 个,均为写操作;读操作由 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-access,P0-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-24):4 个 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 + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUD(v3 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/DeletePrerequisiteSchema;v2 修复: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 P2:O(n²) 改为 Set 查找) |
|
||
| `analytics.tsx` | 37 | 教材分析 Context/Provider/Hook(✅ 新增) |
|
||
| `hooks/use-knowledge-point-actions.ts` | 45 | 知识点操作门面 Hook(v2 拆分:组合 useKpDialogState + useKpCrud,对外 API 不变) |
|
||
| `hooks/use-kp-dialog-state.ts` | 33 | 知识点对话框状态管理 Hook(v2 新增:从 use-knowledge-point-actions 拆分) |
|
||
| `hooks/use-kp-crud.ts` | 52 | 知识点 CRUD 门面 Hook(v3 P2 拆分:组合 useKpCreate/useKpDelete/useKpUpdate,对外 API 不变) |
|
||
| `hooks/use-kp-create.ts` | 43 | 知识点创建 Hook(v3 P2 新增:从 use-kp-crud 拆分) |
|
||
| `hooks/use-kp-delete.ts` | 49 | 知识点删除 Hook(v3 P2 新增:从 use-kp-crud 拆分) |
|
||
| `hooks/use-kp-update.ts` | 36 | 知识点更新 Hook(v3 P2 新增:从 use-kp-crud 拆分) |
|
||
| `hooks/use-text-selection.ts` | 49 | 文本选区捕获 Hook |
|
||
| `hooks/use-graph-data.ts` | 65 | 知识图谱数据加载 Hook(✅ Task 11 新增:派生值模式避免 effect 中 setState;v2 修复:区分 isLoading/isRefreshing 避免 UI 闪烁) |
|
||
| `components/knowledge-graph.tsx` | 408 | 知识图谱主组件(✅ Task 10/15 重写:全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色;v2 新增:添加/删除前置依赖 Dialog;v4 新增:hierarchical/force 布局模式切换,根据布局模式渲染分层 ReactFlow 或 Obsidian 风格力导向图,状态/详情面板/依赖增删对话框共享) |
|
||
| `components/graph-kp-node.tsx` | 92 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条;v2 修复:节点宽度常量化 NODE_WIDTH;v3 修复:类型守卫替代 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-force;v5 视觉对齐 Obsidian Graph View:小圆点节点+极细半透明边线+标签右侧带背景+hover/选中邻居高亮+紫色外圈焦点环;通过 next/dynamic ssr:false + import type 加载避免 SSR canvas/window 报错) |
|
||
| `components/graph-node-detail-panel.tsx` | 171 | 节点详情侧边面板(✅ Task 13 新增:描述/掌握度/关联题目/前置后置列表;v2 修复:移除未使用 textbookId prop;v3 修复:aria-label) |
|
||
| `components/chapter-sidebar-list.tsx` | 325 | 章节侧边栏列表(v2 修复:canEdit 默认 false + orderUpdateFailed i18n key;v3 修复:键盘导航) |
|
||
| `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 P2:17 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 新增:成绩录入草稿 CRUD,upsert + 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 个查询 schema(DeleteGradeRecordSchema/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 添加 EmptyState;entry/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 存储 scores,24 小时过期,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,添加学期和考试筛选 ChipNav;teacher/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-23):parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`,渲染 GradeTrendCard
|
||
- ✅ v3-P3-4 改进(2026-06-23):GradeTrendCard 新增日期范围选择器(全部/近7天/近30天/近90天),通过 nuqs `trendRange` URL 参数持久化,useEffect 中计算截止时间戳避免渲染阶段调用 Date.now()
|
||
- ✅ v4-P3-4 改进(2026-06-23):GradeDistributionChart 色盲友好双重编码——每个分数段使用不同 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/normalize(20 行)
|
||
- ✅ 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 到 ParentExportButton;ParentExportButton 接入 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` 整个组件无 i18n(13 处硬编码中文)~~ 改为 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-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除;v4-P2-6:assertClassInScope 迁移至 lib/scope-check.ts;2026-06-24 新增:batchCreateGradeRecordsByExamAction 按试卷录入每题得分;**P1-1 重构(2026-06-25)**:草稿相关 3 个 Action(saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction)迁移至 actions-draft.ts;通知逻辑 notifyGradeEntered 抽取至 lib/notify.ts;错误消息全部 i18n(t(\"action.*\"));scope 校验错误改用 ScopeCheckErrorCode + t(\"action.scopeError.{code}\") 翻译) |
|
||
| `actions-draft.ts` | 106 | **P1-1 新增(2026-06-25)**:成绩录入草稿 3 个 Server Action(saveGradeDraftAction/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 Action(getDraftLockStatusAction/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 Action(downloadGradeImportTemplateAction/importGradesFromExcelAction/previewGradeImportAction)。权限 GRADE_RECORD_MANAGE + assertClassInScope,调用 import-export.ts 解析 Excel + 匹配学生姓名 + 批量入库。支持模板下载(含班级学生姓名)、预校验、正式导入 |
|
||
| `actions-appeal.ts` | 197 | **P3-7 新增(2026-06-26)**:成绩申诉 5 个 Server Action(createGradeAppealAction/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/withdrawn,originalScore 快照留痕,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-6:assertClassInScope 改从 ./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)**:草稿 CRUD(saveGradeDraft/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)**:成绩录入草稿 CRUD(saveGradeDraft/getGradeDraft/deleteGradeDraft)+ GradeDraftData 接口,从 data-access.ts 拆分。使用 cache() memoize getGradeDraft,isGradeDraftData 类型守卫安全解析 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 应用 buildScopeClassFilter;v3-P2 新增:getExamOptionsForGrades/getSchoolWideGradeSummary;getGradeTrend/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-10:createDefaultBuckets 改为内部函数;P3-24:buildGradeTrendPoints 使用 isGradeTrendType 类型守卫替代 as 断言) |
|
||
| `export.ts` | 290+ | Excel 导出(v2-P1-5 修复:传递 currentUserId 到 data-access;P3 修复:适配 PaginatedGradeRecords 结构 + 传递 scope;P3-6:复用 stats-service.computeAverageScore 替代局部 avg;P3-7:硬编码中文改用 next-intl getTranslations;v4-P1-12 新增:exportStudentGradeRecordsToExcel 家长视角单学生导出) |
|
||
| `schema.ts` | 237 | Zod 校验(含 12 个查询 schema;P3 修复: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/normalize;P3-26:buildScopeClassFilter 迁移至 scope-filter.ts) |
|
||
| `lib/scope-filter.ts` | 56 | DB 行级权限过滤(buildScopeClassFilter;P3-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)**:成绩报告卡数据访问层。getReportCardData(cached)聚合学生成绩+班级信息+班主任+学年+科目排名+总体统计,computeSubjectRank 计算单科排名。三层 scope 校验:owned(学生自己)/children(家长子女)/class_taught(教师班级学生)。静态导入 data-access(getStudentRankInClass/getClassNamesByIds)+ stats-service(computeAverageScore) |
|
||
| `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/SchoolWideGradeSummary;2026-06-24 新增:GradeRecordAnswer/BatchGradeEntryByExamQuestion/BatchGradeEntryByExamItem) |
|
||
| `components/widget-boundary.tsx` | 136 | Widget 边界组件(P1-5 新增,v2-P1-1 已在 3 个页面应用;**P1-9(2026-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 修复:a11y;v2-P1-4:i18n;P3 修复:NaN 日期检查 + fullScore > 0 守卫) |
|
||
| `components/grade-record-list.tsx` | 200+ | 成绩记录列表(v2-P1-4:i18n;P3 修复:safeActionCall 包装删除操作;v4-P1-7:使用 ScoreCell;v4-P1-10:overflow-x-auto;v3-P3-2 新增:多选复选框 + 全选 + 批量删除工具栏 + 批量删除确认对话框,接入 bulkDeleteGradeRecordsAction) |
|
||
| `components/grade-distribution-chart.tsx` | 100 | 分数分布图(v2-P1-4:i18n) |
|
||
| `components/subject-comparison-chart.tsx` | 62 | 科目对比图(v2-P1-4:i18n) |
|
||
| `components/class-comparison-chart.tsx` | 194 | 班级对比图(v2-P1-4:i18n;v3-P3-5 新增:显著性分析区域,基于极差和样本量的经验规则判断班级间差异,含可折叠详细分析) |
|
||
| `components/grade-trend-chart.tsx` | 59 | 趋势图(v2-P1-4:i18n) |
|
||
| `components/grade-record-form.tsx` | 177 | 录入表单(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复: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 个 Hook(BatchGradeEntryTable + 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 htmlFor;v2-P1-4 i18n;P3 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-4:i18n;**P2-2(2026-06-25)**:补全 : JSX.Element 返回值类型) |
|
||
| `components/student-grade-summary.tsx` | 107 | 学生成绩摘要(v2-P1-4:i18n) |
|
||
| `components/export-button.tsx` | 79 | 导出按钮(v2-P1-4:i18n;P3 修复:safeActionCall 包装导出操作;**P2-2(2026-06-25)**:handleExport 补全 : Promise<void> 返回值类型) |
|
||
| `components/analytics-filters.tsx` | 86+ | 分析过滤器(v2-P1-4:i18n;v3-P2 新增:exams/currentExamId/currentSemester props,添加学期和考试筛选 ChipNav) |
|
||
| `components/stats-class-selector.tsx` | 40 | 统计班级选择器(v2-P1-4:i18n) |
|
||
| `components/grade-stats-card.tsx` | 74 | 统计卡片(v2-P1-4:i18n) |
|
||
| `components/class-grade-report.tsx` | 90 | 班级成绩报告(v2-P1-4:i18n) |
|
||
| `components/grade-query-filters.tsx` | 96 | 查询过滤器(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;**P2-2(2026-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)**:协同录入锁状态提示条。三种状态:acquiring(loading)/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 |
|
||
|
||
---
|
||
|
||
## 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.ts,classes 模块仅保留 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 共享删除确认弹窗,基于 ConfirmDeleteDialog,P1-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.ts,2026-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-access,P0-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.ts,classes 模块仅保留 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/detail(header/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/ScheduleGridSkeleton);6 个页面均已包裹 Error Boundary:admin/school/classes + teacher/classes/my + teacher/classes/my/[id] + teacher/classes/schedule + teacher/classes/students(ClassErrorBoundary)、management/grade/dashboard(SchoolErrorBoundary);schedule/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-export(P0-3 修复:从 974 行拆分为 6 个文件) |
|
||
| `actions-teacher.ts` | 100 | 教师班级 CRUD(3 个 Action) |
|
||
| `actions-admin.ts` | 120 | 管理员班级 CRUD(3 个 Action) |
|
||
| `actions-grade.ts` | 110 | 年级组长班级 CRUD(3 个 Action) |
|
||
| `actions-invitations.ts` | 502 | 邀请码与注册 + 批量操作(10 个 Action,P2-4 新增批量导入学生/批量分配教师) |
|
||
| `actions-schedule.ts` | 90 | 班级课表 CRUD(3 个 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 Boundary(P1-C 新增) |
|
||
| components/class-skeleton.tsx | 80 | 班级模块骨架屏组件(5 个:List/Card/Grid/StudentList/ScheduleGrid,P1-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 | 课表 Widget(P0-A 修复:已接入 i18n) |
|
||
| components/class-detail/class-assignments-widget.tsx | 100 | 作业 Widget(P0-A 修复:已接入 i18n) |
|
||
| components/class-detail/class-students-widget.tsx | 120 | 学生 Widget(P0-A 修复:已接入 i18n) |
|
||
| components/class-detail/class-trends-widget.tsx | 150 | 趋势 Widget(P0-A 修复:已接入 i18n) |
|
||
| components/class-detail/edit-class-dialog.tsx | 145 | 编辑班级对话框(P0-A 修复:已接入 i18n) |
|
||
| hooks/use-class-data.ts | 95 | 对话框+表单状态管理 hook(P1-E 抽取) |
|
||
| hooks/use-class-filters.ts | 30 | 按学校筛选年级 hook(P1-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.tsx(class component Error Boundary + i18n + router.refresh 重试)和 school-skeleton.tsx(SchoolListSkeleton 表格骨架 + SchoolCardSkeleton 卡片骨架);4 个页面(schools/grades/departments/academic-year)均已包裹 SchoolErrorBoundary;school.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 Boundary(P1-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 | 学校数据管理 hook(P2-1 修复) |
|
||
| hooks/use-grade-data.ts | 42 | 年级数据管理 hook(P1-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 相关系数 + 风险分级)。
|
||
|
||
**导出函数**:
|
||
- Actions(5 个写 Action):`recordAttendanceAction`(✅ L-6:formData 读取 period)/ `batchRecordAttendanceAction`(✅ L-2:保存成功后调用 `notifyParentsOfAbsence` 异步通知家长,失败不阻断主流程;✅ L-6:每条记录携带 period)/ `updateAttendanceAction`(✅ L-6:formData 读取 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-6:recentRecords 映射 period)/ `getClassAttendanceStats`(✅ 审计修复:SQL 聚合 + 委托 data-access;✅ L-6:studentRecords 映射 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) |
|
||
|
||
---
|
||
|
||
## 2.11 users(用户模块)
|
||
|
||
**职责**:用户资料管理 + 批量导入导出 + 管理员用户列表管理。
|
||
|
||
**导出函数**:
|
||
- Actions:`getUserProfileAction` / `updateUserProfileAction` / `importUsersAction` / `exportUsersAction` / `downloadUserTemplateAction` / `updateUserRoleAction` / `deleteUserAction`
|
||
- Data-access:`getUserProfile` / `getCurrentStudentUser`(✅ P2-20 已修复:从 homework 模块迁移而来,6 个 student 页面通过此函数获取学生身份,不再依赖 homework 模块)/ `getAdminUsers`(管理员用户列表分页查询,支持搜索+角色聚合)/ `getAdminUserRoles`(角色名列表,用于筛选下拉框)
|
||
- 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-access,P0-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 Action(profile 更新 + 模板下载/导入/导出 + 角色更新 + 删除) |
|
||
| `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 导出便捷方法)
|
||
- Services(V4 新增):`DashboardService` 接口(抽象数据依赖,含 4 个角色方法签名)/ `DashboardAnalytics` 接口(监控埋点:Widget 点击 / 空状态 / 错误重试 / 页面停留)/ `DashboardServiceProvider`(React Context 依赖注入)/ `useDashboardService` / `useDashboardAnalytics`
|
||
- Config(V4 新增):`ADMIN_WIDGET_CONFIG` / `TEACHER_WIDGET_CONFIG` / `STUDENT_WIDGET_CONFIG` / `PARENT_WIDGET_CONFIG` / `getWidgetConfig`(配置驱动 Widget 布局,新增角色只需修改配置)
|
||
- Hooks(L4/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 桌面端网格)
|
||
- Types(V4 新增):`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 和 ParentAttentionBanner,parent 模块仅保留数据访问
|
||
- ✅ P0 已修复(2026-06-22 V2):10 个子组件 i18n 遗漏全部补齐,新增 ~50 个翻译键
|
||
- ✅ P1 已修复(2026-06-22 V2):抽象共享组件 `DashboardGreetingHeader`,消除 teacher/student 头部 90% 重复代码
|
||
- ✅ P2 已修复(2026-06-22 V2):为 6 个纯函数添加 31 个单元测试
|
||
- ✅ P2 已修复(2026-06-22 V2):a11y 增强 — admin 表格 `<caption>`、teacher/student 视图语义化标签
|
||
- ✅ P0 已修复(2026-06-22 V4):`filterTodaySchedule` 移除 `as T[]` 类型断言,改为非泛型函数
|
||
- ✅ P1 已修复(2026-06-22 V4):5 个组件内嵌纯函数抽取至 `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` Hook,localStorage 持久化 Widget 显示/隐藏偏好
|
||
- ✅ L5 已实施:通知中心集成 — 新增 `DashboardNotificationWidget` 组件
|
||
- ✅ L6 已实施:实时更新 — 新增 `useDashboardRealtime` Hook(SSE + 指数退避重连)
|
||
- ✅ 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 新增:自定义仪表盘偏好 Hook(localStorage 持久化) |
|
||
| `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 缺少"星标" Tab,data-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~~ 新增客户端分页 UI(PAGE_SIZE=20,ChevronLeft/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-access,2 分钟窗口校验,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-9:sendMessageAction 增加 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 + 客户端分页 UI,PAGE_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-4:fieldErrors + aria-invalid 字段级错误展示;✅ 审计 V1-P0-5:`submittedRef` 防止提交后自动保存竞态) |
|
||
| `components/message-draft-list.tsx` | ✅ 审计 V1-P1-1 新增:草稿列表组件(继续编辑 + 删除草稿 + 乐观更新) |
|
||
| `components/unread-message-badge.tsx` | 未读消息计数徽章(侧边栏,每 30 秒轮询 `getUnreadMessageCountAction`;✅ V2-P2-1:POLL_INTERVAL_MS 常量;✅ V2-P3:间隔从 60s 缩短为 30s 与通知一致) |
|
||
|
||
**客户端行为**:
|
||
- `message-list.tsx`:客户端调用 `getMessagesAction` 搜索消息(useMessageSearch hook,400ms 防抖,请求竞态取消);V2-P1-2 优化:客户端过滤仅在初始数据(type=all)时执行;✅ 审计 V1-P1-2:starred 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 触发新草稿
|
||
|
||
---
|
||
|
||
## 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)`
|
||
- Data-access:`createNotification` / `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-13b:NotificationList 支持优先级 Badge 显示和归档操作;✅ V2-P2-13c:NotificationList 支持按类型筛选)
|
||
- 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 不再反向依赖 messaging,in-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` 支持 priority;actions.ts 新增 `archiveNotificationAction`(含 trackEvent 埋点 notification.archived);NotificationList 组件支持优先级 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` | ~145 | 渠道选择 + 并行分发(✅ V3-P1-4:移除 `sendBatchNotifications` 重复日志) |
|
||
| `data-access.ts` | ~250 | 站内通知 CRUD + 用户联系方式 + 发送日志持久化(notification_logs 表;P0-4 / P1-5 修复后新增通知 CRUD;✅ V2-P2-13b:新增归档函数 + 优先级/归档筛选;✅ V3-P2-8:console 日志添加 TODO 统一日志服务标记) |
|
||
| `preferences.ts` | 166 | 通知偏好 CRUD(P0-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 实时推送 Hook(EventSource + 轮询降级) |
|
||
| `hooks/use-desktop-notifications.ts` | ~100 | ✅ V2-P2-13c 新增:浏览器桌面推送 Hook(Notification API + 权限管理 + 点击跳转) |
|
||
|
||
**组件清单**:
|
||
| 组件 | 职责 |
|
||
|------|------|
|
||
| `components/notification-list.tsx` | 通知列表(消息页底部,展示所有通知,支持标记已读;✅ V2-P0-1:useTranslations 命名空间从 "messages" 切换到 "notifications";✅ V2-P2-13b:支持优先级 Badge 显示和归档操作;✅ V2-P2-13c:支持按类型筛选 + 空状态区分"无通知"/"无筛选结果") |
|
||
| `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,✅ V2-P3:改用 SSE 实时推送 `/api/notifications/stream` + 轮询降级;✅ V2-P0-1:useTranslations 命名空间切换;✅ V2-P2-1:POLL_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` hook(27 行)
|
||
- ✅ 审计重构 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-5(auth 审计)已修复:~~`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)/ `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)/ `toggleAnnouncementPin`(✅ Audit-P1-6:原子化 UPDATE ... SET isPinned = NOT isPinned)/ `markAnnouncementAsRead`(✅ Audit-P1-6:onDuplicateKeyUpdate 实现幂等 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-1:Actions 通过 `getTranslations('announcements')` 返回 i18n 化 message)
|
||
- 被依赖:无
|
||
|
||
**已知问题**:
|
||
- ✅ P1-2 已修复:~~所有写操作直接在 actions 层 `db.insert/update/delete`,未下沉到 data-access~~ 写操作已下沉到 data-access(5 个新函数)
|
||
- ✅ 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 个 Action;Audit-P0-3 新增资源所有权二次校验;Audit-P1-1 i18n 化 message;Audit-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-4:fieldErrors + 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-8:Dialog 内表单通过 `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)
|
||
- Types(V3-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-P02:Grades/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 tab(1 次点击到达)
|
||
- ✅ HABIT-P03:多子女切换无需返回仪表盘
|
||
- ✅ HABIT-P06:仪表盘展示未读/待办数量
|
||
- ✅ A11Y-P02:Overdue 状态增加 AlertTriangle 图标辅助
|
||
- ✅ A11Y-P04:成绩图表容器新增 aria-label
|
||
- ✅ PERF-P01/P02:骨架屏 + 错误边界
|
||
- ✅ PERF-P03:空状态新增"Contact support"引导按钮
|
||
- ✅ PERF-P04:`ChildCard` Link 添加 `prefetch`
|
||
- ✅ MOBILE-P03:移动端子女卡片改为水平滑动 Carousel(snap-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`)
|
||
- 被依赖:无
|
||
|
||
**已知问题**:
|
||
- ⚠️ P1:3 个 parent 组件(`parent-attendance-warning.tsx`/`parent-attendance-rate-card.tsx`/`parent-attendance-calendar.tsx`)直接 import `@/modules/attendance/types`,违反模块解耦原则(应通过 data-access 接口或 shared 类型抽象)
|
||
- ⚠️ P1:parent-attendance-calendar.tsx 重新定义 `STATUS_DOT`/`STATUS_LABEL` 常量,与 attendance 模块重复
|
||
- ⚠️ P1:3 个 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-23):parent/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-23):parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`,渲染 GradeTrendCard;parent/diagnostic/page.tsx 传入 `practiceHrefBase={null}` 隐藏练习按钮
|
||
- ✅ 职责单一,正确复用其他模块 data-access
|
||
|
||
**文件清单**:
|
||
| 文件 | 行数 | 职责 |
|
||
|------|------|------|
|
||
| `data-access.ts` | 243+ | 子女关系 + 仪表盘数据聚合 + 关系校验 + 子女姓名列表(v4 新增 `getChildNameList` + `buildWeeklySchedule`;V3-11 `getChildDashboardData` 并行调用 `getStudentExamResults`) |
|
||
| `types.ts` | 79+ | 类型定义(含 JSDoc,v4 新增 `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 面板 + SiblingSwitcher(v4 重写,集成 Homework/Grade Detail;V3-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-P2:practiceHrefBase={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(选课模块)
|
||
|
||
**职责**:选修课程管理 + 学生选课 + 抽签。
|
||
|
||
**导出函数**:
|
||
- Actions(11 个):`getElectiveCoursesAction` / `createElectiveCourseAction` / `updateElectiveCourseAction` / `deleteElectiveCourseAction` / `getStudentSelectionsAction` / `selectCourseAction` / `dropCourseAction` / `runLotteryAction` / `getAvailableCoursesForStudentAction` / `openSelectionAction` / `closeSelectionAction`(✅ P2-2:8 个写 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-4:FCFS 录取后触发 `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-2:Server 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-25):3 个 `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-25):student 页面拆分"我的选课"与"可选课程"为两个独立 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 Action(P0-4:translateBusinessError 翻译 ElectiveBusinessError) |
|
||
| `data-access.ts` | 258 | 课程 CRUD + scope 过滤 + 共享映射函数(P1-10:移除静默吞错) |
|
||
| `data-access-operations.ts` | 374 | 选课操作(P0-4:ElectiveBusinessError + i18n code;P1-11:parseSchedule 支持完整星期+多时段;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-7:a 转 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-form,useFormContext 替代 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()` 包装实现请求级 memoization;P3-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 抽取的纯函数)
|
||
- Export(v3-P2-4 新增):`exportDiagnosticReportToExcel`(v4-P2-1 增强:班级报告导出新增知识点统计+需关注学生 Sheet;i18n 全覆盖)/ `buildDiagnosticReportFilename`(v4-P2 改为 async,使用 i18n 翻译文件名)
|
||
- Role-config(v4-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/getActiveStudentIdsByClassId;v2-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` prop(string | null),null 时隐藏练习按钮;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 传入 dataScope;student/diagnostic/page.tsx 传 status:"published" + dataScope;parent/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-23):parent/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-23):teacher/diagnostic/student/[studentId]/page.tsx 新增 class_taught 师生关系校验
|
||
- ✅ v4-P2 审计修复(2026-06-24):P0-1 跨模块 import WidgetBoundary → 提升到 `shared/components/widget-boundary.tsx`,diagnostic 和 grades 模块统一从 shared 引用
|
||
- ✅ v4-P2 审计修复(2026-06-24):P0-2/P0-3 教师页面标题 + error.tsx 硬编码 → 接入 `getTranslations("diagnostic")` / `useTranslations("diagnostic")`
|
||
- ✅ v4-P2 审计修复(2026-06-24):P1-1 parent 错误卡片中英文混用 → 提取完整 i18n 键 `error.childLoadFailed` / `error.childLoadFailedDesc`
|
||
- ✅ v4-P2 审计修复(2026-06-24):P1-2 `as` 类型断言 → 改用类型守卫函数 `isReportType` / `isReportStatus` 返回值收窄
|
||
- ✅ v4-P2 审计修复(2026-06-24):P1-3 分享链接指向不存在路由 → 移除分享功能(无报告详情页需求)
|
||
- ✅ v4-P2 审计修复(2026-06-24):P1-4 报告内容硬编码中文 → stats-service 接收 `ReportContentTranslations` 接口参数,actions 层从 next-intl 构建翻译注入
|
||
- ✅ v4-P2 审计修复(2026-06-24):P1-5 Excel 导出表头硬编码 → export.ts 使用 `getTranslations("diagnostic.exportContent")`,`buildDiagnosticReportFilename` 改为 async
|
||
- ✅ v4-P2 审计修复(2026-06-24):P2-1 班级报告导出缺少明细 → 利用 `classId` 字段查询 `getClassMasterySummary`,导出知识点统计 Sheet + 需关注学生 Sheet
|
||
- ✅ v4-P2 审计修复(2026-06-24):P2-2 角色差异通过 props 硬编码 → 新增 `role-config.ts`,`DIAGNOSTIC_ROLE_CONFIG` 配置对象驱动 `practiceHrefBase`,组件接受 `role` prop
|
||
- ✅ v4-P2 审计修复(2026-06-24):P2-3 无年级诊断报告入口 → 新增 `gradeId` 字段(schema + 迁移 0012)、`GradeMasterySummary` 类型、`getGradeMasterySummary` / `buildGradeMasterySummary` / `buildGradeReportContent` / `generateGradeDiagnosticReport` / `generateGradeReportAction` / `GenerateGradeReportSchema`,i18n 新增 `gradeSummary` / `gradeRecommendation` / `gradeNoWeakness`
|
||
- ✅ v4-P2 审计修复(2026-06-24):P2-4 热力图色块缺少键盘导航 → 添加 `tabIndex={0}` + `focus-visible:ring-2` 样式,外层容器 `role` 从 `img` 改为 `group`
|
||
- ✅ v2-P0-1 审计修复(2026-06-25):3 个子路由 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-25):i18n 弱项标签"弱项(<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 Actions),report-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 → 移除客户端 filter,DataScope 过滤已在 data-access 层完成
|
||
- ✅ v2-P2-3 审计修复(2026-06-25):export.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_KEYS,diagnostic/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-25):parent/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+ | 诊断报告 CRUD(v2-P2-6 校验;v2-P1-6 改用 stats-service 纯函数;v4-P1-1:getDiagnosticReports 新增 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 Action(v2-P2-3 删除 2 个死代码读 Action;v4-P1-4/v4-P1-5:publishReportAction 新增全班学生 + 家长通知;v4-P2-3 新增 generateGradeReportAction) |
|
||
| `schema.ts` | 31+ | Zod 校验(4 个 schema,v2-P2-3 删除 2 个死代码 schema;v4-P2-3 新增 GenerateGradeReportSchema) |
|
||
| `types.ts` | 109+ | 类型定义(v4-P2-3 新增 GradeMasterySummary 接口) |
|
||
| `export.ts` | 165+ | Excel 导出(v3-P2-4 新增;v4-P2-1 班级报告导出新增知识点统计+需关注学生 Sheet;i18n 全覆盖) |
|
||
| `role-config.ts` | 40+ | 角色配置驱动(v4-P2-2 新增:DIAGNOSTIC_ROLE_CONFIG 配置对象,通过 role prop 驱动 practiceHrefBase 等角色差异) |
|
||
| `components/class-diagnostic-view.tsx` | 448+ | 班级诊断视图(v2-P1-6 热力图 a11y;v2-P1-4 i18n;v4-P1-8 热力图图例;v4-P1-11 表格 overflow-x-auto;v4-P2-4 热力图色块 tabIndex+focus-visible 键盘导航) |
|
||
| `components/student-diagnostic-view.tsx` | 293+ | 学生诊断视图(v2-P1-4 i18n;v3-P2 新增:practiceHrefBase prop;v4-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 htmlFor;v2-P1-4 i18n;v4-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 + useDiagnosticService(v2-P1-4 新增:Context 注入) |
|
||
| `services/default-diagnostic-service.ts` | 86 | 默认服务实现绑定 Server Actions(v2-P1-4 新增) |
|
||
| `services/diagnostic-monitor.ts` | 95 | DiagnosticMonitor 接口 + noopDiagnosticMonitor(v2-P2-7 新增:监控埋点扩展点) |
|
||
| `services/diagnostic-monitor-context.tsx` | 56 | DiagnosticMonitorProvider + useDiagnosticMonitor(v2-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 新增:管理员系统设置 CRUD,4 分类 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 | 管理员系统设置 CRUD(P0-3 新增:4 分类 Zod 校验 + upsert) |
|
||
| `actions-security.ts` | 165 | 2FA 状态查询/切换 + 最近登录历史(P2-9 新增) |
|
||
| `actions-service.ts` | 60 | 设置页 Server Action wrapper(v1.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 表 CRUD(P0-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 Action(audit-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 + useSettingsService(Context 注入服务接口) |
|
||
| `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-25,audit-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 / logoUrl(img 或 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/actions),8 字符安全字母表(剔除 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 API(k-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 / logoUrl(img 或 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-25,audit-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`(Zod,v3 重构为 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 新增:表单状态与处理逻辑 Hook(step/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)。数据结构从 v1(blocks 数组)升级到 v2(nodes + edges 节点图),旧数据通过 `migrateV1ToV2()` 自动迁移。
|
||
|
||
> 架构变更(2026-06-23):数据结构从 v2 升级到 v3(课文锚点画布格式)。正文节点(textbook_content)作为画布中央核心,教学节点围绕正文组织。新增锚点系统(range/point 两种锚定方式)、占位符注入算法、11 种定制节点类型。旧数据通过 `migrateV2ToV3()` 自动迁移。
|
||
|
||
> 架构变更(2026-07-04,V4 纸感重构):抛弃 React Flow 画布,改用三栏布局(260px 结构树 + 1fr 纸感编辑器 + 380px 详情面板)。正文采用 Tiptap 富文本编辑器,锚点从字符串偏移升级为 Tiptap Mark(AnchorMark 区间锚/AnchorPoint 点锚),v3 旧锚点通过 AnchorMigrationBanner 提示失效。doc.version=4,editor-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`(V4:Tiptap 富文本编辑器)、`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 Mark(AnchorMark/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/fromRfEdges),data-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-23,V3 多角色课案查看):
|
||
> - **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 Flow(nodesDraggable=false, nodesConnectable=false),供学生/家长/管理员/教研组长查看已发布课案
|
||
> - **V3-3 多角色视图**:`LessonPlanCard` 新增 viewMode prop(teacher/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)` 预解析 gradeIds,parent 通过孩子的 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-plans);student 导航新增「我的课案」(/student/lesson-plans);parent 导航新增「孩子课案」(/parent/lesson-plans)
|
||
|
||
> 架构变更(2026-06-24,V3 深度审计修复):
|
||
> - **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-24,V3 深度审计修复续):
|
||
> - **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-25,V3 续审计 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-label(inline-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-25,V4 深度审计修复):
|
||
> - **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/drawer(publish-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→v2(migrateV1ToV2)/ v2→v3(migrateV2ToV3)/ 规范化(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 + getNodeColor(V3 续审计:移除 `as { html?: string; ... }` 断言,改用 isRecord/getArrayLength/getString/getNumber 安全字段提取从 `unknown` 收窄) |
|
||
| `config/block-registry.tsx` | **配置驱动**:BLOCK_REGISTRY 注册表 + BlockRenderer(V3:使用 `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 接口 + noopTracker;hooks:useLessonPlanContextSafe/useLessonPlanContext/useRoleConfig/useLessonPlanService/useLessonPlanTracker/useLessonPlanTrackerSafe |
|
||
| `providers/lesson-plan-provider-setup.tsx` | **页面层 Provider 设置包装组件(V3 续新增)**:LessonPlanProviderSetup 自动注入默认数据服务(createDefaultDataService)和角色配置(TEACHER_ROLE_CONFIG),3 个 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` | 个人模板 CRUD(V3 续审计:使用 `normalizeTemplateBlocks` 替代 `as LessonPlanTemplate["blocks"]` 断言) |
|
||
| `data-access-knowledge.ts` | 按知识点/题目反查课案(V3 续审计:移除两处 `as unknown as` 断言,改用 `getKnowledgePointIds`/`getQuestionIds` 安全字段提取辅助函数) |
|
||
| `actions.ts` | 课案 CRUD/版本/模板 Server Actions(V3:所有 action 使用 `translateFieldErrors` 翻译 Zod 错误;`duplicateLessonPlanAction` 传入 i18n 翻译的副本后缀;`getLessonPlanByIdAction` 返回类型改为 `ActionState<{ plan: LessonPlan }>`) |
|
||
| `actions-publish.ts` | 发布作业 Server Action(V3:actions 层注入 i18n 翻译的作业标题/描述/日期标签;新增 `INVALID_QUESTION_TYPE` 错误码映射) |
|
||
| `actions-ai.ts` | AI 知识点建议 Server Action(V3 续审计:显式 `unknown[]` 类型标注替代隐式 any;统一使用 `handleActionError` + `translateFieldErrors` 处理错误) |
|
||
| `actions-kp.ts` | 知识点选项 Server Action(V3 续审计:显式 `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` | **编辑器 slice(V4 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` | **选中状态 slice(V4 P2-6 新增)**:Zustand `StateCreator`,包含 selectedNodeId 状态 + selectNode 方法 |
|
||
| `hooks/version-slice.ts` | **版本与保存状态 slice(V4 P2-6 新增)**:Zustand `StateCreator`,包含 isDirty/isSaving/lastSavedAt/saveError/isOnline 状态 + hydrate/markSaved/setSaving/replaceDoc/setSaveError/setOnline 方法 |
|
||
| `hooks/history-slice.ts` | **撤销/重做 slice(V5-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 actions;V3 续审计: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` | **只读视图**(V3:MiniMap 使用 `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 新增**:师生交互 Block(designIntent + 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 个 actions;V3 续审计:通过 `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` | 教学目标 Block(V3 续:select onChange 使用 `isObjectiveDimension` 类型守卫替代 `as` 断言) |
|
||
| `components/blocks/key-point-block.tsx` | 重难点 Block(V3 续:select onChange 使用 `isKeyPointType` 类型守卫替代 `as` 断言) |
|
||
| `components/blocks/import-block.tsx` | 导入 Block(V3 续:select onChange 使用 `isImportMethod` 类型守卫替代 `as` 断言) |
|
||
| `components/blocks/new-teaching-block.tsx` | 新授 Block |
|
||
| `components/blocks/summary-block.tsx` | 总结 Block |
|
||
| `components/blocks/homework-block.tsx` | 作业 Block(V3 续:select onChange 使用 `isHomeworkType` 类型守卫替代 `as` 断言) |
|
||
| `components/blocks/blackboard-block.tsx` | 板书 Block(V3 续:select onChange 使用 `isBlackboardLayout` 类型守卫替代 `as` 断言) |
|
||
| `components/blocks/exercise-block.tsx` | 练习 Block(V3 续:select onChange 使用 `isExercisePurpose` 类型守卫替代 `as` 断言) |
|
||
| `components/blocks/reflection-block.tsx` | 反思 Block(V3 续: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 Actions(2026-07-01 新增)** |
|
||
| `actions-review.ts` | **M3 审核工作流 Server Actions(2026-07-01 新增)** |
|
||
| `actions-comments.ts` | **M2 评论 Server Actions(2026-07-01 新增)** |
|
||
| `actions-formative.ts` | **M5 形成性评价 Server Actions(2026-07-01 新增)** |
|
||
| `actions-attachments.ts` | **M6 附件库 Server Actions(2026-07-01 新增)** |
|
||
| `actions-substitutes.ts` | **M12 代课教师 Server Actions(2026-07-01 新增)** |
|
||
| `actions-ai-evaluation.ts` | **M8 AI 评估 Server Actions(2026-07-01 新增)** |
|
||
| `actions-analytics.ts` | **M10 分析仪表盘 Server Actions(2026-07-01 新增)** |
|
||
|
||
> 架构变更(2026-07-01,M1-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.ts,4 种互动类型
|
||
> - **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.ts,3 个分析维度
|
||
> - **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-03,V5 场景缺口修复第一阶段):
|
||
> - **V5-1 自动保存失败 UI 兜底**:version-slice.ts 新增 saveError/isOnline 状态;lesson-plan-editor.tsx 监听 online/offline 事件 + toast 提示 + 重试按钮
|
||
> - **V5-2 撤销/重做**:新增 hooks/history-slice.ts(past/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-access);LessonPlanDataService 接口新增 getLessonPlanAttachments/createLessonPlanAttachment/deleteLessonPlanAttachment 三个方法;新增依赖 @tiptap/extension-image;FileTargetType 新增 "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-03,V5 场景缺口修复第二阶段 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.tsx;actions.ts 新增 duplicateLessonPlanFormAction(FormData 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.tsx;version-history-drawer.tsx 集成版本对比(currentDoc prop + 对比按钮)
|
||
> - **V5-17 A1/A2 AI 反馈闭环**:新增 lib/ai-feedback.ts(generateLessonPlanFeedback 纯服务端函数,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.ts(3 个纯服务端函数:generateDifferentiationSuggestions/checkCurriculumAlignment/generateExplainableAssessment,复用 ai-feedback.ts 的 createAiChatCompletion + Zod + JSON 提取模式);actions-ai.ts 新增 4 个 server actions(generateDifferentiationSuggestionsAction/checkCurriculumAlignmentAction/generateExplainableAssessmentAction/getKnowledgePointsForAlignmentAction);新增 components/ai-differentiation-dialog.tsx(3 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-04,V4 纸感重构 — 抛弃 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 读取 localStorage(dismiss 状态按 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` 接入真实 action;i18n 新增 `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/aiFailed)(zh-CN + en 同步);P1 修复 en locale 下大面积报错:en `calendar` 节键名对齐 zh-CN(weekView/monthView/loadFailed + prev/next/eventMeta/weekDays 子节),en `analytics` 节补齐 totalPublished/totalSubmitted/totalStandardsLinked/loadFailed;P2 修复 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/gradeHead,analytics 因被 `app/(dashboard)/admin/curriculum-map` 引用而保留);`lib/i18n-errors.ts` 新增 `t.has(msg)` 运行时守卫,避免 `as` 断言绕过类型检查(键不存在时返回原消息而非抛出 MISSING_MESSAGE);zh-CN 与 en 顶层键集一致性验证通过(43 = 43);005_architecture_data.json 同步追加 `V4-I18N-1` auditFixes 条目;known-issues.md 追加 3 个 i18n 规则表(键缺失与中英文不同步 / dead 节清理规则 / 动态键翻译守卫)
|
||
|
||
---
|
||
|
||
## 2.27b standards(课标模块,2026-07-01 新增)
|
||
|
||
**职责**:K12 课标管理,支持课标树、按学科+年级筛选、课案与课标多对多关联。
|
||
|
||
**导出函数**:
|
||
- Data-access:getStandards / getStandardsTree / getStandardById / createStandard / updateStandard / deactivateStandard / searchStandards / getStandardsByPlanId / linkPlanToStandard / unlinkPlanFromStandard
|
||
- Actions:10 个 Server Actions,全部使用 requirePermission() + ActionState<T>
|
||
- Schema:createStandardSchema / updateStandardSchema / getStandardsParamsSchema / linkPlanToStandardSchema
|
||
- Types:Standard / StandardTreeNode / LessonPlanStandardLink
|
||
|
||
**权限点**:
|
||
- STANDARD_READ(standard:read)
|
||
- STANDARD_MANAGE(standard:manage)
|
||
- STANDARD_LINK(standard: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 | 学生端 CRUD(10 个函数)+ 共享工具函数(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 | 班级错题数对比柱状图(recharts,tooltip 显示学生数/人均/掌握率) |
|
||
| `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 ai(AI 模块)— ✅ 新增 / 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` | 通知实时推送(SSE,15s 心跳 + 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 对话 Hook(SSE + 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` | 位置持久化 Hook(localStorage + resize 校正)— V3 新增 |
|
||
| **Hook** | `useDragPosition` | `modules/ai/hooks/use-drag-position.ts` | 拖拽位置 Hook(pointer 事件 + 拖拽状态)— 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 新增 |
|
||
| **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/5xx,2 次重试,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/AiClientService,V3 新增 knowledgeGraph/textbookId) |
|
||
| `modules/ai/schema.ts` | ~230 | Zod 验证 schema(8 个输入 + 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` | ~170 | 全局 AI 助手悬浮按钮 — V2 新增 / V4 i18n 修复 |
|
||
| `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 对话 Hook(V3 拆分至 ≤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()` 调用 actions,a11y 增强,原生 input → shadcn Checkbox,reason 改为枚举值) |
|
||
| **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 联合类型等;V4:WeakChapterSourceMeta.chapterId 改为可选) |
|
||
| `modules/adaptive-practice/schema.ts` | ~75 | Zod 验证 schema(V4:discriminatedUnion) |
|
||
| `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 | 教师/年级宏观数据分析查询(V4:N+1 优化,单 SQL GROUP BY) |
|
||
| `modules/adaptive-practice/actions.ts` | ~267 | 7 个 Server Actions(V4:i18n 错误码返回 + 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 | 练习发起器(V4:usePracticeService 注入 + 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` | - | 教师端错误边界(V4:i18n 化) |
|
||
| `app/(dashboard)/management/grade/practice/page.tsx` | ~140 | 年级主任宏观数据分析页 |
|
||
| `app/(dashboard)/management/grade/practice/loading.tsx` | - | 年级端加载骨架 |
|
||
| `app/(dashboard)/management/grade/practice/error.tsx` | - | 年级端错误边界(V4:i18n 化) |
|
||
| `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.*`(发起器;V4:a11y 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 关键操作埋点接口(trackPermissionChange,no-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`(嵌入 RegisterSchema,8-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 Actions(generate/list/delete/validate,USER_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/rejected,rejected 必填 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/owned;class_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 Action(create/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_READ;DataScope.children;表单 childOptions 模式 + 列表)
|
||
- `/student/leave` — 学生请假页(LEAVE_REQUEST_CREATE + LEAVE_REQUEST_READ;DataScope.class_members 由 currentUserId 过滤;表单 defaultStudentId/classId 模式 + 列表)
|
||
- `/teacher/leave` — 教师审批页(LEAVE_REQUEST_REVIEW;DataScope.class_taught;LeaveReviewList + LeaveReviewDialog)
|
||
|
||
**设计要点**:
|
||
- ✅ **幂等考勤同步**:`syncExcusedFromLeaveRequest` 遍历日期区间,仅插入不存在的 excused 记录,避免重复同步;`attendanceSynced` 标志位防止审批重复触发
|
||
- ✅ **跨模块联动容错**:审批通过后调用考勤同步使用 try/catch 包裹,同步失败不阻断审批结果(审批已成功,考勤可由管理员后续手动补录)
|
||
- ✅ **日期重叠校验**:`hasOverlappingLeave` 使用 SQL 条件检测区间重叠,防止重复请假
|
||
- ✅ **DataScope 行级过滤**:`buildScopeFilter` 支持 5 种 scope(all/class_taught/children/class_members/owned),class_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(kw, limit?)` — 题库检索:`CAST(content AS CHAR) LIKE` 模糊匹配 JSON 字段,返回 `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 字段、type、createdAt)
|
||
- `textbooks`(title、subject、grade、publisher、createdAt)
|
||
- `exams`(title、description、status、createdAt)
|
||
- `announcements`(title、content、type、status、createdAt)
|
||
|
||
**文件清单**:
|
||
| 文件 | 行数 | 职责 |
|
||
|------|------|------|
|
||
| `data-access.ts` | 212 | 4 个 search 函数 + 3 个内部纯函数(extractTextFromJson/stripHtml/truncate)+ 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-2:shared/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-3:dashboard 跨模块直接查询 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-4:messaging 绕过 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 → notifications(messaging 调用 `sendNotification` dispatcher 发送通知)
|
||
|
||
### P0-5:classSchedule 表三处写入口 ✅ 已修复
|
||
|
||
- ~~`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-6:proctoring 死代码与重复实现
|
||
|
||
- `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-7:proxy.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-8:list-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-2:actions 层混入数据访问逻辑 ✅ 已修复
|
||
|
||
**已完成修复**(2026-06-17,commit 84d6636):4 个模块的 actions 层 DB 操作全部下沉到 data-access:
|
||
|
||
| 模块 | 问题 Action | 修复内容 |
|
||
|------|------------|---------|
|
||
| exams | `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` | ✅ 新增 7 个 data-access 函数(getExamCreatorId/updateExamWithQuestions/deleteExamById/duplicateExam/getExamPreview/getExamSubjects/getExamGrades),actions.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.ts),P0-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/getKnowledgePointOptions),actions.ts 294→149 行,data-access.ts 138→260 行;✅ 审计新增 3 个级联 data-access 函数(getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter)+ 3 个对应 Server Action + 批量删除/导入导出 data-access 与 Action,actions.ts 149→380+ 行,data-access.ts 260→600+ 行 |
|
||
| announcements | 所有写操作 Action | ✅ 新增 5 个 data-access 函数(insertAnnouncement/updateAnnouncementById/deleteAnnouncementById/publishAnnouncementById/archiveAnnouncementById),actions.ts 242→197 行,data-access.ts 120→171 行 |
|
||
|
||
**剩余未修复模块**(不在本次 P1-2 范围):
|
||
- ✅ users:~~`updateUserProfileAction` 直接 db.update~~ 已下沉到 data-access(P1-1 修复)
|
||
- ✅ scheduling:~~`applyAutoScheduleAction` / `autoScheduleAction` 直接 db.transaction + db.select~~ 已改为调用 `replaceClassSchedule` 统一写入口;`autoScheduleAction` 直查 users 表已改为通过 users data-access(P0-5 / P1-1 修复)
|
||
|
||
### P1-3:auth.ts 混合 5 类职责 ✅ 已完成
|
||
|
||
`src/auth.ts` 原 293 行混合:NextAuth 配置 + 密码安全 DB 操作 + 角色规范化 + IP 解析 + 回调函数。
|
||
|
||
**已完成拆分**(auth.ts 现 193 行,仅保留 NextAuth 配置):
|
||
- ✅ 密码安全 DB 操作 → `shared/lib/password-security-service.ts`(getOrCreatePasswordSecurity / recordFailedLogin / resetFailedLogin,server-only)
|
||
- ✅ 角色规范化 → `shared/lib/role-utils.ts`(normalizeRole / resolvePrimaryRole,纯函数)
|
||
- ✅ bcrypt 哈希规范化 → `shared/lib/bcrypt-utils.ts`(normalizeBcryptHash,纯函数)
|
||
- ✅ IP 解析 → `shared/lib/http-utils.ts`(resolveClientIp,server-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-4:users/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-5:notifications 反向依赖 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.error;conditions 隐式 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-export,4 个页面改为从 `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 获取 session,dynamic 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.ts,classes 模块仅保留读函数)
|
||
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/lib,auth.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 修复:通知表所有权迁移至 notifications,in-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-2,commit 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/finally(20+ 处,使用 `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 | - | - | - | - | - | - | - | ✅scheduling(P0-5:data-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 | - | - | - | - | - | - | - | - | ✅rbac(UserRoleAssignDialog 组件) |
|
||
| **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/notifications(v2-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_BIND;audit-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 → BigInt(parseBase36ToBigInt),避免 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
|
||
```
|
||
|
||
### 业务模块核心 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`。
|