diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ef3be93 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,87 @@ +# 文档索引 + +> Next_Edu 项目文档总索引。按用途分类,标注维护状态。 +> 活跃文档随代码同步维护;已归档文档仅保留历史参考。 + +--- + +## 架构文档(活跃维护) + +| 文档 | 用途 | +|------|------| +| [001 项目概览](architecture/001_project_overview.md) | 项目入口概览:技术栈、角色权限、模块、数据库、路由、架构原则、项目状态 | +| [004 架构影响地图](architecture/004_architecture_impact_map.md) | 全模块·全函数·全参数级别架构图(人类可读) | +| [005 架构数据](architecture/005_architecture_data.json) | AI 友好的结构化架构数据(JSON) | +| [006 功能清单](architecture/006_k12_feature_checklist.md) | 企业级 K12 标准功能模块清单(P0/P1/P2 优先级) | +| [007 差距审计报告](architecture/007_gap_audit_report.md) | 功能差距审计与补齐路线图 | + +## 架构审查报告 + +| 文档 | 用途 | +|------|------| +| [审查汇总](architecture/audit/00_summary.md) | 全项目架构审查汇总报告 | +| [shared 层审查](architecture/audit/shared-audit.md) | 共享基础设施层审查 | +| [核心业务模块审查](architecture/audit/core-business-audit.md) | exams/homework/questions/textbooks 等核心模块审查 | +| [管理模块群审查](architecture/audit/management-modules-audit.md) | school/classes/users/audit 等管理模块审查 | +| [新增模块和其他模块审查](architecture/audit/new-and-other-modules-audit.md) | diagnostic/elective/proctoring/notifications 等新增模块审查 | + +## 专题文档(活跃维护) + +| 文档 | 用途 | +|------|------| +| [无障碍审计](accessibility/a11y-audit.md) | WCAG 2.1 AA 合规审计报告 | +| [视觉回归测试](testing/visual-regression.md) | Playwright 视觉回归测试方案 | +| [通知渠道](notifications/channels.md) | 多渠道通知(站内/短信/微信/邮件)集成文档 | +| [安全扫描](security/scanning.md) | 依赖审计/Snyk/Trivy/OWASP ZAP 安全扫描指南 | +| [灾备计划](dr/dr-plan.md) | 灾难恢复计划(RTO/RPO 目标) | +| [灾备操作手册](dr/dr-runbook.md) | 生产环境故障处理操作手册 | +| [数据库 Schema 变更日志](db/schema-changelog.md) | 数据库迁移变更记录 | + +## 工作日志 + +| 文档 | 用途 | +|------|------| +| [工作日志](work_log.md) | 项目开发进度日志 | + +## 脚本 + +| 文档 | 用途 | +|------|------| +| [考试种子数据脚本](scripts/seed-exams.ts) | 考试模块测试数据生成脚本 | + +--- + +## 已归档文档 + +> 以下文档记录的是历史阶段的设计/实现/分析,当前已由架构文档(004/005/006/007)取代。 +> 保留用于历史参考,不再维护。 + +### 架构历史文档 + +| 文档 | 归档原因 | +|------|---------| +| [002 RBAC 重构方案](architecture/002_rbac_refactoring.md) | 描述修复前的安全隐患,当前所有 Server Action 已接入 `requirePermission()` | +| [002 角色路由 RFC](architecture/002_role_based_routing.md) | 2025-12-23 提案,当前角色域路由已全部实现 | +| [003 UI 重构计划](architecture/003_ui_refactoring_plan.md) | 2026-06-16 重构计划,当前已执行完毕 | + +### 设计历史文档 + +| 文档 | 归档原因 | +|------|---------| +| [002 教师仪表盘实现](design/002_teacher_dashboard_implementation.md) | 2025-12-23 实现记录,已由 004 dashboard 模块章节取代 | +| [003 教材模块实现](design/003_textbooks_module_implementation.md) | 2025-12-23 实现记录,已由 004 textbooks 模块章节取代 | +| [004 题库模块实现](design/004_question_bank_implementation.md) | 2025-12-23 实现记录,已由 004 questions 模块章节取代 | +| [005 考试模块实现](design/005_exam_module_implementation.md) | 考试模块实现设计,已由 004 exams 模块章节取代 | +| [006 作业模块实现](design/006_homework_module_implementation.md) | 2025-12-31 实现记录,已由 004 homework 模块章节取代 | +| [008 教师页面实现](design/008_teacher_pages_implementation.md) | 2026-03-03 页面分析,路由已大幅扩展 | +| [009 功能差距分析](design/009_feature_gap_analysis.md) | 2026-03-03 功能对比,已由 007 差距审计报告取代 | +| [010 QA 测试计划](design/010_qa_test_plan_and_feedback.md) | 2026-03-18 测试方案,测试体系已演进 | + +--- + +## 文档维护规则 + +1. **改码必同步图**:源码修改后须同步更新 004/005 架构文档 +2. **归档不删除**:过时文档添加归档标注,不删除 +3. **活跃文档**:001/004/005/006/007 + 专题文档随代码同步维护 +4. **新增文档**:新增文档须在本索引中登记 diff --git a/docs/architecture/001_project_overview.md b/docs/architecture/001_project_overview.md index 8eb9bb7..9a8e3da 100644 --- a/docs/architecture/001_project_overview.md +++ b/docs/architecture/001_project_overview.md @@ -1,6 +1,7 @@ -# Next_Edu 项目架构分析 +# Next_Edu 项目概览 -> 本文档基于源码逆向分析生成,未参考任何已有文档。 +> 本文档是项目的入口概览,基于源码与架构影响地图(004/005)维护。 +> 详细模块/函数/路由/表结构请查阅 [004 架构影响地图](./004_architecture_impact_map.md) 与 [005 架构数据](./005_architecture_data.json)。 --- @@ -9,10 +10,10 @@ **Next_Edu** 是一个面向 K12 场景的**智慧教务管理系统**,旨在将传统学校的教务流程数字化、智能化。 核心目标: -1. **多角色协同**:管理员、教师、学生、家长四种角色在同一平台协作,各角色有独立的仪表盘和功能入口 +1. **多角色协同**:6 种角色(管理员、教师、学生、家长、年级组长、教研组长)在同一平台协作,各角色有独立的仪表盘和功能入口 2. **AI 赋能教学**:集成大语言模型(智谱/OpenAI/Gemini),支持 AI 自动生成试卷、AI 重写题目、AI 对话辅导 3. **全流程闭环**:从教材管理 → 题库建设 → 试卷组卷 → 考试/作业发布 → 学生作答 → 教师批改 → 数据分析,覆盖教学评估全链路 -4. **知识体系结构化**:教材章节与知识点树形关联,知识点与题目双向链接,支撑精准教学 +4. **知识体系结构化**:教材章节与知识点树形关联,知识点与题目双向链接,支撑精准教学与学情诊断 --- @@ -43,23 +44,28 @@ | 角色 | 路由前缀 | 核心职责 | |------|---------|---------| -| **Admin** | `/admin/*` | 学校管理、年级/班级/部门配置、用户管理、全局设置 | -| **Teacher** | `/teacher/*` | 教材管理、题库建设、试卷组卷、作业发布与批改、班级管理 | -| **Student** | `/student/*` | 课程学习、作业作答、教材阅读、课表查看 | -| **Parent** | `/parent/*` | 子女学情查看、缴费、消息沟通 | +| **Admin** | `/admin/*` | 学校管理、年级/班级/部门配置、用户管理、全局设置、审计日志、排课、选修课 | +| **Teacher** | `/teacher/*` | 教材管理、题库建设、试卷组卷、作业发布与批改、班级管理、考勤、成绩录入、学情诊断 | +| **Student** | `/student/*` | 课程学习、作业作答、教材阅读、课表查看、成绩查询、选课、学情诊断 | +| **Parent** | `/parent/*` | 子女学情查看、成绩/考勤/课表、消息沟通 | +| **Grade Head** (年级组长) | 映射为 `/teacher/*` | 年级管理、年级作业洞察、学情诊断 | +| **Teaching Head** (教研组长) | 映射为 `/teacher/*` | 教研管理、学情诊断 | ### 权限控制机制 ``` 请求 → NextAuth Middleware (proxy.ts) → 检查 session 是否存在 → 无则重定向 /login - → 解析 JWT 中的 role 字段 - → 按路由前缀校验角色匹配 + → 解析 JWT 中的 role/permissions 字段 + → 按路由前缀校验角色与权限点匹配 → 不匹配则重定向到角色首页 ``` -- **角色解析**:`grade_head` 和 `teaching_head` 映射为 `teacher`;多角色用户取优先级 `admin > teacher > parent > student` +- **权限点**:共 **54 个权限点**(`exam:create`、`homework:grade`、`announcement:manage`、`file:upload`、`grade_record:manage`、`attendance:manage`、`message:send`、`schedule:auto`、`elective:select`、`exam:proctor`、`diagnostic:manage` 等),定义于 `src/shared/types/permissions.ts` +- **角色-权限映射**:`ROLE_PERMISSIONS` 常量(`src/shared/lib/permissions.ts`),6 角色到权限列表的映射 +- **角色解析**:`grade_head` 和 `teaching_head` 在路由层映射为 `teacher`;多角色用户取优先级 `admin > teacher > parent > student` - **用户-角色**:多对多关系(`users_to_roles` 表),支持一人多角色 +- **数据范围控制(DataScope)**:6 种行级权限类型 — `all` / `owned` / `class_taught` / `grade_managed` / `class_members` / `children` - **导航隔离**:`NAV_CONFIG` 按角色定义侧边栏菜单,不同角色看到完全不同的功能入口 --- @@ -72,13 +78,15 @@ School ──1:N──→ Grade ──1:N──→ Class ──1:N──→ ClassEnrollment ←──N:1── User(student) │ │ │ ├── ClassSubjectTeacher (班级-科目-教师) - │ └── ClassSchedule (课表) + │ ├── ClassSchedule (课表) + │ └── AttendanceRecords (考勤) │ ├── Department ├── Classroom └── AcademicYear -User ──M:N──→ Role (RBAC) +User ──M:N──→ Role (RBAC) ──→ Permissions (54 个权限点) +User ──M:N──→ ParentStudentRelation (家长-子女) Textbook ──1:N──→ Chapter (树形嵌套) ──1:N──→ KnowledgePoint (树形嵌套) │ @@ -90,27 +98,52 @@ Question ──M:N──→ KnowledgePoint │ │ Exam ──M:N──→ Question (exam_questions) │ ├── ExamSubmission ──1:N──→ SubmissionAnswer │ - └── structure (JSON 层级结构) │ + ├── structure (JSON 层级结构) │ + └── ExamProctoringEvent (监考事件) │ │ HomeworkAssignment ──M:N──→ Question │ ├── sourceExamId → Exam (作业源自试卷) │ ├── HomeworkAssignmentTarget (指定学生) │ └── HomeworkSubmission ──1:N──→ HomeworkAnswer │ │ +GradeRecord (成绩) ──→ Student/Class/Subject/Exam │ +CoursePlan ──1:N──→ CoursePlanItem (课程计划) │ + │ +Announcement (公告) ──→ Grade/Class 定向发布 │ +Message (站内消息) ──→ MessageNotification │ +AuditLog / LoginLog / DataChangeLog (审计日志) │ + │ +ElectiveCourse ──1:N──→ CourseSelection (选课) │ +LearningDiagnosticReport (学情诊断报告) │ +KnowledgePointMastery (知识点掌握度) │ + │ AIProvider (zhipu / openai / gemini / custom) │ └── apiKeyEncrypted (AES 加密存储) │ ``` ### 数据库规模 -- **20+ 张表**,覆盖用户认证、RBAC、学校管理、教学资源、考试系统、作业系统、AI 配置 +- **54 张表**,覆盖用户认证、RBAC、学校管理、教学资源、考试系统、作业系统、AI 配置、审计日志、公告、文件、成绩、课程计划、消息、考勤、排课、选修课、学情诊断、监考 - **ID 策略**:CUID2(`@paralleldrive/cuid2`),128 位 varchar - **索引策略**:所有外键和查询字段均有索引,支持级联删除 +- **Schema 文件**:`src/shared/db/schema.ts`(单文件,54 张表定义) --- ## 五、架构设计 +### 架构原则 + +1. **分层架构**:`app → modules → shared`,三层单向依赖 + - `src/app/`:Next.js App Router 路由层,负责路由分发、页面组装、Suspense/error 边界 + - `src/modules/`:业务模块层,每个模块是一个独立的业务领域 + - `src/shared/`:共享基础设施层,提供数据库、工具函数、权限系统、UI 基础组件 +2. **模块化**:每个模块对应一个业务领域,包含 `components/`、`hooks/`、`actions.ts`、`data-access.ts`、`types.ts`、`schema.ts` +3. **权限校验**:所有 Server Action 必须使用 `requirePermission()` 或 `requireAuth()` 进行权限校验,前端组件使用 `usePermission().hasPermission()` 条件渲染(禁止 `role === "xxx"` 硬编码) +4. **数据访问**:通过 `data-access.ts` 层访问数据库,Server Action 不直接调用 Drizzle ORM +5. **类型安全**:TypeScript strict 模式,Zod schema 校验输入,`ActionState` 统一返回结构 +6. **行级权限**:`DataScope` 提供 6 种数据范围类型,data-access 层按 scope 过滤数据 + ### 分层架构 ``` @@ -121,7 +154,7 @@ AIProvider (zhipu / openai / gemini / custom) │ │ ├── loading.tsx (Suspense 边界) │ │ └── error.tsx (错误边界) │ ├─────────────────────────────────────────────────┤ -│ Feature Modules (src/modules/) │ +│ Feature Modules (src/modules/) — 26 个模块 │ │ ├── auth/ 认证模块 │ │ ├── exams/ 考试模块 │ │ ├── homework/ 作业模块 │ @@ -132,24 +165,45 @@ AIProvider (zhipu / openai / gemini / custom) │ │ ├── dashboard/ 仪表盘模块 │ │ ├── layout/ 布局与导航 │ │ ├── settings/ 设置模块 │ -│ └── users/ 用户管理模块 │ +│ ├── users/ 用户管理模块 │ +│ ├── audit/ 审计日志模块 │ +│ ├── announcements/ 公告模块 │ +│ ├── files/ 文件管理模块 │ +│ ├── grades/ 成绩管理模块 │ +│ ├── course-plans/ 课程计划模块 │ +│ ├── messaging/ 站内消息模块 │ +│ ├── notifications/ 通知模块 │ +│ ├── attendance/ 考勤模块 │ +│ ├── scheduling/ 排课模块 │ +│ ├── diagnostic/ 学情诊断模块 │ +│ ├── elective/ 选课模块 │ +│ ├── proctoring/ 考试监考模块 │ +│ ├── parent/ 家长端模块 │ +│ ├── student/ 学生端模块 │ │ 每个模块: │ │ ├── components/ UI 组件 │ │ ├── hooks/ 自定义 Hook │ │ ├── actions.ts Server Actions │ │ ├── data-access.ts 数据访问层 │ +│ ├── schema.ts Zod 校验 Schema │ │ └── types.ts 类型定义 │ ├─────────────────────────────────────────────────┤ │ Shared (src/shared/) │ │ ├── components/ui/ shadcn/ui 基础组件 (30+) │ │ ├── hooks/ 通用 Hook │ -│ ├── lib/ 工具函数 │ +│ ├── lib/ 工具函数 (auth-guard/ai/ │ +│ │ excel/file-storage/ │ +│ │ rate-limit/password-policy)│ │ ├── db/ 数据库 (schema/relations) │ -│ └── types/ 公共类型 │ +│ └── types/ 公共类型 (permissions/ │ +│ action-state) │ ├─────────────────────────────────────────────────┤ │ API Routes (src/app/api/) │ │ ├── auth/[...nextauth] NextAuth 端点 │ │ ├── ai/chat AI 对话 │ +│ ├── upload / files 文件上传与管理 │ +│ ├── search 全局全文检索 │ +│ ├── export / import Excel 导入导出 │ │ └── onboarding/* 用户引导 │ └─────────────────────────────────────────────────┘ ``` @@ -159,7 +213,8 @@ AIProvider (zhipu / openai / gemini / custom) │ ``` 用户操作 → Client Component → Server Action (actions.ts) - → 数据访问层 (data-access.ts) + → requirePermission() 权限校验 + → 数据访问层 (data-access.ts) 按 DataScope 过滤 → Drizzle ORM (db/index.ts) → MySQL @@ -216,26 +271,112 @@ AI 模式: 选择 AI Provider → 粘贴试卷源文本 → AI 解析生成 → → 已引导: 进入角色仪表盘 ``` +### 5. 学情诊断流程 + +``` +教师/管理员: 基于作业/考试数据 → 生成知识点掌握度 → 生成诊断报告 + ├── 个人报告: 学生知识点雷达图 + 强项/弱项 + └── 班级报告: 知识点热力图 + 需重点关注学生 +学生: 查看本人学情诊断报告 +``` + --- -## 七、部署与运维 +## 七、路由清单 + +### 路由分布 + +| 角色域 | 路由前缀 | 主要页面 | +|--------|---------|---------| +| **认证** | `/login`, `/register`, `/privacy`, `/terms` | 登录、注册、隐私政策、用户协议 | +| **Admin** | `/admin/*` | 仪表盘、学校管理(学校/年级/部门/班级/学年)、审计日志、公告、文件、课程计划、考勤、用户导入、排课(规则/自动/调课)、选修课 | +| **Teacher** | `/teacher/*` | 仪表盘、考试(列表/创建/构建)、题库、教材(列表/阅读)、班级(我的/课表/学生/详情)、作业(列表/创建/详情/提交/批改)、成绩(列表/录入/统计)、课程计划、考勤(列表/点名/统计)、调课申请、学情诊断(列表/学生/班级)、选修课 | +| **Student** | `/student/*` | 仪表盘、学习(作业/课程/教材)、课表、成绩、考勤、学情诊断、选课 | +| **Parent** | `/parent/*` | 仪表盘、子女详情、成绩、考勤 | +| **Management** | `/management/*` | 年级班级管理、年级作业洞察(年级组长) | +| **共享** | `/dashboard`, `/profile`, `/settings`, `/announcements`, `/messages/*` | 角色路由分发、个人资料、设置、公告列表、消息(列表/详情/写消息) | + +### API 路由 + +| 路由 | 方法 | 用途 | +|------|------|------| +| `/api/auth/[...nextauth]` | GET/POST | NextAuth 认证端点 | +| `/api/ai/chat` | POST | AI 对话(流式响应) | +| `/api/upload` | POST | 文件上传 | +| `/api/files/[id]` | GET/DELETE | 文件元数据查询/删除 | +| `/api/files/batch-delete` | POST | 批量删除文件 | +| `/api/search` | GET | 全局全文检索 | +| `/api/export` | POST | Excel 导出 | +| `/api/import` | POST | Excel 解析预览 | +| `/api/onboarding/*` | GET/POST | 用户引导 | + +--- + +## 八、部署与运维 - **构建模式**: `output: "standalone"` — 适配 Docker 容器化部署 - **数据库迁移**: Drizzle Kit (`db:generate` / `db:migrate`) - **数据填充**: `db:seed` 脚本 + `@faker-js/faker` -- **CI/CD**: `.gitea/workflows/ci.yml` — Gitea Actions +- **CI/CD**: `.gitea/workflows/ci.yml` — Gitea Actions(构建/部署/测试) +- **安全扫描**: `.gitea/workflows/security.yml` — npm audit + Snyk + Trivy + OWASP ZAP +- **数据备份**: `.gitea/workflows/ci.yml` scheduled-backup — 每日全量备份 + 异地同步 +- **灾备演练**: `.gitea/workflows/dr-drill.yml` — 每周一 DR 演练 - **测试**: 单元测试 (Vitest) + 集成测试 + E2E (Playwright) --- -## 八、项目规模统计 +## 九、项目状态 + +> 详细功能清单与差距审计请查阅 [006 功能清单](./006_k12_feature_checklist.md) 与 [007 差距审计报告](./007_gap_audit_report.md)。 + +### 优先级完成情况 + +| 优先级 | 状态 | 说明 | +|--------|------|------| +| **P0** (MVP 必须) | ✅ 已完成 | 核心业务、平台基础、合规安全 P0 项已全部实现 | +| **P1** (上线前推荐) | ✅ 已完成 | 站内消息、家长仪表盘、考勤、排课、成绩分析、Excel 导入导出、密码安全、数据变更日志等 P1 项已实现 | +| **P2** (迭代优化) | 🔄 8/14 完成 (57%) | 已完成:选课管理、考试监考、学情诊断、漏洞扫描、灾备方案、视觉回归测试等;剩余:国际化、多租户、AI 批改/学情/备课等 | + +### 主要模块完成情况 + +- ✅ 用户与权限(6 角色、54 权限点、DataScope 行级权限) +- ✅ 学校管理(学校/年级/班级/部门/学年/学科) +- ✅ 教务排课(课程计划、排课规则、自动排课、调课审批、冲突检测) +- ✅ 教材资源(教材/章节/知识点树形结构、知识图谱) +- ✅ 题库与试卷(5 种题型、手动/AI 组卷、试卷预览) +- ✅ 作业与考试(作业发布/作答/批改、统计分析) +- ✅ 成绩分析(录入/查询/统计报表/趋势/对比/导出) +- ✅ 家校沟通(公告、站内消息、家长仪表盘) +- ✅ AI 赋能(AI 对话、AI 出题、多模型配置、API Key 加密) +- ✅ 考勤管理(学生考勤、考勤规则、统计) +- ✅ 日志审计(操作日志、登录日志、数据变更日志) +- ✅ 文件管理(上传/预览/删除、StorageProvider 抽象) +- ✅ 学情诊断(知识点掌握度、个人/班级诊断报告) +- ✅ 选课管理(选修课、选课/退选、抽签模式) +- ✅ 平台基础(全局搜索、Excel 导入导出、通知偏好、深色主题) + +--- + +## 十、项目规模统计 | 维度 | 数量 | |------|------| -| 数据库表 | 20+ | -| 业务模块 | 11 | +| 数据库表 | 54 | +| 业务模块 | 26 | | UI 基础组件 | 30+ | -| 路由页面 | 30+ | -| Server Actions | 50+ | -| API 路由 | 4 | -| 用户角色 | 4 (admin/teacher/student/parent) | +| 路由页面 | 60+ | +| API 路由 | 9 | +| Server Actions | 80+ | +| 用户角色 | 6 (admin/teacher/student/parent/grade_head/teaching_head) | +| 权限点 | 54 | + +--- + +## 相关文档 + +| 文档 | 用途 | +|------|------| +| [004 架构影响地图](./004_architecture_impact_map.md) | 全模块·全函数·全参数级别架构图 | +| [005 架构数据](./005_architecture_data.json) | AI 友好的结构化架构数据 | +| [006 功能清单](./006_k12_feature_checklist.md) | 企业级 K12 标准功能模块清单 | +| [007 差距审计报告](./007_gap_audit_report.md) | 功能差距审计与补齐路线图 | diff --git a/docs/architecture/002_rbac_refactoring.md b/docs/architecture/002_rbac_refactoring.md index 296ae11..165006e 100644 --- a/docs/architecture/002_rbac_refactoring.md +++ b/docs/architecture/002_rbac_refactoring.md @@ -1,3 +1,10 @@ +> ⚠️ **已归档文档** +> 本文档记录的是 2026-06 初的 RBAC 权限体系重构方案,描述的是修复前的安全隐患(如 exams/actions.ts 中的硬编码 getCurrentUser 存根)。 +> 当前已由 [004 架构影响地图](./004_architecture_impact_map.md) 与 [005 架构数据](./005_architecture_data.json) 取代——所有 Server Action 已接入 `requirePermission()`/`requireAuth()`,54 个权限点与 6 角色映射已落地。 +> 保留用于历史参考,不再维护。 + +--- + # 企业级权限体系重构方案 > 基于源码逆向分析,覆盖 proxy.ts / auth.ts / 各模块 actions.ts / 前端组件 diff --git a/docs/architecture/002_role_based_routing.md b/docs/architecture/002_role_based_routing.md index 60ac329..98f6dd7 100644 --- a/docs/architecture/002_role_based_routing.md +++ b/docs/architecture/002_role_based_routing.md @@ -1,3 +1,10 @@ +> ⚠️ **已归档文档** +> 本文档是 2025-12-23 提出的角色路由 RFC(Status: PROPOSED),描述的是路由策略提案。 +> 当前该提案已全部实现——`/admin`、`/teacher`、`/student`、`/parent`、`/management` 角色域路由已落地,详见 [004 架构影响地图](./004_architecture_impact_map.md) 的 routes 章节。 +> 保留用于历史参考,不再维护。 + +--- + # Architecture RFC: Role-Based Routing & Directory Structure **Status**: PROPOSED diff --git a/docs/architecture/003_ui_refactoring_plan.md b/docs/architecture/003_ui_refactoring_plan.md index 055fff6..c93f4a4 100644 --- a/docs/architecture/003_ui_refactoring_plan.md +++ b/docs/architecture/003_ui_refactoring_plan.md @@ -1,3 +1,10 @@ +> ⚠️ **已归档文档** +> 本文档是 2026-06-16 架构审核后的 UI 代码结构重构计划(评分 7.2/10),描述的是待修复的质量问题与重构方案。 +> 当前重构计划已执行完毕,组件拆分、复用抽象、测试保障等改进已落地,最新架构状态详见 [004 架构影响地图](./004_architecture_impact_map.md)。 +> 保留用于历史参考,不再维护。 + +--- + # UI 代码结构重构计划 > 基于 2026-06-16 架构审核,整体评分 7.2/10 diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index d824d70..6c4c308 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -1,3445 +1,1504 @@ # Next_Edu 架构影响地图 -> 全模块·全函数·全参数级别 -> 生成日期:2026-06-16 -> 规则:每次文件修改后须同步更新本文档 +> 重写日期:2026-06-17 +> 目标:一次阅读即可直观理解整个项目架构 +> 规则:源码修改后须同步更新本文档与 `005_architecture_data.json` +> 审查依据:`docs/architecture/audit/` 下 4 份审查报告 --- -## 模块:shared - -### 模块职责 -提供全项目共享的基础设施:数据库连接、Schema、工具函数、权限系统、UI 基础组件、通用 Hooks。 - -### 导出函数 - -#### `cn` -- 签名:`cn(...inputs: ClassValue[]): string` -- 参数说明: - - `inputs`: ClassValue[],来自 `clsx` + `tailwind-merge`,CSS 类名列表 -- 功能:合并 CSS 类名并解决 Tailwind 冲突 -- 依赖:`clsx`, `tailwind-merge` -- 被以下模块使用:**所有模块**的组件(50+ 文件) - -#### `formatDate` -- 签名:`formatDate(date: string | Date, locale?: string): string` -- 参数说明: - - `date`: string | Date,日期值 - - `locale`: string,默认 `"zh-CN"`,`Intl.DateTimeFormat` 的 locale -- 功能:国际化日期格式化 -- 依赖:无 -- 被以下模块使用:exams, homework, dashboard, textbooks - -#### `parseAiChatPayload` -- 签名:`parseAiChatPayload(body: unknown): AiChatRequest` -- 参数说明:`body`: unknown,HTTP 请求体 -- 功能:解析并校验 AI 聊天请求负载 -- 依赖:Zod schema -- 被以下模块使用:`app/api/ai/chat/route.ts` - -#### `encryptAiApiKey` / `decryptAiApiKey` -- 签名:`(value: string) => string` -- 功能:AES 加密/解密 AI Provider API Key -- 依赖:`crypto` (Node.js 内置) -- 被以下模块使用:settings (存储/读取 API Key) - -#### `testAiProviderConfig` / `testAiProviderById` -- 签名:`(input: { apiKey, baseUrl?, model }) => Promise` / `(providerId, overrides?) => Promise` -- 功能:测试 AI Provider 连通性 -- 依赖:`createAiChatCompletion`, `shared/db` -- 被以下模块使用:settings/actions.ts - -#### `createAiChatCompletion` -- 签名:`(input: AiChatRequest) => Promise<{ content, usage }>` -- 功能:调用 AI 模型生成聊天回复 -- 依赖:OpenAI SDK, `shared/db` (读取 provider 配置) -- 被以下模块使用:exams/ai-pipeline.ts, `app/api/ai/chat/route.ts` - -#### `getAuthContext` -- 签名:`getAuthContext(): Promise` -- 功能:获取当前用户的完整认证上下文(userId, roles, permissions, dataScope) -- 依赖:`auth` (NextAuth), `shared/db` (查询角色/班级/年级关系) -- 被以下模块使用:**所有业务模块**的 Server Actions - -#### `requirePermission` -- 签名:`requirePermission(permission: Permission): Promise` -- 参数说明:`permission`: Permission,来自 `shared/types/permissions` 的权限常量 -- 功能:断言当前用户拥有指定权限,否则抛出 PermissionDeniedError -- 依赖:`getAuthContext` -- 被以下模块使用:**所有业务模块**的 Server Actions(exams, homework, questions, textbooks, classes, school, settings, audit, announcements, files, grades, attendance) - -#### `checkPermission` -- 签名:`checkPermission(permission: Permission): Promise<{ allowed: boolean; ctx: AuthContext }>` -- 功能:非抛出版权限检查 -- 依赖:`getAuthContext` -- 被以下模块使用:待扩展 - -#### `requireAuth` -- 签名:`requireAuth(): Promise` -- 功能:仅断言用户已登录 -- 依赖:`getAuthContext` -- 被以下模块使用:待扩展 - -#### `resolvePermissions` -- 签名:`resolvePermissions(roleNames: string[]): Permission[]` -- 参数说明:`roleNames`: string[],用户的角色名称列表 -- 功能:合并多角色的权限列表(去重) -- 依赖:`ROLE_PERMISSIONS` 常量 -- 被以下模块使用:auth.ts (JWT callback) - -#### `logAudit` -- 签名:`logAudit(params: LogAuditParams): Promise` -- 参数说明:`params`: LogAuditParams,含 userId, userName, action, module, targetId?, targetType?, detail?, status? -- 功能:记录操作日志(静默失败,不影响主流程) -- 依赖:`auth` (NextAuth), `shared/db` (auditLogs 表), `next/headers` -- 被以下模块使用:school/actions.ts,其他 Server Actions - -#### `logLoginEvent` -- 签名:`logLoginEvent(params: LogLoginEventParams): Promise` -- 参数说明:`params`: LogLoginEventParams,含 userId?, userEmail, action (signin/signout/signup), status, errorMessage? -- 功能:记录登录日志(不依赖 auth 上下文,可在 NextAuth events 中调用,静默失败) -- 依赖:`shared/db` (loginLogs 表), `next/headers` -- 被以下模块使用:auth.ts (events.signIn, events.signOut) - -#### `isAllowedMimeType` -- 签名:`isAllowedMimeType(mimeType: string): boolean` -- 功能:判断 MIME 类型是否在允许上传的白名单中 -- 依赖:`ALLOWED_MIME_TYPES` 常量 -- 被以下模块使用:`app/api/upload/route.ts`, files/components/file-upload.tsx - -#### `generateStoragePath` -- 签名:`generateStoragePath(originalName: string): string` -- 功能:根据原始文件名生成存储路径 `uploads/YYYY-MM/cuid.ext`(相对于 public/) -- 依赖:`@paralleldrive/cuid2` -- 被以下模块使用:`app/api/upload/route.ts` - -#### `getFileExtension` -- 签名:`getFileExtension(filename: string): string` -- 功能:从文件名中提取小写扩展名(不含点) -- 被以下模块使用:`shared/lib/file-storage` 内部 - -#### `formatFileSize` -- 签名:`formatFileSize(bytes: number): string` -- 功能:将字节数格式化为人类可读字符串(如 `1.5 MB`、`800 KB`) -- 被以下模块使用:files/components/file-upload.tsx, file-list.tsx, file-preview.tsx - -#### `exportToExcel` -- 签名:`exportToExcel(params: { sheets: ExcelSheet[] }): Promise` -- 参数说明:`sheets`: ExcelSheet[],每个含 `name`/`columns`/`rows` -- 功能:将多 sheet 数据导出为 Excel Buffer(表头加粗+冻结首行+自动筛选) -- 依赖:`exceljs` -- 被以下模块使用:users/import-export.exportUsersToExcel, grades/export.exportGradeRecordsToExcel, grades/export.exportClassGradeReportToExcel - -#### `parseExcel` -- 签名:`parseExcel(buffer: Buffer): Promise` -- 返回:ParsedSheet[],每个含 `sheetName`/`rows` -- 功能:从 Buffer 解析 Excel 文件,首行作为表头,返回每 sheet 的行记录数组 -- 依赖:`exceljs` -- 被以下模块使用:users/actions.importUsersAction, `app/api/import/route.ts` - -#### `generateTemplate` -- 签名:`(params: { sheets: TemplateSheet[] }): Promise` -- 参数说明:`sheets`: TemplateSheet[],每个含 `name`/`columns`(含 `note`)/`sampleRows?` -- 功能:生成导入模板 Buffer(表头加粗+第二行填写说明+示例行) -- 依赖:`exceljs` -- 被以下模块使用:users/import-export.generateUserImportTemplate - -#### `validatePassword` -- 签名:`(password: string) => { valid: boolean; errors: string[] }` -- 功能:校验密码是否符合策略(最小长度、大小写、数字等) -- 依赖:`PASSWORD_RULES` 常量 -- 被以下模块使用:settings/actions-password.changePasswordAction, auth.ts - -#### `getPasswordStrength` -- 签名:`(password: string) => "weak" | "medium" | "strong"` -- 功能:基于长度和字符多样性计算密码强度等级(纯函数,可在客户端使用) -- 依赖:无 -- 被以下模块使用:settings/components/password-change-form.tsx - -#### `isAccountLocked` -- 签名:`(failedAttempts: number, lastFailedAt: Date | null) => boolean` -- 功能:判断账户是否应被锁定(失败次数达阈值且锁定时间未过期) -- 依赖:`PASSWORD_RULES.maxLoginAttempts`, `PASSWORD_RULES.lockoutDurationMinutes` -- 被以下模块使用:auth.ts (authorize callback) - -#### `getRemainingLockoutMs` -- 签名:`(failedAttempts: number, lastFailedAt: Date | null) => number` -- 功能:计算剩余锁定时间(毫秒,0 表示已解锁) -- 依赖:`isAccountLocked` -- 被以下模块使用:待扩展 - -#### `rateLimit` -- 签名:`(params: { key: string; limit: number; windowMs: number }) => { success: boolean; remaining: number; resetTime: number; retryAfterMs: number }` -- 功能:内存滑动窗口限流检查(单实例,多实例需替换为 @upstash/ratelimit) -- 依赖:无(Map 存储) -- 被以下模块使用:auth.ts (LOGIN), settings/actions-password (PASSWORD_CHANGE), app/api/ai/chat (AI_CHAT), app/api/upload (UPLOAD), app/api/rate-limit-test - -#### `resetRateLimit` -- 签名:`(key: string) => void` -- 功能:重置指定 key 的限流计数(如登录成功后清除失败计数) -- 被以下模块使用:auth.ts (成功登录后) - -#### `rateLimitKey` -- 签名:`(prefix: string, identifier: string) => string` -- 功能:构建限流 key(如 `login:ip:email`) -- 被以下模块使用:auth.ts, settings/actions-password, app/api/ai/chat, app/api/upload - -#### `rateLimitHeaders` -- 签名:`(result: RateLimitResult) => Record` -- 功能:将限流结果转为 HTTP 响应头(X-RateLimit-Limit/Remaining/Reset, Retry-After) -- 被以下模块使用:app/api/ai/chat, app/api/upload, app/api/rate-limit-test - -#### `logDataChange` -- 签名:`logDataChange(params: LogDataChangeParams): Promise` -- 参数说明:`params`: LogDataChangeParams,含 tableName, recordId, action ("create" | "update" | "delete"), oldValue?, newValue? -- 功能:记录数据变更日志(写入 dataChangeLogs 表,自动从 NextAuth session 获取 changedBy/changedByName,从 headers 获取 ipAddress;静默失败,不阻塞主流程) -- 依赖:`auth` (NextAuth), `shared/db` (dataChangeLogs 表), `next/headers`, `@paralleldrive/cuid2` -- 被以下模块使用:待扩展(数据变更场景) - -#### `StorageProvider` (接口) -- 文件:`lib/storage-provider.ts` -- 定义:文件存储抽象接口,方法 `save(file, storagePath) => Promise`、`read(storagePath) => Promise`、`delete(storagePath) => Promise`、`exists(storagePath) => Promise`、`getUrl(storagePath) => string` -- 功能:抽象文件持久化层,便于未来切换到 OSS/S3 而不修改调用方 -- 被以下模块使用:`app/api/files/batch-delete/route.ts` - -#### `LocalStorageProvider` (类) -- 文件:`lib/storage-provider.ts` -- 实现:`StorageProvider` 接口,文件持久化到 `public/uploads/...`,URL 为 `/uploads/...` -- 依赖:`fs/promises` (mkdir/readFile/writeFile/unlink/access), `path` -- 被以下模块使用:通过 `storageProvider` 实例使用 - -#### `storageProvider` (实例) -- 文件:`lib/storage-provider.ts` -- 类型:`StorageProvider`(默认为 `LocalStorageProvider` 实例) -- 功能:默认存储 Provider 单例,替换此实例可迁移到 OSS/S3 -- 被以下模块使用:`app/api/files/batch-delete/route.ts` - -#### `useA11yId` -- 签名:`useA11yId(prefix: string): string` -- 功能:基于 `React.useId` 生成 SSR 安全的唯一 ID,用于 `aria-describedby`、`aria-labelledby` 等 -- 依赖:`react` -- 被以下模块使用:待扩展(表单组件、a11y 组件) - -#### `mergeA11yProps` -- 签名:`mergeA11yProps>(...props: (T | undefined | null | false)[]): T` -- 功能:合并多组 aria/data 属性,普通属性后者覆盖前者,`aria-*`/`data-*` 字符串属性以空格拼接 -- 依赖:无 -- 被以下模块使用:待扩展 - -#### `describeInput` -- 签名:`describeInput(describedBy?: string, error?: string, hint?: string): { ariaDescribedBy?: string; ariaInvalid?: boolean }` -- 功能:计算输入框的 `aria-describedby`(合并多个 ID)与 `aria-invalid`(error 存在则为 true) -- 依赖:无 -- 被以下模块使用:待扩展(表单组件) - -#### `loadingAria` -- 签名:`loadingAria(isLoading: boolean): { ariaBusy: boolean; ariaLive: "polite" | "assertive" }` -- 功能:提供加载状态的 `aria-busy` 与 `aria-live=polite` 属性 -- 依赖:无 -- 被以下模块使用:待扩展 - -### 导出常量与实例 - -#### `Permissions` (常量对象) -- 文件:`types/permissions.ts` -- 定义:54 个权限常量(`exam:create`, `homework:grade`, `audit_log:read`, `announcement:manage`, `file:upload`, `file:read`, `file:delete`, `grade_record:manage`, `grade_record:read`, `course_plan:manage`, `course_plan:read`, `attendance:manage`, `attendance:read`, `message:send`, `message:read`, `message:delete`, `schedule:auto`, `schedule:adjust`, `elective:manage`, `elective:read`, `elective:select`, `exam:proctor`, `exam:proctor_read`, `diagnostic:manage`, `diagnostic:read` 等) -- 被使用:auth-guard.ts, 所有模块的 actions.ts, 前端组件 - -#### `ROLE_PERMISSIONS` (常量对象) -- 文件:`lib/permissions.ts` -- 定义:角色到权限列表的映射(admin/teacher/student/parent/grade_head/teaching_head) -- 课程计划权限:admin 含 `COURSE_PLAN_MANAGE`+`COURSE_PLAN_READ`;teacher/student/grade_head/teaching_head 含 `COURSE_PLAN_READ` -- 考勤权限:admin/teacher 含 `ATTENDANCE_MANAGE`+`ATTENDANCE_READ`;student/parent/grade_head/teaching_head 含 `ATTENDANCE_READ` -- 消息权限:admin/teacher/grade_head/teaching_head 含 `MESSAGE_SEND`+`MESSAGE_READ`+`MESSAGE_DELETE`;student/parent 含 `MESSAGE_SEND`+`MESSAGE_READ` -- 排课权限:admin 含 `SCHEDULE_AUTO`+`SCHEDULE_ADJUST`;teacher/student/parent/grade_head/teaching_head 无排课权限 -- 学情诊断权限:admin/teacher/grade_head 含 `DIAGNOSTIC_MANAGE`+`DIAGNOSTIC_READ`;teaching_head/student 含 `DIAGNOSTIC_READ` -- 被使用:`resolvePermissions`, auth.ts (JWT callback) - -#### `db` (Drizzle 实例) -- 文件:`db/index.ts` -- 定义:Drizzle ORM 客户端实例(MySQL) -- 被使用:**所有业务模块**的 data-access.ts 和 actions.ts - -#### `questionTypeEnum` -- 文件:`db/schema.ts` (或 schema 枚举导出) -- 定义:题目类型枚举(选择/填空/判断/复合等) -- 被使用:questions, exams, homework - -#### `classEnrollmentStatusEnum` -- 文件:`db/schema.ts` (或 schema 枚举导出) -- 定义:班级注册状态枚举(active/inactive 等) -- 被使用:classes, homework - -#### `PASSWORD_RULES` (常量对象) -- 文件:`lib/password-policy.ts` -- 定义:密码策略配置(minLength: 8, requireUppercase/Lowercase/Number: true, maxLoginAttempts: 5, lockoutDurationMinutes: 30) -- 被使用:auth.ts, settings/actions-password.ts, password-change-form.tsx (via PASSWORD_REQUIREMENT_HINTS) - -#### `RATE_LIMIT_RULES` (常量对象) -- 文件:`lib/rate-limit.ts` -- 定义:预定义限流规则(LOGIN: 5/15min, API: 100/min, UPLOAD: 10/min, AI_CHAT: 20/min, PASSWORD_CHANGE: 5/min) -- 被使用:auth.ts (LOGIN), settings/actions-password (PASSWORD_CHANGE), app/api/ai/chat (AI_CHAT), app/api/upload (UPLOAD) - -### 文件记录 - -#### `db/relations.ts` -- 内容:24+ Drizzle relations(表间关系定义,含 coursePlansRelations、coursePlanItemsRelations、attendanceRecordsRelations、attendanceRulesRelations) -- 被使用:Drizzle ORM 关联查询(所有 data-access.ts) - -#### `next-auth.d.ts` -- 内容:NextAuth 类型扩展(Session.user 增加 id/roles/permissions 等字段) -- 被使用:auth.ts, 所有使用 `useSession`/`auth()` 的代码 - -### 导出组件 - -#### `AuthSessionProvider` -- Props: `{ children: React.ReactNode }` -- 内部使用:`next-auth/react` 的 `SessionProvider` -- 被使用:`app/layout.tsx` - -#### `OnboardingGate` -- Props: 无(内部使用 `useSession`) -- 内部使用:`useSession`, `Permissions`, 多个 Server Action -- 功能:新用户引导流程(角色选择、学校/班级配置) -- 被使用:`app/layout.tsx` - -#### `ThemeProvider` -- Props: `next-themes` 的 `ThemeProviderProps` -- 被使用:`app/layout.tsx` - -#### `EmptyState` -- 文件:`components/ui/empty-state.tsx` -- Props: `{ icon?, title, description, action? }` -- 被使用:exams, homework, questions, textbooks 等模块的列表空状态 - -#### `GlobalSearch` -- 文件:`components/global-search.tsx` -- Props: `{ className?, placeholder? }` -- 功能:全局搜索组件(防抖 300ms 调用 `GET /api/search`,Cmd/Ctrl+K 快捷键聚焦,Escape 关闭,↑/↓ 键盘导航,Enter 跳转;下拉展示 question/textbook/exam/announcement 四类结果,点击外部自动关闭) -- 内部使用:`useDebounce`, `Input`, `Link`, `useRouter` -- 被使用:`layout/components/site-header.tsx` - -#### `Switch` -- 文件:`components/ui/switch.tsx` -- 基于:`@radix-ui/react-switch` -- Props: Radix Switch Root props(含 `checked`, `onCheckedChange`, `disabled`, `id`, `aria-label` 等) -- 功能:开关切换 UI 组件(shadcn 风格,checked/unchecked 两态) -- 被使用:settings/components/notification-preferences-form.tsx - -#### `SkipLink` -- 文件:`components/a11y/skip-link.tsx` -- Props: `{ href?, children?, ...AnchorHTMLAttributes }`,默认 href=`#main-content`,默认文字"跳转到主内容" -- 功能:跳转链接组件,视觉隐藏,获得焦点时高对比度显示,供键盘用户跳过导航直达主内容 -- 被使用:待替换 `app/(dashboard)/layout.tsx` 中的内联 skip-link - -#### `VisuallyHidden` -- 文件:`components/a11y/visually-hidden.tsx` -- Props: `{ children?, ...HTMLAttributes }` -- 功能:视觉隐藏但屏幕阅读器可读,用于图标按钮文字描述、表单辅助说明 -- 被使用:待扩展 - -#### `FocusTrap` -- 文件:`components/a11y/focus-trap.tsx` -- Props: `{ children, active?, initialFocusRef?, restoreFocus?, className? }` -- 功能:焦点陷阱组件,捕获 Tab/Shift+Tab 在容器内循环,支持初始焦点与焦点恢复,用于模态框/对话框 -- 被使用:待扩展(Dialog/Sheet 自定义场景) - -#### `AriaStatus` -- 文件:`components/a11y/aria-status.tsx` -- Props: `{ children?, politeness?, atomic?, ...HTMLAttributes }`,默认 politeness=`polite`,atomic=`true` -- 功能:ARIA 状态通知区域,渲染 `aria-live` 区域(role=status),用于页面级状态通知(如"加载中"、"已保存") -- 被使用:待扩展 - -### 导出 Hooks - -#### `useActionWithToast` -- 签名:`useActionWithToast(): { isPending, execute }` -- 功能:包装 Server Action + toast 反馈 -- 被使用:待扩展 - -#### `useDebounce` -- 签名:`useDebounce(value: T, delay?: number): T` -- 被使用:搜索输入框等 - -#### `useMediaQuery` -- 签名:`useMediaQuery(query: string): boolean` -- 被使用:响应式布局判断 - -#### `useLocalStorage` -- 签名:`useLocalStorage(key: string, initialValue: T): [T, setter]` -- 被使用:exam-form (previewTaskStorageKey) - -#### `usePermission` -- 签名:`usePermission(): { permissions, roles, hasPermission, hasAnyPermission, hasAllPermissions, hasRole }` -- 功能:客户端权限检查 Hook -- 被使用:layout/app-sidebar.tsx, exams/components, homework/components - -### 类型/接口 - -#### `ActionState` -- 定义:`{ success: boolean; message?: string; errors?: Record; data?: T }` -- 被使用:**所有模块**的 Server Action 返回类型 - -#### `Permission` (类型) -- 定义:`Permissions` 值的联合类型 -- 被使用:auth-guard.ts, use-permission.ts - -#### `DataScope` (联合类型) -- 定义:`{ type: "all" } | { type: "owned"; userId: string } | { type: "class_taught"; classIds: string[]; subjectIds?: string[] } | { type: "grade_managed"; gradeIds: string[] } | { type: "class_members" } | { type: "children"; childrenIds: string[] }` -- 被使用:auth-guard.ts, exams/data-access.ts, homework/data-access.ts, dashboard/data-access.ts - -#### `AuthContext` (接口) -- 定义:`{ userId: string; roles: string[]; permissions: Permission[]; dataScope: DataScope }` -- 被使用:auth-guard.ts, 所有调用 `requirePermission` 的 Server Action - -#### `PermissionDeniedError` (类) -- 被使用:所有 Server Action 的 try/catch - -### 数据库表 (shared/db/schema.ts) - -| 表名 | 核心字段 | 被哪些模块使用 | -|------|---------|--------------| -| `users` | id, name, email, emailVerified, image, password, phone, address, gender, age, birthDate, guardianName, guardianPhone, guardianRelation, consentAcceptedAt, gradeId, departmentId, onboardedAt, createdAt, updatedAt | auth, users, dashboard, classes | -| `accounts` | userId, type, provider, providerAccountId, refresh_token, access_token, expires_at, token_type, scope, id_token, session_state | auth | -| `sessions` | sessionToken, userId, expires | auth | -| `verificationTokens` | identifier, token, expires | auth | -| `roles` | id, name, description, createdAt, updatedAt | auth, auth-guard | -| `usersToRoles` | userId, roleId | auth, auth-guard | -| `rolePermissions` | roleId, permission | auth (seed) | -| `knowledgePoints` | id, name, description, anchorText, parentId, chapterId, level, order, createdAt, updatedAt | textbooks, questions | -| `questions` | id, content, type, difficulty, authorId, parentId, createdAt, updatedAt | questions, exams, homework | -| `questionsToKnowledgePoints` | questionId, knowledgePointId | questions | -| `subjects` | id, name, order, code, createdAt, updatedAt | exams, textbooks | -| `textbooks` | id, title, subject, grade, publisher, createdAt, updatedAt | textbooks | -| `chapters` | id, textbookId, title, order, parentId, content, createdAt, updatedAt | textbooks | -| `departments` | id, name, description, createdAt, updatedAt | school | -| `classrooms` | id, name, building, floor, capacity, createdAt, updatedAt | school | -| `academicYears` | id, name, startDate, endDate, isActive, createdAt, updatedAt | school | -| `schools` | id, name, code, createdAt, updatedAt | school, classes | -| `grades` | id, schoolId, name, order, gradeHeadId, teachingHeadId, createdAt, updatedAt | school, classes, exams, auth-guard | -| `classes` | id, schoolId, gradeId, teacherId, name, homeroom, room, invitationCode, schoolName, grade, createdAt, updatedAt | classes, homework, auth-guard | -| `classSubjectTeachers` | classId, teacherId, subjectId, createdAt, updatedAt | classes, auth-guard | -| `classEnrollments` | classId, studentId, status, createdAt | classes, homework | -| `classSchedule` | id, classId, weekday, startTime, endTime, course, location, createdAt, updatedAt | classes | -| `exams` | id, creatorId, title, description, subjectId, gradeId, status, structure, startTime, endTime, createdAt, updatedAt | exams, homework | -| `examQuestions` | examId, questionId, score, order | exams | -| `examSubmissions` | id, examId, studentId, score, status, submittedAt, createdAt, updatedAt | exams | -| `submissionAnswers` | id, submissionId, questionId, answerContent, score, feedback, createdAt, updatedAt | exams | -| `homeworkAssignments` | id, creatorId, sourceExamId, title, description, status, structure, availableAt, dueAt, allowLate, lateDueAt, maxAttempts, createdAt, updatedAt | homework | -| `homeworkAssignmentQuestions` | assignmentId, questionId, score, order | homework | -| `homeworkAssignmentTargets` | assignmentId, studentId, createdAt | homework | -| `homeworkSubmissions` | id, assignmentId, studentId, status, attemptNo, score, submittedAt, startedAt, isLate, createdAt, updatedAt | homework | -| `homeworkAnswers` | id, submissionId, questionId, answerContent, score, feedback, createdAt, updatedAt | homework | -| `aiProviders` | id, provider, baseUrl, model, apiKeyEncrypted, apiKeyLast4, isDefault, createdBy, updatedBy, createdAt, updatedAt | settings, ai | -| `announcements` | id, title, content, type, status, targetGradeId, targetClassId, authorId, publishedAt, createdAt, updatedAt | announcements | -| `auditLogs` | id, userId, userName, action, module, targetId, targetType, detail, ipAddress, userAgent, status, createdAt | audit, shared/lib/audit-logger | -| `loginLogs` | id, userId, userEmail, action, status, ipAddress, userAgent, errorMessage, createdAt | audit, shared/lib/login-logger, auth | -| `dataChangeLogs` | id, tableName, recordId, action (create/update/delete), oldValue, newValue, changedBy, changedByName, ipAddress, createdAt | audit, shared/lib/change-logger | -| `fileAttachments` | id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType, targetId, createdAt | files | -| `gradeRecords` | id, studentId, classId, subjectId, examId, academicYearId, title, score, fullScore, type, semester, recordedBy, remark, createdAt, updatedAt | grades | -| `coursePlans` | id, classId, subjectId, teacherId, academicYearId, semester, totalHours, completedHours, weeklyHours, startDate, endDate, syllabus, objectives, status, createdBy, createdAt, updatedAt | course-plans | -| `coursePlanItems` | id, planId, week, topic, content, hours, textbookChapter, notes, isCompleted, completedAt, createdAt, updatedAt | course-plans | -| `parentStudentRelations` | id, parentId, studentId, relation, createdAt | parent, auth-guard | -| `messages` | id, senderId, receiverId, subject, content, isRead, readAt, parentMessageId, createdAt | messaging | -| `messageNotifications` | id, userId, type, title, content, link, isRead, createdAt | messaging | -| `notificationPreferences` | id, userId (unique FK→users), emailEnabled, smsEnabled, pushEnabled, homeworkNotifications, gradeNotifications, announcementNotifications, messageNotifications, attendanceNotifications, createdAt, updatedAt | messaging, settings | -| `attendanceRecords` | id, studentId, classId, scheduleId, date, status, remark, recordedBy, createdAt, updatedAt | attendance | -| `attendanceRules` | id, classId, lateThresholdMinutes, earlyLeaveThresholdMinutes, enableAutoMark, createdAt, updatedAt | attendance | -| `schedulingRules` | id, classId, maxDailyHours, maxContinuousHours, lunchBreakStart, lunchBreakEnd, morningStart, afternoonEnd, avoidBackToBack, balancedSubjects, createdAt, updatedAt | scheduling | -| `scheduleChanges` | id, originalScheduleId, classId, originalTeacherId, substituteTeacherId, originalDate, newDate, newStartTime, newEndTime, reason, status, requestedBy, approvedBy, createdAt, updatedAt | scheduling | -| `passwordSecurity` | id, userId, failedLoginAttempts, lockedUntil, passwordChangedAt, mustChangePassword, lastPasswordChange, createdAt, updatedAt | auth, settings | -| `knowledgePointMastery` | id, studentId, knowledgePointId, masteryLevel, totalQuestions, correctQuestions, lastAssessedAt, createdAt, updatedAt | diagnostic | -| `learningDiagnosticReports` | id, studentId, generatedBy, reportType, period, summary, strengths, weaknesses, recommendations, overallScore, status, createdAt, updatedAt | diagnostic | +## 目录 + +- [第一部分:全局架构概览](#第一部分全局架构概览) + - [1.1 分层架构图](#11-分层架构图) + - [1.2 模块依赖关系图](#12-模块依赖关系图) + - [1.3 数据流向图(考试流程)](#13-数据流向图考试流程) + - [1.4 核心调用链路](#14-核心调用链路) +- [第二部分:模块清单](#第二部分模块清单) +- [第三部分:已知架构问题和技术债](#第三部分已知架构问题和技术债) +- [附录 A:模块间依赖矩阵](#附录-a模块间依赖矩阵) +- [附录 B:关键参数影响链](#附录-b关键参数影响链) +- [附录 C:核心函数签名索引](#附录-c核心函数签名索引) --- -## 模块:auth +# 第一部分:全局架构概览 -### 模块职责 -处理用户认证(登录/注册/JWT/Session),提供 NextAuth 实例和中间件。通过 events 回调记录登录日志。 -集成密码安全策略(账户锁定、失败登录追踪)和登录速率限制。 +## 1.1 分层架构图 -### 导出函数 +项目采用**严格三层架构**,依赖方向必须单向:`app → modules → shared`。 -#### `auth` -- 签名:`auth(): Promise` (NextAuth 导出) -- 功能:获取当前用户 Session -- 被使用:auth-guard.ts, 所有 Server Component 页面, audit-logger.ts +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 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 │ +│ 教学管理: classes · school · scheduling · attendance · course-plans│ +│ 用户与沟通:users · messaging · notifications · parent · audit │ +│ 扩展功能: elective · proctoring · diagnostic · dashboard │ +│ 其他: 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/ · onboarding-gate · ... │ +│ types/ permissions · action-state │ +└─────────────────────────────────────────────────────────────────────┘ + ▲ + │ 反向依赖(违规,见 1.2) +┌──────────────────────────────┴──────────────────────────────────────┐ +│ 根模块:src/auth.ts (NextAuth 配置) · src/proxy.ts (中间件) │ +│ ⚠️ shared/lib/{audit-logger, change-logger, auth-guard} 反向依赖 │ +│ @/auth,构成循环依赖(详见第三部分 P0-2) │ +└─────────────────────────────────────────────────────────────────────┘ +``` -#### `handlers` -- 签名:`{ GET, POST }` (NextAuth Route Handler) -- 被使用:`app/api/auth/[...nextauth]/route.ts` - -#### `signIn` / `signOut` -- 被使用:login-form.tsx, site-header.tsx - -### authorize 回调(Credentials Provider) - -> 登录流程集成密码安全策略和速率限制: - -1. **速率限制**:按 `IP:email` 维度限流(RATE_LIMIT_RULES.LOGIN: 5次/15分钟),超限返回 null -2. **账户锁定检查**:通过 `isAccountLocked(failedLoginAttempts, lastFailedAt)` 判断,锁定则返回 null -3. **密码验证失败**:调用 `recordFailedLogin` 递增失败次数,达阈值自动锁定 -4. **登录成功**:调用 `resetFailedLogin` 清零失败次数,`resetRateLimit` 清除限流计数 - -### Events 回调 - -#### `events.signIn` -- 签名:`async signIn({ user }) => void` -- 功能:用户登录成功后记录登录日志 -- 依赖:`shared/lib/login-logger.logLoginEvent` -- 调用参数:`{ userId: user.id, userEmail: user.email, action: "signin", status: "success" }` - -#### `events.signOut` -- 签名:`async signOut(message) => void` -- 功能:用户登出后记录登录日志(处理 NextAuth v5 不同 message 形状) -- 依赖:`shared/lib/login-logger.logLoginEvent` -- 调用参数:`{ userId?, userEmail, action: "signout", status: "success" }` - -#### `middleware` (proxy.ts) -- 签名:`middleware(request: NextRequest) => Promise` -- 功能:基于权限点的路由守卫,未登录重定向 /login,无权限重定向角色首页 -- 依赖:`getToken` (next-auth/jwt), `Permissions` -- 被使用:Next.js middleware 层 +**分层规则**: +- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access,不直接访问 DB +- `modules/` 之间通过对方 data-access 通信,不直接查询对方表 +- `shared/` 是被依赖方,不应反向依赖 `app/` 或 `modules/` +- `src/auth.ts` 和 `src/proxy.ts` 位于根目录,属于应用层 --- -## 模块:exams +## 1.2 模块依赖关系图 -### 模块职责 -考试全生命周期管理:创建(手动/AI)、编辑、预览、发布、删除、复制。 +下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。 -### 导出函数 (actions.ts) +### 图例 -#### `createExamAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 参数说明:`formData` 包含 mode, title, subject, grade, difficulty, totalScore, durationMin, scheduledAt -- 功能:手动模式创建考试草稿 -- 依赖:`requirePermission(EXAM_CREATE)`, `shared/db`, `data-access.persistExamDraft` -- 被使用:exam-form.tsx +- `───▶` 合理依赖(通过 data-access 或类型导入) +- `═══▶` 违规依赖(直接查询对方 DB 表) +- `⟳ ` 循环依赖 +- `──▷` UI 组件组合(合理) -#### `createAiExamAction` -- 签名:同上 -- 功能:AI 模式创建考试(调用 AI pipeline 生成题目) -- 依赖:`requirePermission(EXAM_AI_GENERATE)`, `ai-pipeline.generateAiCreateDraftFromSource`, `data-access.persistAiGeneratedExamDraft` -- 被使用:exam-form.tsx +### 1.2.1 核心业务模块依赖 -#### `previewAiExamAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 功能:AI 预览试卷(不持久化) -- 依赖:`requirePermission(EXAM_AI_GENERATE)`, `ai-pipeline.generateAiPreviewData` -- 被使用:exam-ai-generator.tsx (via useExamPreview) +``` + ┌──────────────┐ + │ textbooks │ ◀── 标杆模块(无跨模块 DB 访问) + └──────┬───────┘ + │ ──▷ UI 组合(knowledge-point-dialogs) + │ + ┌──────────────┼──────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌────────────┐ ┌──────────┐ ┌────────────┐ + │ questions │ │ exams │ │ homework │ + └─────┬──────┘ └────┬─────┘ └─────┬──────┘ + │ │ ═══ │ ═══ + │ ═══ │ 直查 classes │ 直查 exams/classes/ + │ 直查 │ 直查 questions │ classEnrollments/users + │ knowledgePoints │ + │ chapters ┌─┴────────┐ │ + │ textbooks │ grades │ │ + └─────────────┤ (成绩) │◀────┘ 仅外键引用(合理) + └────┬─────┘ + │ ═══ + │ 直查 classes/users/subjects + ▼ + ┌──────────┐ + │ classes │ ◀── 耦合最严重模块 + └────┬─────┘ data-access.ts 2104 行 + │ ═══ 混入 homework/scheduling/grades 逻辑 + │ 直查 homeworkAssignments/exams + │ + ┌─────────┼─────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────┐ ┌──────────┐ + │scheduling│ │school│ │ attendance│ + └────┬─────┘ └──────┘ └──────────┘ + │ ═══ + │ classSchedule 表三处写入口 + │ (classes + scheduling/actions + scheduling/data-access) +``` -#### `regenerateAiQuestionAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 功能:AI 重写单个题目 -- 依赖:`requirePermission(EXAM_AI_GENERATE)`, `ai-pipeline.regenerateAiQuestionByInstruction` -- 被使用:exam-preview-question-editor.tsx +### 1.2.2 扩展模块依赖 -#### `updateExamAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 功能:更新考试信息,含资源归属校验(非 admin 只能改自己的) -- 依赖:`requirePermission(EXAM_UPDATE)`, `shared/db` -- 被使用:exam-form.tsx +``` +┌─────────────┐ ═══ 直查 11 张跨模块表 ┌──────────────┐ +│ dashboard │──────────────────────────────▶│ users/classes│ +│ (聚合层) │ ⚠️ P0 严重违规 │ /exams/... │ +└─────────────┘ └──────────────┘ -#### `deleteExamAction` -- 签名:同上 -- 功能:删除考试,含资源归属校验 -- 依赖:`requirePermission(EXAM_DELETE)`, `shared/db` -- 被使用:exam-actions.tsx +┌─────────────┐ ─── 调用 data-access(合理) ┌──────────────┐ +│ parent │──────────────────────────────▶│ classes/ │ +│ (聚合层) │ │ homework/grades│ +└─────────────┘ └──────────────┘ -#### `duplicateExamAction` -- 签名:同上 -- 功能:复制考试 -- 依赖:`requirePermission(EXAM_DUPLICATE)`, `shared/db` -- 被使用:exam-actions.tsx +┌─────────────┐ ═══ 直查 examSubmissions/ ┌──────────────┐ +│ diagnostic │ submissionAnswers/ │ exams/questions│ +│ │ questionsToKnowledgePoints │ /classes │ +└─────────────┘──────────────────────────────▶└──────────────┘ -#### `getExamPreviewAction` -- 签名:`(examId: string) => Promise>` -- 功能:获取考试预览数据 -- 依赖:`requirePermission(EXAM_READ)`, `shared/db` -- 被使用:exam-viewer.tsx +┌─────────────┐ ═══ 直查 exams/examSubmissions ┌──────────────┐ +│ proctoring │ /users │ exams/users │ +└─────────────┘──────────────────────────────▶└──────────────┘ -#### `getSubjectsAction` -- 签名:`() => Promise>` -- 依赖:`requirePermission(EXAM_READ)`, `shared/db` -- 被使用:exam-form.tsx +┌─────────────┐ ⟳ 双向依赖(P0 严重违规) ┌──────────────┐ +│ messaging │◀═══════════════════════════════▶│ notifications │ +│ │ messaging 绕过 dispatcher │ (无独有表) │ +│ │ notifications 反向依赖 messaging │ │ +└─────────────┘ └──────────────┘ -#### `getGradesAction` -- 签名:同上 -- 依赖:`requirePermission(EXAM_READ)`, `shared/db` -- 被使用:exam-form.tsx +┌─────────────┐ ─── 调用 messaging Action ┌──────────────┐ +│ settings │ (通知偏好表单) │ messaging │ +└─────────────┘──────────────────────────────▶└──────────────┘ +``` -### 导出函数 (data-access.ts) +### 1.2.3 循环依赖详情 -#### `getExams` -- 签名:`getExams(params: GetExamsParams & { scope: DataScope }): Promise` -- 参数说明:`scope` 来自 `auth-guard.getAuthContext().dataScope` -- 功能:查询考试列表,含数据权限过滤 -- 依赖:`shared/db`, `DataScope` -- 被使用:teacher/exams/all/page.tsx, homework 创建页面 +``` +shared/lib/audit-logger.ts ──┐ +shared/lib/change-logger.ts ──┼──▶ import { auth } from "@/auth" +shared/lib/auth-guard.ts ──┘ -#### `getExamById` -- 签名:`getExamById(id: string, scope?: DataScope): Promise` -- 被使用:exam 详情/编辑页面 +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 { db, schema } from "@/shared/db" -#### `persistExamDraft` / `persistAiGeneratedExamDraft` -- 被使用:createExamAction, createAiExamAction - -#### `omitScheduledAtFromDescription` -- 功能:从考试描述中移除 scheduledAt 字段(用于编辑场景) -- 被使用:exams/data-access.ts 内部 - -#### `resolveSubjectGradeNames` -- 功能:将 subjectId/gradeId 解析为可读名称 -- 被使用:exams/data-access.ts 内部, buildExamDescription - -#### `buildExamDescription` -- 功能:构建考试描述文本(含科目、年级、时间等) -- 依赖:`resolveSubjectGradeNames`, `omitScheduledAtFromDescription` -- 被使用:createExamAction, createAiExamAction - -#### `GetExamsParams` (类型) -- 定义:查询参数类型(含 subjectId?, gradeId?, status?, keyword? 等过滤条件) -- 被使用:`getExams`, `getQuestionsAction` - -### 导出函数 (ai-pipeline.ts) - -#### `generateAiPreviewData` -- 签名:`(input: { title, subject?, grade?, difficulty, totalScore, durationMin, questionCount?, sourceText, aiProviderId? }) => Promise<{ ok, data?, rawOutput?, message? }>` -- 依赖:`shared/lib/ai.createAiChatCompletion` -- 被使用:previewAiExamAction - -#### `generateAiCreateDraftFromSource` -- 被使用:createAiExamAction - -#### `regenerateAiQuestionByInstruction` -- 被使用:regenerateAiQuestionAction - -#### `generateAiExamDraft` -- 功能:生成 AI 考试草稿(含结构与题目) -- 依赖:`shared/lib/ai.createAiChatCompletion`, `AiQuestionSchema`, `AiGeneratedStructureSchema` -- 被使用:createAiExamAction - -### AI Schema 与类型 (ai-pipeline.ts) - -#### `AiQuestionSchema` -- 类型:Zod schema -- 定义:AI 生成题目的校验 schema(type, content, difficulty, score, options? 等) -- 被使用:`generateAiExamDraft`, `AiGeneratedQuestion` - -#### `AiInsertQuestionSchema` -- 类型:Zod schema -- 定义:插入题目到 DB 的校验 schema(含 authorId, parentId 等 DB 字段) -- 被使用:`persistAiGeneratedExamDraft` - -#### `AiGeneratedQuestion` -- 类型:TypeScript 类型(基于 `AiQuestionSchema` 推断) -- 被使用:`AiPreviewData`, exams/components - -#### `AiGeneratedStructureNode` -- 类型:TypeScript 类型 -- 定义:AI 生成的试卷结构节点(section 标题、题目列表) -- 被使用:`AiGeneratedStructureSchema` - -#### `AiGeneratedStructureNodeSchema` -- 类型:Zod schema -- 定义:`AiGeneratedStructureNode` 的校验 schema -- 被使用:`AiGeneratedStructureSchema` - -#### `AiGeneratedStructureSchema` -- 类型:Zod schema -- 定义:AI 生成的完整试卷结构校验 schema(含 `AiGeneratedStructureNode[]`) -- 被使用:`generateAiExamDraft` - -### 类型/接口 - -#### `Exam` -- 被使用:exams/components, homework/types (sourceExam), dashboard/types - -#### `AiPreviewData` / `AiRewriteQuestionData` -- 被使用:exams/actions.ts, exams/components - -#### `ExamStatus` -- 定义:考试状态枚举类型(draft/published 等) -- 被使用:exams/data-access.ts, exams/components - -#### `ExamDifficulty` -- 定义:考试难度枚举类型 -- 被使用:exams/components, ai-pipeline.ts - -#### `SubmissionStatus` -- 定义:提交状态枚举类型(in_progress/submitted/graded 等) -- 被使用:examSubmissions 相关逻辑 - -#### `ExamSubmission` -- 定义:考试提交记录类型(含 studentId, score, status 等) -- 被使用:exams/data-access.ts, exams/components - -### 导出 Hooks - -#### `useExamPreview` -- 签名:`useExamPreview(): { isPending, execute, previewData, error }` -- 功能:包装 `previewAiExamAction`,管理 AI 预览状态 -- 被使用:exam-ai-generator.tsx - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `exam-form.tsx` | 考试创建/编辑表单(手动 + AI 模式) | -| `exam-ai-generator.tsx` | AI 生成考试配置面板 | -| `exam-viewer.tsx` | 考试预览展示 | -| `exam-actions.tsx` | 考试操作菜单(复制/删除) | -| `exam-data-table.tsx` | 考试列表数据表格 | -| `exam-preview-question-editor.tsx` | AI 预览题目编辑器 | -| `exam-status-badge.tsx` | 考试状态徽章 | -| `exam-card.tsx` | 考试卡片 | -| `exam-list.tsx` | 考试列表 | -| `exam-detail.tsx` | 考试详情 | -| `exam-header.tsx` | 考试头部信息 | -| `exam-info.tsx` | 考试基本信息展示 | -| `exam-question-list.tsx` | 考试题目列表 | -| `exam-question-item.tsx` | 考试题目项 | -| `exam-question-picker.tsx` | 题目选择器 | -| `exam-score-summary.tsx` | 分值汇总 | -| `exam-schedule-picker.tsx` | 考试时间选择器 | -| `exam-subject-grade-select.tsx` | 科目年级选择器 | -| `exam-empty.tsx` | 考试空状态 | -| `assembly/exam-assembly-panel.tsx` | 组卷面板 | -| `assembly/question-pool.tsx` | 题库选择池 | -| `assembly/assembly-cart.tsx` | 已选题目购物车 | -| `assembly/assembly-summary.tsx` | 组卷汇总 | + ⟳ 循环:shared/lib/* → @/auth → shared/lib/* + 影响:shared 层无法独立测试/复用;架构上基础设施不应反向依赖应用层 +``` --- -## 模块:homework +## 1.3 数据流向图(考试流程) -### 模块职责 -作业全生命周期:创建(源自考试)、发布、学生作答、教师批改、数据分析。 +以"考试流程"为例,展示数据从创建到成绩统计的完整流向。 -### 导出函数 (actions.ts) +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 阶段 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 表 │ +│ ⚠️ 违规:persistAiGeneratedExamDraft 直接 insert 到 questions 表 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 阶段 2:学生作答(作业化考试) │ +│ ───────────────────────────────────────────────────────────────── │ +│ student/learning/assignments/[assignmentId]/page.tsx │ +│ └─▶ homework/actions.startHomeworkSubmissionAction │ +│ ├─▶ requirePermission(HOMEWORK_SUBMIT) │ +│ ├─▶ db.query.homeworkAssignments [⚠️ 直接 DB] │ +│ ├─▶ db.insert(homeworkSubmissions) [⚠️ 直接 DB] │ +│ └─▶ 返回 submissionId │ +│ │ +│ └─▶ homework/actions.saveHomeworkAnswerAction │ +│ └─▶ db.transaction(insert homeworkAnswers) [⚠️ 直接 DB] │ +│ │ +│ └─▶ homework/actions.submitHomeworkAction │ +│ └─▶ db.update(homeworkSubmissions.status="submitted") │ +│ │ +│ 数据写入:homeworkSubmissions 表 + homeworkAnswers 表 │ +│ ⚠️ 违规:actions 层直接 DB 操作,应下沉到 data-access │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 阶段 3:教师批改 │ +│ ───────────────────────────────────────────────────────────────── │ +│ teacher/homework/submissions/[submissionId]/page.tsx │ +│ └─▶ homework/actions.gradeHomeworkSubmissionAction │ +│ ├─▶ requirePermission(HOMEWORK_GRADE) │ +│ └─▶ db.update(homeworkAnswers) 循环 [⚠️ 直接 DB] │ +│ │ +│ 数据更新:homeworkAnswers.isCorrect / score / feedback │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 阶段 4:成绩统计与诊断 │ +│ ───────────────────────────────────────────────────────────────── │ +│ teacher/grades/page.tsx │ +│ └─▶ grades/data-access.getGradeRecords │ +│ └─▶ JOIN classes/subjects/users [⚠️ 跨模块直查] │ +│ │ +│ teacher/diagnostic/page.tsx │ +│ └─▶ diagnostic/data-access.updateMasteryFromSubmission │ +│ ├─▶ 查询 examSubmissions [⚠️ 跨模块直查] │ +│ ├─▶ 查询 submissionAnswers [⚠️ 跨模块直查] │ +│ └─▶ 更新 knowledgePointMastery │ +│ │ +│ 数据读取:homeworkSubmissions → grades → knowledgePointMastery │ +└─────────────────────────────────────────────────────────────────────┘ +``` -#### `createHomeworkAssignmentAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 功能:从已有考试创建作业 -- 依赖:`requirePermission(HOMEWORK_CREATE)`, `shared/db`, `exams/data-access.getExams` -- 被使用:homework-assignment-form.tsx - -#### `startHomeworkSubmissionAction` -- 签名:同上 -- 功能:学生开始作答 -- 依赖:`requirePermission(HOMEWORK_SUBMIT)`, `shared/db` -- 被使用:homework-take-view.tsx - -#### `saveHomeworkAnswerAction` -- 签名:同上 -- 功能:保存单题答案 -- 依赖:`requirePermission(HOMEWORK_SUBMIT)`, `shared/db` -- 被使用:homework-take-view.tsx - -#### `submitHomeworkAction` -- 签名:同上 -- 功能:提交作业 -- 依赖:`requirePermission(HOMEWORK_SUBMIT)`, `shared/db` -- 被使用:homework-take-view.tsx - -#### `gradeHomeworkSubmissionAction` -- 签名:同上 -- 功能:教师批改作业 -- 依赖:`requirePermission(HOMEWORK_GRADE)`, `shared/db` -- 被使用:homework-grading-view.tsx - -### 导出函数 (data-access.ts) - -#### `getHomeworkAssignments` -- 签名:`(params?: { creatorId?, ids?, classId?, scope? }) => Promise` -- 依赖:`shared/db`, `DataScope` -- 被使用:teacher 作业列表页, homework-assignment-form.tsx - -#### `getStudentHomeworkAssignments` -- 签名:`(studentId: string) => Promise` -- 被使用:student/dashboard - -#### `getStudentDashboardGrades` -- 签名:`(studentId: string) => Promise` -- 被使用:dashboard/data-access.ts (学生仪表盘) - -#### `getHomeworkAssignmentAnalytics` -- 签名:`(assignmentId: string) => Promise` -- 被使用:homework 错误分析组件 - -#### `getHomeworkAssignmentById` -- 签名:`(assignmentId: string, scope?: DataScope) => Promise` -- 功能:按 ID 获取作业详情(含数据权限过滤) -- 被使用:homework 详情/编辑页面, homework-take-view.tsx - -#### `getHomeworkSubmissionDetails` -- 签名:`(submissionId: string, scope?: DataScope) => Promise` -- 功能:获取作业提交详情(含答案列表、学生信息) -- 被使用:homework-grading-view.tsx - -#### `getDemoStudentUser` -- 签名:`() => Promise<{ id: string; name: string } | null>` -- 功能:获取演示学生用户(用于 demo/预览场景) -- 被使用:homework demo 相关页面 - -#### `getStudentHomeworkTakeData` -- 签名:`(studentId: string, assignmentId: string) => Promise` -- 功能:获取学生作答作业所需完整数据(作业、题目、已答内容、提交状态) -- 被使用:homework-take-view.tsx - -### Schema (schema.ts) - -#### `CreateHomeworkAssignmentSchema` -- 类型:Zod schema -- 定义:创建作业的校验 schema(sourceExamId, title, classIds, dueAt, allowLate 等) -- 被使用:`createHomeworkAssignmentAction` - -#### `CreateHomeworkAssignmentInput` -- 类型:TypeScript 类型(基于 `CreateHomeworkAssignmentSchema` 推断) -- 被使用:`createHomeworkAssignmentAction`, homework-assignment-form.tsx - -#### `GradeHomeworkSchema` -- 类型:Zod schema -- 定义:批改作业的校验 schema(submissionId, answers[{ questionId, score, feedback }]) -- 被使用:`gradeHomeworkSubmissionAction` - -### 类型/接口 - -#### `StudentDashboardGradeProps` -- 被使用:dashboard/types.ts (StudentDashboardProps.grades) - -#### `HomeworkAssignmentListItem` -- 被使用:homework 列表页, homework-assignment-form.tsx - -#### `HomeworkAssignment` -- 定义:作业完整类型(含 sourceExam, targets, questions 等关联) -- 被使用:homework 详情/编辑页面 - -#### `HomeworkAssignmentReviewListItem` -- 被使用:teacher 批改列表 - -#### `HomeworkSubmissionListItem` -- 被使用:teacher 提交列表 - -#### `HomeworkSubmission` -- 定义:作业提交记录类型 -- 被使用:homework-grading-view.tsx - -#### `HomeworkSubmissionDetails` -- 定义:提交详情类型(含学生、答案列表) -- 被使用:homework-grading-view.tsx - -#### `HomeworkAnswer` -- 定义:作业答案类型 -- 被使用:homework-take-view.tsx, homework-grading-view.tsx - -#### `HomeworkAssignmentAnalytics` -- 定义:作业分析数据类型(含错误率、平均分等) -- 被使用:homework 错误分析组件 - -#### `StudentHomeworkAssignmentListItem` -- 被使用:student/dashboard - -#### `StudentHomeworkTakeData` -- 定义:学生作答数据类型(含 assignment, questions, currentAnswers, submission) -- 被使用:homework-take-view.tsx - -#### `TeacherGradeTrendItem` -- 被使用:dashboard (教师仪表盘) - -#### `HomeworkAssignmentStatus` -- 定义:作业状态枚举类型(draft/published/closed 等) -- 被使用:homework/data-access.ts, homework/components - -#### `HomeworkSubmissionStatus` -- 定义:提交状态枚举类型(in_progress/submitted/graded 等) -- 被使用:homework/data-access.ts, homework/components - -#### `HomeworkAssignmentTarget` -- 定义:作业目标学生类型 -- 被使用:homework/data-access.ts - -#### `HomeworkQuestion` -- 定义:作业题目类型(含 score, order) -- 被使用:homework-take-view.tsx - -#### `HomeworkGradingInput` -- 定义:批改输入类型 -- 被使用:homework-grading-view.tsx - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `homework-assignment-form.tsx` | 作业创建表单(源自考试) | -| `homework-take-view.tsx` | 学生作答视图 | -| `homework-grading-view.tsx` | 教师批改视图 | -| `homework-assignment-list.tsx` | 作业列表 | -| `homework-assignment-card.tsx` | 作业卡片 | -| `homework-assignment-detail.tsx` | 作业详情 | -| `homework-submission-list.tsx` | 提交列表 | -| `homework-submission-card.tsx` | 提交卡片 | -| `homework-analytics.tsx` | 作业分析(错误率/平均分) | -| `homework-status-badge.tsx` | 作业状态徽章 | +**关键观察**:考试流程横跨 4 个模块(exams → homework → grades → diagnostic),其中 3 处违规直查破坏了模块封装。 --- -## 模块:questions +## 1.4 核心调用链路 -### 模块职责 -题库管理:题目 CRUD、知识点关联、题型支持(选择/填空/判断/复合)。 +### 1.4.1 调用链路:创建考试(含 AI 出题) -### 导出函数 (actions.ts) +``` +[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) ✅ 合理 + │ └─▶ db.insert(questions) ❌ 违规:应通过 questions/data-access + │ + └─▶ revalidatePath("/teacher/exams") +``` -#### `createNestedQuestion` -- 签名:`(prevState: ActionState | undefined, formData: FormData | CreateQuestionInput) => Promise>` -- 依赖:`requirePermission(QUESTION_CREATE)`, `shared/db` -- 被使用:create-question-dialog.tsx +### 1.4.2 调用链路:学生提交作业 -#### `updateQuestionAction` -- 签名:同上 -- 依赖:`requirePermission(QUESTION_UPDATE)`, `shared/db` -- 被使用:question-actions.tsx +``` +[Client] homework-take-view.tsx + │ + ▼ +[Action] homework/actions.submitHomeworkAction + │ + ├─▶ requirePermission(HOMEWORK_SUBMIT) + │ + ├─▶ db.query.homeworkSubmissions.findFirst ❌ 应在 data-access + │ (校验 submission 归属) + │ + ├─▶ db.update(homeworkSubmissions) ❌ 应在 data-access + │ SET status = "submitted", submittedAt = now() + │ + └─▶ 返回 ActionState<{ submissionId }> +``` -#### `deleteQuestionAction` -- 签名:同上 -- 依赖:`requirePermission(QUESTION_DELETE)`, `shared/db` -- 被使用:question-actions.tsx +### 1.4.3 调用链路:管理员仪表盘聚合 -#### `getQuestionsAction` -- 签名:`(params: GetQuestionsParams) => Promise<...>` -- 依赖:`requirePermission(QUESTION_READ)`, `data-access.getQuestions` -- 被使用:teacher/questions/page.tsx +``` +[Route] /admin/dashboard/page.tsx (Server Component) + │ + ▼ +[DataAccess] dashboard/data-access.getAdminDashboardData + │ + ├─▶ db.query.sessions.count() ❌ 跨模块直查(auth) + ├─▶ db.query.users.count() ❌ 跨模块直查(users) + ├─▶ db.query.usersToRoles ❌ 跨模块直查(users) + ├─▶ db.query.roles ❌ 跨模块直查(users) + ├─▶ db.query.classes.count() ❌ 跨模块直查(classes) + ├─▶ db.query.textbooks.count() ❌ 跨模块直查(textbooks) + ├─▶ db.query.chapters.count() ❌ 跨模块直查(textbooks) + ├─▶ db.query.questions.count() ❌ 跨模块直查(questions) + ├─▶ db.query.exams (含 scope 过滤) ❌ 跨模块直查(exams) + ├─▶ db.query.homeworkAssignments ❌ 跨模块直查(homework) + └─▶ db.query.homeworkSubmissions ❌ 跨模块直查(homework) -#### `getKnowledgePointOptionsAction` -- 签名:`() => Promise` -- 依赖:`requirePermission(QUESTION_READ)`, `shared/db` -- 被使用:create-question-dialog.tsx - -### 导出函数 (data-access.ts) - -#### `getQuestions` -- 签名:`(params: GetQuestionsParams & { scope?: DataScope }) => Promise` -- 功能:查询题目列表(含知识点关联、数据权限过滤) -- 依赖:`shared/db`, `DataScope` -- 被使用:`getQuestionsAction`, teacher/questions/page.tsx - -#### `GetQuestionsParams` (类型) -- 定义:查询参数类型(含 keyword?, type?, difficulty?, knowledgePointId?, parentId? 等过滤条件) -- 被使用:`getQuestions`, `getQuestionsAction` - -### Schema (schema.ts) - -#### `QuestionTypeEnum` -- 类型:Zod enum -- 定义:题目类型枚举(choice/fill/judge/composite 等) -- 被使用:`BaseQuestionSchema`, `CreateQuestionSchema` - -#### `BaseQuestionSchema` -- 类型:Zod schema -- 定义:题目基础校验 schema(type, content, difficulty, knowledgePointIds? 等) -- 被使用:`CreateQuestionSchema` - -#### `CreateQuestionInput` -- 类型:TypeScript 类型(基于 `CreateQuestionSchema` 推断) -- 被使用:`createNestedQuestion`, create-question-dialog.tsx - -#### `CreateQuestionSchema` -- 类型:Zod schema -- 定义:创建题目的完整校验 schema(含 `BaseQuestionSchema` + 选项/答案/子题目) -- 被使用:`createNestedQuestion` - -### 类型/接口 - -#### `Question` -- 被使用:exams (题目选择), homework (作业题目) - -#### `KnowledgePointOption` -- 被使用:create-question-dialog.tsx - -#### `QuestionType` -- 定义:题目类型联合类型(基于 `QuestionTypeEnum`) -- 被使用:questions/components, exams/components, homework/components - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `create-question-dialog.tsx` | 创建题目对话框(含嵌套题目) | -| `question-actions.tsx` | 题目操作菜单(编辑/删除) | -| `question-data-table.tsx` | 题目列表数据表格 | -| `question-card.tsx` | 题目卡片 | -| `question-detail.tsx` | 题目详情展示 | -| `question-type-badge.tsx` | 题目类型徽章 | + ⚠️ 单函数直查 11 张跨模块表,是本次审查最严重的封装违规 + 建议:各模块暴露 getModuleStats(scope) 函数,dashboard 聚合调用 +``` --- -## 模块:textbooks +# 第二部分:模块清单 -### 模块职责 -教材与知识体系管理:教材/章节树形结构、知识点 CRUD、Markdown 内容编辑、知识图谱。 +> 每个模块包含:职责 · 导出函数 · 依赖关系 · 已知问题 · 文件清单 -### 导出函数 (actions.ts) +## 2.1 shared(基础设施层) -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `createTextbookAction` | TEXTBOOK_CREATE | 创建教材 | -| `updateTextbookAction` | TEXTBOOK_UPDATE | 更新教材元信息 | -| `deleteTextbookAction` | TEXTBOOK_DELETE | 删除教材 | -| `createChapterAction` | TEXTBOOK_CREATE | 创建章节 | -| `updateChapterContentAction` | TEXTBOOK_UPDATE | 更新章节内容(Markdown) | -| `deleteChapterAction` | TEXTBOOK_DELETE | 删除章节 | -| `createKnowledgePointAction` | TEXTBOOK_CREATE | 创建知识点 | -| `updateKnowledgePointAction` | TEXTBOOK_UPDATE | 更新知识点 | -| `deleteKnowledgePointAction` | TEXTBOOK_DELETE | 删除知识点 | -| `reorderChaptersAction` | TEXTBOOK_UPDATE | 章节排序 | +**职责**:提供全项目共享的 DB Schema、工具函数、权限系统、UI 基础组件、通用 Hooks。 -### 导出函数 (data-access.ts) +**导出函数**(核心): +- `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()` — 通用工具 -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getTextbooks` | `(query?, subject?, grade?) => Promise` | teacher/textbooks/page.tsx | -| `getTextbookById` | `(id: string) => Promise` | teacher/textbooks/[id]/page.tsx | -| `getChaptersByTextbookId` | `(textbookId: string) => Promise` | textbook-reader.tsx | -| `getKnowledgePointsByChapterId` | `(chapterId: string) => Promise` | textbook-reader.tsx | -| `getKnowledgePointsByTextbookId` | `(textbookId: string) => Promise` | textbook-reader.tsx | +**依赖关系**: +- 被依赖方:**所有模块**依赖 shared +- ⚠️ 反向依赖:`shared/lib/{audit-logger, change-logger, auth-guard}` → `@/auth`(循环依赖) -#### 写操作函数 (data-access.ts) +**已知问题**: +- ❌ P0:`shared/lib/*` ↔ `@/auth` 循环依赖 +- ⚠️ P1:`schema.ts` 1111 行(54 张表混合,超 1000 硬上限) +- ⚠️ P1:`auth.ts` 293 行混合 5 类职责 +- ⚠️ P2:`ai.ts` 218 行混合 5 类职责 +- ⚠️ P2:`onboarding-gate.tsx` 业务逻辑泄漏到 shared -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `persistTextbook` | `(input: { textbookId, title, subject, grade, publisher, creatorId }) => Promise` | createTextbookAction | -| `updateTextbookMeta` | `(textbookId, input: { title?, subject?, grade?, publisher? }) => Promise` | updateTextbookAction | -| `deleteTextbookRecord` | `(textbookId) => Promise` | deleteTextbookAction | -| `persistChapter` | `(input: { chapterId, textbookId, parentId?, title, order }) => Promise` | createChapterAction | -| `updateChapterContent` | `(chapterId, content, textbookId) => Promise` | updateChapterContentAction | -| `deleteChapterRecord` | `(chapterId, textbookId) => Promise` | deleteChapterAction | -| `reorderChapters` | `(chapterId, newIndex, parentId, textbookId) => Promise` | reorderChaptersAction | -| `persistKnowledgePoint` | `(input: { kpId, chapterId, textbookId, name, description?, anchorText? }) => Promise` | createKnowledgePointAction | -| `updateKnowledgePointRecord` | `(kpId, textbookId, input: { name?, description?, anchorText? }) => Promise` | updateKnowledgePointAction | -| `deleteKnowledgePointRecord` | `(kpId, textbookId) => Promise` | deleteKnowledgePointAction | - -### 导出 Hooks - -#### `useTextSelection` -- 签名:`useTextSelection() => { selectedText, createDialogOpen, isCreating, handleContentPointerDown, handleContextMenuChange }` -- 功能:管理教材内容文本选择与知识点创建对话框状态(无参数,内部使用 ref 与状态) -- 被使用:textbook-content-panel.tsx - -#### `useKnowledgePointActions` -- 签名:`useKnowledgePointActions(textbookId, selectedChapterId, highlightedKpId, setHighlightedKpId, onCreateKP, onEditKP) => { editingKp, editKpDialogOpen, ..., requestDeleteKnowledgePoint, confirmDeleteKnowledgePoint, handleUpdateKnowledgePoint }` -- 功能:知识点 CRUD 操作集合(6 参数:textbookId, selectedChapterId, highlightedKpId, setHighlightedKpId, onCreateKP, onEditKP) -- 被使用:textbook-reader.tsx - -### 类型/接口 - -#### `Chapter` -- 被使用:textbooks/components, questions (知识点关联) - -#### `KnowledgePoint` -- 被使用:textbooks/components, questions/types (KnowledgePointOption) - -#### `Textbook` -- 定义:教材类型(含 id, title, subject, grade, publisher 等) -- 被使用:textbooks/components, teacher/textbooks/page.tsx - -#### `TextbookListItem` -- 定义:教材列表项类型 -- 被使用:teacher/textbooks/page.tsx - -#### `ChapterTreeNode` -- 定义:章节树节点类型(含 children[]) -- 被使用:textbook-reader.tsx, chapter-tree.tsx - -#### `KnowledgePointInput` -- 定义:知识点创建/更新输入类型 -- 被使用:useKnowledgePointActions, createKnowledgePointAction - -#### `ChapterInput` -- 定义:章节创建输入类型 -- 被使用:createChapterAction - -#### `ReorderChaptersInput` -- 定义:章节排序输入类型 -- 被使用:reorderChaptersAction - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `textbook-reader.tsx` | 教材阅读器(章节树 + 内容面板 + 知识点) | -| `textbook-list.tsx` | 教材列表 | -| `textbook-card.tsx` | 教材卡片 | -| `textbook-form.tsx` | 教材创建/编辑表单 | -| `textbook-content-panel.tsx` | 教材内容面板(Markdown 渲染 + 文本选择) | -| `chapter-tree.tsx` | 章节树(可拖拽排序) | -| `chapter-node.tsx` | 章节树节点 | -| `chapter-form.tsx` | 章节创建/编辑表单 | -| `chapter-content-editor.tsx` | 章节 Markdown 内容编辑器 | -| `knowledge-point-list.tsx` | 知识点列表 | -| `knowledge-point-item.tsx` | 知识点项 | -| `knowledge-point-form.tsx` | 知识点创建/编辑表单 | -| `knowledge-graph.tsx` | 知识图谱可视化 | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `db/schema.ts` | 1111 | 54 张表定义(超硬上限) | +| `db/relations.ts` | - | 表关系定义 | +| `db/index.ts` | - | Drizzle 客户端 | +| `lib/auth-guard.ts` | - | 认证上下文 + 权限校验 + DataScope | +| `lib/permissions.ts` | - | 角色-权限映射 | +| `lib/ai.ts` | 218 | AI 调用 + Provider 配置 + 加密 | +| `lib/audit-logger.ts` | - | 操作日志 | +| `lib/change-logger.ts` | - | 数据变更日志 | +| `lib/login-logger.ts` | - | 登录日志 | +| `lib/password-policy.ts` | - | 密码策略纯函数 | +| `lib/rate-limit.ts` | - | 内存滑动窗口限流 | +| `lib/excel.ts` | - | Excel 导入导出 | +| `lib/file-storage.ts` | - | 文件存储抽象 | +| `hooks/use-permission.ts` | - | 客户端权限 Hook | +| `components/ui/*` | 34 文件 | shadcn/ui 标准组件 | +| `components/onboarding-gate.tsx` | 312 | 引导流程(业务泄漏) | +| `components/global-search.tsx` | 221 | 全局搜索(业务泄漏) | +| `types/permissions.ts` | 92 | 57 个权限点常量 | --- -## 模块:classes +## 2.2 exams(考试模块) -### 模块职责 -班级管理:班级 CRUD、学生注册/退班、邀请码、课表、学科教师分配。 +**职责**:考试全生命周期管理(创建/编辑/预览/发布/删除/复制)+ AI 辅助出题。 -### 导出函数 (actions.ts) +**导出函数**: +- Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` +- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` +- AI Pipeline:`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `createTeacherClassAction` | CLASS_CREATE | 教师创建班级 | -| `updateTeacherClassAction` | CLASS_UPDATE | 教师更新班级 | -| `deleteTeacherClassAction` | CLASS_DELETE | 教师删除班级 | -| `createGradeClassAction` | CLASS_CREATE | 年级主任创建班级 | -| `updateGradeClassAction` | CLASS_UPDATE | 年级主任更新班级 | -| `deleteGradeClassAction` | CLASS_DELETE | 年级主任删除班级 | -| `enrollStudentByEmailAction` | CLASS_ENROLL | 通过邮箱注册学生 | -| `joinClassByInvitationCodeAction` | CLASS_ENROLL | 通过邀请码加入 | -| `ensureClassInvitationCodeAction` | CLASS_ENROLL | 确保邀请码存在 | -| `regenerateClassInvitationCodeAction` | CLASS_ENROLL | 重新生成邀请码 | -| `setStudentEnrollmentStatusAction` | CLASS_ENROLL | 设置学生状态 | -| `createClassScheduleItemAction` | CLASS_SCHEDULE | 创建课表项 | -| `updateClassScheduleItemAction` | CLASS_SCHEDULE | 更新课表项 | -| `deleteClassScheduleItemAction` | CLASS_SCHEDULE | 删除课表项 | -| `createAdminClassAction` | CLASS_CREATE | 管理员创建班级 | -| `updateAdminClassAction` | CLASS_UPDATE | 管理员更新班级 | -| `deleteAdminClassAction` | CLASS_DELETE | 管理员删除班级 | +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`questions`(类型)、`classes`(❌ 直查)、`school`(❌ 直查 subjects/grades)、`questions`(❌ 直查 insert) +- 被依赖:`homework`(通过 sourceExamId 外键,合理)、`dashboard`(❌ 直查)、`proctoring`(❌ 直查) -### 导出函数 (data-access.ts) +**已知问题**: +- ❌ P0:`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表 +- ❌ P0:`getExams`/`getExamById` 直查 `classes` 表 +- ❌ P1:`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表(应属 school 模块) +- ❌ P1:`actions.ts` 832 行(超 800 建议),多处直接 DB 操作 +- ⚠️ P1:`ai-pipeline.ts` 912 行(超 800 建议),混合 4 类职责 -#### 读操作函数 - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getTeacherClasses` | `(params?: { teacherId? }) => Promise` | teacher/classes/my, dashboard | -| `getAdminClasses` | `() => Promise` | admin 班级管理 | -| `getGradeManagedClasses` | `(userId) => Promise` | grade_head 班级管理 | -| `getStudentClasses` | `(studentId) => Promise` | student/dashboard | -| `getStudentSchedule` | `(studentId) => Promise` | student 课表 | -| `getClassStudents` | `(classId, scope?) => Promise` | teacher/classes/students | -| `getClassSchedule` | `(classId) => Promise` | teacher/classes/schedule | -| `getClassHomeworkInsights` | `(classId) => Promise` | classes 作业洞察 | -| `getGradeHomeworkInsights` | `(gradeId) => Promise` | 年级作业洞察 | -| `getClassById` | `(classId, scope?) => Promise` | 班级详情页 | -| `getClassDetails` | `(classId, scope?) => Promise` | 班级详情(含学生数、教师数) | -| `getClassSubjectTeachers` | `(classId) => Promise` | 班级学科教师分配 | -| `getStudentEnrollmentStatus` | `(classId, studentId) => Promise` | 注册状态查询 | -| `validateInvitationCode` | `(code) => Promise<{ valid: boolean; classId?: string }>` | 邀请码校验 | -| `getClassInsights` | `(classId) => Promise` | 班级洞察数据 | - -#### 写操作函数 - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `persistTeacherClass` | `(input: { classId, teacherId, name, gradeId, ... }) => Promise` | createTeacherClassAction | -| `updateTeacherClassRecord` | `(classId, input) => Promise` | updateTeacherClassAction | -| `deleteTeacherClassRecord` | `(classId) => Promise` | deleteTeacherClassAction | -| `persistGradeClass` | `(input: { classId, gradeId, ... }) => Promise` | createGradeClassAction | -| `updateGradeClassRecord` | `(classId, input) => Promise` | updateGradeClassAction | -| `deleteGradeClassRecord` | `(classId) => Promise` | deleteGradeClassAction | -| `enrollStudentByEmail` | `(classId, email) => Promise<{ studentId }>` | enrollStudentByEmailAction | -| `joinClassByInvitationCode` | `(code, studentId) => Promise<{ classId }>` | joinClassByInvitationCodeAction | -| `ensureInvitationCode` | `(classId) => Promise<{ code }>` | ensureClassInvitationCodeAction | -| `regenerateInvitationCode` | `(classId) => Promise<{ code }>` | regenerateClassInvitationCodeAction | -| `setStudentEnrollmentStatus` | `(classId, studentId, status) => Promise` | setStudentEnrollmentStatusAction | -| `persistClassScheduleItem` | `(input: { scheduleId, classId, ... }) => Promise` | createClassScheduleItemAction | -| `updateClassScheduleItem` | `(scheduleId, input) => Promise` | updateClassScheduleItemAction | -| `deleteClassScheduleItem` | `(scheduleId) => Promise` | deleteClassScheduleItemAction | -| `persistAdminClass` | `(input: { classId, ... }) => Promise` | createAdminClassAction | -| `updateAdminClassRecord` | `(classId, input) => Promise` | updateAdminClassAction | -| `deleteAdminClassRecord` | `(classId) => Promise` | deleteAdminClassAction | -| `assignSubjectTeacher` | `(classId, teacherId, subjectId) => Promise` | 学科教师分配 | -| `removeClassSubjectTeacher` | `(classId, teacherId, subjectId) => Promise` | 移除学科教师 | -| `bulkEnrollStudents` | `(classId, studentIds[]) => Promise` | 批量注册学生 | -| `transferStudent` | `(fromClassId, toClassId, studentId) => Promise` | 学生转班 | -| `getClassInvitationInfo` | `(classId) => Promise<{ code, expiresAt? }>` | 邀请信息查询 | -| `countClassStudents` | `(classId) => Promise` | 学生计数 | - -### 类型/接口 - -#### `TeacherClass` -- 定义:教师班级类型(含 grade, studentCount 等) -- 被使用:teacher/classes/my, dashboard - -#### `AdminClassListItem` -- 定义:管理员班级列表项类型 -- 被使用:admin 班级管理 - -#### `StudentEnrolledClass` -- 定义:学生已加入班级类型 -- 被使用:student/dashboard - -#### `StudentScheduleItem` -- 定义:学生课表项类型 -- 被使用:student 课表 - -#### `ClassStudent` -- 定义:班级学生类型(含 enrollmentStatus) -- 被使用:teacher/classes/students - -#### `ClassScheduleItem` -- 定义:班级课表项类型 -- 被使用:teacher/classes/schedule - -#### `ClassHomeworkInsights` -- 定义:班级作业洞察类型 -- 被使用:classes 作业洞察 - -#### `GradeHomeworkInsights` -- 定义:年级作业洞察类型 -- 被使用:年级作业洞察 - -#### `Class` -- 定义:班级基础类型 -- 被使用:classes/components - -#### `ClassDetails` -- 定义:班级详情类型(含学生数、教师数、课表等) -- 被使用:班级详情页 - -#### `ClassSubjectTeacher` -- 定义:班级学科教师类型 -- 被使用:班级学科教师分配 - -#### `EnrollmentStatus` -- 定义:注册状态枚举类型(active/inactive/pending) -- 被使用:classes/data-access.ts - -#### `ClassInsights` -- 定义:班级洞察数据类型 -- 被使用:班级洞察页面 - -#### `CreateClassInput` -- 定义:创建班级输入类型 -- 被使用:createTeacherClassAction, createAdminClassAction - -#### `UpdateClassInput` -- 定义:更新班级输入类型 -- 被使用:updateTeacherClassAction, updateAdminClassAction - -#### `CreateScheduleItemInput` -- 定义:创建课表项输入类型 -- 被使用:createClassScheduleItemAction - -#### `EnrollmentInput` -- 定义:注册输入类型 -- 被使用:enrollStudentByEmailAction - -#### `InvitationCodeResult` -- 定义:邀请码结果类型 -- 被使用:ensureClassInvitationCodeAction - -#### `ClassListItem` -- 定义:班级列表项类型 -- 被使用:班级列表页 - -#### `ClassFormValues` -- 定义:班级表单值类型 -- 被使用:class-form.tsx - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `class-list.tsx` | 班级列表 | -| `class-card.tsx` | 班级卡片 | -| `class-form.tsx` | 班级创建/编辑表单 | -| `class-detail.tsx` | 班级详情 | -| `class-actions.tsx` | 班级操作菜单 | -| `class-students.tsx` | 班级学生列表 | -| `class-student-card.tsx` | 班级学生卡片 | -| `class-schedule.tsx` | 班级课表 | -| `class-schedule-form.tsx` | 课表项创建/编辑表单 | -| `class-schedule-item.tsx` | 课表项展示 | -| `class-invitation.tsx` | 邀请码组件 | -| `class-invitation-dialog.tsx` | 邀请码对话框 | -| `class-enroll-dialog.tsx` | 学生注册对话框 | -| `class-subject-teachers.tsx` | 学科教师分配 | -| `class-subject-teacher-form.tsx` | 学科教师表单 | -| `class-insights.tsx` | 班级洞察 | -| `class-homework-insights.tsx` | 班级作业洞察 | -| `class-status-badge.tsx` | 班级状态徽章 | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 832 | 10 个 Server Action(超限) | +| `ai-pipeline.ts` | 912 | AI 出题管线(超限) | +| `data-access.ts` | 339 | 考试 CRUD | +| `types.ts` | 31 | 类型定义 | +| `hooks/use-exam-preview.ts` | 295 | 预览 Hook | +| `components/*` | 18 文件 | 考试表单/组卷/预览组件 | --- -## 模块:school +## 2.3 homework(作业模块) -### 模块职责 -学校基础数据管理:学校、年级、部门、学年的 CRUD。 +**职责**:作业全生命周期(创建/发布/作答/批改/分析)。 -### 导出函数 (actions.ts) +**导出函数**: +- Actions:`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` +- Data-access:`getHomeworkAssignments` / `getHomeworkAssignmentById` / `getHomeworkSubmissions` / `getStudentHomeworkAssignments` / `getStudentHomeworkTakeData` / `getStudentDashboardGrades` / `getHomeworkAssignmentAnalytics` / `getHomeworkAssignmentReviewList` / `getHomeworkSubmissionDetails` / `getTeacherGradeTrends` / `getDemoStudentUser` -> 所有 12 个 actions 均使用 `requirePermission()` 进行权限校验。 -> 学校 CRUD actions(createSchoolAction/updateSchoolAction/deleteSchoolAction)在写操作成功后调用 `logAudit()` 记录操作日志。 +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`exams`(❌ 直查 5 处)、`classes`(❌ 直查)、`school`(❌ 直查 subjects)、`users`(❌ 直查) +- 被依赖:`dashboard`(通过 data-access,合理)、`parent`(通过 data-access,合理)、`classes`(❌ classes 反向直查 homework 表) -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `createSchoolAction` | SCHOOL_MANAGE | 创建学校(成功后记录 audit log: school.create) | -| `updateSchoolAction` | SCHOOL_MANAGE | 更新学校(成功后记录 audit log: school.update) | -| `deleteSchoolAction` | SCHOOL_MANAGE | 删除学校(成功后记录 audit log: school.delete) | -| `createGradeAction` | GRADE_MANAGE | 创建年级 | -| `updateGradeAction` | GRADE_MANAGE | 更新年级 | -| `deleteGradeAction` | GRADE_MANAGE | 删除年级 | -| `createDepartmentAction` | SCHOOL_MANAGE | 创建部门 | -| `updateDepartmentAction` | SCHOOL_MANAGE | 更新部门 | -| `deleteDepartmentAction` | SCHOOL_MANAGE | 删除部门 | -| `createAcademicYearAction` | SCHOOL_MANAGE | 创建学年 | -| `updateAcademicYearAction` | SCHOOL_MANAGE | 更新学年 | -| `deleteAcademicYearAction` | SCHOOL_MANAGE | 删除学年 | +**已知问题**: +- ❌ P0:`data-access.ts` 1038 行(超 1000 硬上限),必须拆分 +- ❌ P0:`getStudentDashboardGrades` 混入 150+ 行排名计算业务逻辑 +- ❌ P0:`getHomeworkAssignmentAnalytics` 混入 145+ 行错误率统计业务逻辑 +- ❌ P1:5 处直查 `exams` 表 +- ❌ P1:`actions.ts` 多处直接 DB 操作(`createHomeworkAssignmentAction` 157 行) -### 导出函数 (data-access.ts) - -| 函数 | 被使用 | -|------|--------| -| `getSchools()` | admin 学校管理, onboarding | -| `getGrades()` | admin 年级管理, exams, onboarding | -| `getDepartments()` | admin 部门管理 | -| `getAcademicYears()` | admin 学年管理 | -| `getStaffOptions()` | school 组件 (年级主任选择) | -| `getGradesForStaff(staffId)` | grade_head 视图 | - -### Schema (schema.ts) - -#### `CreateSchoolSchema` -- 类型:Zod schema -- 定义:创建学校的校验 schema(name, code) -- 被使用:`createSchoolAction` - -#### `CreateGradeSchema` -- 类型:Zod schema -- 定义:创建年级的校验 schema(schoolId, name, order, gradeHeadId?, teachingHeadId?) -- 被使用:`createGradeAction` - -#### `CreateDepartmentSchema` -- 类型:Zod schema -- 定义:创建部门的校验 schema(name, description?) -- 被使用:`createDepartmentAction` - -#### `CreateAcademicYearSchema` -- 类型:Zod schema -- 定义:创建学年的校验 schema(name, startDate, endDate, isActive?) -- 被使用:`createAcademicYearAction` - -### 类型/接口 - -#### `SchoolListItem` -- 定义:学校列表项类型(含 id, name, code) -- 被使用:admin 学校管理 - -#### `GradeListItem` -- 定义:年级列表项类型(含 schoolId, name, gradeHeadId?) -- 被使用:admin 年级管理, exams - -#### `DepartmentListItem` -- 定义:部门列表项类型 -- 被使用:admin 部门管理 - -#### `AcademicYearListItem` -- 定义:学年列表项类型 -- 被使用:admin 学年管理 - -#### `StaffOption` -- 定义:员工选项类型(含 id, name) -- 被使用:school 组件 (年级主任选择) - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `school-form.tsx` | 学校创建/编辑表单 | -| `grade-form.tsx` | 年级创建/编辑表单 | -| `department-form.tsx` | 部门创建/编辑表单 | -| `academic-year-form.tsx` | 学年创建/编辑表单 | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 1038 | 作业 CRUD + 学生视角 + 分析 + 批改(超硬上限) | +| `actions.ts` | 387 | 5 个 Server Action | +| `types.ts` | 186 | 类型定义 | +| `schema.ts` | 29 | Zod 校验 | --- -## 模块:dashboard +## 2.4 questions(题库模块) -### 模块职责 -各角色仪表盘数据聚合与展示。 +**职责**:题库管理(题目 CRUD、知识点关联、题型支持)。 -### 导出函数 (data-access.ts) +**导出函数**: +- Actions:`getQuestionsAction` / `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction` +- Data-access:`getQuestions` / `insertQuestionWithRelations`(错放 actions)/ `deleteQuestionRecursive`(错放 actions) -#### `getAdminDashboardData` -- 签名:`getAdminDashboardData(scope?: DataScope): Promise` -- 依赖:`shared/db`, `DataScope` -- 被使用:admin/dashboard/page.tsx +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`textbooks`(❌ actions 直查 knowledgePoints/chapters/textbooks) +- 被依赖:`exams`(通过类型导入,合理)、`textbooks`(UI 组合,合理) -### 类型/接口 +**已知问题**: +- ❌ P1:写操作函数错放在 `actions.ts`(`insertQuestionWithRelations` / `deleteQuestionRecursive`) +- ❌ P1:`getKnowledgePointOptionsAction` 直查 textbooks 模块表 +- ⚠️ P2:`data-access.ts` 仅 129 行,写操作缺失 -#### `StudentDashboardProps` -- 被使用:student-dashboard.tsx -- 依赖:`homework/types.StudentDashboardGradeProps` - -#### `StudentDashboard` -- 定义:学生仪表盘组件(原 `StudentDashboardView`,已重命名) -- 被使用:student/dashboard/page.tsx - -#### `TeacherDashboardData` -- 被使用:teacher-dashboard-view.tsx -- 依赖:`homework/data-access.getTeacherGradeTrends`, `classes/data-access.getTeacherClasses` - -#### `TeacherDashboardProps` -- 定义:教师仪表盘组件 Props 类型 -- 被使用:teacher-dashboard-view.tsx - -#### `AdminDashboardData` -- 定义:管理员仪表盘数据类型(含 activeSessionsCount, userCount, userRoleCounts, classCount 等) -- 被使用:admin/dashboard/page.tsx - -#### `DashboardWidget` -- 定义:仪表盘通用小组件类型(含 title, value, trend? 等) -- 被使用:dashboard/components - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `admin-dashboard-view.tsx` | 管理员仪表盘视图 | -| `teacher-dashboard-view.tsx` | 教师仪表盘视图 | -| `student-dashboard.tsx` | 学生仪表盘视图(原 StudentDashboardView) | -| `parent-dashboard-view.tsx` | 家长仪表盘视图 | -| `dashboard-card.tsx` | 仪表盘卡片 | -| `dashboard-widget.tsx` | 仪表盘小组件 | -| `dashboard-stat.tsx` | 统计数据展示 | -| `dashboard-chart.tsx` | 图表组件 | -| `dashboard-schedule.tsx` | 课表展示 | -| `dashboard-assignment-list.tsx` | 作业列表 | -| `dashboard-assignment-item.tsx` | 作业项 | -| `dashboard-grade-trend.tsx` | 成绩趋势 | -| `dashboard-class-list.tsx` | 班级列表 | -| `dashboard-submission-list.tsx` | 提交列表 | -| `dashboard-empty.tsx` | 仪表盘空状态 | -| `dashboard-header.tsx` | 仪表盘头部 | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 294 | 5 个 Server Action(含错放的 data-access 函数) | +| `data-access.ts` | 129 | 仅读查询 | +| `schema.ts` | 18 | Zod 校验 | +| `types.ts` | 34 | 类型定义 | --- -## 模块:layout +## 2.5 textbooks(教材模块)— 标杆模块 -### 模块职责 -应用布局框架:侧边栏、顶栏、导航配置。 +**职责**:教材与知识体系管理(教材/章节树形结构、知识点 CRUD、Markdown 内容编辑、知识图谱)。 -### 导出组件 +**导出函数**: +- Actions:`getTextbooksAction` / `getTextbookByIdAction` / `createTextbookAction` / `updateTextbookAction` / `deleteTextbookAction` / `getChaptersAction` / `createChapterAction` / `updateChapterAction` / `deleteChapterAction` / `getKnowledgePointsAction` / `createKnowledgePointAction` / `updateKnowledgePointAction` / `deleteKnowledgePointAction` +- Data-access:与 actions 一一对应的 data-access 函数 -#### `AppSidebar` -- 内部使用:`usePermission`, `NAV_CONFIG` -- 功能:根据权限渲染侧边栏导航 +**依赖关系**: +- 依赖:`shared/*`、`@/auth` +- 被依赖:`questions`(❌ 直查)、`exams`(通过类型)、`dashboard`(❌ 直查) -#### `SiteHeader` -- 内部使用:`useSession`, `signOut`, `NotificationDropdown`(来自 messaging 模块), `GlobalSearch`(来自 shared/components), `Breadcrumb` -- 功能:顶部导航栏(含全局搜索 GlobalSearch、通知下拉菜单展示未读通知数、面包屑导航、用户下拉菜单) +**已知问题**: +- ✅ 无跨模块 DB 访问 +- ✅ actions 层编排模式标杆(权限校验 → 调用 data-access → revalidatePath) +- ✅ data-access 层职责单一 -#### `SidebarProvider` -- Props: `{ children: React.ReactNode, defaultOpen?: boolean }` -- 功能:侧边栏状态上下文 Provider(管理展开/折叠状态) -- 被使用:app/layout.tsx - -### 导出 Hooks - -#### `useSidebar` -- 签名:`useSidebar(): { isOpen, toggle, setOpen, isMobile, openMobile, setOpenMobile }` -- 功能:访问侧边栏状态(需在 `SidebarProvider` 内使用) -- 被使用:app-sidebar.tsx, site-header.tsx - -### 类型/接口 - -#### `Role` -- 定义:`type Role = "admin" | "teacher" | "student" | "parent" | "grade_head" | "teaching_head"` -- 被使用:NAV_CONFIG, usePermission - -#### `NavItem` -- 定义:`{ title: string; href: string; icon?: string; permission?: Permission; children?: NavItem[] }` -- 被使用:NAV_CONFIG, app-sidebar.tsx - -### 导出配置 - -#### `NAV_CONFIG` -- 类型:`Record` -- 每个NavItem含 `permission?: Permission` 字段 -- admin 角色菜单包含 "Audit Logs" 项(icon: ScrollText, href: /admin/audit-logs, permission: Permissions.AUDIT_LOG_READ),含子项 Operation Logs 与 Login Logs -- admin 角色菜单的 "School Management" 子菜单包含 "Course Plans" 项(href: /admin/course-plans, permission: Permissions.COURSE_PLAN_MANAGE) -- admin 角色菜单的 "School Management" 子菜单包含 "Import Users" 项(href: /admin/users/import, permission: Permissions.USER_MANAGE) -- admin 角色菜单包含 "Scheduling" 项(icon: CalendarClock, href: /admin/scheduling/rules, permission: Permissions.SCHEDULE_ADJUST),含子项 Rules (/admin/scheduling/rules, permission: SCHEDULE_ADJUST)、Auto Schedule (/admin/scheduling/auto, permission: SCHEDULE_AUTO)、Change Requests (/admin/scheduling/changes, permission: SCHEDULE_ADJUST) -- teacher 角色菜单包含 "Grades" 项(icon: GraduationCap, permission: Permissions.GRADE_RECORD_READ),含子项 All Grades (/teacher/grades)、Batch Entry (/teacher/grades/entry, permission: GRADE_RECORD_MANAGE)、Statistics (/teacher/grades/stats)、Analytics (/teacher/grades/analytics, permission: GRADE_RECORD_READ) -- teacher 角色菜单包含 "Course Plans" 项(icon: CalendarRange, href: /teacher/course-plans, permission: Permissions.COURSE_PLAN_READ) -- teacher 角色菜单包含 "Attendance" 项(icon: CalendarCheck, href: /teacher/attendance, permission: Permissions.ATTENDANCE_MANAGE),含子项 Records (/teacher/attendance)、Take Attendance (/teacher/attendance/sheet, permission: ATTENDANCE_MANAGE)、Statistics (/teacher/attendance/stats, permission: ATTENDANCE_READ) -- teacher 角色菜单包含 "Schedule Changes" 项(icon: CalendarClock, href: /teacher/schedule-changes, permission: Permissions.SCHEDULE_ADJUST) -- teacher 角色菜单包含 "Diagnostic" 项(icon: Stethoscope, href: /teacher/diagnostic, permission: Permissions.DIAGNOSTIC_READ) -- student 角色菜单包含 "My Grades" 项(icon: GraduationCap, href: /student/grades, permission: Permissions.GRADE_RECORD_READ) -- student 角色菜单包含 "Attendance" 项(icon: CalendarCheck, href: /student/attendance, permission: Permissions.ATTENDANCE_READ) -- student 角色菜单包含 "Diagnostic" 项(icon: Stethoscope, href: /student/diagnostic, permission: Permissions.DIAGNOSTIC_READ) -- parent 角色菜单包含 "Dashboard" 项(icon: LayoutDashboard, href: /parent/dashboard,无 permission 字段,仅需登录) -- parent 角色菜单包含 "Grades" 项(icon: GraduationCap, href: /parent/grades, permission: Permissions.GRADE_RECORD_READ) -- parent 角色菜单包含 "Attendance" 项(icon: CalendarCheck, href: /parent/attendance, permission: Permissions.ATTENDANCE_READ) -- parent 角色菜单包含 "Announcements" 项(icon: Megaphone, href: /announcements, permission: Permissions.ANNOUNCEMENT_READ) -- 所有角色(admin/teacher/student/parent)菜单均包含 "Messages" 项(icon: Mail, href: /messages, permission: Permissions.MESSAGE_READ) -- 被使用:app-sidebar.tsx +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 276 | 13 个 Server Action(标杆) | +| `data-access.ts` | 428 | 教材/章节/知识点 CRUD | +| `types.ts` | 79 | 类型定义 | +| `hooks/use-knowledge-point-actions.ts` | 121 | 知识点操作 Hook | +| `components/*` | 12 文件 | 教材编辑/知识图谱组件 | --- -## 模块:settings +## 2.6 grades(成绩模块)— 标杆模块(拆分范例) -### 模块职责 -系统设置:AI Provider 配置、用户偏好、密码安全(修改密码、强度校验)。 +**职责**:成绩分析(录入/查询/统计/导出/趋势对比分析)。 -### 导出函数 (actions.ts) +**导出函数**: +- Actions:`getGradeRecordsAction` / `createGradeRecordAction` / `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction` / `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getClassRankingAction` / `getRankingTrendAction` +- Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getRankingTrend` -> 所有 3 个 actions 均使用 `requirePermission(AI_CONFIGURE)` 进行权限校验。 +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`classes`(❌ 直查 classes/classEnrollments)、`school`(❌ 直查 subjects)、`users`(❌ 直查) +- 被依赖:`parent`(通过 data-access,合理)、`dashboard` -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `getAiProviderSummaries()` | AI_CONFIGURE | 获取 AI Provider 列表 | -| `upsertAiProviderAction(data)` | AI_CONFIGURE | 创建/更新 AI Provider | -| `testAiProviderAction(data)` | AI_CONFIGURE | 测试 AI Provider 连通性 | +**已知问题**: +- ❌ P1:多处直查 `classes`/`classEnrollments`/`subjects`/`users` 表 +- ⚠️ P2:统计计算业务逻辑混入 data-access(`getClassGradeStats` / `getGradeDistribution`) +- ✅ actions 层无直接 DB 访问(标杆) +- ✅ data-access 按职责拆分为 3 个文件(标杆) -### 导出函数 (actions-password.ts) - -#### `changePasswordAction` -- 签名:`(prevState: ActionState, formData: FormData) => Promise>` -- 权限:`requireAuth()`(任何登录用户可修改自己的密码) -- 功能:修改当前用户密码(校验当前密码、新密码策略、速率限制 PASSWORD_CHANGE: 5次/分钟) -- 依赖:`requireAuth`, `validatePassword`, `rateLimit`, bcryptjs (`hash`/`compare`), `shared/db` (users, passwordSecurity) -- 被使用:settings/components/password-change-form.tsx - -### 类型/接口 - -#### `AiProviderSummary` -- 定义:AI Provider 摘要类型(含 id, provider, baseUrl, model, apiKeyLast4, isDefault 等,不含完整 apiKey) -- 被使用:getAiProviderSummaries, settings/components - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `ai-provider-list.tsx` | AI Provider 列表 | -| `ai-provider-form.tsx` | AI Provider 创建/编辑表单 | -| `ai-provider-card.tsx` | AI Provider 卡片 | -| `ai-provider-test-dialog.tsx` | AI Provider 测试对话框 | -| `ai-provider-actions.tsx` | AI Provider 操作菜单 | -| `settings-layout.tsx` | 设置页面布局 | -| `password-change-form.tsx` | 密码修改表单(当前密码/新密码/确认密码 + 强度指示器 + 需求提示) | -| `notification-preferences-form.tsx` | 通知偏好表单(Delivery Channels: push/email/sms + Notification Categories: messages/announcements/homework/grades/attendance;Switch 切换 + 隐藏 checkbox 提交;useActionState 调用 updateNotificationPreferencesAction;toast 反馈) | -| `admin-settings-view.tsx` | 管理员设置视图(General/Appearance/Security/Notifications tab,Security 含 PasswordChangeForm,Notifications 含 NotificationPreferencesForm;接收 notificationPreferences prop) | -| `teacher-settings-view.tsx` | 教师设置视图(同上,含 Notifications tab) | -| `student-settings-view.tsx` | 学生设置视图(同上,含 Notifications tab) | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 312 | 6 个 Server Action | +| `actions-analytics.ts` | 133 | 5 个分析 Action | +| `data-access.ts` | 419 | 成绩 CRUD + 统计 | +| `data-access-analytics.ts` | 293 | 趋势/对比分析 | +| `data-access-ranking.ts` | 121 | 排名查询 | +| `export.ts` | 214 | Excel 导出 | +| `schema.ts` | 52 | Zod 校验 | +| `types.ts` | - | 类型定义 | --- -## 模块:users +## 2.7 classes(班级模块)— 耦合最严重 -### 模块职责 -用户个人资料管理:当前用户查看与更新自己的资料;用户批量导入/导出(Excel)。 +**职责**:班级 CRUD + 学生/教师管理 + 邀请码注册。 -### 模块路径 -`src/modules/users` +**导出函数**: +- Actions:`createTeacherClassAction` / `updateTeacherClassAction` / `deleteTeacherClassAction` / `createAdminClassAction` / `updateAdminClassAction` / `deleteAdminClassAction` / `createGradeClassAction` / `updateGradeClassAction` / `deleteGradeClassAction` +- Data-access:`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `createClassScheduleItem` / `updateClassScheduleItem` / `deleteClassScheduleItem` -### 导出函数 (actions.ts) +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`school`(❌ actions 直查 grades 表)、`homework`(❌ data-access 直查 5 张 homework 表)、`exams`(❌ data-access 直查) +- 被依赖:`exams`/`homework`/`grades`/`attendance`/`scheduling`/`dashboard`/`parent`/`course-plans`/`users`(8+ 处直查 classes 表) -#### `updateUserProfile` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 功能:更新当前用户个人资料(name, phone, address, gender, age 等) -- 依赖:`requireAuth()`, `shared/db` -- 被使用:profile-form.tsx +**已知问题**: +- ❌ P0:`data-access.ts` 2104 行(超 1000 硬上限 2.1 倍),必须拆分 +- ❌ P0:混入 homework 逻辑(`getClassHomeworkInsights` + `getGradeHomeworkInsights` = 532 行) +- ❌ P0:混入 scheduling 逻辑(课表 CRUD,与 scheduling 模块写同一张表) +- ❌ P0:混入 grades 逻辑(`getStudentsSubjectScores`) +- ❌ P0:`classSchedule` 表三处写入口(数据完整性高风险) +- ❌ P1:`actions.ts` 直查 `grades` 表做权限校验 +- ❌ P1:`getSessionTeacherId` 在 data-access 调用 `auth()` -> 注:本模块仅使用 `requireAuth()` 校验登录状态,不涉及权限点(用户只能修改自己的资料)。 - -#### `downloadUserTemplateAction` -- 签名:`() => Promise>` -- 权限:`requirePermission(USER_MANAGE)` -- 功能:生成用户导入模板(返回 base64 编码的 Excel) -- 依赖:`requirePermission`, `import-export.generateUserImportTemplate` -- 被使用:components/user-import-dialog.tsx - -#### `importUsersAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 权限:`requirePermission(USER_MANAGE)` -- 功能:导入用户:接收文件,解析+验证+批量创建(默认密码 123456) -- 依赖:`requirePermission`, `shared/lib/excel.parseExcel`, `import-export.parseUserImportData`, `import-export.batchImportUsers` -- 被使用:components/user-import-dialog.tsx - -#### `exportUsersAction` -- 签名:`(role?: string) => Promise>` -- 权限:`requirePermission(USER_MANAGE)` -- 功能:导出用户列表(返回 base64 编码的 Excel) -- 依赖:`requirePermission`, `import-export.exportUsersToExcel` -- 被使用:待扩展 - -### 导出函数 (data-access.ts) - -#### `getUserProfile` -- 签名:`(userId: string) => Promise` -- 功能:获取用户个人资料(含 name, email, phone, address, gender, age, grade, department 等) -- 依赖:`shared/db` -- 被使用:profile/page.tsx, profile-form.tsx - -### 导出函数 (import-export.ts) - -#### `generateUserImportTemplate` -- 签名:`() => Promise` -- 功能:生成用户导入模板(列:姓名/邮箱/角色/手机/班级邀请码,含示例行) -- 依赖:`shared/lib/excel.generateTemplate` -- 被使用:`downloadUserTemplateAction` - -#### `parseUserImportData` -- 签名:`(rows: Record[]) => UserImportValidation` -- 功能:解析并验证导入行(校验姓名/邮箱格式/角色枚举/邀请码仅 student) -- 依赖:无 -- 被使用:`importUsersAction` - -#### `batchImportUsers` -- 签名:`(records: UserImportRecord[]) => Promise` -- 功能:批量创建用户(默认密码 123456 bcrypt 哈希,自动创建 usersToRoles,student 通过邀请码自动加入班级) -- 依赖:`shared/db`, `bcryptjs`, `@paralleldrive/cuid2` -- 被使用:`importUsersAction` - -#### `exportUsersToExcel` -- 签名:`(params: { scope: DataScope; role?: string }) => Promise` -- 功能:导出用户列表到 Excel(含姓名/邮箱/手机/性别/年龄/角色/创建时间) -- 依赖:`shared/db`, `shared/lib/excel.exportToExcel` -- 被使用:`exportUsersAction`, `app/api/export/route.ts` - -### 类型/接口 - -#### `UserProfile` -- 定义:用户资料类型(含 id, name, email, phone, address, gender, age, gradeId?, departmentId?, onboardedAt? 等) -- 被使用:profile/page.tsx, profile-form.tsx - -#### `UpdateUserProfileInput` -- 定义:更新用户资料输入类型(name?, phone?, address?, gender?, age? 等可选字段) -- 被使用:`updateUserProfile`, profile-form.tsx - -#### `UserImportRecord` -- 定义:`{ name, email, role, phone?, invitationCode? }` -- 被使用:`parseUserImportData`, `batchImportUsers` - -#### `UserImportValidation` -- 定义:`{ valid: UserImportRecord[], invalid: Array<{ row, record, errors }> }` -- 被使用:`parseUserImportData` - -#### `UserImportResult` -- 定义:`{ successCount, failedCount, errors: Array<{ row, email, error }> }` -- 被使用:`batchImportUsers`, `importUsersAction` - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `user-import-dialog.tsx` | 用户批量导入对话框(4 状态:idle/preview/importing/done;下载模板→上传预览→确认导入→结果展示) | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 2104 | 班级 CRUD + 作业洞察 + 课表 + 成绩(严重超限) | +| `actions.ts` | 765 | 9 个 Server Action(三组重复) | +| `types.ts` | 201 | 类型定义(含跨领域类型污染) | --- -## 模块:audit +## 2.8 school(学校模块) -### 模块职责 -操作日志与登录日志查询:提供审计日志的列表展示、筛选与分页能力,支撑管理员审计与合规需求。 +**职责**:学校/学年/部门/年级的 CRUD。 -### 模块路径 -`src/modules/audit` +**导出函数**: +- Actions:`getSchoolsAction` / `createSchoolAction` / `updateSchoolAction` / `deleteSchoolAction` / `getAcademicYearsAction` / `createAcademicYearAction` / `updateAcademicYearAction` / `deleteAcademicYearAction` / `getDepartmentsAction` / `createDepartmentAction` / `updateDepartmentAction` / `deleteDepartmentAction` / `getGradesAction` / `createGradeAction` / `updateGradeAction` / `deleteGradeAction` / `getStaffOptions` +- Data-access:与 actions 对应的只读查询 -### 导出函数 (data-access.ts) +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`users`(⚠️ `getStaffOptions` 直查 users/roles,可接受) +- 被依赖:`exams`(❌ 直查 subjects/grades)、`homework`(❌ 直查 subjects)、`grades`(❌ 直查 subjects)、`questions`(❌ 直查)、`classes`(❌ actions 直查 grades 表)、`course-plans`(合理) -#### `getAuditLogs` -- 签名:`getAuditLogs(params?: AuditLogQueryParams): Promise>` -- 参数说明:`params`: AuditLogQueryParams,含 userId?, module?, action?, status?, page?, pageSize?, startDate?, endDate? -- 功能:分页查询操作日志(支持按模块/操作/状态/日期范围过滤) -- 依赖:`shared/db` (auditLogs 表) -- 被使用:app/(dashboard)/admin/audit-logs/page.tsx +**已知问题**: +- ⚠️ P2:审计日志不一致(仅 school 实体记录,department/academicYear/grade 未记录) +- ⚠️ P2:`getStaffOptions`/`getGrades` 直查 users/roles(展示用,可接受) -#### `getLoginLogs` -- 签名:`getLoginLogs(params?: LoginLogQueryParams): Promise>` -- 参数说明:`params`: LoginLogQueryParams,含 userId?, action?, status?, page?, pageSize?, startDate?, endDate? -- 功能:分页查询登录日志(支持按操作/状态/日期范围过滤) -- 依赖:`shared/db` (loginLogs 表) -- 被使用:app/(dashboard)/admin/audit-logs/login-logs/page.tsx - -#### `getAuditModuleOptions` -- 签名:`getAuditModuleOptions(): Promise` -- 功能:获取操作日志中所有不同的 module 值(用于筛选下拉框) -- 依赖:`shared/db` (auditLogs 表) -- 被使用:app/(dashboard)/admin/audit-logs/page.tsx - -#### `getDataChangeLogs` -- 签名:`getDataChangeLogs(params?: DataChangeLogQueryParams): Promise>` -- 参数说明:`params`: DataChangeLogQueryParams,含 tableName?, recordId?, action?, userId?, page?, pageSize?, startDate?, endDate? -- 功能:分页查询数据变更日志(支持按表名/记录ID/操作/用户/日期范围过滤) -- 依赖:`shared/db` (dataChangeLogs 表) -- 被使用:`getDataChangeLogsAction` - -#### `getDataChangeStats` -- 签名:`getDataChangeStats(): Promise` -- 功能:按 tableName 分组统计数据变更日志数量(用于统计卡片) -- 依赖:`shared/db` (dataChangeLogs 表) -- 被使用:`getDataChangeLogsAction` - -#### `getDataChangeTableOptions` -- 签名:`getDataChangeTableOptions(): Promise` -- 功能:获取数据变更日志中所有不同的 tableName 值(用于筛选下拉框) -- 依赖:`shared/db` (dataChangeLogs 表) -- 被使用:`getDataChangeLogsAction` - -#### `getDataChangeLogsForExport` -- 签名:`getDataChangeLogsForExport(params?: DataChangeLogQueryParams): Promise` -- 功能:导出用:按条件查询全部数据变更日志(取前 100 条,无分页上限封顶) -- 依赖:`getDataChangeLogs` -- 被使用:`exportDataChangeLogsAction` - -#### `getAuditLogsForExport` -- 签名:`getAuditLogsForExport(params?: AuditLogQueryParams): Promise` -- 功能:导出用:按条件查询全部操作日志(取前 100 条) -- 依赖:`getAuditLogs` -- 被使用:`exportAuditLogsAction` - -#### `getLoginLogsForExport` -- 签名:`getLoginLogsForExport(params?: LoginLogQueryParams): Promise` -- 功能:导出用:按条件查询全部登录日志(取前 100 条) -- 依赖:`getLoginLogs` -- 被使用:`exportLoginLogsAction` - -### 导出函数 (actions.ts) - -> 所有 actions 均使用 `requirePermission(AUDIT_LOG_READ)` 进行权限校验(数据变更日志复用 AUDIT_LOG_READ 权限)。 - -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `getDataChangeLogsAction` | AUDIT_LOG_READ | 获取数据变更日志(分页结果 + tableOptions + stats 三者并行加载) | -| `exportAuditLogsAction` | AUDIT_LOG_READ | 导出操作日志为 Excel(返回 `{ buffer, filename }`) | -| `exportLoginLogsAction` | AUDIT_LOG_READ | 导出登录日志为 Excel(返回 `{ buffer, filename }`) | -| `exportDataChangeLogsAction` | AUDIT_LOG_READ | 导出数据变更日志为 Excel(返回 `{ buffer, filename }`) | - -### 类型/接口 (types.ts) - -#### `AuditLog` -- 定义:操作日志类型(含 id, userId, userName, action, module, targetId, targetType, detail, ipAddress, userAgent, status, createdAt) -- 被使用:audit/components, audit/data-access - -#### `LoginLog` -- 定义:登录日志类型(含 id, userId, userEmail, action, status, ipAddress, userAgent, errorMessage, createdAt) -- 被使用:audit/components, audit/data-access - -#### `AuditLogQueryParams` -- 定义:操作日志查询参数类型(含 userId?, module?, action?, status?, page?, pageSize?, startDate?, endDate?) -- 被使用:`getAuditLogs`, audit-logs/page.tsx - -#### `LoginLogQueryParams` -- 定义:登录日志查询参数类型(含 userId?, action?, status?, page?, pageSize?, startDate?, endDate?) -- 被使用:`getLoginLogs`, login-logs/page.tsx - -#### `PaginatedResult` -- 定义:分页结果接口(含 items: T[], total, page, pageSize, totalPages) -- 被使用:`getAuditLogs`, `getLoginLogs`, `getDataChangeLogs`, audit/components - -#### `DataChangeAction` -- 定义:`"create" | "update" | "delete"` -- 被使用:data-access, change-logger, types.DataChangeLog - -#### `DataChangeLog` -- 定义:数据变更日志类型(含 id, tableName, recordId, action, oldValue, newValue, changedBy, changedByName, ipAddress, createdAt) -- 被使用:audit/data-access, audit/actions - -#### `DataChangeStat` -- 定义:`{ tableName: string; count: number }`(按表名分组的变更统计) -- 被使用:`getDataChangeStats`, `getDataChangeLogsAction` - -#### `DataChangeLogQueryParams` -- 定义:数据变更日志查询参数类型(含 tableName?, recordId?, action?, userId?, page?, pageSize?, startDate?, endDate?) -- 被使用:`getDataChangeLogs`, `getDataChangeLogsForExport`, `getDataChangeLogsAction`, `exportDataChangeLogsAction` - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `audit-log-table.tsx` | 操作日志表格(含分页控件) | -| `audit-log-filters.tsx` | 操作日志筛选器(模块/操作/状态/日期,基于 nuqs) | -| `audit-log-view.tsx` | 操作日志视图(筛选+表格+URL 分页) | -| `login-log-table.tsx` | 登录日志表格(含分页控件) | -| `login-log-filters.tsx` | 登录日志筛选器(操作/状态/日期,基于 nuqs) | -| `login-log-view.tsx` | 登录日志视图(筛选+表格+URL 分页) | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 325 | 17 个 Server Action | +| `data-access.ts` | 186 | 只读查询 | +| `schema.ts` | 51 | Zod 校验 | +| `types.ts` | 42 | 类型定义 | --- -## 模块:announcements +## 2.9 scheduling(排课模块) -### 模块职责 -通知公告系统:创建、编辑、发布、归档、删除公告,所有登录用户可查看已发布公告。 +**职责**:自动排课算法 + 课表调整 + 排课规则管理。 -### 模块路径 -`src/modules/announcements` +**导出函数**: +- Actions:`autoScheduleAction` / `applyAutoScheduleAction` / `getSchedulingRulesAction` / `updateSchedulingRulesAction` / `getScheduleChangesAction` / `createScheduleChangeAction` / `updateScheduleChangeAction` / `deleteScheduleChangeAction` +- Data-access:`getSchedulingRules` / `getScheduleChanges` / `getAdminClassesForScheduling` / `getTeachersForScheduling` / `getClassroomsForScheduling` / `getClassSubjectsForScheduling` +- 算法:`findOptimalSlot` / `validateSchedule` / `autoSchedule` / `buildDefaultTimeSlots`(纯函数,标杆) -### 导出函数 (actions.ts) +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`classes`(❌ actions 直查 users + 直写 classSchedule)、`school`(⚠️ 排课辅助查询,可接受) +- 被依赖:与 `classes` 共写 `classSchedule` 表 -> 所有 manage actions 均使用 `requirePermission(ANNOUNCEMENT_MANAGE)` 进行权限校验。 +**已知问题**: +- ❌ P0:`applyAutoScheduleAction` 直接 transaction 写 `classSchedule` 表(第三个写入口) +- ❌ P1:`autoScheduleAction` 直查 `users` 表 +- ⚠️ P2:`actions.ts` 末尾 re-export data-access 函数(反模式) +- ✅ `auto-scheduler.ts` 是算法独立化的最佳实践(纯函数、无 DB、可测试) -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `createAnnouncementAction` | ANNOUNCEMENT_MANAGE | 创建公告(草稿/已发布) | -| `updateAnnouncementAction` | ANNOUNCEMENT_MANAGE | 更新公告 | -| `deleteAnnouncementAction` | ANNOUNCEMENT_MANAGE | 删除公告 | -| `publishAnnouncementAction` | ANNOUNCEMENT_MANAGE | 发布公告(设置 published 状态) | -| `archiveAnnouncementAction` | ANNOUNCEMENT_MANAGE | 归档公告 | -| `getAnnouncementsAction` | requireAuth | 获取公告列表(所有登录用户可读) | - -### 导出函数 (data-access.ts) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getAnnouncements` | `(params?: { status?, type?, page?, pageSize? }) => Promise` | admin/announcements, announcements 页面 | -| `getAnnouncementById` | `(id: string) => Promise` | 编辑页面 | - -### Schema (schema.ts) - -#### `CreateAnnouncementSchema` -- 类型:Zod schema -- 定义:创建公告的校验 schema(title, content, type, status, targetGradeId?, targetClassId?, publishedAt?) -- 被使用:`createAnnouncementAction` - -#### `UpdateAnnouncementSchema` -- 类型:Zod schema -- 定义:更新公告的校验 schema(同上) -- 被使用:`updateAnnouncementAction` - -### 类型/接口 - -#### `Announcement` -- 定义:公告完整类型(含 id, title, content, type, status, targetGradeId, targetClassId, authorId, authorName, publishedAt, createdAt, updatedAt) -- 被使用:announcements/components, 页面 - -#### `AnnouncementListItem` -- 定义:公告列表项类型(同 Announcement) -- 被使用:列表页 - -#### `AnnouncementStatus` -- 定义:`"draft" | "published" | "archived"` -- 被使用:data-access, components - -#### `AnnouncementType` -- 定义:`"school" | "grade" | "class"` -- 被使用:data-access, components - -#### `GetAnnouncementsParams` -- 定义:查询参数类型(status?, type?, page?, pageSize?) -- 被使用:`getAnnouncements`, `getAnnouncementsAction` - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `announcement-list.tsx` | 公告列表(支持状态筛选) | -| `announcement-card.tsx` | 单条公告卡片 | -| `announcement-form.tsx` | 创建/编辑表单 | -| `announcement-detail.tsx` | 详情查看(含发布/归档/删除操作) | -| `admin-announcements-view.tsx` | 管理端公告视图(列表+创建对话框) | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `auto-scheduler.ts` | 310 | 排课算法(纯函数,标杆) | +| `actions.ts` | 302 | 8 个 Server Action | +| `data-access.ts` | 272 | 排课辅助查询 + 规则/变更 CRUD | +| `schema.ts` | - | Zod 校验 | +| `types.ts` | - | 类型定义 | --- -## 模块:files +## 2.10 attendance(考勤模块)— 结构典范 -### 模块职责 -文件上传与管理:通过 API 路由处理文件上传(保存到 `public/uploads/YYYY-MM/`),记录文件元数据到 DB,支持按关联资源(exam/textbook/question/announcement)多态查询、下载与删除。 +**职责**:考勤记录管理 + 统计分析。 -### 模块路径 -`src/modules/files` +**导出函数**: +- Actions:`getAttendanceRecordsAction` / `createAttendanceRecordAction` / `updateAttendanceRecordAction` / `deleteAttendanceRecordAction` / `getStudentAttendanceAction` / `getAttendanceStatsAction` +- Data-access:`getAttendanceRecords` / `createAttendanceRecord` / `updateAttendanceRecord` / `deleteAttendanceRecord` / `getClassStudentsForAttendance` / `getAttendanceStats` -### 导出函数 (data-access.ts) +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`classes`(❌ `getClassStudentsForAttendance` 直查 classEnrollments) +- 被依赖:无 -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `createFileAttachment` | `(data: CreateFileAttachmentInput) => Promise` | `app/api/upload/route.ts` | -| `getFileAttachment` | `(id: string) => Promise` | `app/api/files/[id]/route.ts` | -| `getFileAttachmentsByTarget` | `(targetType: string, targetId: string) => Promise` | 按关联资源查询文件列表 | -| `getFileAttachmentsByUploader` | `(uploaderId: string) => Promise` | 按上传者查询文件列表 | -| `getAllFileAttachments` | `(limit?: number) => Promise` | `app/(dashboard)/admin/files/page.tsx` | -| `deleteFileAttachment` | `(id: string) => Promise` | `app/api/files/[id]/route.ts` | -| `deleteFileAttachments` | `(ids: string[]) => Promise` | `app/api/files/batch-delete/route.ts`(批量删除 DB 记录,失败回退逐条删除) | -| `getFileAttachmentsWithFilters` | `(params: FileAttachmentQueryParams) => Promise` | 管理员文件筛选(mimeType 精确/前缀匹配 + originalName/filename 模糊搜索 + limit/offset 分页) | -| `getFileStats` | `() => Promise` | 文件统计(总数、总大小、按 mimeType 分组) | -| `getFileAttachmentsByIds` | `(ids: string[]) => Promise` | `app/api/files/batch-delete/route.ts`(批量删除前获取磁盘路径) | +**已知问题**: +- ⚠️ P2:`getClassStudentsForAttendance` 直查 `classEnrollments`(应通过 classes data-access) +- ✅ stats 独立拆分为 `data-access-stats.ts`(拆分范例) +- ✅ DataScope 完整接入 6 种 scope 类型 +- ✅ actions 层无直接 DB 访问 -### 类型/接口 (types.ts) - -#### `FileAttachment` -- 定义:文件附件完整类型(含 id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType, targetId, createdAt) -- 被使用:files/components, data-access, API 路由 - -#### `FileUploadResult` -- 定义:上传成功返回类型 `{ id, url, filename, originalName, size, mimeType }` -- 被使用:`app/api/upload/route.ts` 响应, file-upload.tsx 回调 - -#### `FileTargetType` -- 定义:`"exam" | "textbook" | "question" | "announcement"` -- 被使用:types.FileAttachment.targetType, file-upload.tsx - -#### `CreateFileAttachmentInput` -- 定义:创建文件附件记录的输入类型 -- 被使用:`createFileAttachment` - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `file-upload.tsx` | 文件上传组件(拖拽+点击上传,进度条,文件类型校验,调用 `/api/upload`) | -| `file-list.tsx` | 文件列表展示(图标、文件名、大小、下载链接、删除按钮) | -| `file-preview.tsx` | 文件预览(图片直接预览,PDF iframe,其他下载) | -| `file-icon.tsx` | 根据 MIME 类型显示不同图标与颜色 | -| `admin-files-view.tsx` | 管理端文件视图(上传+列表+删除) | - -### API 路由 - -#### `POST /api/upload` -- 文件:`app/api/upload/route.ts` -- 功能:接收 `multipart/form-data`,保存文件到 `public/uploads/YYYY-MM/cuid.ext`,写入 DB,返回 `FileUploadResult` -- 权限:`requireAuth()`(需登录) -- 限制:10MB、白名单 MIME 类型 - -#### `GET /api/files/[id]` -- 文件:`app/api/files/[id]/route.ts` -- 功能:获取文件元数据 -- 权限:`requireAuth()` - -#### `DELETE /api/files/[id]` -- 文件:`app/api/files/[id]/route.ts` -- 功能:删除文件(DB 记录 + 磁盘文件) -- 权限:`requirePermission(FILE_DELETE)` - -#### `POST /api/files/batch-delete` -- 文件:`app/api/files/batch-delete/route.ts` -- 功能:批量删除文件(先查文件记录,通过 `storageProvider.delete` 删除磁盘文件静默失败,再调用 `deleteFileAttachments` 删除 DB 记录) -- 权限:`requirePermission(FILE_DELETE)` -- 请求体:JSON `{ ids: string[] }` -- 响应:`{ success, message, deletedCount, failedIds }` - -#### `POST /api/export` -- 文件:`app/api/export/route.ts` -- 功能:Excel 导出(grades/users/attendance),按 `type` 分发到 `exportGradeRecordsToExcel`/`exportUsersToExcel`,返回 `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` 二进制流 -- 权限:`requireAuth()`(需登录) -- 请求体:JSON `{ type: "grades" | "users" | "attendance", params?: Record }` - -#### `POST /api/import` -- 文件:`app/api/import/route.ts` -- 功能:Excel 解析预览(不写 DB),接收 Excel 文件,调用 `parseExcel` 返回 sheets 预览数据(实际导入由 `users/actions.importUsersAction` 完成) -- 权限:`requirePermission(USER_MANAGE)` -- 请求体:`multipart/form-data`,字段 `file` -- 限制:仅 `.xlsx`/`.xls`,10MB 上限 +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 271 | 6 个 Server Action | +| `data-access.ts` | 271 | 考勤 CRUD | +| `data-access-stats.ts` | 145 | 统计逻辑(拆分范例) | +| `schema.ts` | - | Zod 校验 | +| `types.ts` | - | 类型定义 | --- -## 模块:grades +## 2.11 users(用户模块) -### 模块职责 -成绩分析模块:成绩录入(单条/批量)、查询、统计报表(均分、中位数、标准差、及格率、优秀率、班级排名)、Excel 导出(成绩明细+统计汇总/班级多科目横向对比)、趋势对比分析(成绩趋势、班级对比、科目对比、分数分布、排名趋势)。 +**职责**:用户资料管理 + 批量导入导出。 -### 模块路径 -`src/modules/grades` +**导出函数**: +- Actions:`getUserProfileAction` / `updateUserProfileAction` / `importUsersAction` / `exportUsersAction` / `downloadUserTemplateAction` +- Data-access:`getUserProfile` +- Import-export:`generateUserImportTemplate` / `parseUserImportData` / `batchImportUsers` / `exportUsersToExcel` -### 导出函数 (actions.ts) +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`classes`(❌ `batchImportUsers` 直查 classes + 直写 classEnrollments) +- 被依赖:`dashboard`(❌ 直查)、`grades`(❌ 直查)、`homework`(❌ 直查) -> 所有 manage actions 均使用 `requirePermission(GRADE_RECORD_MANAGE)` 进行权限校验,read actions 使用 `requirePermission(GRADE_RECORD_READ)`。 +**已知问题**: +- ❌ P1:`import-export.ts` 四重职责混合(导入解析 + 导出 + 用户创建 + 班级注册) +- ❌ P1:`batchImportUsers` 跨模块写 `classEnrollments`(classes 模块的写操作) +- ❌ P1:`updateUserProfile` 绕过 data-access 直接 DB 写 +- ⚠️ P2:`data-access.ts` 仅 71 行,写操作缺失 -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `createGradeRecordAction` | GRADE_RECORD_MANAGE | 创建单条成绩记录 | -| `batchCreateGradeRecordsAction` | GRADE_RECORD_MANAGE | 批量录入成绩(班级+科目+考试,表格形式) | -| `updateGradeRecordAction` | GRADE_RECORD_MANAGE | 更新成绩记录 | -| `deleteGradeRecordAction` | GRADE_RECORD_MANAGE | 删除成绩记录 | -| `getGradeRecordsAction` | GRADE_RECORD_READ | 查询成绩列表(按 scope 过滤) | -| `getClassGradeStatsAction` | GRADE_RECORD_READ | 获取班级成绩统计 | -| `getStudentGradeSummaryAction` | GRADE_RECORD_READ | 获取学生成绩汇总(学生/家长只能查自己/子女) | -| `getClassRankingAction` | GRADE_RECORD_READ | 获取班级排名 | -| `getGradeRecordByIdAction` | GRADE_RECORD_READ | 获取单条成绩记录 | -| `exportGradesAction` | GRADE_RECORD_READ | 导出成绩到 Excel(detail=成绩明细+统计汇总,class=班级多科目横向对比总表),返回 base64 buffer | - -### 导出函数 (actions-analytics.ts) - -> 所有 analytics actions 均使用 `requirePermission(GRADE_RECORD_READ)` 进行权限校验。 - -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `getGradeTrendAction` | GRADE_RECORD_READ | 获取成绩趋势(按学生/科目/学期,返回归一化分数趋势点) | -| `getClassComparisonAction` | GRADE_RECORD_READ | 获取班级对比(同年级各班的均分/及格率/优秀率) | -| `getSubjectComparisonAction` | GRADE_RECORD_READ | 获取科目对比(同班级各科目雷达图数据) | -| `getGradeDistributionAction` | GRADE_RECORD_READ | 获取分数分布(90-100/80-89/70-79/60-69/<60 各区间人数) | -| `getRankingTrendAction` | GRADE_RECORD_READ | 获取排名趋势(学生历次考试排名变化,含 DataScope 二次校验) | - -### 导出函数 (data-access.ts) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getGradeRecords` | `(params: GradeQueryParams & { scope: DataScope; currentUserId?: string }) => Promise` | teacher/grades/page.tsx, getGradeRecordsAction | -| `getGradeRecordById` | `(id: string) => Promise` | getGradeRecordByIdAction | -| `createGradeRecord` | `(data: CreateGradeRecordInput, recordedBy: string) => Promise` | createGradeRecordAction | -| `batchCreateGradeRecords` | `(data: BatchCreateGradeRecordInput, recordedBy: string) => Promise` | batchCreateGradeRecordsAction | -| `updateGradeRecord` | `(id: string, data: UpdateGradeRecordInput) => Promise` | updateGradeRecordAction | -| `deleteGradeRecord` | `(id: string) => Promise` | deleteGradeRecordAction | -| `getClassGradeStats` | `(classId, subjectId?, examId?) => Promise` | getClassGradeStatsAction | -| `getStudentGradeSummary` | `(studentId: string) => Promise` | getStudentGradeSummaryAction, student/grades, parent/grades | -| `getClassRanking` | `(classId, subjectId?, examId?) => Promise` | getClassRankingAction, teacher/grades/stats | -| `getClassStudentsForEntry` | `(classId: string) => Promise>` | teacher/grades/entry | -| `getClassGradeStatsWithMeta` | `(classId, subjectId?, examId?) => Promise` | teacher/grades/stats | - -### 导出函数 (data-access-analytics.ts) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getGradeTrend` | `(params: { studentId; subjectId?; semester?; scope: DataScope }) => Promise` | getGradeTrendAction, teacher/grades/analytics | -| `getClassComparison` | `(params: { gradeId; subjectId; examId?; scope: DataScope }) => Promise` | getClassComparisonAction, teacher/grades/analytics | -| `getSubjectComparison` | `(params: { classId; examId?; semester?; scope: DataScope }) => Promise` | getSubjectComparisonAction, teacher/grades/analytics | -| `getGradeDistribution` | `(params: { classId; subjectId?; examId?; scope: DataScope }) => Promise` | getGradeDistributionAction, teacher/grades/analytics | - -### 导出函数 (data-access-ranking.ts) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getRankingTrend` | `(studentId: string, subjectId?, semester?) => Promise` | getRankingTrendAction | - -### 导出函数 (export.ts) - -#### `exportGradeRecordsToExcel` -- 签名:`(params: { classId: string; subjectId?: string; examId?: string; scope: DataScope }) => Promise` -- 功能:导出成绩单(Sheet1 成绩明细,Sheet2 统计汇总:均分/中位数/最高分/最低分/标准差/及格率/优秀率/参考人数) -- 依赖:`shared/lib/excel.exportToExcel`, `data-access.getGradeRecords`, `data-access.getClassGradeStats` -- 被使用:`exportGradesAction`, `app/api/export/route.ts` - -#### `exportClassGradeReportToExcel` -- 签名:`(params: { classId: string; scope: DataScope }) => Promise` -- 功能:导出班级成绩总表(多科目横向对比,含总分/平均分/排名列) -- 依赖:`shared/db`, `shared/lib/excel.exportToExcel`, `data-access.getGradeRecords` -- 被使用:`exportGradesAction` - -#### `formatDateForFile` -- 签名:`(d?: Date) => string` -- 功能:格式化日期为 `YYYY-MM-DD` 用于文件名 -- 依赖:无 -- 被使用:`exportGradesAction` - -### Schema (schema.ts) - -#### `CreateGradeRecordSchema` -- 类型:Zod schema -- 定义:创建成绩记录的校验 schema(studentId, classId, subjectId, examId?, title, score, fullScore?, type?, semester?, remark?) -- 被使用:`createGradeRecordAction` - -#### `BatchCreateGradeRecordSchema` -- 类型:Zod schema -- 定义:批量录入成绩的校验 schema(classId, subjectId, examId?, title, fullScore?, type?, semester?, records[{ studentId, score, remark? }]) -- 被使用:`batchCreateGradeRecordsAction` - -#### `UpdateGradeRecordSchema` -- 类型:Zod schema -- 定义:更新成绩记录的校验 schema(title?, score?, fullScore?, type?, semester?, remark?, examId?) -- 被使用:`updateGradeRecordAction` - -### 类型/接口 (types.ts) - -#### `GradeRecord` -- 定义:成绩记录完整类型(含 id, studentId, classId, subjectId, examId, title, score, fullScore, type, semester, recordedBy, remark, createdAt, updatedAt) -- 被使用:grades/data-access, grades/components - -#### `GradeRecordListItem` -- 定义:成绩列表项类型(含 studentName, className, subjectName, recorderName 等关联名称) -- 被使用:grades/components, teacher/grades - -#### `GradeStats` -- 定义:成绩统计类型(average, median, max, min, stdDev, passRate, excellentRate, count) -- 被使用:grades/components, teacher/grades/stats - -#### `ClassGradeStats` -- 定义:班级成绩统计类型(classId, className, stats: GradeStats, studentCount) -- 被使用:grades/components, teacher/grades/stats - -#### `StudentGradeSummary` -- 定义:学生成绩汇总类型(studentId, studentName, records[], averageScore, rank) -- 被使用:grades/components, student/grades, parent/grades - -#### `ClassRankingItem` -- 定义:班级排名项类型(studentId, studentName, averageScore, rank, recordCount) -- 被使用:grades/components, teacher/grades/stats - -#### `GradeRecordType` -- 定义:`"exam" | "quiz" | "homework" | "other"` -- 被使用:grades/data-access, grades/components - -#### `GradeQueryParams` -- 定义:查询参数类型(classId?, subjectId?, studentId?, type?, semester?, examId?) -- 被使用:`getGradeRecords`, `getGradeRecordsAction` - -#### `GradeTrendPoint` / `GradeTrendResult` -- 定义:成绩趋势类型(Point: date, title, score, fullScore, normalizedScore, type;Result: label, points[], averageScore) -- 被使用:getGradeTrend, getGradeTrendAction, grade-trend-chart - -#### `ClassComparisonItem` -- 定义:班级对比项类型(classId, className, averageScore, passRate, excellentRate, studentCount) -- 被使用:getClassComparison, getClassComparisonAction, class-comparison-chart - -#### `SubjectComparisonItem` -- 定义:科目对比项类型(subjectId, subjectName, averageScore, passRate, excellentRate) -- 被使用:getSubjectComparison, getSubjectComparisonAction, subject-comparison-chart - -#### `GradeDistributionBucket` / `GradeDistributionResult` -- 定义:分数分布类型(Bucket: label, min, max, count, percentage;Result: buckets[], totalCount) -- 被使用:getGradeDistribution, getGradeDistributionAction, grade-distribution-chart - -#### `RankingTrendPoint` / `RankingTrendResult` -- 定义:排名趋势类型(Point: title, date, rank, totalStudents, score;Result: studentName, points[]) -- 被使用:getRankingTrend, getRankingTrendAction - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `grade-record-form.tsx` | 单条成绩录入表单 | -| `batch-grade-entry.tsx` | 批量录入界面(选择班级+科目+考试,表格形式录入每个学生分数) | -| `grade-record-list.tsx` | 成绩列表(含删除功能) | -| `grade-stats-card.tsx` | 统计卡片(均分、中位数、及格率、优秀率等) | -| `grade-query-filters.tsx` | 查询筛选器(班级、科目、考试类型、学期) | -| `student-grade-summary.tsx` | 学生成绩汇总视图 | -| `class-grade-report.tsx` | 班级成绩报表(含统计+排名) | -| `export-button.tsx` | 成绩导出按钮(DropdownMenu 选择 detail/class 报表类型,调用 exportGradesAction 并触发浏览器下载) | -| `grade-trend-chart.tsx` | 成绩趋势折线图(recharts LineChart,归一化分数 0-100) | -| `class-comparison-chart.tsx` | 班级对比柱状图(recharts BarChart,均分/及格率/优秀率) | -| `subject-comparison-chart.tsx` | 科目对比雷达图(recharts RadarChart) | -| `grade-distribution-chart.tsx` | 分数分布柱状图(recharts BarChart,彩色区间 90-100/80-89/70-79/60-69/<60) | +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `import-export.ts` | 291 | 导入解析 + 导出 + 用户创建(职责混合) | +| `actions.ts` | 151 | 5 个 Server Action | +| `data-access.ts` | 71 | 仅 getUserProfile | --- -## 模块:course-plans +## 2.12 dashboard(仪表盘模块) -### 模块职责 -课程计划管理:创建、编辑、删除课程计划(含周计划条目),管理员可管理全部,教师/学生/年级主任/教务主任可查看。 +**职责**:管理员/教师/学生仪表盘数据聚合。 -### 导出函数 (actions.ts) +**导出函数**: +- Data-access:`getAdminDashboardData` / `getTeacherDashboardData` / `getStudentDashboardData` -#### `createCoursePlanAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_MANAGE)` -- 功能:创建课程计划 -- 依赖:`requirePermission`, `shared/db`, `data-access.createCoursePlan` -- 被以下模块使用:course-plan-form.tsx +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`classes`(合理)、`homework`(合理)、`grades`(合理)、❌ 直查 11 张跨模块表 +- 被依赖:无 -#### `updateCoursePlanAction` -- 签名:`(id: string, prevState: ActionState | null, formData: FormData) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_MANAGE)` -- 功能:更新课程计划 -- 依赖:`requirePermission`, `shared/db`, `data-access.updateCoursePlan` -- 被以下模块使用:course-plan-form.tsx +**已知问题**: +- ❌ P0:`getAdminDashboardData` 直查 11 张跨模块表(sessions/users/usersToRoles/roles/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions) +- ⚠️ P2:教师仪表盘直查 `users` 表获取教师姓名 +- ✅ 学生/教师仪表盘正确通过各模块 data-access 获取数据 -#### `deleteCoursePlanAction` -- 签名:`(id: string) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_MANAGE)` -- 功能:删除课程计划 -- 依赖:`requirePermission`, `shared/db`, `data-access.deleteCoursePlan` -- 被以下模块使用:course-plan-detail.tsx +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | - | 仪表盘数据聚合(含违规直查) | +| `types.ts` | - | 类型定义 | +| `components/*` | 14 文件 | 三种角色仪表盘组件 | -#### `getCoursePlansAction` -- 签名:`(params?: GetCoursePlansParams) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_READ)` -- 功能:获取课程计划列表 -- 依赖:`requirePermission`, `data-access.getCoursePlans` +--- -#### `getCoursePlanAction` -- 签名:`(id: string) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_READ)` -- 功能:获取课程计划详情(含周计划条目) -- 依赖:`requirePermission`, `data-access.getCoursePlanById` +## 2.13 messaging(私信模块) -#### `createCoursePlanItemAction` -- 签名:`(prevState: ActionState | null, formData: FormData) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_MANAGE)` -- 功能:创建周计划条目 -- 依赖:`requirePermission`, `shared/db`, `data-access.createCoursePlanItem` -- 被以下模块使用:course-plan-item-editor.tsx +**职责**:站内私信 + 站内通知列表 + 通知偏好。 -#### `updateCoursePlanItemAction` -- 签名:`(id: string, prevState: ActionState | null, formData: FormData) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_MANAGE)` -- 功能:更新周计划条目 -- 依赖:`requirePermission`, `shared/db`, `data-access.updateCoursePlanItem` -- 被以下模块使用:course-plan-item-editor.tsx +**导出函数**: +- Actions:`sendMessageAction` / `getMessagesAction` / `getMessageAction` / `deleteMessageAction` / `getNotificationsAction` / `markNotificationReadAction` / `markAllNotificationsReadAction` / `getNotificationPreferencesAction` / `updateNotificationPreferencesAction` +- Data-access:`createNotification` / `getNotifications` / `getRecipients` +- Notification-preferences:`getNotificationPreferences` / `updateNotificationPreferences` -#### `deleteCoursePlanItemAction` -- 签名:`(id: string) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_MANAGE)` -- 功能:删除周计划条目 -- 依赖:`requirePermission`, `shared/db`, `data-access.deleteCoursePlanItem` -- 被以下模块使用:course-plan-item-editor.tsx +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、❌ 绕过 notifications 直接写 `messageNotifications` +- 被依赖:`notifications`(❌ 反向依赖 messaging 的偏好和 in-app 渠道)、`settings`(通知偏好表单)、`layout`(通知下拉) -#### `toggleCoursePlanItemCompletedAction` -- 签名:`(id: string, completed: boolean) => Promise>` -- 权限:`requirePermission(COURSE_PLAN_MANAGE)` -- 功能:切换周计划条目完成状态 -- 依赖:`requirePermission`, `shared/db`, `data-access.updateCoursePlanItem` -- 被以下模块使用:course-plan-detail.tsx +**已知问题**: +- ❌ P0:`sendMessageAction` 绕过 notifications dispatcher 直接调用 `createNotification`,导致多渠道通知失效 +- ❌ P0:与 notifications 双向依赖 + 职责重叠 +- ⚠️ P1:同时管理 3 类数据(messages + messageNotifications + notificationPreferences) -### 导出函数 (data-access.ts) +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 245 | 9 个 Server Action | +| `data-access.ts` | 252 | 私信 CRUD + 通知 CRUD | +| `notification-preferences.ts` | 166 | 通知偏好 CRUD | +| `schema.ts` | 17 | 私信发送校验 | +| `types.ts` | 108 | 私信 + 通知 + 偏好类型 | -#### `getCoursePlans` -- 签名:`(params?: GetCoursePlansParams) => Promise` -- 功能:查询课程计划列表(含班级/科目/教师名称,支持按 classId/teacherId/subjectId/status 过滤) -- 依赖:`shared.db`, `shared.db.schema.coursePlans/classes/subjects/users` -- 被以下模块使用:admin/course-plans/page.tsx, teacher/course-plans/page.tsx +--- -#### `getCoursePlanById` -- 签名:`(id: string) => Promise` -- 功能:按 ID 获取课程计划详情(含周计划条目列表) -- 依赖:`shared.db`, `shared.db.schema.coursePlans/coursePlanItems/classes/subjects/users` -- 被以下模块使用:admin/course-plans/[id]/page.tsx, teacher/course-plans/[id]/page.tsx +## 2.14 notifications(通知分发模块) -#### `createCoursePlan` -- 签名:`(data: CreateCoursePlanInput, createdBy: string) => Promise` -- 功能:创建课程计划记录 -- 依赖:`shared.db`, `shared.db.schema.coursePlans`, `@paralleldrive/cuid2` -- 被以下模块使用:createCoursePlanAction +**职责**:多渠道通知分发(SMS/Email/WeChat/InApp)。 -#### `updateCoursePlan` -- 签名:`(id: string, data: Partial) => Promise` -- 功能:更新课程计划(部分字段) -- 依赖:`shared.db`, `shared.db.schema.coursePlans` -- 被以下模块使用:updateCoursePlanAction +**导出函数**: +- Actions:`sendNotificationAction` / `sendClassNotificationAction` +- Dispatcher:`sendNotification(payload)` +- Channels:`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender` -#### `deleteCoursePlan` -- 签名:`(id: string) => Promise` -- 功能:删除课程计划(级联删除周计划条目) -- 依赖:`shared.db`, `shared.db.schema.coursePlans` -- 被以下模块使用:deleteCoursePlanAction +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、❌ 反向依赖 `messaging`(偏好 + in-app 渠道 + createNotification) +- 被依赖:无(messaging 绕过它) -#### `createCoursePlanItem` / `updateCoursePlanItem` / `deleteCoursePlanItem` -- 签名:`(data: CreateCoursePlanItemInput) => Promise` / `(id: string, data: Partial) => Promise` / `(id: string) => Promise` -- 功能:周计划条目 CRUD -- 依赖:`shared.db`, `shared.db.schema.coursePlanItems`, `@paralleldrive/cuid2` -- 被以下模块使用:createCoursePlanItemAction, updateCoursePlanItemAction, deleteCoursePlanItemAction, toggleCoursePlanItemCompletedAction +**已知问题**: +- ❌ P0:不拥有任何数据,全部依赖 messaging 模块 +- ❌ P0:与 messaging 双向依赖 +- ❌ P1:类型系统不一致(messaging 按业务类别,notifications 按严重级别) +- ⚠️ P1:`sendClassNotificationAction` 直查 `classes`/`classEnrollments` +- ⚠️ P1:发送日志仅 console,无 `notification_logs` 表 +- ✅ 渠道抽象优秀(接口 + 工厂 + Mock 实现) -#### `reorderCoursePlanItems` -- 签名:`(planId: string, items: ReorderCoursePlanItemInput[]) => Promise` -- 功能:重排周计划条目顺序 -- 依赖:`shared.db`, `shared.db.schema.coursePlanItems` +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `dispatcher.ts` | 152 | 渠道选择 + 并行分发 | +| `data-access.ts` | 86 | 用户偏好 + 联系方式 + 日志 | +| `actions.ts` | 119 | 2 个 Server Action | +| `types.ts` | 70 | 通知负载 + 渠道配置类型 | +| `index.ts` | 38 | 对外导出入口 | +| `channels/*` | 5 文件 | 4 个渠道实现 | -#### `getSubjectOptions` -- 签名:`() => Promise<{id:string;name:string}[]>` -- 功能:获取科目选项列表 -- 依赖:`shared.db`, `shared.db.schema.subjects` -- 被以下模块使用:admin/course-plans/create/page.tsx, admin/course-plans/[id]/edit/page.tsx +--- -### Zod Schema (schema.ts) +## 2.15 audit(审计模块) -| Schema | 用途 | -|--------|------| -| `CreateCoursePlanSchema` | 创建课程计划校验(classId, subjectId, teacherId, academicYearId?, semester?, totalHours?, weeklyHours?, startDate?, endDate?, syllabus?, objectives?, status?) | -| `UpdateCoursePlanSchema` | 更新课程计划校验(所有字段可选,含 completedHours?) | -| `CreateCoursePlanItemSchema` | 创建周计划条目校验(planId, week, topic, content?, hours?, textbookChapter?, notes?) | -| `UpdateCoursePlanItemSchema` | 更新周计划条目校验(所有字段可选,含 isCompleted?, completedAt?) | +**职责**:操作日志 + 登录日志 + 数据变更日志查询与导出。 -### 类型/接口 (types.ts) +**导出函数**: +- Actions:`getAuditLogsAction` / `getLoginLogsAction` / `getDataChangeLogsAction` / `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` +- Data-access:`getAuditLogs` / `getLoginLogs` / `getDataChangeLogs` / `getAuditModuleOptions` -| 类型 | 定义 | +**依赖关系**: +- 依赖:`shared/*`、`@/auth` +- 被依赖:无 + +**已知问题**: +- ⚠️ P2:Excel 导出逻辑内联在 actions 层(应抽取到 `export.ts`) +- ⚠️ P2:三个导出 Action 结构高度重复 +- ✅ data-access 职责清晰,无跨模块问题 + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 212 | 6 个 Server Action(含内联导出) | +| `data-access.ts` | 260 | 日志查询 | +| `types.ts` | - | 类型定义 | + +--- + +## 2.16 announcements(公告模块) + +**职责**:公告 CRUD + 发布/归档。 + +**导出函数**: +- Actions:`getAnnouncementsAction` / `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction` +- Data-access:`getAnnouncements` / `getAnnouncementById` + +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`school`(合理,获取年级列表) +- 被依赖:无 + +**已知问题**: +- ❌ P1:所有写操作直接在 actions 层 `db.insert/update/delete`,未下沉到 data-access +- ⚠️ P2:死代码 `void wasPublished` +- ⚠️ P2:`getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission(ANNOUNCEMENT_READ)` + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 242 | 6 个 Server Action(含直接 DB 写) | +| `data-access.ts` | 120 | 仅 2 个只读函数 | +| `schema.ts` | - | Zod 校验 | +| `types.ts` | - | 类型定义 | + +--- + +## 2.17 files(文件模块) + +**职责**:文件附件 CRUD + 批量删除 + 统计。 + +**导出函数**: +- Data-access:`getAllFileAttachments` / `getFileAttachmentsByOwner` / `getFileAttachmentById` / `createFileAttachment` / `updateFileAttachment` / `deleteFileAttachment` / `batchDeleteFileAttachments` / `getFileStats` + +**依赖关系**: +- 依赖:`shared/*`、`@/auth` +- 被依赖:`app/api/upload` / `app/api/files/[id]` / `app/api/files/batch-delete` + +**已知问题**: +- ⚠️ P2:所有函数 try-catch 吞错误返回空数组/null +- ⚠️ P2:无 `actions.ts`,data-access 被路由直接调用 +- ✅ 职责单一,不跨模块查询 + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 267 | 文件 CRUD + 批量删除 + 统计 | +| `types.ts` | - | 类型定义 | +| `components/*` | 6 文件 | 上传/列表/预览/管理 | + +--- + +## 2.18 course-plans(课程计划模块) + +**职责**:课程计划 CRUD + 周计划项 CRUD + 排序。 + +**导出函数**: +- Actions:`getCoursePlansAction` / `getCoursePlanByIdAction` / `createCoursePlanAction` / `updateCoursePlanAction` / `deleteCoursePlanAction` / `createCoursePlanItemAction` / `updateCoursePlanItemAction` / `deleteCoursePlanItemAction` / `toggleCoursePlanItemCompletedAction` +- Data-access:与 actions 对应 + +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`classes`(合理,getAdminClasses/getStaffOptions)、`school`(合理,getAcademicYears) +- 被依赖:无 + +**已知问题**: +- ⚠️ P2:`getSubjectOptions` 直查 `subjects` 表(subjects 无独立模块,可接受) +- ✅ actions 层使用 `handleError`/`revalidatePlanPaths` 辅助函数(良好范例) + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 320 | 课程计划 + 周计划项 CRUD | +| `actions.ts` | 265 | 9 个 Server Action | +| `schema.ts` | - | Zod 校验 | +| `types.ts` | - | 类型定义 | + +--- + +## 2.19 parent(家长模块) + +**职责**:家长视角的子女数据聚合与展示。 + +**导出函数**: +- Data-access:`getChildren` / `getChildBasicInfo` / `getChildDashboardData` + +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`classes`(合理)、`homework`(合理)、`grades`(合理) +- 被依赖:无 + +**已知问题**: +- ⚠️ P2:`getChildBasicInfo` 多次串行查询,可优化为 join +- ✅ 职责单一,正确复用其他模块 data-access + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 234 | 子女关系 + 仪表盘数据聚合 | +| `types.ts` | 57 | 类型定义 | +| `components/*` | 7 文件 | 子女卡片/详情/仪表盘 | + +--- + +## 2.20 elective(选课模块) + +**职责**:选修课程管理 + 学生选课 + 抽签。 + +**导出函数**: +- Actions:`getElectiveCoursesAction` / `createElectiveCourseAction` / `updateElectiveCourseAction` / `deleteElectiveCourseAction` / `getStudentSelectionsAction` / `selectCourseAction` / `dropCourseAction` / `runLotteryAction` / `getAvailableCoursesForStudentAction` +- Data-access:`getElectiveCourses` / `getElectiveCourseById` / `createElectiveCourse` / `updateElectiveCourse` / `deleteElectiveCourse` / `selectCourse` / `dropCourse` / `runLottery` / `getStudentSelections` / `getAvailableCoursesForStudent` + +**依赖关系**: +- 依赖:`shared/*`、`@/auth` +- 被依赖:无 + +**已知问题**: +- ⚠️ P1:`data-access.ts` 与 `data-access-selections.ts` 重复定义 `mapCourseRow`/`buildCourseSelect`(60 行重复) +- ⚠️ P2:`runLottery` 使用 `Math.random()`,结果不可复现 +- ⚠️ P2:`selectCourse` FCFS 模式存在并发超卖风险 +- ✅ 权限校验完整(ELECTIVE_MANAGE/SELECT/READ) + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 304 | 11 个 Server Action | +| `data-access.ts` | 242 | 课程 CRUD + scope 过滤 | +| `data-access-operations.ts` | 217 | 选课操作(select/drop/lottery) | +| `data-access-selections.ts` | 189 | 选课记录查询 | +| `schema.ts` | 132 | Zod 校验 | +| `types.ts` | 108 | 类型定义 + 标签常量 | + +--- + +## 2.21 proctoring(监考模块) + +**职责**:考试监考事件记录 + 防作弊监控 + 监考面板。 + +**导出函数**: +- Actions:`recordProctoringEventAction` / `getProctoringDashboardAction` +- Data-access:`recordProctoringEvent` / `getExamForProctoring` / `getExamProctoringSummary` / `getStudentProctoringStatuses` / `getRecentProctoringEvents` + +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`exams`(❌ 直查 exams/examSubmissions)、`users`(❌ 直查) +- 被依赖:无 + +**已知问题**: +- ❌ P0:`exam-mode-config.tsx` 未集成到考试表单(死代码,监考功能无法启用) +- ❌ P0:事件上报存在 Server Action 与 REST API 双通道重复 +- ⚠️ P1:跨模块直查 `exams`/`examSubmissions`/`users`(监考本质是考试扩展,可接受但需标注) +- ⚠️ P2:`actions.ts` 直接 import `db` 和 `examSubmissions` + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 388 | 事件记录 + 查询 + 摘要统计 | +| `actions.ts` | 144 | 2 个 Server Action | +| `types.ts` | 136 | 类型定义 + 标签常量 + 阈值常量 | +| `components/anti-cheat-monitor.tsx` | - | 学生端防作弊监控 | +| `components/exam-mode-config.tsx` | - | 考试模式配置(**未集成**) | +| `components/proctoring-dashboard.tsx` | - | 教师监考面板 | + +--- + +## 2.22 diagnostic(学情诊断模块) + +**职责**:知识点掌握度查询 + 诊断报告生成。 + +**导出函数**: +- Actions:`generateStudentDiagnosticReportAction` / `generateClassDiagnosticReportAction` / `publishDiagnosticReportAction` / `deleteDiagnosticReportAction` / `getDiagnosticReportsAction` / `getStudentMasteryAction` +- Data-access:`updateMasteryFromSubmission` / `getStudentMastery` / `getClassMasteryOverview` +- Data-access-reports:`createDiagnosticReport` / `getDiagnosticReport` / `getDiagnosticReports` / `deleteDiagnosticReport` / `publishDiagnosticReport` + +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`exams`(❌ 直查 examSubmissions/submissionAnswers)、`questions`(❌ 直查 questionsToKnowledgePoints)、`classes`(❌ 直查 classEnrollments/classes/users) +- 被依赖:无 + +**已知问题**: +- ❌ P1:`updateMasteryFromSubmission` 跨模块直查 4 张表(与 exams/homework/questions 紧耦合) +- ⚠️ P2:`data-access-reports.ts` 有未使用代码(`round2`) +- ⚠️ P2:班级报告将生成者 ID 存入 `studentId` 字段(schema 设计缺陷 workaround) +- ✅ 与 grades 模块无职责重叠(grades 管分数,diagnostic 管知识点掌握度) + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 254 | 知识点掌握度查询 + 更新 | +| `data-access-reports.ts` | 202 | 诊断报告 CRUD | +| `actions.ts` | 148 | 6 个 Server Action | +| `types.ts` | 97 | 类型定义 | +| `components/*` | 4 文件 | 学生/班级诊断视图 + 雷达图 | + +--- + +## 2.23 settings(设置模块) + +**职责**:AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好。 + +**导出函数**: +- Actions:`getAiProvidersAction` / `createAiProviderAction` / `updateAiProviderAction` / `deleteAiProviderAction` / `testAiProviderAction` +- Actions-password:`changePasswordAction` + +**依赖关系**: +- 依赖:`shared/*`、`@/auth`、`messaging`(通知偏好表单调用 messaging Action) +- 被依赖:无 + +**已知问题**: +- ⚠️ P2:混合 5 类职责(AI Provider + 密码 + 资料 + 主题 + 通知偏好) +- ⚠️ P2:无 `data-access.ts`,`actions.ts` 直接使用 `db` +- ⚠️ P2:`notification-preferences-form.tsx` 跨模块 UI 依赖 +- ✅ 密码修改有速率限制 +- ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验 + +**文件清单**: +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 205 | AI Provider CRUD + 测试 | +| `actions-password.ts` | 113 | 修改密码 | +| `components/*` | 8 文件 | 通用设置 + AI 配置 + 密码 + 主题 + 通知偏好 | + +--- + +## 2.24 auth(认证 UI 模块) + +**职责**:认证页面 UI(登录/注册/布局)。 + +**导出函数**:纯 UI 组件(`LoginForm` / `RegisterForm` / `AuthLayout`) + +**依赖关系**: +- 依赖:`shared/*` +- 被依赖:`app/(auth)/*` + +**已知问题**: +- ✅ 纯 UI 模块,无 data-access/actions/types +- ✅ 认证逻辑由 NextAuth + `shared/lib/auth-guard` 统一处理 + +**文件清单**: +| 文件 | 职责 | |------|------| -| `CoursePlan` | 课程计划基础接口 | -| `CoursePlanItem` | 周计划条目接口 | -| `CoursePlanListItem` | = CoursePlan & { className, subjectName, teacherName } | -| `CoursePlanWithItems` | = CoursePlanListItem & { items: CoursePlanItem[] } | -| `CoursePlanStatus` | `"planning" \| "active" \| "completed" \| "paused"` | -| `CoursePlanSemester` | `"1" \| "2"` | -| `GetCoursePlansParams` | { classId?, teacherId?, subjectId?, status? } | -| `ReorderCoursePlanItemInput` | { id, week } | - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `course-plan-progress.tsx` | 进度条组件(completedHours/totalHours 百分比) | -| `course-plan-list.tsx` | 课程计划列表(支持状态筛选,URL 同步) | -| `course-plan-form.tsx` | 创建/编辑表单(班级、科目、教师、学年、学期、状态、课时、日期、大纲、目标) | -| `course-plan-item-editor.tsx` | 周计划条目编辑器(Dialog,支持创建/编辑/删除/切换完成) | -| `course-plan-detail.tsx` | 详情视图(计划信息、进度、大纲、目标、周计划表格、删除确认) | +| `components/auth-layout.tsx` | 认证页面布局 | +| `components/login-form.tsx` | 登录表单 | +| `components/register-form.tsx` | 注册表单 | --- -## 模块:parent +## 2.25 layout(布局模块) -### 模块职责 -家长端仪表盘:聚合家长关联子女的学习数据(课表、作业、成绩、班级),支持多子女切换查看。家长通过 `parentStudentRelations` 表关联子女,DataScope 解析为 `children` 类型。 +**职责**:应用骨架(侧边栏 + 顶部导航 + 导航配置)。 -### 模块路径 -`src/modules/parent` +**导出函数**:`AppSidebar` / `SidebarProvider` / `SiteHeader` + `navigation` 配置 -### 导出函数 (data-access.ts) +**依赖关系**: +- 依赖:`shared/hooks/use-permission`、`@/auth`(useSession)、`messaging`(通知下拉) +- 被依赖:`app/(dashboard)/layout.tsx` -> 所有函数使用 `cache()` 包装以实现请求级缓存。本模块仅用于读操作聚合,无 Server Action,权限校验在页面层通过 `requireAuth()` 完成。 +**已知问题**: +- ⚠️ P2:用权限反推角色(`permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)`),应改用 `hasRole("student")` +- ✅ navigation.ts 无幽灵路由(13 个已修复) -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getChildren` | `(parentId: string) => Promise` | `getParentDashboardData` 内部, parent/children/[studentId] 页面 | -| `getChildBasicInfo` | `(studentId: string, relation?: string \| null) => Promise` | `getChildDashboardData` 内部 | -| `getChildDashboardData` | `(studentId: string, relation?: string \| null) => Promise` | parent/children/[studentId]/page.tsx, `getParentDashboardData` 内部 | -| `getParentDashboardData` | `(parentId: string) => Promise` | parent/dashboard/page.tsx | - -#### `getChildren` -- 依赖:`shared/db` (parentStudentRelations 表) -- 功能:查询指定家长的所有子女关联记录,按 createdAt 升序 - -#### `getChildBasicInfo` -- 依赖:`shared/db` (users, grades, classEnrollments, classes 表) -- 功能:聚合子女基础信息(姓名、邮箱、头像、年级名称、班级名称、与家长关系) - -#### `getChildDashboardData` -- 依赖:`getChildBasicInfo`, `classes/data-access.getStudentClasses`, `classes/data-access.getStudentSchedule`, `homework/data-access.getStudentHomeworkAssignments`, `homework/data-access.getStudentDashboardGrades`, `grades/data-access.getStudentGradeSummary` -- 功能:并行聚合单个子女的完整仪表盘数据(基础信息、已加入班级、今日课表、作业概览、成绩趋势、成绩汇总) - -#### `getParentDashboardData` -- 依赖:`shared/db` (users 表), `getChildren`, `getChildDashboardData` -- 功能:聚合家长名称与所有子女的仪表盘数据 - -### 类型/接口 (types.ts) - -#### `ParentChildRelation` -- 定义:家长-子女关联记录类型(id, parentId, studentId, relation, createdAt) -- 被使用:`getChildren`, `getParentDashboardData` - -#### `ChildBasicInfo` -- 定义:子女基础信息类型(id, name, email, image, gradeName, className, classId, relation) -- 被使用:`getChildBasicInfo`, `ChildDashboardData.basicInfo` - -#### `ChildScheduleItem` -- 定义:子女今日课表项类型(id, classId, className, course, startTime, endTime, location) -- 被使用:`ChildDashboardData.todaySchedule`, child-schedule-card.tsx - -#### `ChildHomeworkSummary` -- 定义:子女作业概览类型(pendingCount, submittedCount, gradedCount, overdueCount, recentAssignments) -- 被使用:`ChildDashboardData.homeworkSummary`, child-homework-summary.tsx - -#### `ChildDashboardData` -- 定义:单个子女仪表盘数据类型(basicInfo, enrolledClasses, todaySchedule, homeworkSummary, gradeTrend, gradeSummary) -- 依赖:`homework/types.StudentDashboardGradeProps`, `homework/types.StudentHomeworkAssignmentListItem`, `classes/types.StudentEnrolledClass`, `grades/types.StudentGradeSummary` -- 被使用:`getChildDashboardData`, `ParentDashboardData.children`, 所有 child-* 组件 - -#### `ParentDashboardData` -- 定义:家长仪表盘数据类型(parentName, children: ChildDashboardData[]) -- 被使用:`getParentDashboardData`, parent-dashboard.tsx - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `parent-dashboard.tsx` | 主容器组件(问候语、子女卡片网格、空状态) | -| `child-card.tsx` | 子女卡片(头像、姓名、班级、待完成/逾期/平均分统计,点击跳转详情) | -| `child-detail-header.tsx` | 子女详情页头部(返回按钮、头像、姓名、班级、年级、关系) | -| `child-detail-panel.tsx` | 子女详情面板容器(组合 homework/grade/schedule 三个子组件) | -| `child-homework-summary.tsx` | 子女作业概览(pending/submitted/graded/overdue 统计 + 最近作业列表) | -| `child-grade-summary.tsx` | 子女成绩概览(Recharts 折线图趋势 + 最新分数 + 班级排名 + 最近成绩列表,"use client") | -| `child-schedule-card.tsx` | 子女今日课表卡片(课程、时间、地点、班级) | - ---- - -## 模块:messaging - -### 模块职责 -站内消息系统:用户间私信收发(支持回复链)、站内通知(多态类型:message/announcement/homework/grade),SiteHeader 通知下拉菜单展示未读数。 - -### 模块路径 -`src/modules/messaging` - -### 导出函数 (actions.ts) - -> send actions 使用 `requirePermission(MESSAGE_SEND)`,read actions 使用 `requirePermission(MESSAGE_READ)`,delete actions 使用 `requirePermission(MESSAGE_DELETE)`,通知读取使用 `requireAuth()`。 - -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `sendMessageAction` | MESSAGE_SEND | 发送消息(同时为收件人创建通知;支持 parentMessageId 回复) | -| `markMessageAsReadAction` | MESSAGE_READ | 标记消息已读(设置 readAt) | -| `deleteMessageAction` | MESSAGE_DELETE | 删除消息(仅发送者或接收者可删) | -| `getMessagesAction` | MESSAGE_READ | 获取消息列表(收件箱/已发送,分页) | -| `getMessageDetailAction` | MESSAGE_READ | 获取消息详情(含回复线程) | -| `getRecipientsAction` | MESSAGE_SEND | 获取可发送对象列表(按 DataScope 过滤) | -| `getNotificationsAction` | requireAuth | 获取当前用户通知列表(分页) | -| `markNotificationAsReadAction` | requireAuth | 标记单条通知已读 | -| `markAllNotificationsAsReadAction` | requireAuth | 标记所有通知已读 | -| `getNotificationPreferencesAction` | requireAuth | 获取当前用户通知偏好(无记录时自动创建默认记录) | -| `updateNotificationPreferencesAction` | requireAuth | 更新(upsert)当前用户通知偏好(从 FormData 解析 checkbox "on" 为布尔值) | - -### 导出函数 (data-access.ts) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getMessages` | `(userId: string, params?: { type?, page?, pageSize? }) => Promise>` | messages 页面 | -| `getMessageById` | `(id: string, userId: string) => Promise` | 消息详情页 | -| `getMessageThread` | `(rootId: string, userId: string) => Promise` | 消息详情页(回复链) | -| `createMessage` | `(input: CreateMessageInput) => Promise` | sendMessageAction | -| `markMessageAsRead` | `(id: string, userId: string) => Promise` | markMessageAsReadAction, 详情页自动已读 | -| `deleteMessage` | `(id: string, userId: string) => Promise` | deleteMessageAction | -| `getUnreadMessageCount` | `(userId: string) => Promise` | 待扩展 | -| `getNotifications` | `(userId: string, params?: { page?, pageSize? }) => Promise>` | 通知列表/下拉菜单 | -| `createNotification` | `(input: CreateNotificationInput) => Promise` | sendMessageAction(内部调用) | -| `markNotificationAsRead` | `(id: string, userId: string) => Promise` | markNotificationAsReadAction | -| `markAllNotificationsAsRead` | `(userId: string) => Promise` | markAllNotificationsAsReadAction | -| `getUnreadNotificationCount` | `(userId: string) => Promise` | 待扩展 | -| `getRecipients` | `(ctx: AuthContext) => Promise` | compose 页面(按 DataScope 过滤) | - -### 导出函数 (notification-preferences.ts) - -> 文件标记 `"server-only"`,使用 React `cache` 包装读取函数。 - -#### `getNotificationPreferences` -- 签名:`getNotificationPreferences(userId: string): Promise`(cache 包装) -- 功能:获取用户通知偏好;若用户无记录则自动创建一条默认记录(pushEnabled/homeworkNotifications/gradeNotifications/announcementNotifications/messageNotifications/attendanceNotifications 默认 true,emailEnabled/smsEnabled 默认 false),并发冲突时回退到查询 -- 依赖:`shared/db` (notificationPreferences 表), `@paralleldrive/cuid2`, `react` (cache) -- 被使用:`getNotificationPreferencesAction`, `app/(dashboard)/settings/page.tsx` - -#### `upsertNotificationPreferences` -- 签名:`upsertNotificationPreferences(userId: string, input: UpdateNotificationPreferencesInput): Promise` -- 功能:upsert 语义更新通知偏好(存在则部分字段更新,不存在则插入;未提供的字段保留原值或使用默认值) -- 依赖:`shared/db` (notificationPreferences 表), `@paralleldrive/cuid2` -- 被使用:`updateNotificationPreferencesAction` - -### Schema (schema.ts) - -#### `SendMessageSchema` -- 类型:Zod schema -- 定义:发送消息校验 schema(receiverId, subject?, content, parentMessageId?) -- 被使用:`sendMessageAction` - -### 类型/接口 - -#### `Message` -- 定义:消息完整类型(含 id, senderId, receiverId, subject, content, isRead, readAt, parentMessageId, createdAt, senderName, receiverName) -- 被使用:messaging/components, 页面 - -#### `MessageListItem` -- 定义:消息列表项类型(同 Message 精简版) -- 被使用:列表页 - -#### `MessageThread` -- 定义:消息线程类型(root + replies) -- 被使用:详情页 - -#### `Notification` -- 定义:通知完整类型(含 id, userId, type, title, content, link, isRead, createdAt) -- 被使用:notification-dropdown, notification-list - -#### `NotificationListItem` -- 定义:通知列表项类型(同 Notification) -- 被使用:列表页 - -#### `NotificationType` -- 定义:`"message" | "announcement" | "homework" | "grade"` -- 被使用:data-access, components - -#### `MessageType` -- 定义:`"inbox" | "sent"` -- 被使用:getMessages 参数 - -#### `CreateMessageInput` / `CreateNotificationInput` -- 定义:创建消息/通知的输入类型 -- 被使用:data-access.createMessage, createNotification - -#### `RecipientOption` -- 定义:`{ id: string; name: string }` -- 被使用:compose 页面下拉选项 - -#### `NotificationPreferences` -- 定义:通知偏好完整类型(含 id, userId, emailEnabled, smsEnabled, pushEnabled, homeworkNotifications, gradeNotifications, announcementNotifications, messageNotifications, attendanceNotifications, createdAt, updatedAt) -- 被使用:`getNotificationPreferences`, `upsertNotificationPreferences`, `getNotificationPreferencesAction`, `updateNotificationPreferencesAction`, settings/components/notification-preferences-form.tsx - -#### `UpdateNotificationPreferencesInput` -- 定义:更新通知偏好的输入类型(所有字段可选:emailEnabled?, smsEnabled?, pushEnabled?, homeworkNotifications?, gradeNotifications?, announcementNotifications?, messageNotifications?, attendanceNotifications?;未提供则保留原值) -- 被使用:`upsertNotificationPreferences`, `updateNotificationPreferencesAction` - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `message-list.tsx` | 消息列表(收件箱/已发送 Tab 切换,已读/未读标记,usePermission 控制"写消息"按钮) | -| `message-detail.tsx` | 消息详情(含回复线程、回复/删除操作,AlertDialog 删除确认,usePermission 控制按钮可见性) | -| `message-compose.tsx` | 写消息表单(收件人 Select、主题 Input、内容 Textarea,支持回复模式) | -| `notification-dropdown.tsx` | SiteHeader 通知下拉菜单(Bell 图标 + 未读数 Badge,滚动列表,标记已读,查看全部链接) | -| `notification-list.tsx` | 通知完整列表(全部标记已读、单条标记已读、查看链接) | - ---- - -## 模块:notifications - -### 模块职责 -通知渠道集成层:基于用户通知偏好(`notification_preferences` 表)将通知分发到站内消息 / SMS / 微信公众号 / 邮件多渠道。所有渠道实现统一 `NotificationChannelSender` 接口,dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务即可运行)。 - -### 模块路径 -`src/modules/notifications` - -### 依赖关系 -- 依赖 `shared`(db, auth-guard, types) -- 依赖 `messaging`(复用 `notification-preferences.getNotificationPreferences` 和 `data-access.createNotification`) -- 所有渠道文件首行 `import "server-only"`,外部 SDK 使用动态 import - -### 导出函数 (actions.ts) - -> 使用 `requirePermission(MESSAGE_SEND)` 校验权限(项目无独立 NOTIFICATION_SEND 权限点,复用 MESSAGE_SEND)。 - -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `sendNotificationAction` | MESSAGE_SEND | 发送通知给指定用户(按偏好多渠道分发) | -| `sendClassNotificationAction` | MESSAGE_SEND | 发送班级通知(批量发送给班级所有学生;教师只能给自己所教班级发送,通过 dataScope 校验) | - -### 导出函数 (dispatcher.ts) - -> 文件标记 `"server-only"`。 - -#### `sendNotification` -- 签名:`sendNotification(payload: NotificationPayload): Promise` -- 功能:读取用户通知偏好 + 联系方式,按偏好选择渠道(in_app 总是启用;sms 需 smsEnabled+phone;email 需 emailEnabled+email;wechat 需 pushEnabled+openId),并行发送,记录日志 -- 依赖:`data-access.getUserNotificationPreferences`, `data-access.getUserContactInfo`, `data-access.logNotificationSendBatch`, 各渠道 `createXxxSender` -- 被使用:`sendNotificationAction`, `sendClassNotificationAction` - -#### `sendBatchNotifications` -- 签名:`sendBatchNotifications(payloads: NotificationPayload[]): Promise` -- 功能:批量发送通知(每个用户独立选择渠道,并行发送) -- 依赖:`sendNotification` -- 被使用:`sendClassNotificationAction` - -### 导出函数 (data-access.ts) - -> 文件标记 `"server-only"`。 - -| 函数 | 签名 | 核心功能 | -|------|------|---------| -| `getUserNotificationPreferences` | `(userId: string) => Promise` | 获取用户通知偏好(复用 messaging.notification-preferences) | -| `getUserContactInfo` | `(userId: string) => Promise` | 获取用户联系方式(phone/email;wechatOpenId 暂不支持,users 表无此字段;React cache 包装) | -| `logNotificationSend` | `(result: ChannelSendResult) => void` | 记录单条发送日志(当前 console.info;未来可扩展 notification_logs 表) | -| `logNotificationSendBatch` | `(results: ChannelSendResult[]) => void` | 批量记录发送日志 | - -### 渠道实现 (channels/) - -> 所有渠道文件首行 `import "server-only"`,外部 SDK 使用动态 import 避免增加构建体积。 - -| 文件 | 渠道 | 工厂函数 | 说明 | -|------|------|---------|------| -| `sms-channel.ts` | sms | `createSmsSender()` | 支持 aliyun/tencent/mock(根据 `SMS_PROVIDER` 环境变量选择);模板变量替换 title/content | -| `wechat-channel.ts` | wechat | `createWechatSender()` | 微信公众号模板消息;access_token 带缓存(提前 5 分钟刷新);配置完整用真实发送器,否则 Mock | -| `email-channel.ts` | email | `createEmailSender()` | Nodemailer SMTP;HTML 模板按 type 着色;配置 EMAIL_HOST 启用,否则 Mock | -| `in-app-channel.ts` | in_app | `createInAppSender()` | 复用 messaging.data-access.createNotification 写入 message_notifications 表;总是启用 | -| `types.ts` | - | - | 渠道接口定义(NotificationChannelSender, ChannelRecipient) | - -### 环境变量 - -| 变量 | 默认值 | 说明 | -|------|--------|------| -| `SMS_PROVIDER` | `mock` | SMS 渠道 provider:aliyun/tencent/mock | -| `SMS_ACCESS_KEY_ID` | - | SMS AccessKey ID | -| `SMS_ACCESS_KEY_SECRET` | - | SMS AccessKey Secret | -| `SMS_SIGN_NAME` | - | SMS 签名 | -| `SMS_TEMPLATE_CODE` | - | SMS 模板 ID | -| `WECHAT_APP_ID` | - | 微信公众号 AppID | -| `WECHAT_APP_SECRET` | - | 微信公众号 AppSecret | -| `WECHAT_TEMPLATE_ID` | - | 微信模板消息 ID | -| `EMAIL_HOST` | - | SMTP 主机(配置后启用真实发送) | -| `EMAIL_PORT` | `587` | SMTP 端口 | -| `EMAIL_USER` | - | SMTP 用户名 | -| `EMAIL_PASS` | - | SMTP 密码 | -| `EMAIL_FROM` | `noreply@example.com` | 发件人地址 | - -### 类型/接口 - -#### `NotificationChannel` -- 定义:`"in_app" | "email" | "sms" | "wechat"` -- 被使用:所有渠道文件, dispatcher - -#### `NotificationPayload` -- 定义:`{ userId, title, content, type: "info"|"warning"|"error"|"success", metadata?, actionUrl? }` -- 被使用:dispatcher, actions, 所有渠道 - -#### `ChannelSendResult` -- 定义:`{ channel, success, messageId?, error?, sentAt }` -- 被使用:dispatcher, actions, 所有渠道 - -#### `NotificationChannelSender`(接口) -- 定义:`{ channel: NotificationChannel, send(payload, recipient): Promise, sendBatch(items): Promise }` -- 被使用:所有渠道实现, dispatcher - -#### `ChannelRecipient`(接口) -- 定义:`{ userId, phone?, email?, wechatOpenId? }` -- 被使用:所有渠道, data-access.getUserContactInfo - -### 文档 -- `docs/notifications/channels.md`:通知渠道配置说明、Mock 模式、生产环境配置、扩展新渠道指南 - ---- - -## 模块:attendance - -### 模块职责 -学生考勤管理:教师按班级/日期点名(单条/批量)、查询考勤记录、统计出勤率/迟到率,学生/家长查看本人/子女考勤汇总,管理员查看全校考勤记录。支持班级考勤规则配置(迟到阈值、早退阈值、自动标记)。 - -### 模块路径 -`src/modules/attendance` - -### 导出函数 (actions.ts) - -> 所有 manage actions 均使用 `requirePermission(ATTENDANCE_MANAGE)` 进行权限校验,read actions 使用 `requirePermission(ATTENDANCE_READ)`。学生/家长在 `getStudentAttendanceAction` 中进行 DataScope 二次校验(class_members 仅查自己,children 仅查子女)。 - -| 函数 | 权限 | 核心功能 | -|------|------|---------| -| `recordAttendanceAction` | ATTENDANCE_MANAGE | 创建单条考勤记录 | -| `batchRecordAttendanceAction` | ATTENDANCE_MANAGE | 批量点名(班级+日期,表格形式录入每个学生状态) | -| `updateAttendanceAction` | ATTENDANCE_MANAGE | 更新考勤记录(状态、备注) | -| `deleteAttendanceAction` | ATTENDANCE_MANAGE | 删除考勤记录 | -| `getAttendanceAction` | ATTENDANCE_READ | 分页查询考勤记录(按 scope 过滤) | -| `getStudentAttendanceAction` | ATTENDANCE_READ | 获取学生考勤汇总(含 DataScope 二次校验) | -| `getClassAttendanceStatsAction` | ATTENDANCE_READ | 获取班级考勤统计 | -| `getClassAttendanceForDateAction` | ATTENDANCE_READ | 获取班级指定日期考勤(用于点名页加载已有记录) | -| `saveAttendanceRulesAction` | ATTENDANCE_MANAGE | 保存班级考勤规则(upsert) | -| `getAttendanceRulesAction` | ATTENDANCE_READ | 获取班级考勤规则 | - -### 导出函数 (data-access.ts) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getAttendanceRecords` | `(params: AttendanceQueryParams & { scope: DataScope; currentUserId?: string }) => Promise` | getAttendanceAction | -| `getClassAttendanceForDate` | `(classId: string, date: string) => Promise` | getClassAttendanceForDateAction | -| `createAttendanceRecord` | `(data: RecordAttendanceInput, recordedBy: string) => Promise` | recordAttendanceAction | -| `batchCreateAttendanceRecords` | `(data: BatchRecordAttendanceInput, recordedBy: string) => Promise` | batchRecordAttendanceAction | -| `updateAttendanceRecord` | `(id: string, data: UpdateAttendanceInput) => Promise` | updateAttendanceAction | -| `deleteAttendanceRecord` | `(id: string) => Promise` | deleteAttendanceAction | -| `getClassStudentsForAttendance` | `(classId: string) => Promise>` | 点名页学生列表 | -| `getAttendanceRules` | `(classId?: string) => Promise` | getAttendanceRulesAction | -| `upsertAttendanceRules` | `(data: AttendanceRuleInput) => Promise` | saveAttendanceRulesAction | - -### 导出函数 (data-access-stats.ts) - -> 从 data-access.ts 拆分以遵守单文件 ≤300 行规则。 - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getStudentAttendanceSummary` | `(studentId: string, startDate?: string, endDate?: string) => Promise` | getStudentAttendanceAction, student/attendance, parent/attendance | -| `getClassAttendanceStats` | `(classId: string, startDate?: string, endDate?: string) => Promise` | getClassAttendanceStatsAction, teacher/attendance/stats | - -### Schema (schema.ts) - -| Schema | 用途 | -|--------|------| -| `AttendanceStatusEnum` | 考勤状态枚举(present/absent/late/early_leave/excused) | -| `RecordAttendanceSchema` | 单条考勤记录校验(studentId, classId, scheduleId?, date, status, remark?) | -| `BatchRecordAttendanceSchema` | 批量考勤校验(records[{ studentId, classId, scheduleId?, date, status, remark? }]) | -| `UpdateAttendanceSchema` | 更新考勤校验(status?, remark?, scheduleId?) | -| `AttendanceRuleSchema` | 考勤规则校验(classId, lateThresholdMinutes?, earlyLeaveThresholdMinutes?, enableAutoMark?) | - -### 类型/接口 (types.ts) - -| 类型 | 定义 | +**文件清单**: +| 文件 | 职责 | |------|------| -| `AttendanceStatus` | `"present" \| "absent" \| "late" \| "early_leave" \| "excused"` | -| `AttendanceRecord` | 考勤记录完整类型 | -| `AttendanceListItem` | 列表项类型(含 studentName, className, recorderName) | -| `AttendanceStats` | 统计类型(total, present, absent, late, earlyLeave, excused, presentRate, lateRate) | -| `StudentAttendanceSummary` | 学生考勤汇总(studentId, studentName, stats, recentRecords) | -| `ClassAttendanceSummary` | 班级考勤汇总(classId, className, date, stats, studentRecords) | -| `AttendanceRule` | 考勤规则类型(classId, lateThresholdMinutes, earlyLeaveThresholdMinutes, enableAutoMark) | -| `AttendanceQueryParams` | 查询参数(classId?, studentId?, date?, startDate?, endDate?, status?, page?, pageSize?) | -| `PaginatedAttendanceResult` | 分页结果(items, total, page, pageSize, totalPages) | -| `ATTENDANCE_STATUS_LABELS` | 状态中文标签常量 | -| `ATTENDANCE_STATUS_COLORS` | 状态颜色常量(用于 Badge) | - -### 导出组件 (components/) - -| 组件文件 | 功能 | -|---------|------| -| `attendance-sheet.tsx` | 批量点名表单(班级/日期选择器 + 学生表格 + 每行状态 Select + "全部标记到场"按钮) | -| `attendance-record-list.tsx` | 考勤记录列表表格(含删除确认对话框) | -| `attendance-stats-card.tsx` | 统计卡片(总数、到场、缺勤、迟到、早退、请假、出勤率、迟到率) | -| `attendance-filters.tsx` | URL 同步筛选器(班级、状态、日期) | -| `student-attendance-view.tsx` | 学生/家长视图(统计卡片 + 最近记录表格) | -| `attendance-rules-form.tsx` | 考勤规则配置表单(班级选择器、迟到/早退阈值、自动标记勾选) | +| `components/app-sidebar.tsx` | 侧边栏(根据权限渲染) | +| `components/sidebar-provider.tsx` | 侧边栏状态 Context | +| `components/site-header.tsx` | 顶部导航(含通知下拉) | +| `config/navigation.ts` | 导航配置(4 个角色) | --- -## 模块:scheduling +## 2.26 student(学生 UI 模块) -`src/modules/scheduling` +**职责**:学生端 UI 组件(课程视图 + 课表筛选/视图)。 -排课与调课模块:管理员配置班级排课规则(每日课时、连续课时、午休、上下学时间、避免背靠背、科目均衡),自动排课引擎按规则生成周课表,调课/代课申请与审批流程,课表冲突检测。 +**导出函数**:`StudentCoursesView` / `StudentScheduleFilters` / `StudentScheduleView` -> 所有 actions 均使用 `requirePermission()` 进行权限校验:规则配置/调课申请/冲突检测/查询使用 `requirePermission(SCHEDULE_ADJUST)`,自动排课/应用课表/审批调课使用 `requirePermission(SCHEDULE_AUTO)`。admin 角色拥有 SCHEDULE_AUTO+SCHEDULE_ADJUST,teacher 角色无排课权限。 +**依赖关系**: +- 依赖:`shared/*` +- 被依赖:`app/(dashboard)/student/*` -### Server Actions (`actions.ts`) +**已知问题**: +- ⚠️ P2:与 classes 模块的 `schedule-view.tsx`/`schedule-filters.tsx` 可能功能重叠 +- ✅ 纯 UI 模块,数据由页面通过 classes data-access 获取 -| Action | 权限 | 用途 | -|--------|------|------| -| `saveSchedulingRulesAction` | SCHEDULE_ADJUST | 保存班级排课规则(upsert,classId 为空时为全局规则) | -| `autoScheduleAction` | SCHEDULE_AUTO | 根据规则与科目分配生成预览课表(不落库) | -| `applyAutoScheduleAction` | SCHEDULE_AUTO | 将生成的课表写入 classSchedule 表(事务:先删后插) | -| `requestScheduleChangeAction` | SCHEDULE_ADJUST | 提交调课/代课申请(status=pending) | -| `approveScheduleChangeAction` | SCHEDULE_AUTO | 审批通过调课申请(status=approved) | -| `rejectScheduleChangeAction` | SCHEDULE_AUTO | 驳回调课申请(status=rejected) | -| `getScheduleChangesAction` | SCHEDULE_ADJUST | 查询调课申请列表(可按 classId/status/requesterId 过滤) | -| `getClassConflictsAction` | SCHEDULE_ADJUST | 检测班级课表时间重叠冲突 | - -### Data Access (`data-access.ts`) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getSchedulingRules` | `(classId?: string) => Promise` | saveSchedulingRulesAction, autoScheduleAction, admin/scheduling/rules | -| `upsertSchedulingRules` | `(data: SchedulingRuleInput) => Promise` | saveSchedulingRulesAction | -| `getScheduleChanges` | `(params: ScheduleChangeQueryParams) => Promise` | getScheduleChangesAction, admin/scheduling/changes, teacher/schedule-changes | -| `createScheduleChange` | `(data: ScheduleChangeInput, requestedBy: string) => Promise` | requestScheduleChangeAction | -| `updateScheduleChangeStatus` | `(id, status, approverId) => Promise` | approveScheduleChangeAction, rejectScheduleChangeAction | -| `getClassConflicts` | `(classId: string) => Promise` | getClassConflictsAction | -| `getAdminClassesForScheduling` | `() => Promise>` | 所有 scheduling 页面 | -| `getTeachersForScheduling` | `() => Promise>` | teacher/schedule-changes | -| `getClassroomsForScheduling` | `() => Promise>` | autoScheduleAction | -| `getClassSubjectsForScheduling` | `(classId) => Promise>` | autoScheduleAction | - -### Auto Scheduler (`auto-scheduler.ts`) - -| 函数 | 用途 | +**文件清单**: +| 文件 | 职责 | |------|------| -| `autoSchedule` | 贪心+冲突检测排课算法:按科目每周课时降序,为每节课选择第一个满足约束的时段(午休、每日窗口、班级/教师/教室冲突、每日最大课时、避免背靠背) | -| `findOptimalSlot` | 在候选时段中找到第一个满足所有约束的时段 | -| `validateSchedule` | 校验生成的课表是否违反规则,返回冲突列表 | -| `buildDefaultTimeSlots` | 根据上下学时间和午休时间构建默认时段(周一至周五,上午4节+下午4节) | - -### Schemas (`schema.ts`) - -| Schema | 用途 | -|--------|------| -| `SchedulingRuleSchema` | 排课规则校验(classId, maxDailyHours?, maxContinuousHours?, lunchBreakStart?, lunchBreakEnd?, morningStart?, afternoonEnd?, avoidBackToBack?, balancedSubjects?) | -| `ScheduleChangeSchema` | 调课申请校验(classId, originalScheduleId?, originalTeacherId?, substituteTeacherId?, originalDate?, newDate?, newStartTime?, newEndTime?, reason) | -| `AutoScheduleParamsSchema` | 自动排课参数校验(classId, rules, subjects[], teachers[], classrooms[], timeSlots[]) | -| `ScheduleChangeStatusEnum` | 调课状态枚举(pending/approved/rejected/completed) | -| `ApproveScheduleChangeSchema` | 审批校验(changeId, reason?) | - -### Types (`types.ts`) - -| Type | 定义 | -|------|------| -| `ScheduleChangeStatus` | `"pending" \| "approved" \| "rejected" \| "completed"` | -| `SchedulingRule` | 排课规则完整类型 | -| `ScheduleChange` | 调课申请完整类型 | -| `ScheduleChangeListItem` | 列表项类型(含 className, originalTeacherName, substituteTeacherName, requesterName, approverName) | -| `TimeSlot` | `{ weekday, startTime, endTime }` | -| `ScheduleConflict` | `{ type, description, scheduleIds }`(type: teacher_overlap/classroom_overlap/class_overlap/rule_violation) | -| `AutoScheduleResult` | `{ success, scheduledCount, conflictCount, conflicts, schedules }` | -| `GeneratedSchedule` | `{ classId, weekday, startTime, endTime, course, location, teacherId, subjectId }` | -| `AutoScheduleParams` | `{ classId, rules, subjects, teachers, classrooms, timeSlots }` | -| `ScheduleChangeQueryParams` | `{ classId?, status?, requesterId? }` | -| `SCHEDULE_CHANGE_STATUS_LABELS` | 状态英文标签常量 | -| `SCHEDULE_CHANGE_STATUS_COLORS` | 状态颜色常量(用于 Badge) | - -### Components (`components/`) - -| 组件 | 用途 | -|------|------| -| `scheduling-rules-form.tsx` | 排课规则配置表单(班级选择器、每日最大课时、连续课时、午休时间、上下学时间、避免背靠背、科目均衡) | -| `auto-schedule-panel.tsx` | 自动排课面板(班级选择→预览→应用流程) | -| `auto-schedule-result.tsx` | 排课结果预览(课表表格 + 冲突/警告列表) | -| `schedule-change-form.tsx` | 调课/代课申请表单(班级、原任课教师、代课教师、原日期、新日期、新时间、原因) | -| `schedule-change-list.tsx` | 调课申请列表表格(含审批/驳回对话框,canApprove 控制审批按钮可见性) | -| `schedule-conflicts-view.tsx` | 冲突检测视图(班级选择器 + 检测按钮 + 冲突结果列表) | +| `components/student-courses-view.tsx` | 学生课程视图 | +| `components/student-schedule-filters.tsx` | 课表筛选器 | +| `components/student-schedule-view.tsx` | 学生课表视图 | --- -## 模块:proctoring +# 第三部分:已知架构问题和技术债 -`src/modules/proctoring` +## 3.1 P0 严重问题(必须立即修复) -考试监考模块:监考模式考试实时监控、防作弊事件采集、教师监考面板、学生端防作弊监控、考试模式配置。 +### P0-1:文件超 1000 行硬上限(3 个文件) -> 权限:教师监考面板使用 `requirePermission(EXAM_PROCTOR)`;学生端上报事件使用 `requireAuth()`(学生上报自己的事件,不需要管理权限)。前端组件使用 `usePermission().hasPermission(EXAM_PROCTOR)` 控制权限。 +| 文件 | 行数 | 问题 | 拆分建议 | +|------|------|------|---------| +| `classes/data-access.ts` | 2104 | 混入 homework/scheduling/grades 逻辑 | 拆为 data-access.ts + data-access-enrollments.ts + data-access-insights.ts;迁移 homework/scheduling/grades 逻辑回所属模块 | +| `homework/data-access.ts` | 1038 | 混入排名计算业务逻辑 | 拆为 data-access.ts + data-access-student.ts + data-access-analytics.ts + data-access-grading.ts | +| `shared/db/schema.ts` | 1111 | 54 张表混合 | 按业务域拆分为 schema/auth.ts + schema/academic.ts + schema/exam.ts + ...,通过 index.ts 聚合 | -### Server Actions (`actions.ts`) +### P0-2:shared/lib ↔ auth 循环依赖 -| Action | 权限 | 用途 | -|--------|------|------| -| `recordProctoringEventAction` | requireAuth() | 学生端上报监考事件(含 submission 归属校验) | -| `getProctoringDashboardAction` | EXAM_PROCTOR | 获取监考面板数据(摘要+学生状态+最近事件) | +``` +shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/* +``` -### Data Access (`data-access.ts`) +**影响**:shared 层无法独立测试/复用;架构上基础设施不应反向依赖应用层。 -`import "server-only"` +**解耦建议**: +- 创建 `shared/lib/session.ts` 封装 session 获取 +- logger 函数改为接收 `session` 参数(由调用方传入) +- 或通过依赖注入打破循环 -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `recordProctoringEvent` | `(input: RecordProctoringEventInput) => Promise` | actions.recordProctoringEventAction, API /api/proctoring/event | -| `getProctoringEvents` | `(examId, filters?) => Promise` | 待扩展 | -| `getProctoringEventsBySubmission` | `(submissionId) => Promise` | 待扩展 | -| `getExamProctoringSummary` | `(examId) => Promise` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx | -| `getStudentProctoringStatuses` | `(examId) => Promise` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx | -| `getExamForProctoring` | `(examId) => Promise<{id,title,examMode,config} | null>` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx | -| `getRecentProctoringEvents` | `(examId, limit?) => Promise` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx | +### P0-3:dashboard 跨模块直接查询 11 张表 -### Types (`types.ts`) +`dashboard/data-access.ts` 的 `getAdminDashboardData` 直查 sessions/users/usersToRoles/roles/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions。 -| 类型 | 定义 | -|------|------| -| `ProctoringEventType` | `"tab_switch" \| "window_blur" \| "copy_attempt" \| "paste_attempt" \| "right_click" \| "devtools_open" \| "fullscreen_exit" \| "idle_timeout"` | -| `ExamMode` | `"homework" \| "timed" \| "proctored"` | -| `ProctoringEvent` | `{ id, submissionId, studentId, examId, eventType, eventDetail?, occurredAt, createdAt }` | -| `ProctoringEventWithDetails` | `ProctoringEvent & { studentName, examTitle }` | -| `ExamProctoringSummary` | `{ examId, examTitle, examMode, totalStudents, startedStudents, submittedStudents, totalEvents, abnormalStudents, eventsByType }` | -| `StudentProctoringStatus` | `{ studentId, studentName, submissionId, submissionStatus, eventCount, lastEventAt, isAbnormal, eventsByType }` | -| `ProctoringDashboardData` | `{ summary, students, recentEvents }` | -| `ExamModeConfig` | `{ examMode, durationMinutes, shuffleQuestions, allowLateStart, lateStartGraceMinutes, antiCheatEnabled }` | -| `PROCTORING_EVENT_LABELS` | 事件类型中文标签常量 | -| `EXAM_MODE_LABELS` | 考试模式中文标签常量 | -| `ABNORMAL_EVENT_THRESHOLD` | 异常学生事件数阈值(3) | +**解耦建议**: +- 各模块暴露 `getModuleStats(scope?)` 函数 +- dashboard 聚合调用:`Promise.all([getUsersStats(scope), getClassStats(scope), ...])` +- 至少消除 dashboard 中重复实现的 exam/homework scope 过滤 -### Components (`components/`) +### P0-4:messaging 绕过 notifications 直接写通知 -| 组件 | 用途 | -|------|------| -| `proctoring-dashboard.tsx` | 教师监考面板(实时学生状态、异常事件统计、异常学生高亮、10 秒轮询、usePermission 权限控制) | -| `anti-cheat-monitor.tsx` | 学生端防作弊监控(visibilitychange/blur/copy/paste/contextmenu/keydown/fullscreenchange 监听、空闲超时检测、强制全屏、警告提示、事件上报) | -| `exam-mode-config.tsx` | 考试模式配置(react-hook-form Controller,作业/限时/监考模式选择,限时设置时长,监考设置防作弊选项) | +`messaging/actions.ts` 第 66-72 行直接调用 `createNotification`,导致用户通知偏好失效、多渠道通知无效。 -### API Routes +**解耦建议**: +- 方案 A(推荐):notifications 吞并 messaging 的通知部分,messaging 仅保留 `messages` 表 +- 方案 B:messaging 改为调用 `notifications.sendNotification`,消除双向依赖 -| 路由 | 方法 | 权限 | 用途 | -|------|------|------|------| -| `/api/proctoring/event` | POST | requireAuth() | 接收学生端上报的监考事件(含 submission 归属校验) | +### P0-5:classSchedule 表三处写入口 + +- `classes/data-access.ts`(createClassScheduleItem 等) +- `scheduling/actions.ts`(applyAutoScheduleAction 直接 transaction 写入) +- `scheduling/data-access.ts`(间接) + +**影响**:数据完整性高风险。 + +**解耦建议**:统一 classSchedule 写入口到 scheduling 模块,classes 模块仅保留读权限。 + +### P0-6:proctoring 死代码与重复实现 + +- `exam-mode-config.tsx` 未集成到考试表单(监考功能无法启用) +- 事件上报存在 Server Action 与 REST API 双通道重复 + +**解耦建议**: +- 将 `exam-mode-config.tsx` 集成到 `exam-form.tsx`,或迁移到 exams 模块 +- 删除未使用的 `/api/proctoring/event` 路由 --- -## 模块:diagnostic +## 3.2 P1 较严重问题(短期执行) -`src/modules/diagnostic` +### P1-1:跨模块直接 DB 查询普遍存在 -学情诊断报告模块:基于知识点掌握度(`knowledgePointMastery` 表)生成个人/班级诊断报告,掌握度雷达图(学生 vs 班级平均),强项/弱项分析,知识点掌握度热力图,需重点关注学生列表,报告发布/删除管理。 +| 被访问表 | 访问次数 | 应归属模块 | 主要违规者 | +|---------|---------|-----------|-----------| +| `classes` | 8+ | classes | exams, homework, grades, dashboard | +| `classEnrollments` | 6+ | classes | homework, grades, attendance, users | +| `users` | 6+ | users | 多个模块 | +| `subjects` | 6+ | school | exams, homework, questions, grades | +| `exams` | 5+ | exams | homework, grades, dashboard, classes | +| `homeworkAssignments` | 5+ | homework | classes(反向直查) | -> 权限:所有 Server Actions 使用 `requirePermission()` 校验。生成/发布/删除报告使用 `requirePermission(DIAGNOSTIC_MANAGE)`,查询/详情使用 `requirePermission(DIAGNOSTIC_READ)`。admin/teacher/grade_head 角色拥有 DIAGNOSTIC_MANAGE+DIAGNOSTIC_READ,teaching_head/student 角色仅有 DIAGNOSTIC_READ。前端组件使用 `usePermission().hasPermission(DIAGNOSTIC_MANAGE)` 控制生成/发布/删除按钮可见性(无 `role === "xxx"` 硬编码)。页面路由通过 `getAuthContext()` 进行 DataScope 二次校验:`class_members` 仅查自己,`children` 仅查子女,`class_taught` 必须包含 classId。 +**解耦建议**:在各模块 data-access 暴露查询接口: +- `classes/data-access`:`getClassGradeIdsByClassIds` / `getClassStudentsByClassId` / `getActiveClassStudents` +- `exams/data-access`:`getExamForHomeworkCreation` +- `school/data-access`:`getSubjectOptions` / `getGradeOptions` +- `users/data-access`:`getUserNameByIds` / `getStudentInfo` +- `textbooks/data-access`:`getKnowledgePointOptions` +- `questions/data-access`:`insertQuestionWithRelations` / `deleteQuestionRecursive` -### Server Actions (`actions.ts`) +### P1-2:actions 层混入数据访问逻辑 -`"use server"` +| 模块 | 问题 Action | 违规 | +|------|------------|------| +| exams | `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` | 直接 db.query + db.insert/update/delete | +| homework | `createHomeworkAssignmentAction`(157 行)/ `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` | 直接 DB 操作 | +| questions | `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` | 内联 db.transaction | +| announcements | 所有写操作 Action | 直接 db.insert/update/delete | +| users | `updateUserProfileAction` | 直接 db.update | +| scheduling | `applyAutoScheduleAction` / `autoScheduleAction` | 直接 db.transaction + db.select | -| Action | 权限 | 用途 | -|--------|------|------| -| `generateStudentReportAction` | DIAGNOSTIC_MANAGE | 生成学生个人诊断报告(formData: studentId, period) | -| `generateClassReportAction` | DIAGNOSTIC_MANAGE | 生成班级诊断报告(formData: classId, period) | -| `publishReportAction` | DIAGNOSTIC_MANAGE | 发布诊断报告(formData: id,status → published) | -| `deleteReportAction` | DIAGNOSTIC_MANAGE | 删除诊断报告(formData: id) | -| `getDiagnosticReportsAction` | DIAGNOSTIC_READ | 查询诊断报告列表(params: DiagnosticReportQueryParams) | -| `getDiagnosticReportByIdAction` | DIAGNOSTIC_READ | 获取诊断报告详情(id) | +**解耦建议**:actions 层仅做"权限校验 → 解析 → 调用 data-access → revalidatePath → 返回",所有 DB 操作下沉到 data-access。 -### Data Access (`data-access.ts` + `data-access-reports.ts`) +### P1-3:auth.ts 混合 5 类职责 -`import "server-only"`(两个文件均以此开头) +`src/auth.ts` 293 行混合:NextAuth 配置 + 密码安全 DB 操作 + 角色规范化 + IP 解析 + 回调函数。 -#### `data-access.ts`(掌握度相关) +**解耦建议**: +- 密码安全 DB 操作 → `shared/lib/password-security-service.ts` +- 角色规范化 → `shared/lib/role-utils.ts` +- IP 解析 → `shared/lib/http-utils.ts`(与三个 logger 共用) +- `authorize` 回调拆分为 `checkRateLimit` / `checkAccountLockout` / `verifyPassword` / `loadUserRoles` -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getStudentMastery` | `(studentId: string) => Promise` | getStudentMasterySummary, teacher/diagnostic/student/[studentId] | -| `getStudentMasterySummary` | `(studentId: string) => Promise` | generateDiagnosticReport, teacher/diagnostic/student/[studentId], student/diagnostic | -| `updateMasteryFromSubmission` | `(submissionId: string) => Promise` | 待扩展(作业/考试提交后触发,onDuplicateKeyUpdate upsert) | -| `getClassMasterySummary` | `(classId: string) => Promise` | generateClassDiagnosticReport, teacher/diagnostic/class/[classId] | -| `getKnowledgePointStats` | `(classId?: string, gradeId?: string) => Promise` | teacher/diagnostic/student/[studentId](班级平均对比) | +### P1-4:users/import-export.ts 四重职责 -#### `data-access-reports.ts`(报告相关) +同时处理:导入解析 + 导出 + 用户创建(含密码哈希)+ 班级注册(跨模块写 classEnrollments)。 -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `generateDiagnosticReport` | `(studentId, period, generatedBy) => Promise` | generateStudentReportAction | -| `generateClassDiagnosticReport` | `(classId, period, generatedBy) => Promise` | generateClassReportAction | -| `getDiagnosticReports` | `(filters: DiagnosticReportQueryParams) => Promise` | getDiagnosticReportsAction, teacher/diagnostic, teacher/diagnostic/student/[studentId], student/diagnostic | -| `getDiagnosticReportById` | `(id: string) => Promise` | getDiagnosticReportByIdAction | -| `publishDiagnosticReport` | `(id: string) => Promise` | publishReportAction | -| `deleteDiagnosticReport` | `(id: string) => Promise` | deleteReportAction | +**解耦建议**: +- 拆分为 `import.ts`(解析+校验)+ `export.ts`(模板生成+列表导出) +- 用户创建逻辑迁移至 `data-access.ts` 的 `createUser` +- classEnrollments 写入改为调用 `classes/data-access.enrollStudentByInvitationCode` -### Types (`types.ts`) +### P1-5:notifications 反向依赖 messaging -| Type | 定义 | -|------|------| -| `DiagnosticReportType` | `"individual" \| "class" \| "grade"` | -| `DiagnosticReportStatus` | `"draft" \| "published" \| "archived"` | -| `KnowledgePointMastery` | `{ id, studentId, knowledgePointId, masteryLevel(0-100), totalQuestions, correctQuestions, lastAssessedAt, createdAt, updatedAt }` | -| `MasteryWithKnowledgePoint` | `KnowledgePointMastery & { knowledgePointName, knowledgePointDescription }` | -| `StudentMasterySummary` | `{ studentId, studentName, averageMastery, totalKnowledgePoints, strengths(≥80), weaknesses(<60), allMastery }` | -| `DiagnosticReport` | `{ id, studentId, generatedBy, reportType, period, summary, strengths[], weaknesses[], recommendations[], overallScore, status, createdAt, updatedAt }` | -| `DiagnosticReportWithDetails` | `DiagnosticReport & { studentName, generatedByName }` | -| `ClassMasterySummary` | `{ classId, className, studentCount, averageMastery, knowledgePointStats[], studentsNeedingAttention[] }` | -| `KnowledgePointStat` | `{ knowledgePointId, knowledgePointName, averageMastery, masteredCount(≥80), notMasteredCount(<60), totalStudents }` | -| `DiagnosticReportQueryParams` | `{ studentId?, reportType?, status?, period? }` | -| `MasteryRadarPoint` | `{ knowledgePoint, student(0-100), classAverage?(0-100) }` | +`notifications/data-access.ts` 和 `in-app-channel.ts` 反向依赖 messaging 模块的偏好和 createNotification。 -### Components (`components/`) +**解耦建议**:与 P0-4 一并解决,将 `messageNotifications` 和 `notificationPreferences` 表所有权移交 notifications 模块。 -所有组件以 `"use client"` 开头。 +### P1-6:三个 logger 重复实现 IP/Header 提取 -| 组件 | 用途 | -|------|------| -| `mastery-radar-chart.tsx` | 知识点掌握度雷达图(recharts RadarChart,学生 vs 班级平均对比,无数据时显示 EmptyState) | -| `student-diagnostic-view.tsx` | 学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示) | -| `class-diagnostic-view.tsx` | 班级诊断视图(概览卡片、知识点掌握度热力图[绿≥80/黄60-79/橙40-59/红<40]、知识点排名表、需重点关注学生表[链接到学生视图]、生成班级报告表单[DIAGNOSTIC_MANAGE]) | -| `report-list.tsx` | 诊断报告列表(reportType/status 过滤器[URL searchParams]、报告表格、发布/删除操作[DIAGNOSTIC_MANAGE]、确认对话框) | +`audit-logger.ts` / `change-logger.ts` / `login-logger.ts` / `auth.ts` 四处重复实现 IP/User-Agent 提取逻辑,且实现略有差异。 -### 数据库表 - -| 表 | 用途 | -|----|------| -| `knowledgePointMastery` | 知识点掌握度记录(复合主键 studentId+knowledgePointId,onDuplicateKeyUpdate upsert) | -| `learningDiagnosticReports` | 学情诊断报告(reportType: individual/class/grade;status: draft/published/archived) | - -### 页面路由 - -| 路由 | 组件 | 权限 | DataScope 校验 | -|------|------|------|----------------| -| `/teacher/diagnostic` | ReportList | diagnostic:read | class_members 仅查看自己报告 | -| `/teacher/diagnostic/student/[studentId]` | StudentDiagnosticView | diagnostic:read | class_members 仅自己,children 仅子女 | -| `/teacher/diagnostic/class/[classId]` | ClassDiagnosticView | diagnostic:read | class_taught 必须包含 classId,class_members/children → notFound | -| `/student/diagnostic` | StudentDiagnosticView | diagnostic:read | class_members 仅查自己(ctx.userId) | +**解耦建议**:提取 `shared/lib/http-utils.ts`,导出 `getClientIp()` 和 `getUserAgent()` 统一复用。 --- -## 模块:elective +## 3.3 P2 代码质量问题(机会修复) -`src/modules/elective` - -选课管理模块:选修课程 CRUD、选课开放/关闭、学生选课/退课、抽签模式批量录取(runLottery)、FCFS 即时录取、DataScope 行级过滤(admin 全部、teacher 所教、grade_head 所管年级、student 可选课程)。 - -> 权限:管理操作使用 `requirePermission(ELECTIVE_MANAGE)`;读取使用 `requirePermission(ELECTIVE_READ)`;学生选课/退课使用 `requirePermission(ELECTIVE_SELECT)`。前端组件使用 `usePermission().hasPermission()` 控制权限。`getStudentSelectionsAction` 对 class_members/children 进行 DataScope 二次校验。 - -### Server Actions (`actions.ts`) - -| Action | 权限 | 用途 | -|--------|------|------| -| `createElectiveCourseAction` | ELECTIVE_MANAGE | 创建选修课程(formData: name, subjectId?, teacherId, gradeId?, description?, capacity?, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, selectionMode?, credit?) | -| `updateElectiveCourseAction` | ELECTIVE_MANAGE | 更新选修课程(id + formData) | -| `deleteElectiveCourseAction` | ELECTIVE_MANAGE | 删除选修课程(formData: courseId) | -| `openSelectionAction` | ELECTIVE_MANAGE | 开放选课(formData: courseId) | -| `closeSelectionAction` | ELECTIVE_MANAGE | 关闭选课(formData: courseId) | -| `runLotteryAction` | ELECTIVE_MANAGE | 执行抽签录取(formData: courseId),返回 {enrolled, waitlist} | -| `selectCourseAction` | ELECTIVE_SELECT | 学生选课(formData: courseId, priority?) | -| `dropCourseAction` | ELECTIVE_SELECT | 学生退课(formData: courseId) | -| `getElectiveCoursesAction` | ELECTIVE_READ | 查询选修课程列表(按 DataScope 过滤,传 currentUserId) | -| `getStudentSelectionsAction` | ELECTIVE_READ | 查询学生选课记录(DataScope 二次校验:class_members 仅自己,children 仅子女) | -| `getAvailableCoursesAction` | ELECTIVE_SELECT | 获取学生可选课程(status=open 且匹配年级) | - -### Data Access - -#### `data-access.ts` (`import "server-only"`) - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getElectiveCourses` | `(params?: GetElectiveCoursesParams & { scope?: DataScope; currentUserId?: string }) => Promise` | getElectiveCoursesAction, admin/elective, teacher/elective | -| `getElectiveCourseById` | `(id: string) => Promise` | updateElectiveCourseAction, admin/elective/[id]/edit | -| `createElectiveCourse` | `(data: CreateElectiveCourseInput, teacherId: string) => Promise` | createElectiveCourseAction | -| `updateElectiveCourse` | `(id: string, data: Partial) => Promise` | updateElectiveCourseAction | -| `deleteElectiveCourse` | `(id: string) => Promise` | deleteElectiveCourseAction | -| `openSelection` | `(courseId: string) => Promise` | openSelectionAction | -| `closeSelection` | `(courseId: string) => Promise` | closeSelectionAction | -| `getSubjectOptions` | `() => Promise<{id, name}[]>` | admin/elective/create, admin/elective/[id]/edit | - -> `buildScopeFilter(scope, userId)` 内部函数:owned/class_taught 按 `teacherId` 过滤,grade_managed 按 `gradeIds` 过滤,class_members/children 返回 null(学生通过 `getAvailableCoursesForStudent` 获取可选课程)。 - -#### `data-access-selections.ts` (`import "server-only"`) - -> 从 data-access.ts 拆分以遵守单文件 ≤300 行规则。 - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `getCourseSelections` | `(courseId: string) => Promise` | 待扩展 | -| `getStudentSelections` | `(studentId: string) => Promise` | getStudentSelectionsAction, student/elective | -| `getStudentGradeId` | `(studentId: string) => Promise` | getAvailableCoursesForStudent | -| `getAvailableCoursesForStudent` | `(studentId: string, gradeId?: string \| null) => Promise` | getAvailableCoursesAction, student/elective | - -#### `data-access-operations.ts` (`import "server-only"`) - -> 从 data-access.ts 拆分以遵守单文件 ≤300 行规则。包含选课/退课/抽签的写操作。 - -| 函数 | 签名 | 被使用 | -|------|------|--------| -| `runLottery` | `(courseId: string) => Promise<{enrolled: number, waitlist: number}>` | runLotteryAction | -| `selectCourse` | `(courseId: string, studentId: string, priority?: number) => Promise<{status: CourseSelectionStatus, message: string}>` | selectCourseAction | -| `dropCourse` | `(courseId: string, studentId: string) => Promise` | dropCourseAction | - -### Schema (`schema.ts`) - -| Schema | 用途 | -|--------|------| -| `ElectiveCourseStatusEnum` | 课程状态枚举(draft/open/closed/cancelled) | -| `ElectiveSelectionModeEnum` | 选课模式枚举(fcfs/lottery) | -| `CourseSelectionStatusEnum` | 选课状态枚举(selected/enrolled/waitlist/dropped/rejected) | -| `CreateElectiveCourseSchema` | 创建课程校验(name 必填,teacherId 必填,capacity 1-500 默认 30,selectionMode 默认 fcfs,credit 默认 1.0) | -| `UpdateElectiveCourseSchema` | 更新课程校验(所有字段可选,含 status) | -| `SelectCourseSchema` | 选课校验(courseId 必填,priority 1-10 可选) | -| `DropCourseSchema` | 退课校验(courseId 必填) | -| `RunLotterySchema` | 抽签校验(courseId 必填) | - -### 类型/接口 (`types.ts`) - -| 类型 | 定义 | -|------|------| -| `ElectiveCourseStatus` | `"draft" \| "open" \| "closed" \| "cancelled"` | -| `ElectiveSelectionMode` | `"fcfs" \| "lottery"` | -| `CourseSelectionStatus` | `"selected" \| "enrolled" \| "waitlist" \| "dropped" \| "rejected"` | -| `ElectiveCourse` | 课程完整类型(id, name, subjectId?, teacherId, gradeId?, description?, capacity, enrolledCount, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, status, selectionMode, credit, createdAt, updatedAt) | -| `ElectiveCourseWithDetails` | `ElectiveCourse & { teacherName?, subjectName?, gradeName? }` | -| `CourseSelection` | 选课记录类型(id, courseId, studentId, status, priority?, selectedAt, enrolledAt?, droppedAt?, lotteryRank?, createdAt, updatedAt) | -| `CourseSelectionWithDetails` | `CourseSelection & { courseName?, studentName?, courseCapacity?, courseEnrolledCount?, courseStatus? }` | -| `GetElectiveCoursesParams` | 查询参数(status?, gradeId?, subjectId?, teacherId?) | -| `ELECTIVE_STATUS_LABELS` | 课程状态标签常量 | -| `ELECTIVE_STATUS_COLORS` | 课程状态颜色常量(Badge variant) | -| `SELECTION_MODE_LABELS` | 选课模式标签常量 | -| `COURSE_SELECTION_STATUS_LABELS` | 选课状态标签常量 | -| `COURSE_SELECTION_STATUS_COLORS` | 选课状态颜色常量(Badge variant) | - -### 导出组件 (`components/`) - -| 组件文件 | 功能 | -|---------|------| -| `elective-course-list.tsx` | 课程卡片列表(管理员/教师视图,含编辑/开放/关闭/抽签/删除操作按钮,usePermission 权限控制) | -| `elective-course-form.tsx` | 课程创建/编辑表单(name, subjectId, teacherId, gradeId, description, capacity, classroom, schedule, dates, selectionMode, credit) | -| `student-selection-view.tsx` | 学生选课视图(可选课程列表 + 我的选课记录,含选课/退课按钮) | - -### 路由页面 - -| 路由 | 组件 | 权限 | 说明 | -|------|------|------|------| -| `/admin/elective` | ElectiveCourseList | elective:manage | 管理员选修课程列表(scope=all) | -| `/admin/elective/create` | ElectiveCourseForm | elective:manage | 创建选修课程 | -| `/admin/elective/[id]/edit` | ElectiveCourseForm (edit) | elective:manage | 编辑选修课程 | -| `/teacher/elective` | ElectiveCourseList (teacher) | elective:manage | 教师选修课程列表(scope=class_taught/owned,按 teacherId 过滤) | -| `/student/elective` | StudentSelectionView | elective:select | 学生选课页面(可选课程 + 我的选课) | +| 序号 | 问题 | 模块 | +|------|------|------| +| P2-1 | `exams/ai-pipeline.ts` 912 行,混合 4 类职责 | exams | +| P2-2 | `exams/actions.ts` 832 行(超 800 建议) | exams | +| P2-3 | `shared/lib/ai.ts` 218 行,混合 5 类职责 | shared | +| P2-4 | `onboarding-gate.tsx` 业务逻辑泄漏到 shared | shared | +| P2-5 | `global-search.tsx` 业务类型硬编码在 shared | shared | +| P2-6 | `proxy.ts` 硬编码权限字符串,未复用 Permissions 常量 | proxy | +| P2-7 | `useA11yId` Hook 错放在 lib/ 而非 hooks/ | shared | +| P2-8 | `schema.ts` 分节编号混乱(section 12 出现在 14b 之后) | 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 吞错误 | files | +| P2-14 | `elective` runLottery 使用 Math.random | elective | +| P2-15 | `elective` selectCourse FCFS 并发超卖风险 | elective | +| P2-16 | `diagnostic` 班级报告 studentId 字段复用 | diagnostic | +| P2-17 | `layout` 用权限反推角色 | layout | +| P2-18 | `scheduling/actions.ts` 末尾 re-export data-access | scheduling | +| P2-19 | `ExamAssembly` / `ExamPreviewQuestionEditor` 10 个 props | exams | +| P2-20 | `homework/data-access.getDemoStudentUser` 使用 `auth()` 而非 auth-guard | homework | --- -## 模块间依赖矩阵 +## 3.4 解耦优先级路线图 -| ↓ 使用 → | shared | auth | exams | homework | questions | textbooks | classes | school | dashboard | layout | settings | users | audit | announcements | files | grades | course-plans | parent | messaging | attendance | scheduling | proctoring | diagnostic | elective | -|----------|--------|------|-------|----------|-----------|-----------|---------|--------|-----------|--------|----------|-------|-------|---------------|-------|-------|-------------|--------|-----------|------------|-----------|------------|------------|----------| -| **shared** | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **auth** | db,schema,permissions,login-logger | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **exams** | db,auth-guard,types,ai | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **homework** | db,auth-guard,types | auth | data-access.getExams | - | - | - | schema | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **questions** | db,auth-guard,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **textbooks** | db,auth-guard,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **classes** | db,auth-guard,types | auth | - | homework-insights | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **school** | db,auth-guard,types,audit-logger | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **dashboard** | db,types | auth | - | data-access.getTeacherGradeTrends,getStudentDashboardGrades | - | - | data-access.getTeacherClasses,getStudentClasses,getStudentSchedule | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **layout** | hooks.usePermission | auth(useSession) | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | notification-dropdown | - | - | - | - | -| **settings** | db,auth-guard,ai,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **users** | db,auth-guard(requireAuth,requirePermission),types,lib.excel | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **audit** | db,auth-guard.requirePermission,types.permissions | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **announcements** | db,auth-guard,types | auth | - | - | - | - | - | - | data-access.getGrades | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **files** | db,auth-guard(requireAuth,requirePermission),types,lib/file-storage | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **grades** | db,auth-guard,types,lib.excel | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **course-plans** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getAdminClasses,getStaffOptions | data-access.getAcademicYears | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **parent** | db,auth-guard(requireAuth),types | auth | - | data-access.getStudentHomeworkAssignments,getStudentDashboardGrades | - | - | data-access.getStudentClasses,getStudentSchedule | - | - | - | - | - | - | - | - | data-access.getStudentGradeSummary | - | - | - | - | - | - | -| **messaging** | db,auth-guard(requirePermission,requireAuth),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **notifications** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | notification-preferences,data-access.createNotification | - | - | - | - | - | -| **attendance** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getTeacherClasses,getAdminClasses | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **scheduling** | db,auth-guard(requirePermission,getAuthContext),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **proctoring** | db,auth-guard(requirePermission,requireAuth),types,components.ui,hooks.usePermission | auth | schema.exams,examSubmissions,examProctoringEvents | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **diagnostic** | db,auth-guard(requirePermission,getAuthContext),types,hooks.usePermission,components.ui | auth | schema.examSubmissions,submissionAnswers,questionsToKnowledgePoints | - | - | - | schema.classes,classEnrollments | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -| **elective** | db,auth-guard.requirePermission,types,types.DataScope,hooks.usePermission,components.ui | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | +### 立即执行(P0) +1. 拆分 `classes/data-access.ts`(2104 行 → 按职责拆 3-4 个文件) +2. 拆分 `homework/data-access.ts`(1038 行 → 分离排名逻辑) +3. 修复 `shared/lib` ↔ `auth` 循环依赖 +4. dashboard 改为通过各模块 data-access 获取数据 +5. messaging 写通知改为通过 notifications dispatcher +6. 统一 classSchedule 写入口到 scheduling 模块 +7. 集成 proctoring/exam-mode-config 到考试表单 + +### 短期执行(P1) +8. actions 层移除直接 DB 操作(exams/homework/questions/announcements/users/scheduling) +9. 拆分 `auth.ts` +10. 拆分 `users/import-export.ts` +11. 消除 notifications → messaging 反向依赖 +12. 提取 `shared/lib/http-utils.ts` 统一 IP 提取 +13. 各模块暴露跨模块查询接口(见 P1-1) + +### 中期执行(P2) +14. 建立模块间数据访问规范(通过对方 data-access 或导出查询函数) +15. `schema.ts` 按业务域分节 +16. 拆分 `exams/ai-pipeline.ts` +17. shared 层业务逻辑下沉到 modules 层 +18. 代码质量问题逐项修复 --- -## 关键参数影响链 +## 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 实现 | + +--- + +# 附录 A:模块间依赖矩阵 + +> 行表示使用方,列表示被使用方。`✅` 合理依赖,`❌` 违规直查,`⟳` 循环依赖。 + +| ↓ 使用 → | shared | auth | exams | homework | questions | textbooks | classes | school | dashboard | users | grades | messaging | notifications | 其他 | +|----------|--------|------|-------|----------|-----------|-----------|---------|--------|-----------|-------|--------|-----------|---------------|------| +| **shared** | - | ⟳ | - | - | - | - | - | - | - | - | - | - | - | - | +| **auth(root)** | ✅ db/lib | - | - | - | - | - | - | - | - | - | - | - | - | - | +| **exams** | ✅ | ✅ | - | - | ✅类型/❌insert | - | ❌直查 | ❌直查 | - | - | - | - | - | - | +| **homework** | ✅ | ✅ | ❌直查5处 | - | ✅关系 | - | ❌直查 | ❌直查 | - | ❌直查 | - | - | - | - | +| **questions** | ✅ | ✅ | - | - | - | ❌直查 | - | - | - | - | - | - | - | - | +| **textbooks** | ✅ | ✅ | - | - | ✅UI | - | - | - | - | - | - | - | - | - | +| **classes** | ✅ | ✅ | ❌直查 | ❌直查5表 | - | - | - | ❌直查 | - | - | ❌混入 | - | - | - | +| **school** | ✅ | ✅ | - | - | - | - | - | - | - | ⚠️可接受 | - | - | - | - | +| **grades** | ✅ | ✅ | ✅外键 | ✅外键 | - | - | ❌直查 | ❌直查 | - | ❌直查 | - | - | - | - | +| **dashboard** | ✅ | ✅ | ❌直查 | ✅/❌直查 | ❌直查 | ❌直查 | ✅/❌直查 | - | - | ❌直查 | - | - | - | - | +| **users** | ✅ | ✅ | - | - | - | - | ❌写enrollments | - | - | - | - | - | - | - | +| **messaging** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | ❌绕过 | - | +| **notifications** | ✅ | ✅ | - | - | - | - | ❌直查 | - | - | - | - | ⟳反向依赖 | - | - | +| **attendance** | ✅ | ✅ | - | - | - | - | ❌直查 | - | - | - | - | - | - | - | +| **scheduling** | ✅ | ✅ | - | - | - | - | ❌写schedule | - | - | ❌直查 | - | - | - | - | +| **proctoring** | ✅ | ✅ | ❌直查 | - | - | - | - | - | - | ❌直查 | - | - | - | - | +| **diagnostic** | ✅ | ✅ | ❌直查 | ❌直查 | ❌直查 | - | ❌直查 | - | - | ❌直查 | - | - | - | - | +| **parent** | ✅ | ✅ | - | ✅ | - | - | ✅ | - | - | - | ✅ | - | - | - | +| **elective** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | +| **course-plans** | ✅ | ✅ | - | - | - | - | ✅ | ✅ | - | ✅ | - | - | - | - | +| **audit** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | +| **announcements** | ✅ | ✅ | - | - | - | - | - | ✅ | - | - | - | - | - | - | +| **files** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | +| **settings** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | ✅ | - | - | +| **layout** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | ✅ | - | - | + +--- + +# 附录 B:关键参数影响链 ### `userId` 1. 由 `auth.ts` JWT callback 从 `users` 表查询产生,存入 JWT -2. 通过 `session.user.id` 传递到所有 Server Components 和 Client Components +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` 默认值写入 `electiveCourses` 表(createElectiveCourseAction),作为 `studentId` 查询学生选课(selectCourseAction/dropCourseAction/getStudentSelectionsAction) -9. 在 elective/data-access.ts 中 `getElectiveCourses({ scope, currentUserId })` 按 `teacherId` 过滤(class_taught/owned scope) +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.ts` 的 `createExamAction` 产生,通过 CUID2 生成,写入 `exams` 表 +1. 由 `exams/actions.createExamAction` 产生(CUID2),写入 `exams` 表 2. 被 `exams/data-access.getExamById(id)` 读取 -3. 被 `exams/actions.ts` 的 `updateExamAction`/`deleteExamAction`/`duplicateExamAction` 用于定位考试 -4. 传入 `homework/actions.ts` 的 `createHomeworkAssignmentAction` 的 `sourceExamId` 参数 +3. 被 `exams/actions` 的 `updateExamAction`/`deleteExamAction`/`duplicateExamAction` 用于定位考试 +4. 传入 `homework/actions.createHomeworkAssignmentAction` 的 `sourceExamId` 参数 5. 在 `homeworkAssignments` 表中作为外键关联到源考试 6. 被 `homework/data-access.getHomeworkAssignmentAnalytics` 用于追溯作业来源 ### `classId` -1. 由 `classes/actions.ts` 的 `createTeacherClassAction`/`createAdminClassAction` 产生 +1. 由 `classes/actions` 的 `createTeacherClassAction`/`createAdminClassAction` 产生 2. 被 `classes/data-access.getClassStudents(classId)` 读取学生列表 3. 被 `classes/data-access.getClassSchedule(classId)` 读取课表 -4. 被 `classes/data-access.getClassHomeworkInsights(classId)` 读取作业洞察 +4. 被 `classes/data-access.getClassHomeworkInsights(classId)` 读取作业洞察(❌ 应属 homework) 5. 被 `homework/data-access.getHomeworkAssignments({ classId })` 过滤作业列表 6. 在 `auth-guard.ts` 中通过 `classSubjectTeachers` 查询教师关联的 classIds,构建 `DataScope.class_taught` ### `permission` -1. 由 `shared/types/permissions.ts` 的 `Permissions` 常量定义(57 个权限点,含 `AUDIT_LOG_READ`、`ANNOUNCEMENT_MANAGE`、`FILE_UPLOAD`、`FILE_READ`、`FILE_DELETE`、`GRADE_RECORD_MANAGE`、`GRADE_RECORD_READ`、`COURSE_PLAN_MANAGE`、`COURSE_PLAN_READ`、`ATTENDANCE_MANAGE`、`ATTENDANCE_READ`、`MESSAGE_SEND`、`MESSAGE_READ`、`MESSAGE_DELETE`、`SCHEDULE_AUTO`、`SCHEDULE_ADJUST`、`DIAGNOSTIC_MANAGE`、`DIAGNOSTIC_READ`、`ELECTIVE_MANAGE`、`ELECTIVE_READ`、`ELECTIVE_SELECT` 等) -2. 在 `shared/lib/permissions.ts` 中通过 `ROLE_PERMISSIONS` 映射角色到权限列表(admin 角色包含 `AUDIT_LOG_READ`、`COURSE_PLAN_MANAGE`+`COURSE_PLAN_READ`;admin/teacher 含 `FILE_UPLOAD/READ/DELETE` 及 `GRADE_RECORD_MANAGE/READ`;teacher/student/grade_head/teaching_head 含 `COURSE_PLAN_READ`;student/parent 含 `FILE_READ` 及 `GRADE_RECORD_READ`;admin/teacher 含 `ATTENDANCE_MANAGE`+`ATTENDANCE_READ`,student/parent/grade_head/teaching_head 含 `ATTENDANCE_READ`;admin/teacher/parent/grade_head/teaching_head 含 `MESSAGE_SEND/READ/DELETE`;student 含 `MESSAGE_READ/DELETE` 但无 `MESSAGE_SEND`;admin 含 `SCHEDULE_AUTO`+`SCHEDULE_ADJUST`,teacher/student/parent/grade_head/teaching_head 无排课权限;admin/teacher/grade_head 含 `DIAGNOSTIC_MANAGE`+`DIAGNOSTIC_READ`,teaching_head/student 含 `DIAGNOSTIC_READ`;admin/teacher 含 `ELECTIVE_MANAGE`+`ELECTIVE_READ`,student 含 `ELECTIVE_SELECT`+`ELECTIVE_READ`,grade_head/teaching_head 含 `ELECTIVE_READ`) +1. 由 `shared/types/permissions.ts` 的 `Permissions` 常量定义(57 个权限点) +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 层断言权限(如 audit-logs 页面使用 `requirePermission(AUDIT_LOG_READ)`;`DELETE /api/files/[id]` 使用 `requirePermission(FILE_DELETE)`;messaging/actions.ts 使用 `requirePermission(MESSAGE_SEND/READ/DELETE)`;attendance/actions.ts 使用 `requirePermission(ATTENDANCE_MANAGE/READ)`;users/actions.ts 的 `importUsersAction`/`exportUsersAction`/`downloadUserTemplateAction` 使用 `requirePermission(USER_MANAGE)`;grades/actions.ts 的 `exportGradesAction` 使用 `requirePermission(GRADE_RECORD_READ)`;`POST /api/import` 使用 `requirePermission(USER_MANAGE)`;scheduling/actions.ts 使用 `requirePermission(SCHEDULE_AUTO/SCHEDULE_ADJUST)`;diagnostic/actions.ts 使用 `requirePermission(DIAGNOSTIC_MANAGE/READ)`;elective/actions.ts 使用 `requirePermission(ELECTIVE_MANAGE/READ/SELECT)`) +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` 字段过滤侧边栏菜单(Audit Logs 菜单项使用 `Permissions.AUDIT_LOG_READ`;Messages 菜单项使用 `Permissions.MESSAGE_READ`;Attendance 菜单项 teacher 使用 `Permissions.ATTENDANCE_MANAGE`,student/parent 使用 `Permissions.ATTENDANCE_READ`;Import Users 菜单项使用 `Permissions.USER_MANAGE`;Scheduling 菜单项 admin 使用 `Permissions.SCHEDULE_ADJUST`/`SCHEDULE_AUTO`;teacher Schedule Changes 菜单项使用 `Permissions.SCHEDULE_ADJUST`;teacher/student Diagnostic 菜单项使用 `Permissions.DIAGNOSTIC_READ`;Electives 菜单项 admin/teacher 使用 `Permissions.ELECTIVE_MANAGE`,student 使用 `Permissions.ELECTIVE_SELECT`) +7. 在 `layout/config/navigation.ts` 中作为 `NavItem.permission` 字段过滤侧边栏菜单 ### `DataScope` 1. 由 `auth-guard.ts` 的 `resolveDataScope(userId, roles)` 根据用户角色和 DB 关系动态计算 -2. 传递到 `exams/data-access.getExams({ scope })` 进行行级过滤 -3. 传递到 `homework/data-access.getHomeworkAssignments({ scope })` 进行行级过滤 -4. 传递到 `dashboard/data-access.getAdminDashboardData(scope)` 进行统计过滤 -5. 在 exams/actions.ts 的 `updateExamAction`/`deleteExamAction` 中用于判断是否需要资源归属校验(`scope.type !== "all"`) -6. 对 parent 角色,`resolveDataScope` 查询 `parentStudentRelations` 表构建 `{ type: "children", childrenIds: string[] }`,传递到 `grades/data-access.getStudentGradeSummary` 等函数进行行级过滤 -7. 在 `parent/children/[studentId]/page.tsx` 中通过 `ctx.dataScope.type === "children" && !ctx.dataScope.childrenIds.includes(studentId)` 二次校验家长拥有该子女 -8. 传递到 `attendance/data-access.getAttendanceRecords({ scope })` 进行行级过滤(class_taught 按教师班级过滤,children 按子女过滤,class_members 仅查自己,all 查全部) -9. 在 `attendance/actions.ts` 的 `getStudentAttendanceAction` 中对 class_members/children 进行 DataScope 二次校验 -10. 传递到 `elective/data-access.getElectiveCourses({ scope, currentUserId })` 进行行级过滤(owned/class_taught 按 `teacherId` 过滤,grade_managed 按 `gradeIds` 过滤,class_members/children 返回 null 由 `getAvailableCoursesForStudent` 处理) -11. 在 `elective/actions.ts` 的 `getStudentSelectionsAction` 中对 class_members/children 进行 DataScope 二次校验 +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`。本附录仅列出关键函数。 -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/` | 角色路由分发 | server | auth_required | 重定向到 `/dashboard` | +### shared 层核心函数 -### auth/* 子路由 +```typescript +// shared/lib/auth-guard.ts +getAuthContext(): Promise +requirePermission(permission: Permission): Promise +requireAuth(): Promise +checkPermission(permission: Permission): Promise<{ allowed: boolean; ctx: AuthContext }> +resolveDataScope(userId: string, roleNames: string[]): Promise -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/login` | LoginForm | client | public | 登录页面 | -| `/register` | RegisterForm + registerAction | server | public | 注册页面(含未成年人信息保护、隐私政策/用户协议同意勾选) | -| `/privacy` | PrivacyPage | server | public | 隐私政策页面(信息收集/使用/保护、用户权利、Cookie、未成年人保护条款、联系方式) | -| `/terms` | TermsPage | server | public | 用户协议页面(服务说明、注册、行为规范、知识产权、免责、变更终止、法律适用) | +// shared/lib/permissions.ts +resolvePermissions(roleNames: string[]): Permission[] -### admin/school/* 子路由 +// shared/lib/audit-logger.ts +logAudit(params: LogAuditParams): Promise -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/school` | SchoolManagementHome | server | school:manage | 学校管理首页(聚合入口) | -| `/admin/school/schools` | SchoolList | server | school:manage | 学校列表(dataAccess: school/data-access.getSchools) | -| `/admin/school/grades` | GradeList | server | grade:manage | 年级列表(dataAccess: school/data-access.getGrades) | -| `/admin/school/grades/insights` | GradeInsights | server | grade:manage | 年级洞察(dataAccess: classes/data-access.getGradeHomeworkInsights) | -| `/admin/school/departments` | DepartmentList | server | school:manage | 部门列表(dataAccess: school/data-access.getDepartments) | -| `/admin/school/classes` | AdminClassList | server | school:manage | 班级列表(dataAccess: classes/data-access.getAdminClasses) | -| `/admin/school/academic-year` | AcademicYearList | server | school:manage | 学年列表(dataAccess: school/data-access.getAcademicYears) | +// shared/lib/login-logger.ts +logLoginEvent(params: LogLoginEventParams): Promise -### admin/audit-logs/* 子路由 +// shared/lib/change-logger.ts +logDataChange(params: LogDataChangeParams): Promise -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/audit-logs` | AuditLogView | server | audit_log:read | 操作日志列表(dataAccess: audit/data-access.getAuditLogs, getAuditModuleOptions;权限:requirePermission(AUDIT_LOG_READ)) | -| `/admin/audit-logs/login-logs` | LoginLogView | server | audit_log:read | 登录日志列表(dataAccess: audit/data-access.getLoginLogs;权限:requirePermission(AUDIT_LOG_READ)) | +// shared/lib/ai.ts +createAiChatCompletion(input: AiChatRequest): Promise<{ content, usage }> +parseAiChatPayload(body: unknown): AiChatRequest +encryptAiApiKey(value: string): string +decryptAiApiKey(value: string): string -### admin/announcements/* 子路由 +// 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 -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/announcements` | AdminAnnouncementsView | client | announcement:manage | 公告管理首页(列表+创建对话框;dataAccess: announcements/data-access.getAnnouncements, school/data-access.getGrades) | -| `/admin/announcements/[id]` | AnnouncementForm | client | announcement:manage | 编辑公告(dataAccess: announcements/data-access.getAnnouncementById, school/data-access.getGrades;actions: updateAnnouncementAction) | +// shared/lib/rate-limit.ts +rateLimit(params: { key: string; limit: number; windowMs: number }): RateLimitResult +rateLimitKey(prefix: string, identifier: string): string +rateLimitHeaders(result: RateLimitResult): Record -### admin/files/* 路由 +// shared/lib/excel.ts +exportToExcel(params: { sheets: ExcelSheet[] }): Promise +parseExcel(buffer: Buffer): Promise +generateTemplate(params: { sheets: TemplateSheet[] }): Promise -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/files` | AdminFilesView | client | file:read | 管理员文件管理页面(上传+列表+删除;dataAccess: files/data-access.getAllFileAttachments;API: POST /api/upload, DELETE /api/files/[id] 需 file:delete) | +// shared/lib/file-storage.ts +isAllowedMimeType(mimeType: string): boolean +generateStoragePath(originalName: string): string +formatFileSize(bytes: number): string -### admin/course-plans/* 路由 +// shared/lib/utils.ts +cn(...inputs: ClassValue[]): string +formatDate(date: string | Date, locale?: string): string +``` -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/course-plans` | CoursePlanList | client | course_plan:manage | 管理员课程计划列表(支持状态筛选;dataAccess: course-plans/data-access.getCoursePlans) | -| `/admin/course-plans/create` | CoursePlanForm (create) | client | course_plan:manage | 创建课程计划(actions: createCoursePlanAction;dataAccess: classes/data-access.getAdminClasses, course-plans/data-access.getSubjectOptions, classes/data-access.getStaffOptions, school/data-access.getAcademicYears) | -| `/admin/course-plans/[id]` | CoursePlanDetail | client | course_plan:manage | 课程计划详情(含周计划表格;actions: deleteCoursePlanAction, createCoursePlanItemAction, updateCoursePlanItemAction, deleteCoursePlanItemAction, toggleCoursePlanItemCompletedAction;dataAccess: course-plans/data-access.getCoursePlanById) | -| `/admin/course-plans/[id]/edit` | CoursePlanForm (edit) | client | course_plan:manage | 编辑课程计划(actions: updateCoursePlanAction;dataAccess: course-plans/data-access.getCoursePlanById, classes/data-access.getAdminClasses, course-plans/data-access.getSubjectOptions, classes/data-access.getStaffOptions, school/data-access.getAcademicYears) | +### 业务模块核心 Actions -### admin/users/* 路由 +```typescript +// exams/actions.ts +createExamAction(prevState: ActionState, formData: FormData): Promise> +createAiExamAction(prevState: ActionState, formData: FormData): Promise> +updateExamAction(prevState: ActionState, formData: FormData): Promise +deleteExamAction(prevState: ActionState, formData: FormData): Promise +duplicateExamAction(prevState: ActionState, formData: FormData): Promise> -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/users/import` | UserImportPage (含 UserImportDialog) | server | user:manage | 用户批量导入页面(说明卡片+字段文档表+导入对话框;actions: users/actions.downloadUserTemplateAction, users/actions.importUsersAction;权限:requirePermission(USER_MANAGE)) | +// homework/actions.ts +createHomeworkAssignmentAction(prevState: ActionState, formData: FormData): Promise +startHomeworkSubmissionAction(prevState: ActionState, formData: FormData): Promise> +saveHomeworkAnswerAction(prevState: ActionState, formData: FormData): Promise +submitHomeworkAction(prevState: ActionState, formData: FormData): Promise +gradeHomeworkSubmissionAction(prevState: ActionState, formData: FormData): Promise -### announcements/* 路由 +// classes/actions.ts +createTeacherClassAction(prevState: ActionState, formData: FormData): Promise +createAdminClassAction(prevState: ActionState, formData: FormData): Promise +createGradeClassAction(prevState: ActionState, formData: FormData): Promise +// + update/delete 各 3 个,共 9 个 -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/announcements` | AnnouncementList | client | auth_required | 所有登录用户可查看的公告列表(仅 published;dataAccess: announcements/data-access.getAnnouncements) | +// grades/actions.ts +getGradeRecordsAction(params: GetGradeRecordsParams): Promise> +createGradeRecordAction(prevState: ActionState, formData: FormData): Promise +exportGradesAction(params: ExportGradesParams): Promise> -### management/* 路由 +// scheduling/actions.ts +autoScheduleAction(prevState: ActionState, formData: FormData): Promise +applyAutoScheduleAction(prevState: ActionState, formData: FormData): Promise +``` -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/management/grade/classes` | GradeManagedClasses | server | grade:manage | 年级主任管理的班级(dataAccess: classes/data-access.getGradeManagedClasses) | -| `/management/grade/insights` | GradeInsightsView | server | grade:manage | 年级洞察视图(dataAccess: classes/data-access.getGradeHomeworkInsights) | +### 业务模块核心 Data-access -### student/learning/* 路由 +```typescript +// exams/data-access.ts +getExams(params: GetExamsParams & { scope: DataScope }): Promise<{ items: Exam[]; total: number }> +getExamById(id: string, scope: DataScope): Promise +persistExamDraft(input: ExamDraftInput): Promise<{ examId: string }> +persistAiGeneratedExamDraft(input: AiExamDraftInput): Promise<{ examId: string }> -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/student/learning/assignments` | StudentAssignmentList | server | homework:submit | 学生作业列表(dataAccess: homework/data-access.getStudentHomeworkAssignments) | -| `/student/learning/assignments/[assignmentId]` | HomeworkTakeView | client | homework:submit | 学生作答页面(actions: startHomeworkSubmissionAction, saveHomeworkAnswerAction, submitHomeworkAction) | -| `/student/learning/courses` | StudentCourseList | server | class:read | 学生课程列表(dataAccess: classes/data-access.getStudentClasses) | -| `/student/learning/textbooks` | StudentTextbookList | server | textbook:read | 学生教材列表(dataAccess: textbooks/data-access.getTextbooks) | -| `/student/learning/textbooks/[id]` | TextbookReader | client | textbook:read | 学生教材阅读器(dataAccess: textbooks/data-access.getTextbookById, getChaptersByTextbookId) | -| `/student/schedule` | StudentSchedule | server | class:read | 学生课表(dataAccess: classes/data-access.getStudentSchedule) | +// homework/data-access.ts +getHomeworkAssignments(params: GetHomeworkAssignmentsParams & { scope: DataScope }): Promise<{ items, total }> +getStudentHomeworkAssignments(studentId: string): Promise +getStudentDashboardGrades(studentId: string): Promise +getHomeworkAssignmentAnalytics(assignmentId: string): Promise -### teacher/homework/* 路由 +// classes/data-access.ts +getAdminClasses(scope: DataScope): Promise +getTeacherClasses(teacherId: string): Promise +getStudentClasses(studentId: string): Promise +getClassStudents(classId: string): Promise +getClassHomeworkInsights(classId: string): Promise // ❌ 应属 homework -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/teacher/homework` | HomeworkHome | server | homework:create | 作业管理首页(聚合入口) | -| `/teacher/homework/assignments` | HomeworkAssignmentList | server | homework:create | 作业列表(dataAccess: homework/data-access.getHomeworkAssignments) | -| `/teacher/homework/assignments/create` | HomeworkAssignmentForm | client | homework:create | 创建作业(actions: createHomeworkAssignmentAction) | -| `/teacher/homework/assignments/[id]` | HomeworkAssignmentDetail | server | homework:create | 作业详情(dataAccess: homework/data-access.getHomeworkAssignmentById) | -| `/teacher/homework/assignments/[id]/submissions` | HomeworkSubmissionList | server | homework:grade | 作业提交列表(dataAccess: homework/data-access.getHomeworkSubmissions) | -| `/teacher/homework/submissions` | HomeworkReviewList | server | homework:grade | 批改列表(dataAccess: homework/data-access.getHomeworkAssignmentReviewList) | -| `/teacher/homework/submissions/[submissionId]` | HomeworkGradingView | client | homework:grade | 批改页面(actions: gradeHomeworkSubmissionAction, dataAccess: getHomeworkSubmissionDetails) | +// grades/data-access.ts +getGradeRecords(params: GetGradeRecordsParams & { scope: DataScope }): Promise +getStudentGradeSummary(studentId: string): Promise +getClassRanking(classId: string, examId?: string): Promise -### teacher 其他路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/teacher/exams` | ExamDataTable | server | exam:read | 考试列表(dataAccess: exams/data-access.getExams) | -| `/teacher/exams/[id]/build` | ExamAssemblyPanel | client | exam:update | 组卷页面(components: assembly/*, actions: updateExamAction) | -| `/teacher/exams/[id]/proctoring` | ProctoringDashboard | server+client | exam:proctor | 教师监考面板(dataAccess: proctoring/data-access.getExamForProctoring,getExamProctoringSummary,getStudentProctoringStatuses,getRecentProctoringEvents;权限:requirePermission(EXAM_PROCTOR);组件:proctoring/components/proctoring-dashboard.tsx,10 秒轮询刷新) | -| `/teacher/exams/grading` | ExamGradingList | server | exam:read | 考试批改列表 | -| `/teacher/exams/grading/[submissionId]` | ExamGradingView | client | exam:read | 考试批改页面 | -| `/teacher/classes/my/[id]` | ClassDetail | server | class:read | 班级详情(dataAccess: classes/data-access.getClassDetails) | -| `/teacher/classes` | TeacherClassList | server | class:read | 教师班级列表(dataAccess: classes/data-access.getTeacherClasses) | -| `/teacher/course-plans` | CoursePlanList (teacher) | client | course_plan:read | 教师课程计划列表(按当前用户 teacherId 过滤;dataAccess: course-plans/data-access.getCoursePlans) | -| `/teacher/course-plans/[id]` | CoursePlanDetail (teacher) | client | course_plan:read | 教师课程计划详情(只读,无编辑按钮;dataAccess: course-plans/data-access.getCoursePlanById) | - -### parent/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/parent/dashboard` | ParentDashboard | server | auth_required | 家长仪表盘首页(问候语 + 子女卡片网格;dataAccess: parent/data-access.getParentDashboardData;权限:requireAuth()) | -| `/parent/children/[studentId]` | ChildDetailHeader + ChildDetailPanel | server | auth_required | 子女详情页(头部 + 作业/成绩/课表面板;dataAccess: parent/data-access.getChildDashboardData;权限:requireAuth() + 二次校验 ctx.dataScope.childrenIds 包含 studentId) | -| `/parent/grades` | ParentGradesView | server | grade_record:read | 家长成绩视图(dataAccess: grades/data-access.getStudentGradeSummary,按 DataScope.children 过滤) | - -### messages/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/messages` | MessageList + NotificationList | server | message:read | 消息首页(收件箱/已发送列表 + 通知列表;dataAccess: messaging/data-access.getMessages, getNotifications;权限:requirePermission(MESSAGE_READ)) | -| `/messages/[id]` | MessageDetail | server | message:read | 消息详情(含回复线程;dataAccess: messaging/data-access.getMessageById, getMessageThread;actions: markMessageAsReadAction 自动已读;权限:requirePermission(MESSAGE_READ)) | -| `/messages/compose` | MessageCompose | server | message:send | 写消息页面(支持 reply 模式 via searchParams: receiverId, subject, parentMessageId;dataAccess: messaging/data-access.getRecipients;权限:requirePermission(MESSAGE_SEND)) | - -### attendance/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/teacher/attendance` | AttendanceRecordList + AttendanceFilters | server | attendance:manage | 教师考勤记录列表(dataAccess: attendance/data-access.getAttendanceRecords, classes/data-access.getTeacherClasses;权限:requirePermission(ATTENDANCE_MANAGE)) | -| `/teacher/attendance/sheet` | AttendanceSheet | client | attendance:manage | 批量点名页面(班级/日期选择 + 学生表格;actions: batchRecordAttendanceAction, getClassAttendanceForDateAction, getClassStudentsForAttendance;权限:requirePermission(ATTENDANCE_MANAGE)) | -| `/teacher/attendance/stats` | AttendanceStatsCard | server | attendance:read | 班级考勤统计(dataAccess: attendance/data-access-stats.getClassAttendanceStats, classes/data-access.getTeacherClasses;权限:requirePermission(ATTENDANCE_READ)) | -| `/student/attendance` | StudentAttendanceView | server | attendance:read | 学生考勤视图(统计卡片 + 最近记录;dataAccess: attendance/data-access-stats.getStudentAttendanceSummary;权限:requirePermission(ATTENDANCE_READ),DataScope.class_members 仅查自己) | -| `/parent/attendance` | StudentAttendanceView (per child) | server | attendance:read | 家长考勤视图(遍历子女,每个子女展示 StudentAttendanceView;dataAccess: parent/data-access.getChildren, attendance/data-access-stats.getStudentAttendanceSummary;权限:requirePermission(ATTENDANCE_READ),DataScope.children 仅查子女) | -| `/admin/attendance` | AttendanceRecordList | server | attendance:manage | 管理员考勤总览(dataAccess: attendance/data-access.getAttendanceRecords(scope=all), classes/data-access.getAdminClasses;权限:requirePermission(ATTENDANCE_MANAGE)) | - -### scheduling/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/scheduling/rules` | SchedulingRulesForm | server | schedule:adjust | 排课规则配置页面(dataAccess: scheduling/actions.getAdminClassesForScheduling, getSchedulingRules;actions: saveSchedulingRulesAction;权限:requirePermission(SCHEDULE_ADJUST)) | -| `/admin/scheduling/auto` | AutoSchedulePanel + AutoScheduleResultView | server | schedule:auto | 自动排课页面(dataAccess: scheduling/actions.getAdminClassesForScheduling;actions: autoScheduleAction, applyAutoScheduleAction;权限:requirePermission(SCHEDULE_AUTO)) | -| `/admin/scheduling/changes` | ScheduleChangeList + ScheduleConflictsView | server | schedule:adjust | 调课申请审批+冲突检测页面(dataAccess: scheduling/actions.getAdminClassesForScheduling, getScheduleChanges;actions: approveScheduleChangeAction, rejectScheduleChangeAction, getClassConflictsAction;权限:requirePermission(SCHEDULE_ADJUST);审批操作需 SCHEDULE_AUTO) | -| `/teacher/schedule-changes` | ScheduleChangeForm + ScheduleChangeList | server | schedule:adjust | 教师调课/代课申请页面(dataAccess: scheduling/actions.getAdminClassesForScheduling, getTeachersForScheduling, getScheduleChanges(requesterId=ctx.userId);actions: requestScheduleChangeAction;权限:requirePermission(SCHEDULE_ADJUST);admin 角色查看全部申请) | - -### grades/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/teacher/grades` | 成绩管理首页 | server | grade_record:read | 成绩列表(dataAccess: grades/actions.getGradeRecordsAction) | -| `/teacher/grades/entry` | 批量成绩录入 | server | grade_record:manage | 批量录入(actions: batchCreateGradeRecordsAction, createGradeRecordAction) | -| `/teacher/grades/stats` | 成绩统计报表 | server | grade_record:read | 班级统计+排名(dataAccess: getClassGradeStatsAction, getClassRankingAction) | -| `/teacher/grades/analytics` | 成绩趋势对比分析 | server | grade_record:read | 分析仪表盘(4 个分析图表并行加载;dataAccess: getGradeTrend, getGradeDistribution, getSubjectComparison, getClassComparison;权限:requirePermission(GRADE_RECORD_READ)) | -| `/student/grades` | StudentGradesView | server | grade_record:read | 学生成绩视图(dataAccess: getStudentGradeSummary,DataScope.class_members 仅查自己) | -| `/parent/grades` | ParentGradesView | server | grade_record:read | 家长成绩视图(dataAccess: getStudentGradeSummary,按 DataScope.children 过滤) | - -### settings/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/settings` | 角色分发设置页 | server | auth_required | 根据权限渲染 Admin/Teacher/Student 设置视图(含 General/Appearance/Security/Notifications tab,Security tab 含 PasswordChangeForm,Notifications tab 含 NotificationPreferencesForm;dataAccess: messaging/notification-preferences.getNotificationPreferences) | -| `/settings/security` | SecuritySettingsPage | server | auth_required | 安全设置独立页面(PasswordChangeForm + 安全提示;权限:requireAuth()) | - -### diagnostic/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/teacher/diagnostic` | ReportList | client | diagnostic:read | 学情诊断报告列表(reportType/status 过滤器[URL searchParams];dataAccess: diagnostic/data-access-reports.getDiagnosticReports;actions: publishReportAction, deleteReportAction[DIAGNOSTIC_MANAGE];权限:requirePermission(DIAGNOSTIC_READ);DataScope.class_members 仅查看自己报告) | -| `/teacher/diagnostic/student/[studentId]` | StudentDiagnosticView | client | diagnostic:read | 学生学情诊断视图(概览卡片+雷达图+强项/弱项+生成报告[DIAGNOSTIC_MANAGE]+最新报告;dataAccess: getStudentMasterySummary, getKnowledgePointStats[班级平均对比], getDiagnosticReports;actions: generateStudentReportAction;权限:getAuthContext + DataScope 二次校验,class_members 仅自己,children 仅子女) | -| `/teacher/diagnostic/class/[classId]` | ClassDiagnosticView | client | diagnostic:read | 班级学情诊断视图(概览+知识点热力图+排名表+需重点关注学生+生成班级报告[DIAGNOSTIC_MANAGE];dataAccess: getClassMasterySummary;actions: generateClassReportAction;权限:getAuthContext + DataScope 校验,class_taught 必须包含 classId,class_members/children → notFound) | -| `/student/diagnostic` | StudentDiagnosticView | client | diagnostic:read | 学生本人学情诊断视图(概览+雷达图+强项/弱项+最新报告;dataAccess: getStudentMasterySummary(ctx.userId), getDiagnosticReports(studentId=ctx.userId);权限:requirePermission(DIAGNOSTIC_READ),DataScope.class_members 仅查自己) | - -### elective/* 路由 - -| 路由 | 组件 | 类型 | 权限 | 说明 | -|------|------|------|------|------| -| `/admin/elective` | ElectiveCourseList | server | elective:manage | 管理员选修课程列表(dataAccess: getElectiveCourses(scope=all);actions: deleteElectiveCourseAction, openSelectionAction, closeSelectionAction, runLotteryAction;权限:requirePermission(ELECTIVE_MANAGE)) | -| `/admin/elective/create` | ElectiveCourseForm | client | elective:manage | 创建选修课程(actions: createElectiveCourseAction;dataAccess: getSubjectOptions;权限:requirePermission(ELECTIVE_MANAGE)) | -| `/admin/elective/[id]/edit` | ElectiveCourseForm (edit) | client | elective:manage | 编辑选修课程(actions: updateElectiveCourseAction;dataAccess: getElectiveCourseById, getSubjectOptions;权限:requirePermission(ELECTIVE_MANAGE)) | -| `/teacher/elective` | ElectiveCourseList (teacher) | server | elective:manage | 教师选修课程列表(dataAccess: getElectiveCourses(scope=class_taught/owned, currentUserId);actions: deleteElectiveCourseAction, openSelectionAction, closeSelectionAction, runLotteryAction;权限:requirePermission(ELECTIVE_MANAGE);按 teacherId 过滤) | -| `/student/elective` | StudentSelectionView | server | elective:select | 学生选课页面(dataAccess: getAvailableCoursesForStudent, getStudentSelections;actions: selectCourseAction, dropCourseAction;权限:requirePermission(ELECTIVE_SELECT)) | - -### API 路由(含速率限制) - -| 路由 | 方法 | 限流规则 | 说明 | -|------|------|---------|------| -| `/api/auth/[...nextauth]` | GET, POST | — | NextAuth 认证(登录流程内置 LOGIN 限流: 5次/15分钟) | -| `/api/ai/chat` | POST | AI_CHAT: 20次/分钟 | AI 聊天(按 userId 限流,超限返回 429 + Retry-After 头) | -| `/api/upload` | POST | UPLOAD: 10次/分钟 | 文件上传(按 userId 限流,超限返回 429 + Retry-After 头) | -| `/api/rate-limit-test` | GET | PASSWORD_CHANGE: 5次/分钟 | 限流测试端点(按 userId 限流,用于手动验证 429 响应) | -| `/api/export` | POST | — | Excel 导出 | -| `/api/import` | POST | — | Excel 解析预览 | -| `/api/files/[id]` | GET, DELETE | — | 文件元数据/删除 | -| `/api/files/batch-delete` | POST | — | 批量删除文件(需 FILE_DELETE 权限,先删磁盘文件再删 DB 记录) | -| `/api/search` | GET | — | 全文检索(questions/textbooks/exams/announcements,需登录;参数 q/type/page/pageSize) | -| `/api/onboarding/*` | GET, POST | — | 用户引导 | +// scheduling/auto-scheduler.ts(纯函数,标杆) +findOptimalSlot(input: FindOptimalSlotInput): ScheduleSlot | null +validateSchedule(schedule: ScheduleItem[]): ValidationResult +autoSchedule(input: AutoScheduleInput): AutoScheduleResult +buildDefaultTimeSlots(): TimeSlot[] +``` --- -## DevOps 与脚本 +## 文档维护说明 -### CI 配置 (`.gitea/workflows/ci.yml`) +- **修改源码后**:同步更新本文档对应模块章节 + `005_architecture_data.json` +- **新增模块**:在第二部分添加模块清单 + 更新 1.1 分层架构图 + 更新附录 A 依赖矩阵 +- **新增/删除导出函数**:更新对应模块的"导出函数"清单 + 附录 C +- **修改依赖关系**:更新 1.2 模块依赖关系图 + 附录 A 依赖矩阵 +- **新增路由**:更新 `005_architecture_data.json` 的 `routes` 节点 +- **新增数据库表**:更新 `shared/db/schema.ts` 分节 + `005` 的 `dbTables` 节点 -| Job | 触发条件 | 说明 | -|-----|---------|------| -| `build-deploy` | push/PR to main | 构建、测试、部署到 Docker(自托管 runner CDCD) | -| `security-scan` | push/PR to main(needs: build-deploy) | 完整安全扫描:npm audit + Snyk + Trivy FS + OWASP ZAP 基线扫描,所有步骤 continue-on-error,上传 security-reports artifact(audit-report.json/trivy-fs-report.json/snyk.sarif) | -| `scheduled-backup` | schedule cron `0 2 * * *` | 每天凌晨 2 点执行数据库备份→校验完整性→异地同步,上传 backups/ artifact(保留 30 天) | -| `backup-verify` | schedule(needs: scheduled-backup) | 下载备份 artifact,独立校验备份完整性,运行健康检查,上传 backup-verify-report artifact(保留 7 天) | -| `weekly-dr-drill` | schedule(needs: backup-verify,每周触发) | 灾备演练:从备份恢复到测试数据库,验证数据完整性,上传 dr-drill-report artifact(保留 90 天) | - -### 灾备演练工作流 (`.gitea/workflows/dr-drill.yml`) - -独立灾备演练工作流,触发方式:定时 `cron 0 4 * * 1`(每周一凌晨 4 点)/ 手动 `workflow_dispatch`(可指定 backup_file、no_cleanup)。 - -| 步骤 | 说明 | -|------|------| -| 安装 MySQL 客户端 | apt-get install mysql-client | -| 准备备份 | 下载 db-backup artifact 或现场执行 backup-db.sh | -| 执行演练 | 运行 scripts/dr-drill.sh(创建测试库→恢复→完整性检查→冒烟测试→清理→报告) | -| 上传报告 | dr-drill-report artifact(保留 90 天) | -| 失败通知 | webhook 通知运维团队(DR_NOTIFICATION_WEBHOOK) | - -### 安全扫描工作流 (`.gitea/workflows/security.yml`) - -独立深度安全扫描工作流,触发方式:定时 `cron 0 3 * * 1`(每周一凌晨 3 点)/ 手动 `workflow_dispatch`(可指定 target_url、skip_dast)。 - -| 步骤 | 工具 | 类型 | 输出 | -|------|------|------|------| -| 依赖扫描 | npm audit | 依赖 | audit-report.json | -| 深度依赖 + 静态分析 | Snyk(severity-threshold=medium) | 依赖 + 代码 | snyk.sarif | -| 文件系统扫描 | Trivy fs | 代码 + 依赖 | trivy-fs-report.json | -| 容器镜像扫描 | Trivy image(构建 nextjs-app:scan 镜像) | 容器 | trivy-image-report.json | -| DAST | OWASP ZAP baseline | 动态 | 控制台报告 | -| 汇总报告 | shell + jq | 汇总 | security-summary.md | - -所有报告上传为 artifact `security-reports-full`。安全扫描配置文件:`.gitea/suppressions.json`(Snyk 漏洞抑制)、`.trivyignore`(Trivy CVE 忽略列表)。 - -### 运维脚本 (`scripts/`) - -| 脚本 | 用途 | -|------|------| -| `scripts/audit.sh` | Bash 依赖审计脚本,运行 `npm audit --audit-level=moderate`,失败时生成 audit-report.json | -| `scripts/audit.ps1` | PowerShell 版本依赖审计脚本(Windows 环境) | -| `scripts/security-scan.sh` | Bash 本地安全扫描脚本:npm audit + Trivy fs,彩色报告,退出码 0=无高危/1=有高危 | -| `scripts/security-scan.ps1` | PowerShell 版本本地安全扫描脚本(Windows 环境) | -| `scripts/backup-db.sh` | MySQL 数据库备份脚本,从 DATABASE_URL 解析连接信息,gzip 压缩备份,保留 30 天 | -| `scripts/restore-db.sh` | MySQL 数据库恢复脚本,从指定备份文件恢复 | -| `scripts/test-backup.sh` | 备份流程测试脚本,执行一次备份并验证 | -| `scripts/backup-verify.sh` | 备份完整性校验脚本:检查文件存在/大小/gzip 完整性/SQL 内容结构/SQL 语法(可选,需 DATABASE_URL),退出码 0=通过/1=失败 | -| `scripts/backup-offsite-sync.sh` | 异地备份同步脚本:支持 S3/OSS/NFS 后端,同步后校验文件数量,清理远程过期备份(保留 90 天),使用 aws-cli/rclone/ossutil/rsync | -| `scripts/dr-drill.sh` | 灾备演练脚本(Bash):创建测试库→从备份恢复→数据完整性检查→冒烟测试→清理→生成报告到 docs/dr/reports/,退出码 0=成功/1=失败 | -| `scripts/dr-drill.ps1` | 灾备演练脚本(Windows PowerShell 5.1+):功能同 Bash 版本 | -| `scripts/failover.sh` | 故障切换脚本:检测主库健康→提升备库→更新应用配置→重启应用→验证切换,支持手动/半自动/演练模式 | -| `scripts/health-check.sh` | 健康检查脚本:检查应用 HTTP/数据库连接/磁盘空间/备份新鲜度,输出 JSON 报告,退出码 0=健康/1=异常 | - -### package.json 脚本 - -| 脚本 | 命令 | 说明 | -|------|------|------| -| `audit` | `npm audit --audit-level=moderate` | 依赖安全审计 | -| `audit:report` | `npm audit --json > audit-report.json` | 生成 JSON 审计报告 | -| `security:audit` | `npm audit --audit-level=moderate` | 依赖安全审计(security 别名) | -| `security:scan` | `bash scripts/security-scan.sh` | 本地完整安全扫描(npm audit + Trivy fs) | -| `backup` | `bash scripts/backup-db.sh` | 执行数据库备份 | -| `restore` | `bash scripts/restore-db.sh` | 执行数据库恢复 | -| `dr:backup-verify` | `bash scripts/backup-verify.sh` | 校验备份完整性 | -| `dr:offsite-sync` | `bash scripts/backup-offsite-sync.sh` | 异地备份同步 | -| `dr:drill` | `bash scripts/dr-drill.sh` | 灾备演练(Bash) | -| `dr:drill:ps1` | `powershell -ExecutionPolicy Bypass -File scripts/dr-drill.ps1` | 灾备演练(PowerShell) | -| `dr:health-check` | `bash scripts/health-check.sh` | 健康检查(JSON 报告) | -| `dr:failover` | `bash scripts/failover.sh` | 故障切换 | - -### 灾备文档 (`docs/dr/`) - -| 文档 | 用途 | -|------|------| -| `docs/dr/dr-plan.md` | 灾备计划文档:RTO/RPO 定义(4h/24h)、备份策略、故障切换流程、联系人列表、恢复步骤 | -| `docs/dr/dr-runbook.md` | 灾备操作手册:数据库故障/应用故障/备份失败/异地同步失败/演练失败/磁盘不足场景的诊断与处理 | -| `docs/dr/reports/` | 灾备演练报告存档目录(Markdown 格式,由 dr-drill.sh 生成) | -| `docs/dr/logs/` | 故障切换日志目录(由 failover.sh 生成) | - -### 灾备环境变量 (`.env.example`) - -| 变量 | 用途 | -|------|------| -| `BACKUP_OFFSITE_BACKEND` | 异地备份后端类型: s3/oss/nfs/none | -| `BACKUP_OFFSITE_REMOTE` | 远程存储路径 | -| `BACKUP_OFFSITE_BUCKET` | 存储桶名称(仅 s3/oss) | -| `BACKUP_OFFSITE_ACCESS_KEY` | 访问密钥 | -| `BACKUP_OFFSITE_SECRET_KEY` | 秘密密钥 | -| `BACKUP_OFFSITE_REGION` | 区域(默认 us-east-1) | -| `BACKUP_OFFSITE_RETENTION_DAYS` | 远程备份保留天数(默认 90) | -| `DR_DRILL_TEST_DB` | 演练测试数据库名(默认 next_edu_dr_drill) | -| `HEALTH_CHECK_URL` | 应用健康检查 URL(默认 http://localhost:8015) | -| `DATABASE_URL_STANDBY` | 备库连接 URL(故障切换时使用) | -| `FAILOVER_APP_NAME` | 应用容器名(默认 nextjs-app) | - ---- - -## E2E 测试 (`tests/e2e/`) - -| 测试文件 | 覆盖范围 | 依赖 | -|---------|---------|------| -| `smoke-auth.spec.ts` | 登录/注册页面控件渲染冒烟测试 | 无需 DB | -| `auth-business-flow.spec.ts` | 注册→登录→访问受保护区域完整流程 | DATABASE_URL | -| `full-route-regression.spec.ts` | 全路由清单完整性 + 公开/受保护路由守卫 | 无需 DB(守卫测试) | -| `auth.spec.ts` | 认证页面(登录/注册/隐私/协议)渲染 + 未认证重定向 | 无需 DB | -| `navigation.spec.ts` | admin/teacher/student 导航链接无 404 | DATABASE_URL + 测试账号 | -| `announcements.spec.ts` | 公告页面未认证重定向 + 登录后渲染 | 部分需 DATABASE_URL | -| `grades.spec.ts` | 成绩页面未认证重定向 + 登录后渲染 | 部分需 DATABASE_URL | - -### Playwright 配置 (`playwright.config.ts`) - -- `testDir`: `./tests`(顶层,由各 project 通过 `testDir` 限定范围) -- `baseURL`: `http://127.0.0.1:3000` -- `webServer`: 自动启动 `npm run dev`,端口 3000,超时 180s -- `webServer.env`: 注入 `SKIP_ENV_VALIDATION=1`、`NEXTAUTH_SECRET`、`NEXTAUTH_URL`、`DATABASE_URL`(测试库) -- `projects`: - - `chromium`(E2E 测试,`testDir: ./tests/e2e`,CI 通道为 undefined,本地为 chrome) - - `visual-chromium`(视觉回归测试,`testDir: ./tests/visual`,CI 通道为 undefined,本地为 chrome) -- `snapshotPathTemplate`: `{testDir}/__screenshots__/{testFilePath}/{arg}{ext}` -- `expect.toHaveScreenshot`: `maxDiffPixelRatio: 0.01`、`animations: "disabled"`、`caret: "hide"` -- `retries`: CI 2 次,本地 0 次 -- `workers`: CI 2 个,本地默认 - ---- - -## 视觉回归测试 (`tests/visual/`) - -| 测试文件 | 覆盖范围 | 依赖 | -|---------|---------|------| -| `homepage.spec.ts` | 登录页在 desktop/tablet/mobile × light/dark 下的快照 | 无需 DB | -| `admin-dashboard.spec.ts` | 管理员仪表盘整页 + 侧边栏/主内容区组件快照 | DATABASE_URL + admin 账号 | -| `teacher-dashboard.spec.ts` | 教师仪表盘整页 + 侧边栏/主内容区组件快照 | DATABASE_URL + teacher 账号 | -| `student-dashboard.spec.ts` | 学生仪表盘整页 + 侧边栏/主内容区组件快照 | DATABASE_URL + student 账号 | - -### 视觉测试辅助 (`tests/visual/helpers/`) - -| 文件 | 用途 | -|------|------| -| `auth.ts` | 登录辅助 `setupAuthState(role)`、`loginByUI(page, role)`,测试账号默认 admin@xiaoxue.edu.cn / 123456 | -| `visual-helpers.ts` | `setViewport`、`setTheme`、`waitForPageReady`、`maskDynamicElements`、`buildMaskOption` | - -### 视觉测试配置 (`tests/visual/visual.config.ts`) - -- 视口: desktop 1920×1080、tablet 768×1024、mobile 375×812 -- 主题: light、dark -- 快照目录: `tests/visual/__screenshots__/` -- storageState 目录: `tests/visual/.auth/`(已加入 .gitignore) -- 默认容差: `maxDiffPixelRatio: 0.01` +> 完整路由表、DevOps 脚本、E2E 测试等信息见 `005_architecture_data.json`。 diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index c0cb415..9662f70 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -2,22 +2,64 @@ "_meta": { "project": "Next_Edu", "description": "K12 智慧教务管理系统", - "generatedAt": "2026-06-16", - "formatVersion": "1.0", - "rule": "每次文件修改后须同步更新本文件" + "generatedAt": "2026-06-17", + "formatVersion": "1.1", + "rule": "每次文件修改后须同步更新本文件", + "lastUpdate": "补充架构概览、模块依赖图、已知问题、数据库表清单;补全 EXAM_PROCTOR 权限与 proctoring 依赖矩阵" + }, + "architectureOverview": { + "layers": [ + { + "name": "app", + "description": "路由层,Next.js App Router,按角色分组(admin/teacher/student/parent/management)", + "path": "src/app/" + }, + { + "name": "modules", + "description": "业务模块层,按业务领域划分(25 个模块)", + "path": "src/modules/" + }, + { + "name": "shared", + "description": "基础设施层,提供共享能力(db/lib/hooks/components/types)", + "path": "src/shared/" + } + ], + "dependencyRule": "app → modules → shared (单向依赖,上层可依赖下层,下层不可依赖上层)", + "violations": [ + "shared/lib ↔ auth 循环依赖(audit-logger/change-logger/auth-guard → @/auth → shared/lib)", + "dashboard 直查 11 张跨模块表(sessions/users/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions/usersToRoles/roles)", + "messaging 绕过 notifications dispatcher 直接调用 createNotification", + "classes/data-access.ts 混入 homework/scheduling/grades 跨模块逻辑", + "classSchedule 表存在三处写入口(classes/scheduling-actions/scheduling-data-access)" + ] }, "techStack": { "framework": "Next.js 16 (App Router)", "language": "TypeScript (strict)", "ui": "React 19 + Tailwind CSS v4 + shadcn/ui", - "state": ["Zustand", "nuqs", "React Hook Form"], + "state": [ + "Zustand", + "nuqs", + "React Hook Form" + ], "database": "MySQL + Drizzle ORM 0.45", "auth": "NextAuth v5 (JWT strategy)", "validation": "Zod 4", "ai": "OpenAI SDK (multi-provider)", - "testing": ["Vitest", "Playwright"] + "testing": [ + "Vitest", + "Playwright" + ] }, - "roles": ["admin", "teacher", "student", "parent", "grade_head", "teaching_head"], + "roles": [ + "admin", + "teacher", + "student", + "parent", + "grade_head", + "teaching_head" + ], "permissions": { "EXAM_CREATE": "exam:create", "EXAM_READ": "exam:read", @@ -70,15 +112,201 @@ "DIAGNOSTIC_READ": "diagnostic:read", "ELECTIVE_MANAGE": "elective:manage", "ELECTIVE_READ": "elective:read", - "ELECTIVE_SELECT": "elective:select" + "ELECTIVE_SELECT": "elective:select", + "EXAM_PROCTOR": "exam:proctor", + "EXAM_PROCTOR_READ": "exam:proctor_read" }, "rolePermissions": { - "admin": ["EXAM_CREATE","EXAM_READ","EXAM_UPDATE","EXAM_DELETE","EXAM_DUPLICATE","EXAM_PUBLISH","EXAM_AI_GENERATE","HOMEWORK_CREATE","HOMEWORK_GRADE","QUESTION_CREATE","QUESTION_READ","QUESTION_UPDATE","QUESTION_DELETE","TEXTBOOK_CREATE","TEXTBOOK_READ","TEXTBOOK_UPDATE","TEXTBOOK_DELETE","CLASS_CREATE","CLASS_READ","CLASS_UPDATE","CLASS_DELETE","CLASS_ENROLL","CLASS_SCHEDULE","SCHOOL_MANAGE","GRADE_MANAGE","USER_MANAGE","AI_CHAT","AI_CONFIGURE","SETTINGS_ADMIN","AUDIT_LOG_READ","ANNOUNCEMENT_MANAGE","ANNOUNCEMENT_READ","GRADE_RECORD_MANAGE","GRADE_RECORD_READ","FILE_UPLOAD","FILE_READ","FILE_DELETE","COURSE_PLAN_MANAGE","COURSE_PLAN_READ","ATTENDANCE_MANAGE","ATTENDANCE_READ","MESSAGE_SEND","MESSAGE_READ","MESSAGE_DELETE","SCHEDULE_AUTO","SCHEDULE_ADJUST","DIAGNOSTIC_MANAGE","DIAGNOSTIC_READ","ELECTIVE_MANAGE","ELECTIVE_READ"], - "teacher": ["EXAM_CREATE","EXAM_READ","EXAM_UPDATE","EXAM_DELETE","EXAM_DUPLICATE","EXAM_PUBLISH","EXAM_AI_GENERATE","HOMEWORK_CREATE","HOMEWORK_GRADE","QUESTION_CREATE","QUESTION_READ","QUESTION_UPDATE","QUESTION_DELETE","TEXTBOOK_CREATE","TEXTBOOK_READ","TEXTBOOK_UPDATE","CLASS_READ","CLASS_ENROLL","CLASS_SCHEDULE","AI_CHAT","ANNOUNCEMENT_READ","GRADE_RECORD_MANAGE","GRADE_RECORD_READ","FILE_UPLOAD","FILE_READ","FILE_DELETE","COURSE_PLAN_READ","ATTENDANCE_MANAGE","ATTENDANCE_READ","MESSAGE_SEND","MESSAGE_READ","MESSAGE_DELETE","DIAGNOSTIC_MANAGE","DIAGNOSTIC_READ","ELECTIVE_MANAGE","ELECTIVE_READ"], - "student": ["EXAM_READ","HOMEWORK_SUBMIT","QUESTION_READ","TEXTBOOK_READ","CLASS_READ","AI_CHAT","ANNOUNCEMENT_READ","GRADE_RECORD_READ","FILE_READ","COURSE_PLAN_READ","ATTENDANCE_READ","MESSAGE_READ","MESSAGE_DELETE","DIAGNOSTIC_READ","ELECTIVE_SELECT","ELECTIVE_READ"], - "parent": ["EXAM_READ","TEXTBOOK_READ","CLASS_READ","ANNOUNCEMENT_READ","GRADE_RECORD_READ","FILE_READ","ATTENDANCE_READ","MESSAGE_SEND","MESSAGE_READ","MESSAGE_DELETE"], - "grade_head": ["EXAM_CREATE","EXAM_READ","EXAM_UPDATE","EXAM_DELETE","EXAM_DUPLICATE","EXAM_PUBLISH","EXAM_AI_GENERATE","HOMEWORK_CREATE","HOMEWORK_GRADE","QUESTION_CREATE","QUESTION_READ","QUESTION_UPDATE","QUESTION_DELETE","TEXTBOOK_CREATE","TEXTBOOK_READ","TEXTBOOK_UPDATE","CLASS_CREATE","CLASS_READ","CLASS_UPDATE","CLASS_ENROLL","CLASS_SCHEDULE","GRADE_MANAGE","AI_CHAT","ANNOUNCEMENT_READ","GRADE_RECORD_READ","COURSE_PLAN_READ","ATTENDANCE_READ","MESSAGE_SEND","MESSAGE_READ","MESSAGE_DELETE","DIAGNOSTIC_MANAGE","DIAGNOSTIC_READ","ELECTIVE_READ"], - "teaching_head": ["EXAM_CREATE","EXAM_READ","EXAM_UPDATE","EXAM_DELETE","EXAM_DUPLICATE","EXAM_PUBLISH","EXAM_AI_GENERATE","HOMEWORK_CREATE","HOMEWORK_GRADE","QUESTION_CREATE","QUESTION_READ","QUESTION_UPDATE","QUESTION_DELETE","TEXTBOOK_CREATE","TEXTBOOK_READ","TEXTBOOK_UPDATE","CLASS_READ","GRADE_MANAGE","AI_CHAT","ANNOUNCEMENT_READ","GRADE_RECORD_READ","COURSE_PLAN_READ","ATTENDANCE_READ","MESSAGE_SEND","MESSAGE_READ","MESSAGE_DELETE","DIAGNOSTIC_READ","ELECTIVE_READ"] + "admin": [ + "EXAM_CREATE", + "EXAM_READ", + "EXAM_UPDATE", + "EXAM_DELETE", + "EXAM_DUPLICATE", + "EXAM_PUBLISH", + "EXAM_AI_GENERATE", + "HOMEWORK_CREATE", + "HOMEWORK_GRADE", + "QUESTION_CREATE", + "QUESTION_READ", + "QUESTION_UPDATE", + "QUESTION_DELETE", + "TEXTBOOK_CREATE", + "TEXTBOOK_READ", + "TEXTBOOK_UPDATE", + "TEXTBOOK_DELETE", + "CLASS_CREATE", + "CLASS_READ", + "CLASS_UPDATE", + "CLASS_DELETE", + "CLASS_ENROLL", + "CLASS_SCHEDULE", + "SCHOOL_MANAGE", + "GRADE_MANAGE", + "USER_MANAGE", + "AI_CHAT", + "AI_CONFIGURE", + "SETTINGS_ADMIN", + "AUDIT_LOG_READ", + "ANNOUNCEMENT_MANAGE", + "ANNOUNCEMENT_READ", + "GRADE_RECORD_MANAGE", + "GRADE_RECORD_READ", + "FILE_UPLOAD", + "FILE_READ", + "FILE_DELETE", + "COURSE_PLAN_MANAGE", + "COURSE_PLAN_READ", + "ATTENDANCE_MANAGE", + "ATTENDANCE_READ", + "MESSAGE_SEND", + "MESSAGE_READ", + "MESSAGE_DELETE", + "SCHEDULE_AUTO", + "SCHEDULE_ADJUST", + "DIAGNOSTIC_MANAGE", + "DIAGNOSTIC_READ", + "ELECTIVE_MANAGE", + "ELECTIVE_READ", + "EXAM_PROCTOR", + "EXAM_PROCTOR_READ" + ], + "teacher": [ + "EXAM_CREATE", + "EXAM_READ", + "EXAM_UPDATE", + "EXAM_DELETE", + "EXAM_DUPLICATE", + "EXAM_PUBLISH", + "EXAM_AI_GENERATE", + "HOMEWORK_CREATE", + "HOMEWORK_GRADE", + "QUESTION_CREATE", + "QUESTION_READ", + "QUESTION_UPDATE", + "QUESTION_DELETE", + "TEXTBOOK_CREATE", + "TEXTBOOK_READ", + "TEXTBOOK_UPDATE", + "CLASS_READ", + "CLASS_ENROLL", + "CLASS_SCHEDULE", + "AI_CHAT", + "ANNOUNCEMENT_READ", + "GRADE_RECORD_MANAGE", + "GRADE_RECORD_READ", + "FILE_UPLOAD", + "FILE_READ", + "FILE_DELETE", + "COURSE_PLAN_READ", + "ATTENDANCE_MANAGE", + "ATTENDANCE_READ", + "MESSAGE_SEND", + "MESSAGE_READ", + "MESSAGE_DELETE", + "DIAGNOSTIC_MANAGE", + "DIAGNOSTIC_READ", + "ELECTIVE_MANAGE", + "ELECTIVE_READ", + "EXAM_PROCTOR", + "EXAM_PROCTOR_READ" + ], + "student": [ + "EXAM_READ", + "HOMEWORK_SUBMIT", + "QUESTION_READ", + "TEXTBOOK_READ", + "CLASS_READ", + "AI_CHAT", + "ANNOUNCEMENT_READ", + "GRADE_RECORD_READ", + "FILE_READ", + "COURSE_PLAN_READ", + "ATTENDANCE_READ", + "MESSAGE_READ", + "MESSAGE_DELETE", + "DIAGNOSTIC_READ", + "ELECTIVE_SELECT", + "ELECTIVE_READ" + ], + "parent": [ + "EXAM_READ", + "TEXTBOOK_READ", + "CLASS_READ", + "ANNOUNCEMENT_READ", + "GRADE_RECORD_READ", + "FILE_READ", + "ATTENDANCE_READ", + "MESSAGE_SEND", + "MESSAGE_READ", + "MESSAGE_DELETE" + ], + "grade_head": [ + "EXAM_CREATE", + "EXAM_READ", + "EXAM_UPDATE", + "EXAM_DELETE", + "EXAM_DUPLICATE", + "EXAM_PUBLISH", + "EXAM_AI_GENERATE", + "HOMEWORK_CREATE", + "HOMEWORK_GRADE", + "QUESTION_CREATE", + "QUESTION_READ", + "QUESTION_UPDATE", + "QUESTION_DELETE", + "TEXTBOOK_CREATE", + "TEXTBOOK_READ", + "TEXTBOOK_UPDATE", + "CLASS_CREATE", + "CLASS_READ", + "CLASS_UPDATE", + "CLASS_ENROLL", + "CLASS_SCHEDULE", + "GRADE_MANAGE", + "AI_CHAT", + "ANNOUNCEMENT_READ", + "GRADE_RECORD_READ", + "COURSE_PLAN_READ", + "ATTENDANCE_READ", + "MESSAGE_SEND", + "MESSAGE_READ", + "MESSAGE_DELETE", + "DIAGNOSTIC_MANAGE", + "DIAGNOSTIC_READ", + "ELECTIVE_READ", + "EXAM_PROCTOR_READ" + ], + "teaching_head": [ + "EXAM_CREATE", + "EXAM_READ", + "EXAM_UPDATE", + "EXAM_DELETE", + "EXAM_DUPLICATE", + "EXAM_PUBLISH", + "EXAM_AI_GENERATE", + "HOMEWORK_CREATE", + "HOMEWORK_GRADE", + "QUESTION_CREATE", + "QUESTION_READ", + "QUESTION_UPDATE", + "QUESTION_DELETE", + "TEXTBOOK_CREATE", + "TEXTBOOK_READ", + "TEXTBOOK_UPDATE", + "CLASS_READ", + "GRADE_MANAGE", + "AI_CHAT", + "ANNOUNCEMENT_READ", + "GRADE_RECORD_READ", + "COURSE_PLAN_READ", + "ATTENDANCE_READ", + "MESSAGE_SEND", + "MESSAGE_READ", + "MESSAGE_DELETE", + "DIAGNOSTIC_READ", + "ELECTIVE_READ" + ] }, "dataScopeTypes": { "all": "管理员:无过滤", @@ -99,65 +327,106 @@ "file": "lib/utils.ts", "signature": "cn(...inputs: ClassValue[]): string", "purpose": "合并CSS类名并解决Tailwind冲突", - "deps": ["clsx", "tailwind-merge"], - "usedBy": ["*所有模块组件"] + "deps": [ + "clsx", + "tailwind-merge" + ], + "usedBy": [ + "*所有模块组件" + ] }, { "name": "formatDate", "file": "lib/utils.ts", "signature": "formatDate(date: string | Date, locale?: string): string", - "params": {"date": "日期值", "locale": "Intl locale,默认zh-CN"}, + "params": { + "date": "日期值", + "locale": "Intl locale,默认zh-CN" + }, "purpose": "国际化日期格式化", "deps": [], - "usedBy": ["exams", "homework", "dashboard", "textbooks"] + "usedBy": [ + "exams", + "homework", + "dashboard", + "textbooks" + ] }, { "name": "parseAiChatPayload", "file": "lib/ai.ts", "signature": "parseAiChatPayload(body: unknown): AiChatRequest", "purpose": "解析并校验AI聊天请求负载", - "deps": ["zod"], - "usedBy": ["app/api/ai/chat/route.ts"] + "deps": [ + "zod" + ], + "usedBy": [ + "app/api/ai/chat/route.ts" + ] }, { "name": "encryptAiApiKey", "file": "lib/ai.ts", "signature": "encryptAiApiKey(value: string): string", "purpose": "AES加密AI Provider API Key", - "deps": ["crypto"], - "usedBy": ["settings/actions.ts"] + "deps": [ + "crypto" + ], + "usedBy": [ + "settings/actions.ts" + ] }, { "name": "decryptAiApiKey", "file": "lib/ai.ts", "signature": "decryptAiApiKey(value: string): string", "purpose": "AES解密AI Provider API Key", - "deps": ["crypto"], - "usedBy": ["settings/actions.ts", "ai.ts内部"] + "deps": [ + "crypto" + ], + "usedBy": [ + "settings/actions.ts", + "ai.ts内部" + ] }, { "name": "testAiProviderConfig", "file": "lib/ai.ts", "signature": "testAiProviderConfig(input: { apiKey: string; baseUrl?: string; model: string }): Promise", "purpose": "测试AI Provider连通性(直接配置)", - "deps": ["createAiChatCompletion"], - "usedBy": ["settings/actions.ts"] + "deps": [ + "createAiChatCompletion" + ], + "usedBy": [ + "settings/actions.ts" + ] }, { "name": "testAiProviderById", "file": "lib/ai.ts", "signature": "testAiProviderById(providerId: string, overrides?: { baseUrl?: string; model?: string }): Promise", "purpose": "测试AI Provider连通性(按ID)", - "deps": ["shared/db", "createAiChatCompletion"], - "usedBy": ["settings/actions.ts"] + "deps": [ + "shared/db", + "createAiChatCompletion" + ], + "usedBy": [ + "settings/actions.ts" + ] }, { "name": "createAiChatCompletion", "file": "lib/ai.ts", "signature": "createAiChatCompletion(input: AiChatRequest): Promise<{ content: string; usage: unknown }>", "purpose": "调用AI模型生成聊天回复", - "deps": ["openai", "shared/db"], - "usedBy": ["exams/ai-pipeline.ts", "app/api/ai/chat/route.ts"] + "deps": [ + "openai", + "shared/db" + ], + "usedBy": [ + "exams/ai-pipeline.ts", + "app/api/ai/chat/route.ts" + ] }, { "name": "getAiErrorMessage", @@ -165,7 +434,9 @@ "signature": "getAiErrorMessage(v: unknown): string", "purpose": "从AI错误中提取可读消息", "deps": [], - "usedBy": ["exams/ai-pipeline.ts"] + "usedBy": [ + "exams/ai-pipeline.ts" + ] }, { "name": "getAuthContext", @@ -173,8 +444,13 @@ "signature": "getAuthContext(): Promise", "returns": "AuthContext { userId, roles, permissions, dataScope }", "purpose": "获取当前用户完整认证上下文", - "deps": ["auth (NextAuth)", "shared/db"], - "usedBy": ["所有业务模块的Server Actions"] + "deps": [ + "auth (NextAuth)", + "shared/db" + ], + "usedBy": [ + "所有业务模块的Server Actions" + ] }, { "name": "requirePermission", @@ -182,15 +458,21 @@ "signature": "requirePermission(permission: Permission): Promise", "throws": "PermissionDeniedError", "purpose": "断言当前用户拥有指定权限", - "deps": ["getAuthContext"], - "usedBy": ["所有业务模块的Server Actions"] + "deps": [ + "getAuthContext" + ], + "usedBy": [ + "所有业务模块的Server Actions" + ] }, { "name": "checkPermission", "file": "lib/auth-guard.ts", "signature": "checkPermission(permission: Permission): Promise<{ allowed: boolean; ctx: AuthContext }>", "purpose": "非抛出版权限检查", - "deps": ["getAuthContext"], + "deps": [ + "getAuthContext" + ], "usedBy": [] }, { @@ -198,7 +480,9 @@ "file": "lib/auth-guard.ts", "signature": "requireAuth(): Promise", "purpose": "仅断言用户已登录", - "deps": ["getAuthContext"], + "deps": [ + "getAuthContext" + ], "usedBy": [] }, { @@ -206,40 +490,67 @@ "file": "lib/permissions.ts", "signature": "resolvePermissions(roleNames: string[]): Permission[]", "purpose": "合并多角色的权限列表(去重)", - "deps": ["ROLE_PERMISSIONS"], - "usedBy": ["auth.ts (JWT callback)"] + "deps": [ + "ROLE_PERMISSIONS" + ], + "usedBy": [ + "auth.ts (JWT callback)" + ] }, { "name": "logAudit", "file": "lib/audit-logger.ts", "signature": "logAudit(params: LogAuditParams): Promise", "purpose": "记录操作日志(静默失败)", - "deps": ["auth", "shared.db", "shared.db.schema.auditLogs", "next/headers"], - "usedBy": ["school/actions.ts", "其他Server Actions"] + "deps": [ + "auth", + "shared.db", + "shared.db.schema.auditLogs", + "next/headers" + ], + "usedBy": [ + "school/actions.ts", + "其他Server Actions" + ] }, { "name": "logLoginEvent", "file": "lib/login-logger.ts", "signature": "logLoginEvent(params: LogLoginEventParams): Promise", "purpose": "记录登录日志(signin/signout/signup,静默失败)", - "deps": ["shared.db", "shared.db.schema.loginLogs", "next/headers"], - "usedBy": ["auth.ts (events.signIn, events.signOut)"] + "deps": [ + "shared.db", + "shared.db.schema.loginLogs", + "next/headers" + ], + "usedBy": [ + "auth.ts (events.signIn, events.signOut)" + ] }, { "name": "isAllowedMimeType", "file": "lib/file-storage.ts", "signature": "isAllowedMimeType(mimeType: string): boolean", "purpose": "判断MIME类型是否在允许上传的白名单中", - "deps": ["ALLOWED_MIME_TYPES 常量"], - "usedBy": ["app/api/upload/route.ts", "files/components/file-upload.tsx"] + "deps": [ + "ALLOWED_MIME_TYPES 常量" + ], + "usedBy": [ + "app/api/upload/route.ts", + "files/components/file-upload.tsx" + ] }, { "name": "generateStoragePath", "file": "lib/file-storage.ts", "signature": "generateStoragePath(originalName: string): string", "purpose": "根据原始文件名生成存储路径 uploads/YYYY-MM/cuid.ext(相对于public/)", - "deps": ["@paralleldrive/cuid2"], - "usedBy": ["app/api/upload/route.ts"] + "deps": [ + "@paralleldrive/cuid2" + ], + "usedBy": [ + "app/api/upload/route.ts" + ] }, { "name": "getFileExtension", @@ -247,7 +558,9 @@ "signature": "getFileExtension(filename: string): string", "purpose": "从文件名中提取小写扩展名(不含点)", "deps": [], - "usedBy": ["shared/lib/file-storage 内部"] + "usedBy": [ + "shared/lib/file-storage 内部" + ] }, { "name": "formatFileSize", @@ -255,16 +568,28 @@ "signature": "formatFileSize(bytes: number): string", "purpose": "将字节数格式化为人类可读字符串(如 1.5 MB、800 KB)", "deps": [], - "usedBy": ["files/components/file-upload.tsx", "files/components/file-list.tsx", "files/components/file-preview.tsx"] + "usedBy": [ + "files/components/file-upload.tsx", + "files/components/file-list.tsx", + "files/components/file-preview.tsx" + ] }, { "name": "exportToExcel", "file": "lib/excel.ts", "signature": "exportToExcel(params: { sheets: ExcelSheet[] }): Promise", - "params": {"sheets": "ExcelSheet[],每个含 name/columns/rows"}, + "params": { + "sheets": "ExcelSheet[],每个含 name/columns/rows" + }, "purpose": "将多 sheet 数据导出为 Excel Buffer(表头加粗+冻结首行+自动筛选)", - "deps": ["exceljs"], - "usedBy": ["users/import-export.exportUsersToExcel", "grades/export.exportGradeRecordsToExcel", "grades/export.exportClassGradeReportToExcel"] + "deps": [ + "exceljs" + ], + "usedBy": [ + "users/import-export.exportUsersToExcel", + "grades/export.exportGradeRecordsToExcel", + "grades/export.exportClassGradeReportToExcel" + ] }, { "name": "parseExcel", @@ -272,25 +597,40 @@ "signature": "parseExcel(buffer: Buffer): Promise", "returns": "ParsedSheet[],每个含 sheetName/rows", "purpose": "从 Buffer 解析 Excel 文件,首行作为表头,返回每 sheet 的行记录数组", - "deps": ["exceljs"], - "usedBy": ["users/actions.importUsersAction", "app/api/import/route.ts"] + "deps": [ + "exceljs" + ], + "usedBy": [ + "users/actions.importUsersAction", + "app/api/import/route.ts" + ] }, { "name": "generateTemplate", "file": "lib/excel.ts", "signature": "generateTemplate(params: { sheets: TemplateSheet[] }): Promise", - "params": {"sheets": "TemplateSheet[],每个含 name/columns(含 note)/sampleRows?"}, + "params": { + "sheets": "TemplateSheet[],每个含 name/columns(含 note)/sampleRows?" + }, "purpose": "生成导入模板 Buffer(表头加粗+第二行填写说明+示例行)", - "deps": ["exceljs"], - "usedBy": ["users/import-export.generateUserImportTemplate"] + "deps": [ + "exceljs" + ], + "usedBy": [ + "users/import-export.generateUserImportTemplate" + ] }, { "name": "useA11yId", "file": "lib/a11y.ts", "signature": "useA11yId(prefix: string): string", "purpose": "基于React.useId生成SSR安全的唯一ID,用于aria-describedby、aria-labelledby等", - "deps": ["react"], - "usedBy": ["待扩展(表单组件、a11y组件)"] + "deps": [ + "react" + ], + "usedBy": [ + "待扩展(表单组件、a11y组件)" + ] }, { "name": "mergeA11yProps", @@ -298,7 +638,9 @@ "signature": "mergeA11yProps>(...props: (T | undefined | null | false)[]): T", "purpose": "合并多组aria/data属性,普通属性后者覆盖前者,aria-*/data-*字符串属性以空格拼接", "deps": [], - "usedBy": ["待扩展"] + "usedBy": [ + "待扩展" + ] }, { "name": "describeInput", @@ -306,7 +648,9 @@ "signature": "describeInput(describedBy?: string, error?: string, hint?: string): { ariaDescribedBy?: string; ariaInvalid?: boolean }", "purpose": "计算输入框的aria-describedby(合并多个ID)与aria-invalid(error存在则为true)", "deps": [], - "usedBy": ["待扩展(表单组件)"] + "usedBy": [ + "待扩展(表单组件)" + ] }, { "name": "loadingAria", @@ -314,7 +658,9 @@ "signature": "loadingAria(isLoading: boolean): { ariaBusy: boolean; ariaLive: 'polite' | 'assertive' }", "purpose": "提供加载状态的aria-busy与aria-live=polite属性", "deps": [], - "usedBy": ["待扩展"] + "usedBy": [ + "待扩展" + ] } ], "hooks": [ @@ -347,120 +693,1110 @@ "file": "hooks/use-permission.ts", "signature": "usePermission(): { permissions: Permission[]; roles: string[]; hasPermission: (p: Permission) => boolean; hasAnyPermission: (...p: Permission[]) => boolean; hasAllPermissions: (...p: Permission[]) => boolean; hasRole: (r: string) => boolean }", "purpose": "客户端权限检查Hook", - "usedBy": ["layout/app-sidebar.tsx", "exams/components", "homework/components"] + "usedBy": [ + "layout/app-sidebar.tsx", + "exams/components", + "homework/components" + ] }, { "name": "logDataChange", "file": "lib/change-logger.ts", "signature": "logDataChange(params: LogDataChangeParams): Promise", "purpose": "记录数据变更日志(写入 dataChangeLogs 表,自动获取 changedBy/changedByName/ipAddress;静默失败)", - "deps": ["auth", "shared/db (dataChangeLogs)", "next/headers", "@paralleldrive/cuid2"], - "usedBy": ["待扩展(数据变更场景)"] + "deps": [ + "auth", + "shared/db (dataChangeLogs)", + "next/headers", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "待扩展(数据变更场景)" + ] }, { "name": "StorageProvider", "file": "lib/storage-provider.ts", "signature": "interface StorageProvider { save(file: Buffer, storagePath: string): Promise; read(storagePath: string): Promise; delete(storagePath: string): Promise; exists(storagePath: string): Promise; getUrl(storagePath: string): string }", "purpose": "文件存储抽象接口,便于未来切换到 OSS/S3", - "usedBy": ["app/api/files/batch-delete/route.ts"] + "usedBy": [ + "app/api/files/batch-delete/route.ts" + ] }, { "name": "LocalStorageProvider", "file": "lib/storage-provider.ts", "signature": "class LocalStorageProvider implements StorageProvider", "purpose": "本地磁盘存储实现,文件持久化到 public/uploads/...,URL 为 /uploads/...", - "deps": ["fs/promises", "path"], - "usedBy": ["通过 storageProvider 实例使用"] + "deps": [ + "fs/promises", + "path" + ], + "usedBy": [ + "通过 storageProvider 实例使用" + ] }, { "name": "storageProvider", "file": "lib/storage-provider.ts", "signature": "const storageProvider: StorageProvider", "purpose": "默认存储 Provider 单例(LocalStorageProvider 实例),替换此实例可迁移到 OSS/S3", - "usedBy": ["app/api/files/batch-delete/route.ts"] + "usedBy": [ + "app/api/files/batch-delete/route.ts" + ] } ], "components": [ - {"name": "AuthSessionProvider", "file": "components/auth-session-provider.tsx", "purpose": "NextAuth SessionProvider包装", "usedBy": ["app/layout.tsx"]}, - {"name": "OnboardingGate", "file": "components/onboarding-gate.tsx", "purpose": "新用户引导流程", "usedBy": ["app/layout.tsx"]}, - {"name": "ThemeProvider", "file": "components/theme-provider.tsx", "purpose": "next-themes主题切换", "usedBy": ["app/layout.tsx"]}, - {"name": "EmptyState", "file": "components/ui/empty-state.tsx", "purpose": "列表空状态展示", "usedBy": ["exams", "homework", "questions", "textbooks"]}, - {"name": "GlobalSearch", "file": "components/global-search.tsx", "props": "{ className?, placeholder? }", "purpose": "全局搜索组件(防抖300ms调用 GET /api/search,Cmd/Ctrl+K快捷键聚焦,Escape关闭,↑/↓键盘导航,Enter跳转;下拉展示 question/textbook/exam/announcement 四类结果)", "internalDeps": ["useDebounce", "Input", "Link", "useRouter"], "usedBy": ["layout/components/site-header.tsx"]}, - {"name": "Switch", "file": "components/ui/switch.tsx", "basedOn": "@radix-ui/react-switch", "props": "Radix Switch Root props (checked, onCheckedChange, disabled, id, aria-label)", "purpose": "开关切换UI组件(shadcn风格,checked/unchecked两态)", "usedBy": ["settings/components/notification-preferences-form.tsx"]} + { + "name": "AuthSessionProvider", + "file": "components/auth-session-provider.tsx", + "purpose": "NextAuth SessionProvider包装", + "usedBy": [ + "app/layout.tsx" + ] + }, + { + "name": "OnboardingGate", + "file": "components/onboarding-gate.tsx", + "purpose": "新用户引导流程", + "usedBy": [ + "app/layout.tsx" + ] + }, + { + "name": "ThemeProvider", + "file": "components/theme-provider.tsx", + "purpose": "next-themes主题切换", + "usedBy": [ + "app/layout.tsx" + ] + }, + { + "name": "EmptyState", + "file": "components/ui/empty-state.tsx", + "purpose": "列表空状态展示", + "usedBy": [ + "exams", + "homework", + "questions", + "textbooks" + ] + }, + { + "name": "GlobalSearch", + "file": "components/global-search.tsx", + "props": "{ className?, placeholder? }", + "purpose": "全局搜索组件(防抖300ms调用 GET /api/search,Cmd/Ctrl+K快捷键聚焦,Escape关闭,↑/↓键盘导航,Enter跳转;下拉展示 question/textbook/exam/announcement 四类结果)", + "internalDeps": [ + "useDebounce", + "Input", + "Link", + "useRouter" + ], + "usedBy": [ + "layout/components/site-header.tsx" + ] + }, + { + "name": "Switch", + "file": "components/ui/switch.tsx", + "basedOn": "@radix-ui/react-switch", + "props": "Radix Switch Root props (checked, onCheckedChange, disabled, id, aria-label)", + "purpose": "开关切换UI组件(shadcn风格,checked/unchecked两态)", + "usedBy": [ + "settings/components/notification-preferences-form.tsx" + ] + } ], "constants": [ - {"name": "ROLE_PERMISSIONS", "file": "lib/permissions.ts", "type": "const", "description": "角色-权限映射表", "usedBy": ["resolvePermissions", "auth.ts JWT callback"]}, - {"name": "Permissions", "file": "types/permissions.ts", "type": "const", "description": "38个权限点常量对象(含 FILE_UPLOAD/FILE_READ/FILE_DELETE)", "usedBy": ["auth-guard", "use-permission", "所有actions"]}, - {"name": "db", "file": "db/index.ts", "type": "const", "description": "Drizzle ORM 实例", "usedBy": ["所有业务模块"]}, - {"name": "questionTypeEnum", "file": "db/schema.ts", "type": "const", "description": "题目类型枚举", "usedBy": ["questions", "exams"]}, - {"name": "classEnrollmentStatusEnum", "file": "db/schema.ts", "type": "const", "description": "选课状态枚举", "usedBy": ["classes"]} + { + "name": "ROLE_PERMISSIONS", + "file": "lib/permissions.ts", + "type": "const", + "description": "角色-权限映射表", + "usedBy": [ + "resolvePermissions", + "auth.ts JWT callback" + ] + }, + { + "name": "Permissions", + "file": "types/permissions.ts", + "type": "const", + "description": "38个权限点常量对象(含 FILE_UPLOAD/FILE_READ/FILE_DELETE)", + "usedBy": [ + "auth-guard", + "use-permission", + "所有actions" + ] + }, + { + "name": "db", + "file": "db/index.ts", + "type": "const", + "description": "Drizzle ORM 实例", + "usedBy": [ + "所有业务模块" + ] + }, + { + "name": "questionTypeEnum", + "file": "db/schema.ts", + "type": "const", + "description": "题目类型枚举", + "usedBy": [ + "questions", + "exams" + ] + }, + { + "name": "classEnrollmentStatusEnum", + "file": "db/schema.ts", + "type": "const", + "description": "选课状态枚举", + "usedBy": [ + "classes" + ] + } ], "files": [ - {"path": "db/relations.ts", "description": "20+ 个 Drizzle relations 定义", "usedBy": ["所有业务模块的关联查询"]}, - {"path": "next-auth.d.ts", "description": "NextAuth Session/JWT 类型扩展声明", "usedBy": ["auth.ts", "useSession"]} + { + "path": "db/relations.ts", + "description": "20+ 个 Drizzle relations 定义", + "usedBy": [ + "所有业务模块的关联查询" + ] + }, + { + "path": "next-auth.d.ts", + "description": "NextAuth Session/JWT 类型扩展声明", + "usedBy": [ + "auth.ts", + "useSession" + ] + } ], "types": [ - {"name": "ActionState", "file": "types/action-state.ts", "definition": "ActionState = { success: boolean; message?: string; errors?: Record; data?: T }", "usedBy": ["所有模块Server Action"]}, - {"name": "Permission", "file": "types/permissions.ts", "definition": "Permission = (typeof Permissions)[keyof typeof Permissions]", "usedBy": ["auth-guard", "use-permission", "所有actions"]}, - {"name": "DataScope", "file": "types/permissions.ts", "definition": "DataScope = { type: 'all' } | { type: 'owned'; userId: string } | { type: 'class_taught'; classIds: string[]; subjectIds?: string[] } | { type: 'grade_managed'; gradeIds: string[] } | { type: 'class_members' } | { type: 'children'; childrenIds: string[] }", "usedBy": ["auth-guard", "exams/data-access", "homework/data-access", "dashboard/data-access", "grades/data-access", "parent/children/[studentId]/page.tsx"]}, - {"name": "AuthContext", "file": "types/permissions.ts", "definition": "AuthContext = { userId: string; roles: string[]; permissions: Permission[]; dataScope: DataScope }", "usedBy": ["auth-guard", "所有调用requirePermission的Server Action"]}, - {"name": "PermissionDeniedError", "file": "lib/auth-guard.ts", "definition": "class PermissionDeniedError extends Error { constructor(permission: string) }", "usedBy": ["所有Server Action的try/catch"]} + { + "name": "ActionState", + "file": "types/action-state.ts", + "definition": "ActionState = { success: boolean; message?: string; errors?: Record; data?: T }", + "usedBy": [ + "所有模块Server Action" + ] + }, + { + "name": "Permission", + "file": "types/permissions.ts", + "definition": "Permission = (typeof Permissions)[keyof typeof Permissions]", + "usedBy": [ + "auth-guard", + "use-permission", + "所有actions" + ] + }, + { + "name": "DataScope", + "file": "types/permissions.ts", + "definition": "DataScope = { type: 'all' } | { type: 'owned'; userId: string } | { type: 'class_taught'; classIds: string[]; subjectIds?: string[] } | { type: 'grade_managed'; gradeIds: string[] } | { type: 'class_members' } | { type: 'children'; childrenIds: string[] }", + "usedBy": [ + "auth-guard", + "exams/data-access", + "homework/data-access", + "dashboard/data-access", + "grades/data-access", + "parent/children/[studentId]/page.tsx" + ] + }, + { + "name": "AuthContext", + "file": "types/permissions.ts", + "definition": "AuthContext = { userId: string; roles: string[]; permissions: Permission[]; dataScope: DataScope }", + "usedBy": [ + "auth-guard", + "所有调用requirePermission的Server Action" + ] + }, + { + "name": "PermissionDeniedError", + "file": "lib/auth-guard.ts", + "definition": "class PermissionDeniedError extends Error { constructor(permission: string) }", + "usedBy": [ + "所有Server Action的try/catch" + ] + } ] }, "dbTables": { - "users": {"fields": ["id","name","email","emailVerified","image","password","phone","address","gender","age","birthDate","guardianName","guardianPhone","guardianRelation","consentAcceptedAt","gradeId","departmentId","onboardedAt","createdAt","updatedAt"], "usedBy": ["auth","users","dashboard","classes"]}, - "accounts": {"fields": ["userId","type","provider","providerAccountId","refresh_token","access_token","expires_at","token_type","scope","id_token","session_state"], "usedBy": ["auth"]}, - "sessions": {"fields": ["sessionToken","userId","expires"], "usedBy": ["auth"]}, - "verificationTokens": {"fields": ["identifier","token","expires"], "usedBy": ["auth"]}, - "roles": {"fields": ["id","name","description","createdAt","updatedAt"], "usedBy": ["auth","auth-guard"]}, - "usersToRoles": {"fields": ["userId","roleId"], "usedBy": ["auth","auth-guard"]}, - "rolePermissions": {"fields": ["roleId","permission"], "usedBy": ["auth (seed)"]}, - "knowledgePoints": {"fields": ["id","name","description","anchorText","parentId","chapterId","level","order","createdAt","updatedAt"], "usedBy": ["textbooks","questions"]}, - "questions": {"fields": ["id","content","type","difficulty","authorId","parentId","createdAt","updatedAt"], "usedBy": ["questions","exams","homework"]}, - "questionsToKnowledgePoints": {"fields": ["questionId","knowledgePointId"], "usedBy": ["questions"]}, - "subjects": {"fields": ["id","name","code","order","createdAt","updatedAt"], "usedBy": ["exams","textbooks"]}, - "textbooks": {"fields": ["id","title","subject","grade","publisher","createdAt","updatedAt"], "usedBy": ["textbooks"]}, - "chapters": {"fields": ["id","textbookId","title","order","parentId","content","createdAt","updatedAt"], "usedBy": ["textbooks"]}, - "departments": {"fields": ["id","name","description","createdAt","updatedAt"], "usedBy": ["school"]}, - "classrooms": {"fields": ["id","name","building","floor","capacity","createdAt","updatedAt"], "usedBy": ["school"]}, - "academicYears": {"fields": ["id","name","startDate","endDate","isActive","createdAt","updatedAt"], "usedBy": ["school"]}, - "schools": {"fields": ["id","name","code","createdAt","updatedAt"], "usedBy": ["school","classes"]}, - "grades": {"fields": ["id","schoolId","name","order","gradeHeadId","teachingHeadId","createdAt","updatedAt"], "usedBy": ["school","classes","exams","auth-guard"]}, - "classes": {"fields": ["id","schoolId","gradeId","teacherId","name","homeroom","room","invitationCode","schoolName","grade","createdAt","updatedAt"], "usedBy": ["classes","homework","auth-guard"]}, - "classSubjectTeachers": {"fields": ["classId","teacherId","subjectId","createdAt","updatedAt"], "usedBy": ["classes","auth-guard"]}, - "classEnrollments": {"fields": ["classId","studentId","status","createdAt"], "usedBy": ["classes","homework"]}, - "classSchedule": {"fields": ["id","classId","weekday","startTime","endTime","course","location","createdAt","updatedAt"], "usedBy": ["classes"]}, - "exams": {"fields": ["id","creatorId","title","description","subjectId","gradeId","status","structure","startTime","endTime","createdAt","updatedAt"], "usedBy": ["exams","homework"]}, - "examQuestions": {"fields": ["examId","questionId","score","order"], "usedBy": ["exams"]}, - "examSubmissions": {"fields": ["id","examId","studentId","score","submittedAt","status","createdAt","updatedAt"], "usedBy": ["exams"]}, - "submissionAnswers": {"fields": ["id","submissionId","questionId","answerContent","score","feedback","createdAt","updatedAt"], "usedBy": ["exams"]}, - "homeworkAssignments": {"fields": ["id","creatorId","sourceExamId","title","description","status","availableAt","dueAt","allowLate","lateDueAt","maxAttempts","structure","createdAt","updatedAt"], "usedBy": ["homework"]}, - "homeworkAssignmentQuestions": {"fields": ["assignmentId","questionId","score","order"], "usedBy": ["homework"]}, - "homeworkAssignmentTargets": {"fields": ["assignmentId","studentId","createdAt"], "usedBy": ["homework"]}, - "homeworkSubmissions": {"fields": ["id","assignmentId","studentId","status","attemptNo","score","submittedAt","startedAt","isLate","createdAt","updatedAt"], "usedBy": ["homework"]}, - "homeworkAnswers": {"fields": ["id","submissionId","questionId","answerContent","score","feedback","createdAt","updatedAt"], "usedBy": ["homework"]}, - "aiProviders": {"fields": ["id","provider","baseUrl","model","apiKeyEncrypted","apiKeyLast4","isDefault","createdBy","updatedBy","createdAt","updatedAt"], "usedBy": ["settings","ai"]}, - "auditLogs": {"fields": ["id","userId","userName","action","module","targetId","targetType","detail","ipAddress","userAgent","status","createdAt"], "usedBy": ["audit","shared/lib/audit-logger"]}, - "loginLogs": {"fields": ["id","userId","userEmail","action","status","ipAddress","userAgent","errorMessage","createdAt"], "usedBy": ["audit","shared/lib/login-logger","auth"]}, - "dataChangeLogs": {"fields": ["id","tableName","recordId","action","oldValue","newValue","changedBy","changedByName","ipAddress","createdAt"], "usedBy": ["audit","shared/lib/change-logger"]}, - "announcements": {"fields": ["id","title","content","type","status","targetGradeId","targetClassId","authorId","publishedAt","createdAt","updatedAt"], "usedBy": ["announcements"]}, - "fileAttachments": {"fields": ["id","filename","originalName","mimeType","size","storagePath","url","uploaderId","targetType","targetId","createdAt"], "usedBy": ["files"]}, - "gradeRecords": {"fields": ["id","studentId","classId","subjectId","examId","academicYearId","title","score","fullScore","type","semester","recordedBy","remark","createdAt","updatedAt"], "usedBy": ["grades"]}, - "coursePlans": {"fields": ["id","classId","subjectId","teacherId","academicYearId","semester","totalHours","completedHours","weeklyHours","startDate","endDate","syllabus","objectives","status","createdBy","createdAt","updatedAt"], "usedBy": ["course-plans"]}, - "coursePlanItems": {"fields": ["id","planId","week","topic","content","hours","textbookChapter","notes","isCompleted","completedAt","createdAt","updatedAt"], "usedBy": ["course-plans"]}, - "parentStudentRelations": {"fields": ["id","parentId","studentId","relation","createdAt"], "usedBy": ["parent","auth-guard"]}, - "messages": {"fields": ["id","senderId","receiverId","subject","content","isRead","readAt","parentMessageId","createdAt"], "usedBy": ["messaging"]}, - "messageNotifications": {"fields": ["id","userId","type","title","content","link","isRead","createdAt"], "usedBy": ["messaging"]}, - "notificationPreferences": {"fields": ["id","userId","emailEnabled","smsEnabled","pushEnabled","homeworkNotifications","gradeNotifications","announcementNotifications","messageNotifications","attendanceNotifications","createdAt","updatedAt"], "usedBy": ["messaging","settings"]}, - "attendanceRecords": {"fields": ["id","studentId","classId","scheduleId","date","status","remark","recordedBy","createdAt","updatedAt"], "usedBy": ["attendance"]}, - "attendanceRules": {"fields": ["id","classId","lateThresholdMinutes","earlyLeaveThresholdMinutes","enableAutoMark","createdAt","updatedAt"], "usedBy": ["attendance"]}, - "schedulingRules": {"fields": ["id","classId","maxDailyHours","maxContinuousHours","lunchBreakStart","lunchBreakEnd","morningStart","afternoonEnd","avoidBackToBack","balancedSubjects","createdAt","updatedAt"], "usedBy": ["scheduling"]}, - "scheduleChanges": {"fields": ["id","originalScheduleId","classId","originalTeacherId","substituteTeacherId","originalDate","newDate","newStartTime","newEndTime","reason","status","requestedBy","approvedBy","createdAt","updatedAt"], "usedBy": ["scheduling"]}, - "passwordSecurity": {"fields": ["id","userId","failedLoginAttempts","lockedUntil","passwordChangedAt","mustChangePassword","lastPasswordChange","createdAt","updatedAt"], "usedBy": ["auth","settings"]}, - "knowledgePointMastery": {"fields": ["id","studentId","knowledgePointId","masteryLevel","totalQuestions","correctQuestions","lastAssessedAt","createdAt","updatedAt"], "usedBy": ["diagnostic"], "description": "知识点掌握度记录(复合主键 studentId+knowledgePointId,onDuplicateKeyUpdate upsert)"}, - "learningDiagnosticReports": {"fields": ["id","studentId","generatedBy","reportType","period","summary","strengths","weaknesses","recommendations","overallScore","status","createdAt","updatedAt"], "usedBy": ["diagnostic"], "description": "学情诊断报告(reportType: individual/class/grade;status: draft/published/archived)"}, - "electiveCourses": {"fields": ["id","name","subjectId","teacherId","gradeId","description","capacity","enrolledCount","classroom","schedule","startDate","endDate","selectionStartAt","selectionEndAt","status","selectionMode","credit","createdAt","updatedAt"], "usedBy": ["elective"], "description": "选修课程(status: draft/open/closed/cancelled;selectionMode: fcfs/lottery)"}, - "courseSelections": {"fields": ["id","courseId","studentId","status","priority","selectedAt","enrolledAt","droppedAt","lotteryRank","createdAt","updatedAt"], "usedBy": ["elective"], "description": "选课记录(复合主键 courseId+studentId;status: selected/enrolled/waitlist/dropped/rejected)"} + "users": { + "fields": [ + "id", + "name", + "email", + "emailVerified", + "image", + "password", + "phone", + "address", + "gender", + "age", + "birthDate", + "guardianName", + "guardianPhone", + "guardianRelation", + "consentAcceptedAt", + "gradeId", + "departmentId", + "onboardedAt", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "auth", + "users", + "dashboard", + "classes" + ] + }, + "accounts": { + "fields": [ + "userId", + "type", + "provider", + "providerAccountId", + "refresh_token", + "access_token", + "expires_at", + "token_type", + "scope", + "id_token", + "session_state" + ], + "usedBy": [ + "auth" + ] + }, + "sessions": { + "fields": [ + "sessionToken", + "userId", + "expires" + ], + "usedBy": [ + "auth" + ] + }, + "verificationTokens": { + "fields": [ + "identifier", + "token", + "expires" + ], + "usedBy": [ + "auth" + ] + }, + "roles": { + "fields": [ + "id", + "name", + "description", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "auth", + "auth-guard" + ] + }, + "usersToRoles": { + "fields": [ + "userId", + "roleId" + ], + "usedBy": [ + "auth", + "auth-guard" + ] + }, + "rolePermissions": { + "fields": [ + "roleId", + "permission" + ], + "usedBy": [ + "auth (seed)" + ] + }, + "knowledgePoints": { + "fields": [ + "id", + "name", + "description", + "anchorText", + "parentId", + "chapterId", + "level", + "order", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "textbooks", + "questions" + ] + }, + "questions": { + "fields": [ + "id", + "content", + "type", + "difficulty", + "authorId", + "parentId", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "questions", + "exams", + "homework" + ] + }, + "questionsToKnowledgePoints": { + "fields": [ + "questionId", + "knowledgePointId" + ], + "usedBy": [ + "questions" + ] + }, + "subjects": { + "fields": [ + "id", + "name", + "code", + "order", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "exams", + "textbooks" + ] + }, + "textbooks": { + "fields": [ + "id", + "title", + "subject", + "grade", + "publisher", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "textbooks" + ] + }, + "chapters": { + "fields": [ + "id", + "textbookId", + "title", + "order", + "parentId", + "content", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "textbooks" + ] + }, + "departments": { + "fields": [ + "id", + "name", + "description", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "school" + ] + }, + "classrooms": { + "fields": [ + "id", + "name", + "building", + "floor", + "capacity", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "school" + ] + }, + "academicYears": { + "fields": [ + "id", + "name", + "startDate", + "endDate", + "isActive", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "school" + ] + }, + "schools": { + "fields": [ + "id", + "name", + "code", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "school", + "classes" + ] + }, + "grades": { + "fields": [ + "id", + "schoolId", + "name", + "order", + "gradeHeadId", + "teachingHeadId", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "school", + "classes", + "exams", + "auth-guard" + ] + }, + "classes": { + "fields": [ + "id", + "schoolId", + "gradeId", + "teacherId", + "name", + "homeroom", + "room", + "invitationCode", + "schoolName", + "grade", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "classes", + "homework", + "auth-guard" + ] + }, + "classSubjectTeachers": { + "fields": [ + "classId", + "teacherId", + "subjectId", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "classes", + "auth-guard" + ] + }, + "classEnrollments": { + "fields": [ + "classId", + "studentId", + "status", + "createdAt" + ], + "usedBy": [ + "classes", + "homework" + ] + }, + "classSchedule": { + "fields": [ + "id", + "classId", + "weekday", + "startTime", + "endTime", + "course", + "location", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "classes" + ] + }, + "exams": { + "fields": [ + "id", + "creatorId", + "title", + "description", + "subjectId", + "gradeId", + "status", + "structure", + "startTime", + "endTime", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "exams", + "homework" + ] + }, + "examQuestions": { + "fields": [ + "examId", + "questionId", + "score", + "order" + ], + "usedBy": [ + "exams" + ] + }, + "examSubmissions": { + "fields": [ + "id", + "examId", + "studentId", + "score", + "submittedAt", + "status", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "exams" + ] + }, + "submissionAnswers": { + "fields": [ + "id", + "submissionId", + "questionId", + "answerContent", + "score", + "feedback", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "exams" + ] + }, + "homeworkAssignments": { + "fields": [ + "id", + "creatorId", + "sourceExamId", + "title", + "description", + "status", + "availableAt", + "dueAt", + "allowLate", + "lateDueAt", + "maxAttempts", + "structure", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "homework" + ] + }, + "homeworkAssignmentQuestions": { + "fields": [ + "assignmentId", + "questionId", + "score", + "order" + ], + "usedBy": [ + "homework" + ] + }, + "homeworkAssignmentTargets": { + "fields": [ + "assignmentId", + "studentId", + "createdAt" + ], + "usedBy": [ + "homework" + ] + }, + "homeworkSubmissions": { + "fields": [ + "id", + "assignmentId", + "studentId", + "status", + "attemptNo", + "score", + "submittedAt", + "startedAt", + "isLate", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "homework" + ] + }, + "homeworkAnswers": { + "fields": [ + "id", + "submissionId", + "questionId", + "answerContent", + "score", + "feedback", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "homework" + ] + }, + "aiProviders": { + "fields": [ + "id", + "provider", + "baseUrl", + "model", + "apiKeyEncrypted", + "apiKeyLast4", + "isDefault", + "createdBy", + "updatedBy", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "settings", + "ai" + ] + }, + "auditLogs": { + "fields": [ + "id", + "userId", + "userName", + "action", + "module", + "targetId", + "targetType", + "detail", + "ipAddress", + "userAgent", + "status", + "createdAt" + ], + "usedBy": [ + "audit", + "shared/lib/audit-logger" + ] + }, + "loginLogs": { + "fields": [ + "id", + "userId", + "userEmail", + "action", + "status", + "ipAddress", + "userAgent", + "errorMessage", + "createdAt" + ], + "usedBy": [ + "audit", + "shared/lib/login-logger", + "auth" + ] + }, + "dataChangeLogs": { + "fields": [ + "id", + "tableName", + "recordId", + "action", + "oldValue", + "newValue", + "changedBy", + "changedByName", + "ipAddress", + "createdAt" + ], + "usedBy": [ + "audit", + "shared/lib/change-logger" + ] + }, + "announcements": { + "fields": [ + "id", + "title", + "content", + "type", + "status", + "targetGradeId", + "targetClassId", + "authorId", + "publishedAt", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "announcements" + ] + }, + "fileAttachments": { + "fields": [ + "id", + "filename", + "originalName", + "mimeType", + "size", + "storagePath", + "url", + "uploaderId", + "targetType", + "targetId", + "createdAt" + ], + "usedBy": [ + "files" + ] + }, + "gradeRecords": { + "fields": [ + "id", + "studentId", + "classId", + "subjectId", + "examId", + "academicYearId", + "title", + "score", + "fullScore", + "type", + "semester", + "recordedBy", + "remark", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "grades" + ] + }, + "coursePlans": { + "fields": [ + "id", + "classId", + "subjectId", + "teacherId", + "academicYearId", + "semester", + "totalHours", + "completedHours", + "weeklyHours", + "startDate", + "endDate", + "syllabus", + "objectives", + "status", + "createdBy", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "course-plans" + ] + }, + "coursePlanItems": { + "fields": [ + "id", + "planId", + "week", + "topic", + "content", + "hours", + "textbookChapter", + "notes", + "isCompleted", + "completedAt", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "course-plans" + ] + }, + "parentStudentRelations": { + "fields": [ + "id", + "parentId", + "studentId", + "relation", + "createdAt" + ], + "usedBy": [ + "parent", + "auth-guard" + ] + }, + "messages": { + "fields": [ + "id", + "senderId", + "receiverId", + "subject", + "content", + "isRead", + "readAt", + "parentMessageId", + "createdAt" + ], + "usedBy": [ + "messaging" + ] + }, + "messageNotifications": { + "fields": [ + "id", + "userId", + "type", + "title", + "content", + "link", + "isRead", + "createdAt" + ], + "usedBy": [ + "messaging" + ] + }, + "notificationPreferences": { + "fields": [ + "id", + "userId", + "emailEnabled", + "smsEnabled", + "pushEnabled", + "homeworkNotifications", + "gradeNotifications", + "announcementNotifications", + "messageNotifications", + "attendanceNotifications", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "messaging", + "settings" + ] + }, + "attendanceRecords": { + "fields": [ + "id", + "studentId", + "classId", + "scheduleId", + "date", + "status", + "remark", + "recordedBy", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "attendance" + ] + }, + "attendanceRules": { + "fields": [ + "id", + "classId", + "lateThresholdMinutes", + "earlyLeaveThresholdMinutes", + "enableAutoMark", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "attendance" + ] + }, + "schedulingRules": { + "fields": [ + "id", + "classId", + "maxDailyHours", + "maxContinuousHours", + "lunchBreakStart", + "lunchBreakEnd", + "morningStart", + "afternoonEnd", + "avoidBackToBack", + "balancedSubjects", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "scheduling" + ] + }, + "scheduleChanges": { + "fields": [ + "id", + "originalScheduleId", + "classId", + "originalTeacherId", + "substituteTeacherId", + "originalDate", + "newDate", + "newStartTime", + "newEndTime", + "reason", + "status", + "requestedBy", + "approvedBy", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "scheduling" + ] + }, + "passwordSecurity": { + "fields": [ + "id", + "userId", + "failedLoginAttempts", + "lockedUntil", + "passwordChangedAt", + "mustChangePassword", + "lastPasswordChange", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "auth", + "settings" + ] + }, + "knowledgePointMastery": { + "fields": [ + "id", + "studentId", + "knowledgePointId", + "masteryLevel", + "totalQuestions", + "correctQuestions", + "lastAssessedAt", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "diagnostic" + ], + "description": "知识点掌握度记录(复合主键 studentId+knowledgePointId,onDuplicateKeyUpdate upsert)" + }, + "learningDiagnosticReports": { + "fields": [ + "id", + "studentId", + "generatedBy", + "reportType", + "period", + "summary", + "strengths", + "weaknesses", + "recommendations", + "overallScore", + "status", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "diagnostic" + ], + "description": "学情诊断报告(reportType: individual/class/grade;status: draft/published/archived)" + }, + "electiveCourses": { + "fields": [ + "id", + "name", + "subjectId", + "teacherId", + "gradeId", + "description", + "capacity", + "enrolledCount", + "classroom", + "schedule", + "startDate", + "endDate", + "selectionStartAt", + "selectionEndAt", + "status", + "selectionMode", + "credit", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "elective" + ], + "description": "选修课程(status: draft/open/closed/cancelled;selectionMode: fcfs/lottery)" + }, + "courseSelections": { + "fields": [ + "id", + "courseId", + "studentId", + "status", + "priority", + "selectedAt", + "enrolledAt", + "droppedAt", + "lotteryRank", + "createdAt", + "updatedAt" + ], + "usedBy": [ + "elective" + ], + "description": "选课记录(复合主键 courseId+studentId;status: selected/enrolled/waitlist/dropped/rejected)" + } } }, "auth": { @@ -468,14 +1804,60 @@ "description": "用户认证:NextAuth配置、JWT/Session callbacks、events回调(登录日志)、middleware。集成密码安全策略(账户锁定、失败登录追踪)和登录速率限制", "exports": { "functions": [ - {"name": "auth", "signature": "auth(): Promise", "purpose": "获取当前用户Session", "usedBy": ["auth-guard.ts", "所有Server Component页面", "audit-logger.ts"]}, - {"name": "handlers", "signature": "{ GET, POST }", "purpose": "NextAuth Route Handler", "usedBy": ["app/api/auth/[...nextauth]/route.ts"]}, - {"name": "signIn", "signature": "signIn(provider, options?)", "purpose": "登录", "usedBy": ["login-form.tsx"]}, - {"name": "signOut", "signature": "signOut(options?)", "purpose": "登出", "usedBy": ["site-header.tsx"]} + { + "name": "auth", + "signature": "auth(): Promise", + "purpose": "获取当前用户Session", + "usedBy": [ + "auth-guard.ts", + "所有Server Component页面", + "audit-logger.ts" + ] + }, + { + "name": "handlers", + "signature": "{ GET, POST }", + "purpose": "NextAuth Route Handler", + "usedBy": [ + "app/api/auth/[...nextauth]/route.ts" + ] + }, + { + "name": "signIn", + "signature": "signIn(provider, options?)", + "purpose": "登录", + "usedBy": [ + "login-form.tsx" + ] + }, + { + "name": "signOut", + "signature": "signOut(options?)", + "purpose": "登出", + "usedBy": [ + "site-header.tsx" + ] + } ], "events": [ - {"name": "signIn", "signature": "async signIn({ user }) => void", "purpose": "用户登录成功后记录登录日志", "deps": ["shared/lib/login-logger.logLoginEvent"], "params": "{ userId: user.id, userEmail: user.email, action: 'signin', status: 'success' }"}, - {"name": "signOut", "signature": "async signOut(message) => void", "purpose": "用户登出后记录登录日志(处理NextAuth v5不同message形状)", "deps": ["shared/lib/login-logger.logLoginEvent"], "params": "{ userId?, userEmail, action: 'signout', status: 'success' }"} + { + "name": "signIn", + "signature": "async signIn({ user }) => void", + "purpose": "用户登录成功后记录登录日志", + "deps": [ + "shared/lib/login-logger.logLoginEvent" + ], + "params": "{ userId: user.id, userEmail: user.email, action: 'signin', status: 'success' }" + }, + { + "name": "signOut", + "signature": "async signOut(message) => void", + "purpose": "用户登出后记录登录日志(处理NextAuth v5不同message形状)", + "deps": [ + "shared/lib/login-logger.logLoginEvent" + ], + "params": "{ userId?, userEmail, action: 'signout', status: 'success' }" + } ] }, "middleware": { @@ -499,89 +1881,584 @@ "description": "考试全生命周期:创建(手动/AI)、编辑、预览、发布、删除、复制", "exports": { "actions": [ - {"name": "createExamAction", "permission": "EXAM_CREATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "手动模式创建考试草稿", "deps": ["requirePermission","shared/db","data-access.persistExamDraft"], "usedBy": ["exam-form.tsx"]}, - {"name": "createAiExamAction", "permission": "EXAM_AI_GENERATE", "signature": "同上", "purpose": "AI模式创建考试", "deps": ["requirePermission","ai-pipeline.generateAiCreateDraftFromSource","data-access.persistAiGeneratedExamDraft"], "usedBy": ["exam-form.tsx"]}, - {"name": "previewAiExamAction", "permission": "EXAM_AI_GENERATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "AI预览试卷(不持久化)", "deps": ["requirePermission","ai-pipeline.generateAiPreviewData"], "usedBy": ["exam-ai-generator.tsx"]}, - {"name": "regenerateAiQuestionAction", "permission": "EXAM_AI_GENERATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "AI重写单个题目", "deps": ["requirePermission","ai-pipeline.regenerateAiQuestionByInstruction"], "usedBy": ["exam-preview-question-editor.tsx"]}, - {"name": "updateExamAction", "permission": "EXAM_UPDATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新考试(含资源归属校验)", "deps": ["requirePermission","shared/db"], "usedBy": ["exam-form.tsx"]}, - {"name": "deleteExamAction", "permission": "EXAM_DELETE", "signature": "同上", "purpose": "删除考试(含资源归属校验)", "deps": ["requirePermission","shared/db"], "usedBy": ["exam-actions.tsx"]}, - {"name": "duplicateExamAction", "permission": "EXAM_DUPLICATE", "signature": "同上", "purpose": "复制考试", "deps": ["requirePermission","shared/db"], "usedBy": ["exam-actions.tsx"]}, - {"name": "getExamPreviewAction", "permission": "EXAM_READ", "signature": "(examId: string) => Promise }>>", "purpose": "获取考试预览数据", "deps": ["requirePermission","shared/db"], "usedBy": ["exam-viewer.tsx"]}, - {"name": "getSubjectsAction", "permission": "EXAM_READ", "signature": "() => Promise>", "purpose": "获取科目列表", "deps": ["requirePermission","shared/db"], "usedBy": ["exam-form.tsx"]}, - {"name": "getGradesAction", "permission": "EXAM_READ", "signature": "同上", "purpose": "获取年级列表", "deps": ["requirePermission","shared/db"], "usedBy": ["exam-form.tsx"]} + { + "name": "createExamAction", + "permission": "EXAM_CREATE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "手动模式创建考试草稿", + "deps": [ + "requirePermission", + "shared/db", + "data-access.persistExamDraft" + ], + "usedBy": [ + "exam-form.tsx" + ] + }, + { + "name": "createAiExamAction", + "permission": "EXAM_AI_GENERATE", + "signature": "同上", + "purpose": "AI模式创建考试", + "deps": [ + "requirePermission", + "ai-pipeline.generateAiCreateDraftFromSource", + "data-access.persistAiGeneratedExamDraft" + ], + "usedBy": [ + "exam-form.tsx" + ] + }, + { + "name": "previewAiExamAction", + "permission": "EXAM_AI_GENERATE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "AI预览试卷(不持久化)", + "deps": [ + "requirePermission", + "ai-pipeline.generateAiPreviewData" + ], + "usedBy": [ + "exam-ai-generator.tsx" + ] + }, + { + "name": "regenerateAiQuestionAction", + "permission": "EXAM_AI_GENERATE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "AI重写单个题目", + "deps": [ + "requirePermission", + "ai-pipeline.regenerateAiQuestionByInstruction" + ], + "usedBy": [ + "exam-preview-question-editor.tsx" + ] + }, + { + "name": "updateExamAction", + "permission": "EXAM_UPDATE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "更新考试(含资源归属校验)", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "exam-form.tsx" + ] + }, + { + "name": "deleteExamAction", + "permission": "EXAM_DELETE", + "signature": "同上", + "purpose": "删除考试(含资源归属校验)", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "exam-actions.tsx" + ] + }, + { + "name": "duplicateExamAction", + "permission": "EXAM_DUPLICATE", + "signature": "同上", + "purpose": "复制考试", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "exam-actions.tsx" + ] + }, + { + "name": "getExamPreviewAction", + "permission": "EXAM_READ", + "signature": "(examId: string) => Promise }>>", + "purpose": "获取考试预览数据", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "exam-viewer.tsx" + ] + }, + { + "name": "getSubjectsAction", + "permission": "EXAM_READ", + "signature": "() => Promise>", + "purpose": "获取科目列表", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "exam-form.tsx" + ] + }, + { + "name": "getGradesAction", + "permission": "EXAM_READ", + "signature": "同上", + "purpose": "获取年级列表", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "exam-form.tsx" + ] + } ], "dataAccess": [ - {"name": "getExams", "signature": "getExams(params: GetExamsParams & { scope: DataScope }): Promise", "purpose": "查询考试列表(含数据权限过滤)", "usedBy": ["teacher/exams/all/page.tsx", "homework创建页面"]}, - {"name": "getExamById", "signature": "getExamById(id: string, scope?: DataScope): Promise", "purpose": "按ID获取考试详情", "usedBy": ["exam详情/编辑页面"]}, - {"name": "persistExamDraft", "signature": "persistExamDraft(input: { examId, title, creatorId, subjectId, gradeId, scheduledAt?, description }): Promise", "purpose": "持久化手动考试草稿", "usedBy": ["createExamAction"]}, - {"name": "persistAiGeneratedExamDraft", "signature": "persistAiGeneratedExamDraft(input: { examId, title, creatorId, subjectId, gradeId, scheduledAt?, description, structure, generated }): Promise", "purpose": "持久化AI生成考试草稿", "usedBy": ["createAiExamAction"]}, - {"name": "omitScheduledAtFromDescription", "signature": "(description: string | null) => string", "purpose": "从描述中移除scheduledAt信息", "usedBy": ["exams/data-access内部"]}, - {"name": "resolveSubjectGradeNames", "signature": "(input: { subjectId?, gradeId? }) => Promise<{ subjectName?, gradeName? }>", "purpose": "解析科目与年级名称", "usedBy": ["exams/data-access内部"]}, - {"name": "buildExamDescription", "signature": "(input: { subject, grade, difficulty, totalScore, durationMin, scheduledAt?, questionCount? }) => string", "purpose": "构建考试描述文本", "usedBy": ["exams/data-access内部"]}, - {"name": "GetExamsParams", "type": "type", "definition": "{ q?, status?, difficulty?, page?, pageSize? }", "usedBy": ["getExams", "getExamsAction"]} + { + "name": "getExams", + "signature": "getExams(params: GetExamsParams & { scope: DataScope }): Promise", + "purpose": "查询考试列表(含数据权限过滤)", + "usedBy": [ + "teacher/exams/all/page.tsx", + "homework创建页面" + ] + }, + { + "name": "getExamById", + "signature": "getExamById(id: string, scope?: DataScope): Promise", + "purpose": "按ID获取考试详情", + "usedBy": [ + "exam详情/编辑页面" + ] + }, + { + "name": "persistExamDraft", + "signature": "persistExamDraft(input: { examId, title, creatorId, subjectId, gradeId, scheduledAt?, description }): Promise", + "purpose": "持久化手动考试草稿", + "usedBy": [ + "createExamAction" + ] + }, + { + "name": "persistAiGeneratedExamDraft", + "signature": "persistAiGeneratedExamDraft(input: { examId, title, creatorId, subjectId, gradeId, scheduledAt?, description, structure, generated }): Promise", + "purpose": "持久化AI生成考试草稿", + "usedBy": [ + "createAiExamAction" + ] + }, + { + "name": "omitScheduledAtFromDescription", + "signature": "(description: string | null) => string", + "purpose": "从描述中移除scheduledAt信息", + "usedBy": [ + "exams/data-access内部" + ] + }, + { + "name": "resolveSubjectGradeNames", + "signature": "(input: { subjectId?, gradeId? }) => Promise<{ subjectName?, gradeName? }>", + "purpose": "解析科目与年级名称", + "usedBy": [ + "exams/data-access内部" + ] + }, + { + "name": "buildExamDescription", + "signature": "(input: { subject, grade, difficulty, totalScore, durationMin, scheduledAt?, questionCount? }) => string", + "purpose": "构建考试描述文本", + "usedBy": [ + "exams/data-access内部" + ] + }, + { + "name": "GetExamsParams", + "type": "type", + "definition": "{ q?, status?, difficulty?, page?, pageSize? }", + "usedBy": [ + "getExams", + "getExamsAction" + ] + } ], "aiPipeline": [ - {"name": "generateAiPreviewData", "signature": "(input: { title, subject?, grade?, difficulty, totalScore, durationMin, questionCount?, sourceText, aiProviderId? }) => Promise<{ ok, data?, rawOutput?, message? }>", "purpose": "AI预览生成", "deps": ["shared/lib/ai.createAiChatCompletion"], "usedBy": ["previewAiExamAction"]}, - {"name": "generateAiCreateDraftFromSource", "signature": "同上", "purpose": "AI从源文本生成完整考试", "deps": ["shared/lib/ai.createAiChatCompletion"], "usedBy": ["createAiExamAction"]}, - {"name": "regenerateAiQuestionByInstruction", "signature": "(input: { instruction, originalQuestion, sourceText?, aiProviderId? }) => Promise<{ ok, data?, message? }>", "purpose": "AI按指令重写题目", "deps": ["shared/lib/ai.createAiChatCompletion"], "usedBy": ["regenerateAiQuestionAction"]}, - {"name": "AiQuestionSchema", "type": "const", "description": "zod schema 校验AI生成题目", "usedBy": ["ai-pipeline内部"]}, - {"name": "AiInsertQuestionSchema", "type": "const", "description": "zod schema 校验AI插入题目", "usedBy": ["ai-pipeline内部"]}, - {"name": "AiGeneratedQuestion", "type": "type", "definition": "{ id, type, difficulty, score, content }", "usedBy": ["ai-pipeline内部", "exams/components"]}, - {"name": "AiGeneratedStructureNode", "type": "type", "definition": "{ id, type: \"group\"|\"question\", title?, questionId?, score?, children? }", "usedBy": ["ai-pipeline内部", "exams/components"]}, - {"name": "AiGeneratedStructureNodeSchema", "type": "const", "description": "zod schema 校验AI生成结构节点(递归)", "usedBy": ["ai-pipeline内部"]}, - {"name": "AiGeneratedStructureSchema", "type": "const", "description": "zod schema 校验AI生成结构", "usedBy": ["ai-pipeline内部"]}, - {"name": "generateAiExamDraft", "type": "function", "signature": "(input) => Promise", "purpose": "生成AI考试草稿", "deps": ["shared/lib/ai.createAiChatCompletion"], "usedBy": ["ai-pipeline内部"]} + { + "name": "generateAiPreviewData", + "signature": "(input: { title, subject?, grade?, difficulty, totalScore, durationMin, questionCount?, sourceText, aiProviderId? }) => Promise<{ ok, data?, rawOutput?, message? }>", + "purpose": "AI预览生成", + "deps": [ + "shared/lib/ai.createAiChatCompletion" + ], + "usedBy": [ + "previewAiExamAction" + ] + }, + { + "name": "generateAiCreateDraftFromSource", + "signature": "同上", + "purpose": "AI从源文本生成完整考试", + "deps": [ + "shared/lib/ai.createAiChatCompletion" + ], + "usedBy": [ + "createAiExamAction" + ] + }, + { + "name": "regenerateAiQuestionByInstruction", + "signature": "(input: { instruction, originalQuestion, sourceText?, aiProviderId? }) => Promise<{ ok, data?, message? }>", + "purpose": "AI按指令重写题目", + "deps": [ + "shared/lib/ai.createAiChatCompletion" + ], + "usedBy": [ + "regenerateAiQuestionAction" + ] + }, + { + "name": "AiQuestionSchema", + "type": "const", + "description": "zod schema 校验AI生成题目", + "usedBy": [ + "ai-pipeline内部" + ] + }, + { + "name": "AiInsertQuestionSchema", + "type": "const", + "description": "zod schema 校验AI插入题目", + "usedBy": [ + "ai-pipeline内部" + ] + }, + { + "name": "AiGeneratedQuestion", + "type": "type", + "definition": "{ id, type, difficulty, score, content }", + "usedBy": [ + "ai-pipeline内部", + "exams/components" + ] + }, + { + "name": "AiGeneratedStructureNode", + "type": "type", + "definition": "{ id, type: \"group\"|\"question\", title?, questionId?, score?, children? }", + "usedBy": [ + "ai-pipeline内部", + "exams/components" + ] + }, + { + "name": "AiGeneratedStructureNodeSchema", + "type": "const", + "description": "zod schema 校验AI生成结构节点(递归)", + "usedBy": [ + "ai-pipeline内部" + ] + }, + { + "name": "AiGeneratedStructureSchema", + "type": "const", + "description": "zod schema 校验AI生成结构", + "usedBy": [ + "ai-pipeline内部" + ] + }, + { + "name": "generateAiExamDraft", + "type": "function", + "signature": "(input) => Promise", + "purpose": "生成AI考试草稿", + "deps": [ + "shared/lib/ai.createAiChatCompletion" + ], + "usedBy": [ + "ai-pipeline内部" + ] + } ], "types": [ - {"name": "Exam", "definition": "{ id, title, subject, grade, status, difficulty, totalScore, durationMin, questionCount, scheduledAt?, createdAt, updatedAt?, tags? }", "usedBy": ["exams/components", "homework/types", "dashboard/types"]}, - {"name": "AiPreviewData", "definition": "{ title, rawOutput?, sections?, questions? }", "usedBy": ["exams/actions", "exams/components"]}, - {"name": "AiRewriteQuestionData", "definition": "{ type, difficulty, score, content }", "usedBy": ["exams/actions", "exams/components"]}, - {"name": "ExamStatus", "type": "type", "definition": "\"draft\" | \"published\" | \"archived\"", "usedBy": ["exams/components", "exams/data-access"]}, - {"name": "ExamDifficulty", "type": "type", "definition": "1 | 2 | 3 | 4 | 5", "usedBy": ["exams/components", "exams/data-access"]}, - {"name": "SubmissionStatus", "type": "type", "definition": "\"pending\" | \"graded\"", "usedBy": ["exams/components", "exams/data-access"]}, - {"name": "ExamSubmission", "type": "interface", "definition": "{ id, examId, examTitle, studentName, submittedAt, score?, status }", "usedBy": ["exams/components"]} + { + "name": "Exam", + "definition": "{ id, title, subject, grade, status, difficulty, totalScore, durationMin, questionCount, scheduledAt?, createdAt, updatedAt?, tags? }", + "usedBy": [ + "exams/components", + "homework/types", + "dashboard/types" + ] + }, + { + "name": "AiPreviewData", + "definition": "{ title, rawOutput?, sections?, questions? }", + "usedBy": [ + "exams/actions", + "exams/components" + ] + }, + { + "name": "AiRewriteQuestionData", + "definition": "{ type, difficulty, score, content }", + "usedBy": [ + "exams/actions", + "exams/components" + ] + }, + { + "name": "ExamStatus", + "type": "type", + "definition": "\"draft\" | \"published\" | \"archived\"", + "usedBy": [ + "exams/components", + "exams/data-access" + ] + }, + { + "name": "ExamDifficulty", + "type": "type", + "definition": "1 | 2 | 3 | 4 | 5", + "usedBy": [ + "exams/components", + "exams/data-access" + ] + }, + { + "name": "SubmissionStatus", + "type": "type", + "definition": "\"pending\" | \"graded\"", + "usedBy": [ + "exams/components", + "exams/data-access" + ] + }, + { + "name": "ExamSubmission", + "type": "interface", + "definition": "{ id, examId, examTitle, studentName, submittedAt, score?, status }", + "usedBy": [ + "exams/components" + ] + } ], "components": [ - {"name": "ExamPaperPreview", "file": "assembly/exam-paper-preview.tsx", "purpose": "试卷预览"}, - {"name": "QuestionBankList", "file": "assembly/question-bank-list.tsx", "purpose": "题库列表(组卷选择)"}, - {"name": "SelectedQuestionList", "file": "assembly/selected-question-list.tsx", "purpose": "已选题目列表", "types": ["ExamNode"]}, - {"name": "StructureEditor", "file": "assembly/structure-editor.tsx", "purpose": "试卷结构编辑器"}, - {"name": "ExamActions", "file": "exam-actions.tsx", "purpose": "考试操作按钮(删除/复制等)"}, - {"name": "ExamAiGenerator", "file": "exam-ai-generator.tsx", "purpose": "AI生成考试面板"}, - {"name": "ExamAssembly", "file": "exam-assembly.tsx", "purpose": "组卷主界面"}, - {"name": "ExamBasicInfoForm", "file": "exam-basic-info-form.tsx", "purpose": "考试基本信息表单"}, - {"name": "ExamCard", "file": "exam-card.tsx", "purpose": "考试卡片"}, - {"name": "examColumns", "file": "exam-columns.tsx", "type": "ColumnDef[]", "purpose": "考试表格列定义"}, - {"name": "ExamDataTable", "file": "exam-data-table.tsx", "purpose": "考试数据表格"}, - {"name": "ExamFilters", "file": "exam-filters.tsx", "purpose": "考试筛选器"}, - {"name": "formSchema", "file": "exam-form-types.ts", "type": "const", "purpose": "表单zod schema"}, - {"name": "ExamFormValues", "file": "exam-form-types.ts", "type": "type", "purpose": "表单值类型"}, - {"name": "PreviewQuestion", "file": "exam-form-types.ts", "type": "type", "purpose": "预览题目类型"}, - {"name": "EditableQuestionContent", "file": "exam-form-types.ts", "type": "type", "purpose": "可编辑题目内容"}, - {"name": "PreviewSnapshotMeta", "file": "exam-form-types.ts", "type": "type", "purpose": "预览快照元数据"}, - {"name": "PreviewBackgroundTask", "file": "exam-form-types.ts", "type": "type", "purpose": "预览后台任务"}, - {"name": "aiProviderLabels", "file": "exam-form-types.ts", "type": "const", "purpose": "AI Provider标签映射"}, - {"name": "defaultValues", "file": "exam-form-types.ts", "type": "const", "purpose": "表单默认值"}, - {"name": "previewTaskStorageKey", "file": "exam-form-types.ts", "type": "const", "purpose": "预览任务localStorage key"}, - {"name": "ExamForm", "file": "exam-form.tsx", "purpose": "考试表单(创建/编辑)"}, - {"name": "ExamGrid", "file": "exam-grid.tsx", "purpose": "考试网格视图"}, - {"name": "ExamModeSelector", "file": "exam-mode-selector.tsx", "purpose": "考试模式选择(手动/AI)"}, - {"name": "ExamPreviewDialog", "file": "exam-preview-dialog.tsx", "purpose": "考试预览对话框"}, - {"name": "ExamPreviewQuestionEditor", "file": "exam-preview-question-editor.tsx", "purpose": "预览题目编辑器"}, - {"name": "buildPreviewNodes", "file": "exam-preview-utils.ts", "type": "function", "purpose": "构建预览节点"}, - {"name": "parseEditableContent", "file": "exam-preview-utils.ts", "type": "function", "purpose": "解析可编辑内容"}, - {"name": "flattenPreviewQuestions", "file": "exam-preview-utils.ts", "type": "function", "purpose": "扁平化预览题目"}, - {"name": "findPreviewQuestionNode", "file": "exam-preview-utils.ts", "type": "function", "purpose": "查找预览题目节点"}, - {"name": "updatePreviewQuestionNodeInList", "file": "exam-preview-utils.ts", "type": "function", "purpose": "更新预览题目节点"}, - {"name": "buildPreviewSignature", "file": "exam-preview-utils.ts", "type": "function", "purpose": "构建预览签名"}, - {"name": "buildPreviewPayload", "file": "exam-preview-utils.ts", "type": "function", "purpose": "构建预览payload"}, - {"name": "buildPreviewRequestData", "file": "exam-preview-utils.ts", "type": "function", "purpose": "构建预览请求数据"}, - {"name": "ExamViewer", "file": "exam-viewer.tsx", "purpose": "考试查看器"}, - {"name": "QuestionOptionsEditor", "file": "question-options-editor.tsx", "purpose": "题目选项编辑器"}, - {"name": "QuestionSubQuestionsEditor", "file": "question-sub-questions-editor.tsx", "purpose": "子题目编辑器"} + { + "name": "ExamPaperPreview", + "file": "assembly/exam-paper-preview.tsx", + "purpose": "试卷预览" + }, + { + "name": "QuestionBankList", + "file": "assembly/question-bank-list.tsx", + "purpose": "题库列表(组卷选择)" + }, + { + "name": "SelectedQuestionList", + "file": "assembly/selected-question-list.tsx", + "purpose": "已选题目列表", + "types": [ + "ExamNode" + ] + }, + { + "name": "StructureEditor", + "file": "assembly/structure-editor.tsx", + "purpose": "试卷结构编辑器" + }, + { + "name": "ExamActions", + "file": "exam-actions.tsx", + "purpose": "考试操作按钮(删除/复制等)" + }, + { + "name": "ExamAiGenerator", + "file": "exam-ai-generator.tsx", + "purpose": "AI生成考试面板" + }, + { + "name": "ExamAssembly", + "file": "exam-assembly.tsx", + "purpose": "组卷主界面" + }, + { + "name": "ExamBasicInfoForm", + "file": "exam-basic-info-form.tsx", + "purpose": "考试基本信息表单" + }, + { + "name": "ExamCard", + "file": "exam-card.tsx", + "purpose": "考试卡片" + }, + { + "name": "examColumns", + "file": "exam-columns.tsx", + "type": "ColumnDef[]", + "purpose": "考试表格列定义" + }, + { + "name": "ExamDataTable", + "file": "exam-data-table.tsx", + "purpose": "考试数据表格" + }, + { + "name": "ExamFilters", + "file": "exam-filters.tsx", + "purpose": "考试筛选器" + }, + { + "name": "formSchema", + "file": "exam-form-types.ts", + "type": "const", + "purpose": "表单zod schema" + }, + { + "name": "ExamFormValues", + "file": "exam-form-types.ts", + "type": "type", + "purpose": "表单值类型" + }, + { + "name": "PreviewQuestion", + "file": "exam-form-types.ts", + "type": "type", + "purpose": "预览题目类型" + }, + { + "name": "EditableQuestionContent", + "file": "exam-form-types.ts", + "type": "type", + "purpose": "可编辑题目内容" + }, + { + "name": "PreviewSnapshotMeta", + "file": "exam-form-types.ts", + "type": "type", + "purpose": "预览快照元数据" + }, + { + "name": "PreviewBackgroundTask", + "file": "exam-form-types.ts", + "type": "type", + "purpose": "预览后台任务" + }, + { + "name": "aiProviderLabels", + "file": "exam-form-types.ts", + "type": "const", + "purpose": "AI Provider标签映射" + }, + { + "name": "defaultValues", + "file": "exam-form-types.ts", + "type": "const", + "purpose": "表单默认值" + }, + { + "name": "previewTaskStorageKey", + "file": "exam-form-types.ts", + "type": "const", + "purpose": "预览任务localStorage key" + }, + { + "name": "ExamForm", + "file": "exam-form.tsx", + "purpose": "考试表单(创建/编辑)" + }, + { + "name": "ExamGrid", + "file": "exam-grid.tsx", + "purpose": "考试网格视图" + }, + { + "name": "ExamModeSelector", + "file": "exam-mode-selector.tsx", + "purpose": "考试模式选择(手动/AI)" + }, + { + "name": "ExamPreviewDialog", + "file": "exam-preview-dialog.tsx", + "purpose": "考试预览对话框" + }, + { + "name": "ExamPreviewQuestionEditor", + "file": "exam-preview-question-editor.tsx", + "purpose": "预览题目编辑器" + }, + { + "name": "buildPreviewNodes", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "构建预览节点" + }, + { + "name": "parseEditableContent", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "解析可编辑内容" + }, + { + "name": "flattenPreviewQuestions", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "扁平化预览题目" + }, + { + "name": "findPreviewQuestionNode", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "查找预览题目节点" + }, + { + "name": "updatePreviewQuestionNodeInList", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "更新预览题目节点" + }, + { + "name": "buildPreviewSignature", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "构建预览签名" + }, + { + "name": "buildPreviewPayload", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "构建预览payload" + }, + { + "name": "buildPreviewRequestData", + "file": "exam-preview-utils.ts", + "type": "function", + "purpose": "构建预览请求数据" + }, + { + "name": "ExamViewer", + "file": "exam-viewer.tsx", + "purpose": "考试查看器" + }, + { + "name": "QuestionOptionsEditor", + "file": "question-options-editor.tsx", + "purpose": "题目选项编辑器" + }, + { + "name": "QuestionSubQuestionsEditor", + "file": "question-sub-questions-editor.tsx", + "purpose": "子题目编辑器" + } ], "hooks": [ - {"name": "useExamPreview", "file": "use-exam-preview.ts", "signature": "(form: UseFormReturn) => { previewOpen, setPreviewOpen, previewLoading, previewNodes, handlePreview, handleBackgroundPreview, handleOpenPreviewTask, handleRewriteSelectedQuestion }", "purpose": "考试预览Hook", "usedBy": ["exam-form.tsx"]} + { + "name": "useExamPreview", + "file": "use-exam-preview.ts", + "signature": "(form: UseFormReturn) => { previewOpen, setPreviewOpen, previewLoading, previewNodes, handlePreview, handleBackgroundPreview, handleOpenPreviewTask, handleRewriteSelectedQuestion }", + "purpose": "考试预览Hook", + "usedBy": [ + "exam-form.tsx" + ] + } ] } }, @@ -590,63 +2467,395 @@ "description": "作业全生命周期:创建(源自考试)、发布、学生作答、教师批改、数据分析", "exports": { "actions": [ - {"name": "createHomeworkAssignmentAction", "permission": "HOMEWORK_CREATE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "从已有考试创建作业", "deps": ["requirePermission","shared/db","exams/data-access.getExams"], "usedBy": ["homework-assignment-form.tsx"]}, - {"name": "startHomeworkSubmissionAction", "permission": "HOMEWORK_SUBMIT", "signature": "同上", "purpose": "学生开始作答", "deps": ["requirePermission","shared/db"], "usedBy": ["homework-take-view.tsx"]}, - {"name": "saveHomeworkAnswerAction", "permission": "HOMEWORK_SUBMIT", "signature": "同上", "purpose": "保存单题答案", "deps": ["requirePermission","shared/db"], "usedBy": ["homework-take-view.tsx"]}, - {"name": "submitHomeworkAction", "permission": "HOMEWORK_SUBMIT", "signature": "同上", "purpose": "提交作业", "deps": ["requirePermission","shared/db"], "usedBy": ["homework-take-view.tsx"]}, - {"name": "gradeHomeworkSubmissionAction", "permission": "HOMEWORK_GRADE", "signature": "同上", "purpose": "教师批改作业", "deps": ["requirePermission","shared/db"], "usedBy": ["homework-grading-view.tsx"]} + { + "name": "createHomeworkAssignmentAction", + "permission": "HOMEWORK_CREATE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "从已有考试创建作业", + "deps": [ + "requirePermission", + "shared/db", + "exams/data-access.getExams" + ], + "usedBy": [ + "homework-assignment-form.tsx" + ] + }, + { + "name": "startHomeworkSubmissionAction", + "permission": "HOMEWORK_SUBMIT", + "signature": "同上", + "purpose": "学生开始作答", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "homework-take-view.tsx" + ] + }, + { + "name": "saveHomeworkAnswerAction", + "permission": "HOMEWORK_SUBMIT", + "signature": "同上", + "purpose": "保存单题答案", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "homework-take-view.tsx" + ] + }, + { + "name": "submitHomeworkAction", + "permission": "HOMEWORK_SUBMIT", + "signature": "同上", + "purpose": "提交作业", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "homework-take-view.tsx" + ] + }, + { + "name": "gradeHomeworkSubmissionAction", + "permission": "HOMEWORK_GRADE", + "signature": "同上", + "purpose": "教师批改作业", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "homework-grading-view.tsx" + ] + } ], "dataAccess": [ - {"name": "getTeacherGradeTrends", "signature": "(teacherId: string, limit?: number) => Promise", "usedBy": ["dashboard (教师仪表盘)"]}, - {"name": "getHomeworkAssignments", "signature": "(params?: { creatorId?, ids?, classId?, scope? }) => Promise", "usedBy": ["teacher作业列表页", "homework-assignment-form.tsx"]}, - {"name": "getHomeworkAssignmentReviewList", "signature": "(params: { creatorId: string; scope? }) => Promise", "usedBy": ["teacher批改列表"]}, - {"name": "getHomeworkSubmissions", "signature": "(params?: { assignmentId?, classId?, creatorId?, scope? }) => Promise", "usedBy": ["teacher提交列表"]}, - {"name": "getStudentHomeworkAssignments", "signature": "(studentId: string) => Promise", "usedBy": ["student/dashboard"]}, - {"name": "getStudentDashboardGrades", "signature": "(studentId: string) => Promise", "usedBy": ["dashboard/data-access.ts"]}, - {"name": "getHomeworkAssignmentAnalytics", "signature": "(assignmentId: string) => Promise", "usedBy": ["homework错误分析组件"]}, - {"name": "getHomeworkAssignmentById", "signature": "(id: string, scope?: DataScope) => Promise<...>", "purpose": "按ID获取作业详情", "usedBy": ["homework详情页"]}, - {"name": "getHomeworkSubmissionDetails", "signature": "(submissionId: string) => Promise", "purpose": "获取提交详情(含答案)", "usedBy": ["homework-grading-view.tsx"]}, - {"name": "getDemoStudentUser", "signature": "() => Promise<{ id: string; name: string } | null>", "purpose": "获取演示学生用户", "usedBy": ["homework内部"]}, - {"name": "getStudentHomeworkTakeData", "signature": "(assignmentId: string, studentId: string) => Promise", "purpose": "获取学生作答数据", "usedBy": ["homework-take-view.tsx"]} + { + "name": "getTeacherGradeTrends", + "signature": "(teacherId: string, limit?: number) => Promise", + "usedBy": [ + "dashboard (教师仪表盘)" + ] + }, + { + "name": "getHomeworkAssignments", + "signature": "(params?: { creatorId?, ids?, classId?, scope? }) => Promise", + "usedBy": [ + "teacher作业列表页", + "homework-assignment-form.tsx" + ] + }, + { + "name": "getHomeworkAssignmentReviewList", + "signature": "(params: { creatorId: string; scope? }) => Promise", + "usedBy": [ + "teacher批改列表" + ] + }, + { + "name": "getHomeworkSubmissions", + "signature": "(params?: { assignmentId?, classId?, creatorId?, scope? }) => Promise", + "usedBy": [ + "teacher提交列表" + ] + }, + { + "name": "getStudentHomeworkAssignments", + "signature": "(studentId: string) => Promise", + "usedBy": [ + "student/dashboard" + ] + }, + { + "name": "getStudentDashboardGrades", + "signature": "(studentId: string) => Promise", + "usedBy": [ + "dashboard/data-access.ts" + ] + }, + { + "name": "getHomeworkAssignmentAnalytics", + "signature": "(assignmentId: string) => Promise", + "usedBy": [ + "homework错误分析组件" + ] + }, + { + "name": "getHomeworkAssignmentById", + "signature": "(id: string, scope?: DataScope) => Promise<...>", + "purpose": "按ID获取作业详情", + "usedBy": [ + "homework详情页" + ] + }, + { + "name": "getHomeworkSubmissionDetails", + "signature": "(submissionId: string) => Promise", + "purpose": "获取提交详情(含答案)", + "usedBy": [ + "homework-grading-view.tsx" + ] + }, + { + "name": "getDemoStudentUser", + "signature": "() => Promise<{ id: string; name: string } | null>", + "purpose": "获取演示学生用户", + "usedBy": [ + "homework内部" + ] + }, + { + "name": "getStudentHomeworkTakeData", + "signature": "(assignmentId: string, studentId: string) => Promise", + "purpose": "获取学生作答数据", + "usedBy": [ + "homework-take-view.tsx" + ] + } ], "schema": [ - {"name": "CreateHomeworkAssignmentSchema", "type": "const", "description": "zod schema 创建作业", "usedBy": ["createHomeworkAssignmentAction", "homework-assignment-form.tsx"]}, - {"name": "CreateHomeworkAssignmentInput", "type": "type", "definition": "z.infer", "usedBy": ["createHomeworkAssignmentAction"]}, - {"name": "GradeHomeworkSchema", "type": "const", "description": "zod schema 批改作业", "usedBy": ["gradeHomeworkSubmissionAction", "homework-grading-view.tsx"]} + { + "name": "CreateHomeworkAssignmentSchema", + "type": "const", + "description": "zod schema 创建作业", + "usedBy": [ + "createHomeworkAssignmentAction", + "homework-assignment-form.tsx" + ] + }, + { + "name": "CreateHomeworkAssignmentInput", + "type": "type", + "definition": "z.infer", + "usedBy": [ + "createHomeworkAssignmentAction" + ] + }, + { + "name": "GradeHomeworkSchema", + "type": "const", + "description": "zod schema 批改作业", + "usedBy": [ + "gradeHomeworkSubmissionAction", + "homework-grading-view.tsx" + ] + } ], "types": [ - {"name": "StudentDashboardGradeProps", "definition": "{ trend, recent, ranking }", "usedBy": ["dashboard/types.ts"]}, - {"name": "HomeworkAssignmentListItem", "usedBy": ["homework列表页"]}, - {"name": "StudentHomeworkTakeData", "usedBy": ["homework-take-view.tsx"]}, - {"name": "HomeworkAssignmentStatus", "type": "type", "definition": "作业状态枚举", "usedBy": ["homework/components", "homework/data-access"]}, - {"name": "HomeworkSubmissionStatus", "type": "type", "definition": "提交状态枚举", "usedBy": ["homework/components", "homework/data-access"]}, - {"name": "TeacherGradeTrendItem", "type": "type", "definition": "教师年级趋势项", "usedBy": ["dashboard (教师仪表盘)"]}, - {"name": "HomeworkAssignmentReviewListItem", "type": "type", "definition": "批改列表项", "usedBy": ["teacher批改列表"]}, - {"name": "HomeworkSubmissionListItem", "type": "type", "definition": "提交列表项", "usedBy": ["teacher提交列表"]}, - {"name": "HomeworkQuestionContent", "type": "type", "definition": "作业题目内容", "usedBy": ["homework/components"]}, - {"name": "HomeworkSubmissionAnswerDetails", "type": "type", "definition": "提交答案详情", "usedBy": ["homework-grading-view.tsx"]}, - {"name": "HomeworkSubmissionDetails", "type": "type", "definition": "提交详情(含答案列表)", "usedBy": ["homework-grading-view.tsx"]}, - {"name": "StudentHomeworkProgressStatus", "type": "type", "definition": "学生作业进度状态", "usedBy": ["student/dashboard"]}, - {"name": "StudentHomeworkAssignmentListItem", "type": "type", "definition": "学生作业列表项", "usedBy": ["student/dashboard"]}, - {"name": "StudentHomeworkPerformanceItem", "type": "type", "definition": "学生表现项", "usedBy": ["student/dashboard"]}, - {"name": "StudentHomeworkPerformanceSummary", "type": "type", "definition": "学生表现汇总", "usedBy": ["student/dashboard"]}, - {"name": "StudentHomeworkTakeQuestion", "type": "type", "definition": "学生作答题目", "usedBy": ["homework-take-view.tsx"]}, - {"name": "HomeworkAssignmentQuestionAnalytics", "type": "type", "definition": "作业题目分析", "usedBy": ["homework错误分析组件"]}, - {"name": "HomeworkAssignmentAnalytics", "type": "type", "definition": "作业整体分析", "usedBy": ["homework错误分析组件"]}, - {"name": "StudentHomeworkScoreAnalytics", "type": "type", "definition": "学生成绩分析", "usedBy": ["student/dashboard"]}, - {"name": "StudentRanking", "type": "type", "definition": "学生排名", "usedBy": ["student/dashboard"]} + { + "name": "StudentDashboardGradeProps", + "definition": "{ trend, recent, ranking }", + "usedBy": [ + "dashboard/types.ts" + ] + }, + { + "name": "HomeworkAssignmentListItem", + "usedBy": [ + "homework列表页" + ] + }, + { + "name": "StudentHomeworkTakeData", + "usedBy": [ + "homework-take-view.tsx" + ] + }, + { + "name": "HomeworkAssignmentStatus", + "type": "type", + "definition": "作业状态枚举", + "usedBy": [ + "homework/components", + "homework/data-access" + ] + }, + { + "name": "HomeworkSubmissionStatus", + "type": "type", + "definition": "提交状态枚举", + "usedBy": [ + "homework/components", + "homework/data-access" + ] + }, + { + "name": "TeacherGradeTrendItem", + "type": "type", + "definition": "教师年级趋势项", + "usedBy": [ + "dashboard (教师仪表盘)" + ] + }, + { + "name": "HomeworkAssignmentReviewListItem", + "type": "type", + "definition": "批改列表项", + "usedBy": [ + "teacher批改列表" + ] + }, + { + "name": "HomeworkSubmissionListItem", + "type": "type", + "definition": "提交列表项", + "usedBy": [ + "teacher提交列表" + ] + }, + { + "name": "HomeworkQuestionContent", + "type": "type", + "definition": "作业题目内容", + "usedBy": [ + "homework/components" + ] + }, + { + "name": "HomeworkSubmissionAnswerDetails", + "type": "type", + "definition": "提交答案详情", + "usedBy": [ + "homework-grading-view.tsx" + ] + }, + { + "name": "HomeworkSubmissionDetails", + "type": "type", + "definition": "提交详情(含答案列表)", + "usedBy": [ + "homework-grading-view.tsx" + ] + }, + { + "name": "StudentHomeworkProgressStatus", + "type": "type", + "definition": "学生作业进度状态", + "usedBy": [ + "student/dashboard" + ] + }, + { + "name": "StudentHomeworkAssignmentListItem", + "type": "type", + "definition": "学生作业列表项", + "usedBy": [ + "student/dashboard" + ] + }, + { + "name": "StudentHomeworkPerformanceItem", + "type": "type", + "definition": "学生表现项", + "usedBy": [ + "student/dashboard" + ] + }, + { + "name": "StudentHomeworkPerformanceSummary", + "type": "type", + "definition": "学生表现汇总", + "usedBy": [ + "student/dashboard" + ] + }, + { + "name": "StudentHomeworkTakeQuestion", + "type": "type", + "definition": "学生作答题目", + "usedBy": [ + "homework-take-view.tsx" + ] + }, + { + "name": "HomeworkAssignmentQuestionAnalytics", + "type": "type", + "definition": "作业题目分析", + "usedBy": [ + "homework错误分析组件" + ] + }, + { + "name": "HomeworkAssignmentAnalytics", + "type": "type", + "definition": "作业整体分析", + "usedBy": [ + "homework错误分析组件" + ] + }, + { + "name": "StudentHomeworkScoreAnalytics", + "type": "type", + "definition": "学生成绩分析", + "usedBy": [ + "student/dashboard" + ] + }, + { + "name": "StudentRanking", + "type": "type", + "definition": "学生排名", + "usedBy": [ + "student/dashboard" + ] + } ], "components": [ - {"name": "HomeworkAssignmentExamContentCard", "file": "homework-assignment-exam-content-card.tsx", "purpose": "作业考试内容卡片"}, - {"name": "HomeworkAssignmentExamErrorExplorerLazy", "file": "homework-assignment-exam-error-explorer-lazy.tsx", "purpose": "作业错误分析(懒加载)"}, - {"name": "HomeworkAssignmentExamErrorExplorer", "file": "homework-assignment-exam-error-explorer.tsx", "purpose": "作业错误分析探索器"}, - {"name": "HomeworkAssignmentExamPreviewPane", "file": "homework-assignment-exam-preview-pane.tsx", "purpose": "作业考试预览面板"}, - {"name": "HomeworkAssignmentForm", "file": "homework-assignment-form.tsx", "purpose": "作业创建表单"}, - {"name": "HomeworkAssignmentQuestionErrorDetailPanel", "file": "homework-assignment-question-error-detail-panel.tsx", "purpose": "题目错误详情面板"}, - {"name": "HomeworkAssignmentQuestionErrorOverviewCard", "file": "homework-assignment-question-error-overview-card.tsx", "purpose": "题目错误概览卡片"}, - {"name": "HomeworkGradingView", "file": "homework-grading-view.tsx", "purpose": "作业批改视图"}, - {"name": "HomeworkTakeView", "file": "homework-take-view.tsx", "purpose": "学生作答视图"}, - {"name": "HomeworkReviewView", "file": "student-homework-review-view.tsx", "purpose": "学生作业复习视图"} + { + "name": "HomeworkAssignmentExamContentCard", + "file": "homework-assignment-exam-content-card.tsx", + "purpose": "作业考试内容卡片" + }, + { + "name": "HomeworkAssignmentExamErrorExplorerLazy", + "file": "homework-assignment-exam-error-explorer-lazy.tsx", + "purpose": "作业错误分析(懒加载)" + }, + { + "name": "HomeworkAssignmentExamErrorExplorer", + "file": "homework-assignment-exam-error-explorer.tsx", + "purpose": "作业错误分析探索器" + }, + { + "name": "HomeworkAssignmentExamPreviewPane", + "file": "homework-assignment-exam-preview-pane.tsx", + "purpose": "作业考试预览面板" + }, + { + "name": "HomeworkAssignmentForm", + "file": "homework-assignment-form.tsx", + "purpose": "作业创建表单" + }, + { + "name": "HomeworkAssignmentQuestionErrorDetailPanel", + "file": "homework-assignment-question-error-detail-panel.tsx", + "purpose": "题目错误详情面板" + }, + { + "name": "HomeworkAssignmentQuestionErrorOverviewCard", + "file": "homework-assignment-question-error-overview-card.tsx", + "purpose": "题目错误概览卡片" + }, + { + "name": "HomeworkGradingView", + "file": "homework-grading-view.tsx", + "purpose": "作业批改视图" + }, + { + "name": "HomeworkTakeView", + "file": "homework-take-view.tsx", + "purpose": "学生作答视图" + }, + { + "name": "HomeworkReviewView", + "file": "student-homework-review-view.tsx", + "purpose": "学生作业复习视图" + } ] } }, @@ -655,34 +2864,188 @@ "description": "题库管理:题目CRUD、知识点关联、多题型支持", "exports": { "actions": [ - {"name": "createNestedQuestion", "permission": "QUESTION_CREATE", "signature": "(prevState: ActionState | undefined, formData: FormData | CreateQuestionInput) => Promise>", "purpose": "创建题目(含嵌套)", "deps": ["requirePermission","shared/db"], "usedBy": ["create-question-dialog.tsx"]}, - {"name": "updateQuestionAction", "permission": "QUESTION_UPDATE", "signature": "同上", "purpose": "更新题目", "deps": ["requirePermission","shared/db"], "usedBy": ["question-actions.tsx"]}, - {"name": "deleteQuestionAction", "permission": "QUESTION_DELETE", "signature": "同上", "purpose": "删除题目", "deps": ["requirePermission","shared/db"], "usedBy": ["question-actions.tsx"]}, - {"name": "getQuestionsAction", "permission": "QUESTION_READ", "signature": "(params: GetQuestionsParams) => Promise<...>", "purpose": "查询题目列表", "deps": ["requirePermission","data-access.getQuestions"], "usedBy": ["teacher/questions/page.tsx"]}, - {"name": "getKnowledgePointOptionsAction", "permission": "QUESTION_READ", "signature": "() => Promise", "purpose": "获取知识点选项", "deps": ["requirePermission","shared/db"], "usedBy": ["create-question-dialog.tsx"]} + { + "name": "createNestedQuestion", + "permission": "QUESTION_CREATE", + "signature": "(prevState: ActionState | undefined, formData: FormData | CreateQuestionInput) => Promise>", + "purpose": "创建题目(含嵌套)", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "create-question-dialog.tsx" + ] + }, + { + "name": "updateQuestionAction", + "permission": "QUESTION_UPDATE", + "signature": "同上", + "purpose": "更新题目", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "question-actions.tsx" + ] + }, + { + "name": "deleteQuestionAction", + "permission": "QUESTION_DELETE", + "signature": "同上", + "purpose": "删除题目", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "question-actions.tsx" + ] + }, + { + "name": "getQuestionsAction", + "permission": "QUESTION_READ", + "signature": "(params: GetQuestionsParams) => Promise<...>", + "purpose": "查询题目列表", + "deps": [ + "requirePermission", + "data-access.getQuestions" + ], + "usedBy": [ + "teacher/questions/page.tsx" + ] + }, + { + "name": "getKnowledgePointOptionsAction", + "permission": "QUESTION_READ", + "signature": "() => Promise", + "purpose": "获取知识点选项", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "create-question-dialog.tsx" + ] + } ], "dataAccess": [ - {"name": "getQuestions", "signature": "(params?: GetQuestionsParams) => Promise<{ data: Question[], meta: { page, pageSize, total, totalPages } }>", "type": "cache function", "purpose": "查询题目列表(缓存)", "usedBy": ["getQuestionsAction", "teacher/questions/page.tsx"]}, - {"name": "GetQuestionsParams", "type": "type", "definition": "{ q?, page?, pageSize?, ids?, knowledgePointId?, type?, difficulty? }", "usedBy": ["getQuestions", "getQuestionsAction"]} + { + "name": "getQuestions", + "signature": "(params?: GetQuestionsParams) => Promise<{ data: Question[], meta: { page, pageSize, total, totalPages } }>", + "type": "cache function", + "purpose": "查询题目列表(缓存)", + "usedBy": [ + "getQuestionsAction", + "teacher/questions/page.tsx" + ] + }, + { + "name": "GetQuestionsParams", + "type": "type", + "definition": "{ q?, page?, pageSize?, ids?, knowledgePointId?, type?, difficulty? }", + "usedBy": [ + "getQuestions", + "getQuestionsAction" + ] + } ], "schema": [ - {"name": "QuestionTypeEnum", "type": "const", "description": "zod enum: z.enum([\"single_choice\", \"multiple_choice\", \"text\", \"judgment\", \"composite\"])", "usedBy": ["CreateQuestionSchema", "questions/components"]}, - {"name": "BaseQuestionSchema", "type": "const", "description": "zod schema 基础题目校验", "usedBy": ["CreateQuestionSchema"]}, - {"name": "CreateQuestionInput", "type": "type", "definition": "z.infer", "usedBy": ["createNestedQuestion", "create-question-dialog.tsx"]}, - {"name": "CreateQuestionSchema", "type": "const", "description": "zod schema 创建题目(递归支持嵌套)", "usedBy": ["createNestedQuestion", "create-question-dialog.tsx"]} + { + "name": "QuestionTypeEnum", + "type": "const", + "description": "zod enum: z.enum([\"single_choice\", \"multiple_choice\", \"text\", \"judgment\", \"composite\"])", + "usedBy": [ + "CreateQuestionSchema", + "questions/components" + ] + }, + { + "name": "BaseQuestionSchema", + "type": "const", + "description": "zod schema 基础题目校验", + "usedBy": [ + "CreateQuestionSchema" + ] + }, + { + "name": "CreateQuestionInput", + "type": "type", + "definition": "z.infer", + "usedBy": [ + "createNestedQuestion", + "create-question-dialog.tsx" + ] + }, + { + "name": "CreateQuestionSchema", + "type": "const", + "description": "zod schema 创建题目(递归支持嵌套)", + "usedBy": [ + "createNestedQuestion", + "create-question-dialog.tsx" + ] + } ], "types": [ - {"name": "Question", "definition": "{ id, content, type, difficulty, createdAt, updatedAt, author, knowledgePoints, childrenCount? }", "usedBy": ["exams (题目选择)", "homework (作业题目)"]}, - {"name": "KnowledgePointOption", "definition": "{ id, name, chapterId, chapterTitle, textbookId, textbookTitle, subject, grade }", "usedBy": ["create-question-dialog.tsx"]}, - {"name": "QuestionType", "type": "type", "definition": "z.infer", "usedBy": ["questions/components", "exams/components"]} + { + "name": "Question", + "definition": "{ id, content, type, difficulty, createdAt, updatedAt, author, knowledgePoints, childrenCount? }", + "usedBy": [ + "exams (题目选择)", + "homework (作业题目)" + ] + }, + { + "name": "KnowledgePointOption", + "definition": "{ id, name, chapterId, chapterTitle, textbookId, textbookTitle, subject, grade }", + "usedBy": [ + "create-question-dialog.tsx" + ] + }, + { + "name": "QuestionType", + "type": "type", + "definition": "z.infer", + "usedBy": [ + "questions/components", + "exams/components" + ] + } ], "components": [ - {"name": "CreateQuestionButton", "file": "create-question-button.tsx", "purpose": "创建题目按钮"}, - {"name": "CreateQuestionDialog", "file": "create-question-dialog.tsx", "purpose": "创建题目对话框"}, - {"name": "QuestionActions", "file": "question-actions.tsx", "purpose": "题目操作按钮"}, - {"name": "columns", "file": "question-columns.tsx", "type": "ColumnDef[]", "purpose": "题目表格列定义"}, - {"name": "QuestionDataTable", "file": "question-data-table.tsx", "purpose": "题目数据表格"}, - {"name": "QuestionFilters", "file": "question-filters.tsx", "purpose": "题目筛选器"} + { + "name": "CreateQuestionButton", + "file": "create-question-button.tsx", + "purpose": "创建题目按钮" + }, + { + "name": "CreateQuestionDialog", + "file": "create-question-dialog.tsx", + "purpose": "创建题目对话框" + }, + { + "name": "QuestionActions", + "file": "question-actions.tsx", + "purpose": "题目操作按钮" + }, + { + "name": "columns", + "file": "question-columns.tsx", + "type": "ColumnDef[]", + "purpose": "题目表格列定义" + }, + { + "name": "QuestionDataTable", + "file": "question-data-table.tsx", + "purpose": "题目数据表格" + }, + { + "name": "QuestionFilters", + "file": "question-filters.tsx", + "purpose": "题目筛选器" + } ] } }, @@ -691,63 +3054,338 @@ "description": "教材与知识体系:教材/章节树形结构、知识点CRUD、Markdown内容编辑、知识图谱", "exports": { "actions": [ - {"name": "createTextbookAction", "permission": "TEXTBOOK_CREATE", "signature": "(prevState, formData) => Promise", "purpose": "创建教材"}, - {"name": "updateTextbookAction", "permission": "TEXTBOOK_UPDATE", "signature": "(textbookId, prevState, formData) => Promise", "purpose": "更新教材元信息"}, - {"name": "deleteTextbookAction", "permission": "TEXTBOOK_DELETE", "signature": "(textbookId) => Promise", "purpose": "删除教材"}, - {"name": "createChapterAction", "permission": "TEXTBOOK_CREATE", "signature": "(textbookId, parentId?, prevState, formData) => Promise", "purpose": "创建章节"}, - {"name": "updateChapterContentAction", "permission": "TEXTBOOK_UPDATE", "signature": "(chapterId, content, textbookId) => Promise", "purpose": "更新章节内容(Markdown)"}, - {"name": "deleteChapterAction", "permission": "TEXTBOOK_DELETE", "signature": "(chapterId, textbookId) => Promise", "purpose": "删除章节"}, - {"name": "createKnowledgePointAction", "permission": "TEXTBOOK_CREATE", "signature": "(chapterId, textbookId, prevState, formData) => Promise", "purpose": "创建知识点"}, - {"name": "updateKnowledgePointAction", "permission": "TEXTBOOK_UPDATE", "signature": "(kpId, textbookId, prevState, formData) => Promise", "purpose": "更新知识点"}, - {"name": "deleteKnowledgePointAction", "permission": "TEXTBOOK_DELETE", "signature": "(kpId, textbookId) => Promise", "purpose": "删除知识点"}, - {"name": "reorderChaptersAction", "permission": "TEXTBOOK_UPDATE", "signature": "(chapterId, newIndex, parentId, textbookId) => Promise", "purpose": "章节排序"} + { + "name": "createTextbookAction", + "permission": "TEXTBOOK_CREATE", + "signature": "(prevState, formData) => Promise", + "purpose": "创建教材" + }, + { + "name": "updateTextbookAction", + "permission": "TEXTBOOK_UPDATE", + "signature": "(textbookId, prevState, formData) => Promise", + "purpose": "更新教材元信息" + }, + { + "name": "deleteTextbookAction", + "permission": "TEXTBOOK_DELETE", + "signature": "(textbookId) => Promise", + "purpose": "删除教材" + }, + { + "name": "createChapterAction", + "permission": "TEXTBOOK_CREATE", + "signature": "(textbookId, parentId?, prevState, formData) => Promise", + "purpose": "创建章节" + }, + { + "name": "updateChapterContentAction", + "permission": "TEXTBOOK_UPDATE", + "signature": "(chapterId, content, textbookId) => Promise", + "purpose": "更新章节内容(Markdown)" + }, + { + "name": "deleteChapterAction", + "permission": "TEXTBOOK_DELETE", + "signature": "(chapterId, textbookId) => Promise", + "purpose": "删除章节" + }, + { + "name": "createKnowledgePointAction", + "permission": "TEXTBOOK_CREATE", + "signature": "(chapterId, textbookId, prevState, formData) => Promise", + "purpose": "创建知识点" + }, + { + "name": "updateKnowledgePointAction", + "permission": "TEXTBOOK_UPDATE", + "signature": "(kpId, textbookId, prevState, formData) => Promise", + "purpose": "更新知识点" + }, + { + "name": "deleteKnowledgePointAction", + "permission": "TEXTBOOK_DELETE", + "signature": "(kpId, textbookId) => Promise", + "purpose": "删除知识点" + }, + { + "name": "reorderChaptersAction", + "permission": "TEXTBOOK_UPDATE", + "signature": "(chapterId, newIndex, parentId, textbookId) => Promise", + "purpose": "章节排序" + } ], "dataAccess": [ - {"name": "getTextbooks", "signature": "(query?, subject?, grade?) => Promise", "usedBy": ["teacher/textbooks/page.tsx"]}, - {"name": "getTextbookById", "signature": "(id) => Promise", "usedBy": ["teacher/textbooks/[id]/page.tsx"]}, - {"name": "getChaptersByTextbookId", "signature": "(textbookId) => Promise", "usedBy": ["textbook-reader.tsx"]}, - {"name": "getKnowledgePointsByChapterId", "signature": "(chapterId) => Promise", "usedBy": ["textbook-reader.tsx"]}, - {"name": "getKnowledgePointsByTextbookId", "signature": "(textbookId) => Promise", "usedBy": ["textbook-reader.tsx"]}, - {"name": "createTextbook", "signature": "(input: CreateTextbookInput) => Promise", "purpose": "创建教材", "usedBy": ["createTextbookAction"]}, - {"name": "updateTextbook", "signature": "(id: string, input: UpdateTextbookInput) => Promise", "purpose": "更新教材", "usedBy": ["updateTextbookAction"]}, - {"name": "deleteTextbook", "signature": "(id: string) => Promise", "purpose": "删除教材", "usedBy": ["deleteTextbookAction"]}, - {"name": "createChapter", "signature": "(input: CreateChapterInput) => Promise", "purpose": "创建章节", "usedBy": ["createChapterAction"]}, - {"name": "updateChapterContent", "signature": "(input: UpdateChapterContentInput) => Promise", "purpose": "更新章节内容", "usedBy": ["updateChapterContentAction"]}, - {"name": "deleteChapter", "signature": "(chapterId: string, textbookId: string) => Promise", "purpose": "删除章节", "usedBy": ["deleteChapterAction"]}, - {"name": "createKnowledgePoint", "signature": "(input: CreateKnowledgePointInput) => Promise", "purpose": "创建知识点", "usedBy": ["createKnowledgePointAction"]}, - {"name": "updateKnowledgePoint", "signature": "(input: UpdateKnowledgePointInput) => Promise", "purpose": "更新知识点", "usedBy": ["updateKnowledgePointAction"]}, - {"name": "deleteKnowledgePoint", "signature": "(kpId: string, textbookId: string) => Promise", "purpose": "删除知识点", "usedBy": ["deleteKnowledgePointAction"]}, - {"name": "reorderChapters", "signature": "(chapterId: string, newIndex: number, parentId: string | null, textbookId: string) => Promise", "purpose": "章节排序", "usedBy": ["reorderChaptersAction"]} + { + "name": "getTextbooks", + "signature": "(query?, subject?, grade?) => Promise", + "usedBy": [ + "teacher/textbooks/page.tsx" + ] + }, + { + "name": "getTextbookById", + "signature": "(id) => Promise", + "usedBy": [ + "teacher/textbooks/[id]/page.tsx" + ] + }, + { + "name": "getChaptersByTextbookId", + "signature": "(textbookId) => Promise", + "usedBy": [ + "textbook-reader.tsx" + ] + }, + { + "name": "getKnowledgePointsByChapterId", + "signature": "(chapterId) => Promise", + "usedBy": [ + "textbook-reader.tsx" + ] + }, + { + "name": "getKnowledgePointsByTextbookId", + "signature": "(textbookId) => Promise", + "usedBy": [ + "textbook-reader.tsx" + ] + }, + { + "name": "createTextbook", + "signature": "(input: CreateTextbookInput) => Promise", + "purpose": "创建教材", + "usedBy": [ + "createTextbookAction" + ] + }, + { + "name": "updateTextbook", + "signature": "(id: string, input: UpdateTextbookInput) => Promise", + "purpose": "更新教材", + "usedBy": [ + "updateTextbookAction" + ] + }, + { + "name": "deleteTextbook", + "signature": "(id: string) => Promise", + "purpose": "删除教材", + "usedBy": [ + "deleteTextbookAction" + ] + }, + { + "name": "createChapter", + "signature": "(input: CreateChapterInput) => Promise", + "purpose": "创建章节", + "usedBy": [ + "createChapterAction" + ] + }, + { + "name": "updateChapterContent", + "signature": "(input: UpdateChapterContentInput) => Promise", + "purpose": "更新章节内容", + "usedBy": [ + "updateChapterContentAction" + ] + }, + { + "name": "deleteChapter", + "signature": "(chapterId: string, textbookId: string) => Promise", + "purpose": "删除章节", + "usedBy": [ + "deleteChapterAction" + ] + }, + { + "name": "createKnowledgePoint", + "signature": "(input: CreateKnowledgePointInput) => Promise", + "purpose": "创建知识点", + "usedBy": [ + "createKnowledgePointAction" + ] + }, + { + "name": "updateKnowledgePoint", + "signature": "(input: UpdateKnowledgePointInput) => Promise", + "purpose": "更新知识点", + "usedBy": [ + "updateKnowledgePointAction" + ] + }, + { + "name": "deleteKnowledgePoint", + "signature": "(kpId: string, textbookId: string) => Promise", + "purpose": "删除知识点", + "usedBy": [ + "deleteKnowledgePointAction" + ] + }, + { + "name": "reorderChapters", + "signature": "(chapterId: string, newIndex: number, parentId: string | null, textbookId: string) => Promise", + "purpose": "章节排序", + "usedBy": [ + "reorderChaptersAction" + ] + } ], "hooks": [ - {"name": "useTextSelection", "file": "hooks/use-text-selection.ts", "signature": "() => { selectedText, setSelectedText, selectionRef, contentRef, setCreateDialogOpen, setIsCreating, createDialogOpen, isCreating, handleContentPointerDown, handleContextMenuChange }", "purpose": "文本选区Hook(无参数)", "usedBy": ["textbook-content-panel.tsx"]}, - {"name": "useKnowledgePointActions", "file": "hooks/use-knowledge-point-actions.ts", "signature": "(textbookId, selectedChapterId, selectedChapterTextbookId, highlightedKpId, setHighlightedKpId, onKpCreated?) => { editingKp, setEditingKp, editKpDialogOpen, setEditKpDialogOpen, isUpdatingKp, questionDialogOpen, setQuestionDialogOpen, targetKpForQuestion, setTargetKpForQuestion, deleteConfirmOpen, setDeleteConfirmOpen, handleCreateKnowledgePoint, requestDeleteKP, confirmDeleteKP, handleUpdateKP }", "purpose": "知识点操作Hook(6参数)", "usedBy": ["textbook-reader.tsx"]} + { + "name": "useTextSelection", + "file": "hooks/use-text-selection.ts", + "signature": "() => { selectedText, setSelectedText, selectionRef, contentRef, setCreateDialogOpen, setIsCreating, createDialogOpen, isCreating, handleContentPointerDown, handleContextMenuChange }", + "purpose": "文本选区Hook(无参数)", + "usedBy": [ + "textbook-content-panel.tsx" + ] + }, + { + "name": "useKnowledgePointActions", + "file": "hooks/use-knowledge-point-actions.ts", + "signature": "(textbookId, selectedChapterId, selectedChapterTextbookId, highlightedKpId, setHighlightedKpId, onKpCreated?) => { editingKp, setEditingKp, editKpDialogOpen, setEditKpDialogOpen, isUpdatingKp, questionDialogOpen, setQuestionDialogOpen, targetKpForQuestion, setTargetKpForQuestion, deleteConfirmOpen, setDeleteConfirmOpen, handleCreateKnowledgePoint, requestDeleteKP, confirmDeleteKP, handleUpdateKP }", + "purpose": "知识点操作Hook(6参数)", + "usedBy": [ + "textbook-reader.tsx" + ] + } ], "types": [ - {"name": "Chapter", "definition": "{ id, textbookId, title, order, parentId, content?, children? }", "usedBy": ["textbooks/components", "questions (知识点关联)"]}, - {"name": "KnowledgePoint", "definition": "{ id, name, description?, anchorText?, parentId?, chapterId?, level, order }", "usedBy": ["textbooks/components", "questions/types"]}, - {"name": "Textbook", "type": "type", "definition": "{ id, title, subject, grade, publisher, createdAt, updatedAt }", "usedBy": ["textbooks/components", "textbooks/data-access"]}, - {"name": "CreateTextbookInput", "type": "type", "definition": "创建教材输入", "usedBy": ["createTextbook", "createTextbookAction"]}, - {"name": "UpdateTextbookInput", "type": "type", "definition": "更新教材输入", "usedBy": ["updateTextbook", "updateTextbookAction"]}, - {"name": "CreateChapterInput", "type": "type", "definition": "创建章节输入", "usedBy": ["createChapter", "createChapterAction"]}, - {"name": "UpdateChapterContentInput", "type": "type", "definition": "更新章节内容输入", "usedBy": ["updateChapterContent", "updateChapterContentAction"]}, - {"name": "CreateKnowledgePointInput", "type": "type", "definition": "创建知识点输入", "usedBy": ["createKnowledgePoint", "createKnowledgePointAction"]}, - {"name": "UpdateKnowledgePointInput", "type": "type", "definition": "更新知识点输入", "usedBy": ["updateKnowledgePoint", "updateKnowledgePointAction"]} + { + "name": "Chapter", + "definition": "{ id, textbookId, title, order, parentId, content?, children? }", + "usedBy": [ + "textbooks/components", + "questions (知识点关联)" + ] + }, + { + "name": "KnowledgePoint", + "definition": "{ id, name, description?, anchorText?, parentId?, chapterId?, level, order }", + "usedBy": [ + "textbooks/components", + "questions/types" + ] + }, + { + "name": "Textbook", + "type": "type", + "definition": "{ id, title, subject, grade, publisher, createdAt, updatedAt }", + "usedBy": [ + "textbooks/components", + "textbooks/data-access" + ] + }, + { + "name": "CreateTextbookInput", + "type": "type", + "definition": "创建教材输入", + "usedBy": [ + "createTextbook", + "createTextbookAction" + ] + }, + { + "name": "UpdateTextbookInput", + "type": "type", + "definition": "更新教材输入", + "usedBy": [ + "updateTextbook", + "updateTextbookAction" + ] + }, + { + "name": "CreateChapterInput", + "type": "type", + "definition": "创建章节输入", + "usedBy": [ + "createChapter", + "createChapterAction" + ] + }, + { + "name": "UpdateChapterContentInput", + "type": "type", + "definition": "更新章节内容输入", + "usedBy": [ + "updateChapterContent", + "updateChapterContentAction" + ] + }, + { + "name": "CreateKnowledgePointInput", + "type": "type", + "definition": "创建知识点输入", + "usedBy": [ + "createKnowledgePoint", + "createKnowledgePointAction" + ] + }, + { + "name": "UpdateKnowledgePointInput", + "type": "type", + "definition": "更新知识点输入", + "usedBy": [ + "updateKnowledgePoint", + "updateKnowledgePointAction" + ] + } ], "components": [ - {"name": "ChapterSidebarList", "purpose": "章节侧边栏列表"}, - {"name": "CreateChapterDialog", "purpose": "创建章节对话框"}, - {"name": "CreateKnowledgePointDialog", "purpose": "创建知识点对话框"}, - {"name": "KnowledgeGraph", "purpose": "知识图谱可视化"}, - {"name": "KnowledgePointDialogs", "purpose": "知识点对话框集合"}, - {"name": "KnowledgePointList", "purpose": "知识点列表"}, - {"name": "KnowledgePointPanel", "purpose": "知识点面板"}, - {"name": "TextbookCard", "purpose": "教材卡片"}, - {"name": "TextbookContentPanel", "purpose": "教材内容面板"}, - {"name": "TextbookFilters", "purpose": "教材筛选器"}, - {"name": "TextbookFormDialog", "purpose": "教材表单对话框"}, - {"name": "TextbookReader", "purpose": "教材阅读器"}, - {"name": "TextbookSettingsDialog", "purpose": "教材设置对话框"} + { + "name": "ChapterSidebarList", + "purpose": "章节侧边栏列表" + }, + { + "name": "CreateChapterDialog", + "purpose": "创建章节对话框" + }, + { + "name": "CreateKnowledgePointDialog", + "purpose": "创建知识点对话框" + }, + { + "name": "KnowledgeGraph", + "purpose": "知识图谱可视化" + }, + { + "name": "KnowledgePointDialogs", + "purpose": "知识点对话框集合" + }, + { + "name": "KnowledgePointList", + "purpose": "知识点列表" + }, + { + "name": "KnowledgePointPanel", + "purpose": "知识点面板" + }, + { + "name": "TextbookCard", + "purpose": "教材卡片" + }, + { + "name": "TextbookContentPanel", + "purpose": "教材内容面板" + }, + { + "name": "TextbookFilters", + "purpose": "教材筛选器" + }, + { + "name": "TextbookFormDialog", + "purpose": "教材表单对话框" + }, + { + "name": "TextbookReader", + "purpose": "教材阅读器" + }, + { + "name": "TextbookSettingsDialog", + "purpose": "教材设置对话框" + } ] } }, @@ -756,99 +3394,616 @@ "description": "班级管理:班级CRUD、学生注册/退班、邀请码、课表、学科教师分配", "exports": { "actions": [ - {"name": "createTeacherClassAction", "permission": "CLASS_CREATE", "signature": "(prevState, formData) => Promise>", "purpose": "教师创建班级"}, - {"name": "updateTeacherClassAction", "permission": "CLASS_UPDATE", "signature": "(classId, prevState, formData) => Promise", "purpose": "教师更新班级"}, - {"name": "deleteTeacherClassAction", "permission": "CLASS_DELETE", "signature": "(classId) => Promise", "purpose": "教师删除班级"}, - {"name": "createGradeClassAction", "permission": "CLASS_CREATE", "signature": "(prevState, formData) => Promise>", "purpose": "年级主任创建班级"}, - {"name": "updateGradeClassAction", "permission": "CLASS_UPDATE", "signature": "(classId, prevState, formData) => Promise", "purpose": "年级主任更新班级"}, - {"name": "deleteGradeClassAction", "permission": "CLASS_DELETE", "signature": "(classId) => Promise", "purpose": "年级主任删除班级"}, - {"name": "enrollStudentByEmailAction", "permission": "CLASS_ENROLL", "signature": "(classId, prevState, formData) => Promise", "purpose": "通过邮箱注册学生"}, - {"name": "joinClassByInvitationCodeAction", "permission": "CLASS_ENROLL", "signature": "(prevState, formData) => Promise>", "purpose": "通过邀请码加入"}, - {"name": "ensureClassInvitationCodeAction", "permission": "CLASS_ENROLL", "signature": "(classId) => Promise>", "purpose": "确保邀请码存在"}, - {"name": "regenerateClassInvitationCodeAction", "permission": "CLASS_ENROLL", "signature": "(classId) => Promise>", "purpose": "重新生成邀请码"}, - {"name": "setStudentEnrollmentStatusAction", "permission": "CLASS_ENROLL", "signature": "(classId, studentId, status) => Promise", "purpose": "设置学生状态"}, - {"name": "createClassScheduleItemAction", "permission": "CLASS_SCHEDULE", "signature": "(prevState, formData) => Promise>", "purpose": "创建课表项"}, - {"name": "updateClassScheduleItemAction", "permission": "CLASS_SCHEDULE", "signature": "(scheduleId, prevState, formData) => Promise", "purpose": "更新课表项"}, - {"name": "deleteClassScheduleItemAction", "permission": "CLASS_SCHEDULE", "signature": "(scheduleId) => Promise", "purpose": "删除课表项"}, - {"name": "createAdminClassAction", "permission": "CLASS_CREATE", "signature": "(prevState, formData) => Promise>", "purpose": "管理员创建班级"}, - {"name": "updateAdminClassAction", "permission": "CLASS_UPDATE", "signature": "(classId, prevState, formData) => Promise", "purpose": "管理员更新班级"}, - {"name": "deleteAdminClassAction", "permission": "CLASS_DELETE", "signature": "(classId) => Promise", "purpose": "管理员删除班级"} + { + "name": "createTeacherClassAction", + "permission": "CLASS_CREATE", + "signature": "(prevState, formData) => Promise>", + "purpose": "教师创建班级" + }, + { + "name": "updateTeacherClassAction", + "permission": "CLASS_UPDATE", + "signature": "(classId, prevState, formData) => Promise", + "purpose": "教师更新班级" + }, + { + "name": "deleteTeacherClassAction", + "permission": "CLASS_DELETE", + "signature": "(classId) => Promise", + "purpose": "教师删除班级" + }, + { + "name": "createGradeClassAction", + "permission": "CLASS_CREATE", + "signature": "(prevState, formData) => Promise>", + "purpose": "年级主任创建班级" + }, + { + "name": "updateGradeClassAction", + "permission": "CLASS_UPDATE", + "signature": "(classId, prevState, formData) => Promise", + "purpose": "年级主任更新班级" + }, + { + "name": "deleteGradeClassAction", + "permission": "CLASS_DELETE", + "signature": "(classId) => Promise", + "purpose": "年级主任删除班级" + }, + { + "name": "enrollStudentByEmailAction", + "permission": "CLASS_ENROLL", + "signature": "(classId, prevState, formData) => Promise", + "purpose": "通过邮箱注册学生" + }, + { + "name": "joinClassByInvitationCodeAction", + "permission": "CLASS_ENROLL", + "signature": "(prevState, formData) => Promise>", + "purpose": "通过邀请码加入" + }, + { + "name": "ensureClassInvitationCodeAction", + "permission": "CLASS_ENROLL", + "signature": "(classId) => Promise>", + "purpose": "确保邀请码存在" + }, + { + "name": "regenerateClassInvitationCodeAction", + "permission": "CLASS_ENROLL", + "signature": "(classId) => Promise>", + "purpose": "重新生成邀请码" + }, + { + "name": "setStudentEnrollmentStatusAction", + "permission": "CLASS_ENROLL", + "signature": "(classId, studentId, status) => Promise", + "purpose": "设置学生状态" + }, + { + "name": "createClassScheduleItemAction", + "permission": "CLASS_SCHEDULE", + "signature": "(prevState, formData) => Promise>", + "purpose": "创建课表项" + }, + { + "name": "updateClassScheduleItemAction", + "permission": "CLASS_SCHEDULE", + "signature": "(scheduleId, prevState, formData) => Promise", + "purpose": "更新课表项" + }, + { + "name": "deleteClassScheduleItemAction", + "permission": "CLASS_SCHEDULE", + "signature": "(scheduleId) => Promise", + "purpose": "删除课表项" + }, + { + "name": "createAdminClassAction", + "permission": "CLASS_CREATE", + "signature": "(prevState, formData) => Promise>", + "purpose": "管理员创建班级" + }, + { + "name": "updateAdminClassAction", + "permission": "CLASS_UPDATE", + "signature": "(classId, prevState, formData) => Promise", + "purpose": "管理员更新班级" + }, + { + "name": "deleteAdminClassAction", + "permission": "CLASS_DELETE", + "signature": "(classId) => Promise", + "purpose": "管理员删除班级" + } ], "dataAccess": [ - {"name": "getTeacherClasses", "signature": "(params?: { teacherId?: string }) => Promise", "usedBy": ["teacher/classes/my", "dashboard"]}, - {"name": "getAdminClasses", "signature": "() => Promise", "usedBy": ["admin班级管理"]}, - {"name": "getGradeManagedClasses", "signature": "(userId) => Promise", "usedBy": ["grade_head班级管理"]}, - {"name": "getStudentClasses", "signature": "(studentId) => Promise", "usedBy": ["student/dashboard"]}, - {"name": "getStudentSchedule", "signature": "(studentId) => Promise", "usedBy": ["student课表"]}, - {"name": "getClassStudents", "signature": "(params?: { classId?, q?, status?, teacherId? }) => Promise", "usedBy": ["teacher/classes/students"]}, - {"name": "getClassSchedule", "signature": "(params?: { classId?, teacherId? }) => Promise", "usedBy": ["teacher/classes/schedule"]}, - {"name": "getClassHomeworkInsights", "signature": "(params: { classId, teacherId?, limit? }) => Promise", "usedBy": ["classes作业洞察"]}, - {"name": "getGradeHomeworkInsights", "signature": "(params: { gradeId, limit? }) => Promise", "usedBy": ["年级作业洞察"]}, - {"name": "getTeacherIdForMutations", "signature": "() => Promise", "purpose": "获取当前教师ID(用于写操作)", "usedBy": ["classes写操作内部"]}, - {"name": "getClassSubjects", "signature": "(classId: string) => Promise", "purpose": "获取班级学科列表", "usedBy": ["class-detail组件"]}, - {"name": "getTeacherOptions", "signature": "() => Promise", "purpose": "获取教师选项列表", "usedBy": ["class-detail组件"]}, - {"name": "getTeacherTeachingSubjects", "signature": "(teacherId: string) => Promise<...>", "purpose": "获取教师所教学科", "usedBy": ["classes内部"]}, - {"name": "getManagedGrades", "signature": "(userId: string) => Promise<...>", "purpose": "获取所管年级", "usedBy": ["grade_head视图"]}, - {"name": "getStudentsSubjectScores", "signature": "(...) => Promise<...>", "purpose": "获取学生学科成绩", "usedBy": ["classes内部"]}, - {"name": "getClassStudentSubjectScoresV2", "signature": "(...) => Promise<...>", "purpose": "获取班级学生学科成绩V2", "usedBy": ["classes内部"]}, - {"name": "createTeacherClass", "signature": "(input) => Promise", "purpose": "教师创建班级", "usedBy": ["createTeacherClassAction"]}, - {"name": "createAdminClass", "signature": "(input) => Promise", "purpose": "管理员创建班级", "usedBy": ["createAdminClassAction"]}, - {"name": "ensureClassInvitationCode", "signature": "(classId: string) => Promise<{ code: string }>", "purpose": "确保邀请码存在", "usedBy": ["ensureClassInvitationCodeAction"]}, - {"name": "regenerateClassInvitationCode", "signature": "(classId: string) => Promise<{ code: string }>", "purpose": "重新生成邀请码", "usedBy": ["regenerateClassInvitationCodeAction"]}, - {"name": "enrollStudentByInvitationCode", "signature": "(code: string, studentId: string) => Promise<{ classId: string }>", "purpose": "通过邀请码注册学生", "usedBy": ["joinClassByInvitationCodeAction"]}, - {"name": "enrollTeacherByInvitationCode", "signature": "(code: string, teacherId: string) => Promise<...>", "purpose": "通过邀请码注册教师", "usedBy": ["classes内部"]}, - {"name": "updateTeacherClass", "signature": "(classId: string, input) => Promise", "purpose": "教师更新班级", "usedBy": ["updateTeacherClassAction"]}, - {"name": "updateAdminClass", "signature": "(classId: string, input) => Promise", "purpose": "管理员更新班级", "usedBy": ["updateAdminClassAction"]}, - {"name": "setClassSubjectTeachers", "signature": "(classId: string, assignments: ClassSubjectTeacherAssignment[]) => Promise", "purpose": "设置班级学科教师", "usedBy": ["classes内部"]}, - {"name": "deleteTeacherClass", "signature": "(classId: string) => Promise", "purpose": "教师删除班级", "usedBy": ["deleteTeacherClassAction"]}, - {"name": "deleteAdminClass", "signature": "(classId: string) => Promise", "purpose": "管理员删除班级", "usedBy": ["deleteAdminClassAction"]}, - {"name": "enrollStudentByEmail", "signature": "(classId: string, email: string) => Promise", "purpose": "通过邮箱注册学生", "usedBy": ["enrollStudentByEmailAction"]}, - {"name": "setStudentEnrollmentStatus", "signature": "(classId: string, studentId: string, status: string) => Promise", "purpose": "设置学生状态", "usedBy": ["setStudentEnrollmentStatusAction"]}, - {"name": "createClassScheduleItem", "signature": "(input: CreateClassScheduleItemInput) => Promise", "purpose": "创建课表项", "usedBy": ["createClassScheduleItemAction"]}, - {"name": "updateClassScheduleItem", "signature": "(scheduleId: string, input: UpdateClassScheduleItemInput) => Promise", "purpose": "更新课表项", "usedBy": ["updateClassScheduleItemAction"]}, - {"name": "deleteClassScheduleItem", "signature": "(scheduleId: string) => Promise", "purpose": "删除课表项", "usedBy": ["deleteClassScheduleItemAction"]} + { + "name": "getTeacherClasses", + "signature": "(params?: { teacherId?: string }) => Promise", + "usedBy": [ + "teacher/classes/my", + "dashboard" + ] + }, + { + "name": "getAdminClasses", + "signature": "() => Promise", + "usedBy": [ + "admin班级管理" + ] + }, + { + "name": "getGradeManagedClasses", + "signature": "(userId) => Promise", + "usedBy": [ + "grade_head班级管理" + ] + }, + { + "name": "getStudentClasses", + "signature": "(studentId) => Promise", + "usedBy": [ + "student/dashboard" + ] + }, + { + "name": "getStudentSchedule", + "signature": "(studentId) => Promise", + "usedBy": [ + "student课表" + ] + }, + { + "name": "getClassStudents", + "signature": "(params?: { classId?, q?, status?, teacherId? }) => Promise", + "usedBy": [ + "teacher/classes/students" + ] + }, + { + "name": "getClassSchedule", + "signature": "(params?: { classId?, teacherId? }) => Promise", + "usedBy": [ + "teacher/classes/schedule" + ] + }, + { + "name": "getClassHomeworkInsights", + "signature": "(params: { classId, teacherId?, limit? }) => Promise", + "usedBy": [ + "classes作业洞察" + ] + }, + { + "name": "getGradeHomeworkInsights", + "signature": "(params: { gradeId, limit? }) => Promise", + "usedBy": [ + "年级作业洞察" + ] + }, + { + "name": "getTeacherIdForMutations", + "signature": "() => Promise", + "purpose": "获取当前教师ID(用于写操作)", + "usedBy": [ + "classes写操作内部" + ] + }, + { + "name": "getClassSubjects", + "signature": "(classId: string) => Promise", + "purpose": "获取班级学科列表", + "usedBy": [ + "class-detail组件" + ] + }, + { + "name": "getTeacherOptions", + "signature": "() => Promise", + "purpose": "获取教师选项列表", + "usedBy": [ + "class-detail组件" + ] + }, + { + "name": "getTeacherTeachingSubjects", + "signature": "(teacherId: string) => Promise<...>", + "purpose": "获取教师所教学科", + "usedBy": [ + "classes内部" + ] + }, + { + "name": "getManagedGrades", + "signature": "(userId: string) => Promise<...>", + "purpose": "获取所管年级", + "usedBy": [ + "grade_head视图" + ] + }, + { + "name": "getStudentsSubjectScores", + "signature": "(...) => Promise<...>", + "purpose": "获取学生学科成绩", + "usedBy": [ + "classes内部" + ] + }, + { + "name": "getClassStudentSubjectScoresV2", + "signature": "(...) => Promise<...>", + "purpose": "获取班级学生学科成绩V2", + "usedBy": [ + "classes内部" + ] + }, + { + "name": "createTeacherClass", + "signature": "(input) => Promise", + "purpose": "教师创建班级", + "usedBy": [ + "createTeacherClassAction" + ] + }, + { + "name": "createAdminClass", + "signature": "(input) => Promise", + "purpose": "管理员创建班级", + "usedBy": [ + "createAdminClassAction" + ] + }, + { + "name": "ensureClassInvitationCode", + "signature": "(classId: string) => Promise<{ code: string }>", + "purpose": "确保邀请码存在", + "usedBy": [ + "ensureClassInvitationCodeAction" + ] + }, + { + "name": "regenerateClassInvitationCode", + "signature": "(classId: string) => Promise<{ code: string }>", + "purpose": "重新生成邀请码", + "usedBy": [ + "regenerateClassInvitationCodeAction" + ] + }, + { + "name": "enrollStudentByInvitationCode", + "signature": "(code: string, studentId: string) => Promise<{ classId: string }>", + "purpose": "通过邀请码注册学生", + "usedBy": [ + "joinClassByInvitationCodeAction" + ] + }, + { + "name": "enrollTeacherByInvitationCode", + "signature": "(code: string, teacherId: string) => Promise<...>", + "purpose": "通过邀请码注册教师", + "usedBy": [ + "classes内部" + ] + }, + { + "name": "updateTeacherClass", + "signature": "(classId: string, input) => Promise", + "purpose": "教师更新班级", + "usedBy": [ + "updateTeacherClassAction" + ] + }, + { + "name": "updateAdminClass", + "signature": "(classId: string, input) => Promise", + "purpose": "管理员更新班级", + "usedBy": [ + "updateAdminClassAction" + ] + }, + { + "name": "setClassSubjectTeachers", + "signature": "(classId: string, assignments: ClassSubjectTeacherAssignment[]) => Promise", + "purpose": "设置班级学科教师", + "usedBy": [ + "classes内部" + ] + }, + { + "name": "deleteTeacherClass", + "signature": "(classId: string) => Promise", + "purpose": "教师删除班级", + "usedBy": [ + "deleteTeacherClassAction" + ] + }, + { + "name": "deleteAdminClass", + "signature": "(classId: string) => Promise", + "purpose": "管理员删除班级", + "usedBy": [ + "deleteAdminClassAction" + ] + }, + { + "name": "enrollStudentByEmail", + "signature": "(classId: string, email: string) => Promise", + "purpose": "通过邮箱注册学生", + "usedBy": [ + "enrollStudentByEmailAction" + ] + }, + { + "name": "setStudentEnrollmentStatus", + "signature": "(classId: string, studentId: string, status: string) => Promise", + "purpose": "设置学生状态", + "usedBy": [ + "setStudentEnrollmentStatusAction" + ] + }, + { + "name": "createClassScheduleItem", + "signature": "(input: CreateClassScheduleItemInput) => Promise", + "purpose": "创建课表项", + "usedBy": [ + "createClassScheduleItemAction" + ] + }, + { + "name": "updateClassScheduleItem", + "signature": "(scheduleId: string, input: UpdateClassScheduleItemInput) => Promise", + "purpose": "更新课表项", + "usedBy": [ + "updateClassScheduleItemAction" + ] + }, + { + "name": "deleteClassScheduleItem", + "signature": "(scheduleId: string) => Promise", + "purpose": "删除课表项", + "usedBy": [ + "deleteClassScheduleItemAction" + ] + } ], "types": [ - {"name": "TeacherClass", "type": "type", "definition": "教师班级对象", "usedBy": ["getTeacherClasses", "dashboard"]}, - {"name": "AssignmentSummary", "type": "type", "definition": "作业摘要", "usedBy": ["classes/components"]}, - {"name": "TeacherOption", "type": "type", "definition": "教师选项", "usedBy": ["getTeacherOptions", "class-detail组件"]}, - {"name": "DEFAULT_CLASS_SUBJECTS", "type": "const", "definition": "默认班级学科列表", "usedBy": ["classes内部"]}, - {"name": "ClassSubject", "type": "type", "definition": "班级学科", "usedBy": ["getClassSubjects", "classes/components"]}, - {"name": "ClassSubjectTeacherAssignment", "type": "type", "definition": "班级学科教师分配", "usedBy": ["setClassSubjectTeachers"]}, - {"name": "AdminClassListItem", "type": "type", "definition": "管理员班级列表项", "usedBy": ["getAdminClasses", "getGradeManagedClasses"]}, - {"name": "CreateTeacherClassInput", "type": "type", "definition": "教师创建班级输入", "usedBy": ["createTeacherClass"]}, - {"name": "UpdateTeacherClassInput", "type": "type", "definition": "教师更新班级输入", "usedBy": ["updateTeacherClass"]}, - {"name": "ClassStudent", "type": "type", "definition": "班级学生", "usedBy": ["getClassStudents", "classes/components"]}, - {"name": "ClassScheduleItem", "type": "type", "definition": "班级课表项", "usedBy": ["getClassSchedule", "classes/components"]}, - {"name": "CreateClassScheduleItemInput", "type": "type", "definition": "创建课表项输入", "usedBy": ["createClassScheduleItem"]}, - {"name": "UpdateClassScheduleItemInput", "type": "type", "definition": "更新课表项输入", "usedBy": ["updateClassScheduleItem"]}, - {"name": "StudentEnrolledClass", "type": "type", "definition": "学生已注册班级", "usedBy": ["getStudentClasses", "dashboard"]}, - {"name": "StudentScheduleItem", "type": "type", "definition": "学生课表项", "usedBy": ["getStudentSchedule", "dashboard"]}, - {"name": "ScoreStats", "type": "type", "definition": "成绩统计", "usedBy": ["classes/components"]}, - {"name": "ClassHomeworkAssignmentStats", "type": "type", "definition": "班级作业统计", "usedBy": ["classes/components"]}, - {"name": "ClassHomeworkInsights", "type": "type", "definition": "班级作业洞察", "usedBy": ["getClassHomeworkInsights", "classes/components"]}, - {"name": "GradeHomeworkClassSummary", "type": "type", "definition": "年级作业班级汇总", "usedBy": ["classes/components"]}, - {"name": "GradeHomeworkInsights", "type": "type", "definition": "年级作业洞察", "usedBy": ["getGradeHomeworkInsights", "classes/components"]} + { + "name": "TeacherClass", + "type": "type", + "definition": "教师班级对象", + "usedBy": [ + "getTeacherClasses", + "dashboard" + ] + }, + { + "name": "AssignmentSummary", + "type": "type", + "definition": "作业摘要", + "usedBy": [ + "classes/components" + ] + }, + { + "name": "TeacherOption", + "type": "type", + "definition": "教师选项", + "usedBy": [ + "getTeacherOptions", + "class-detail组件" + ] + }, + { + "name": "DEFAULT_CLASS_SUBJECTS", + "type": "const", + "definition": "默认班级学科列表", + "usedBy": [ + "classes内部" + ] + }, + { + "name": "ClassSubject", + "type": "type", + "definition": "班级学科", + "usedBy": [ + "getClassSubjects", + "classes/components" + ] + }, + { + "name": "ClassSubjectTeacherAssignment", + "type": "type", + "definition": "班级学科教师分配", + "usedBy": [ + "setClassSubjectTeachers" + ] + }, + { + "name": "AdminClassListItem", + "type": "type", + "definition": "管理员班级列表项", + "usedBy": [ + "getAdminClasses", + "getGradeManagedClasses" + ] + }, + { + "name": "CreateTeacherClassInput", + "type": "type", + "definition": "教师创建班级输入", + "usedBy": [ + "createTeacherClass" + ] + }, + { + "name": "UpdateTeacherClassInput", + "type": "type", + "definition": "教师更新班级输入", + "usedBy": [ + "updateTeacherClass" + ] + }, + { + "name": "ClassStudent", + "type": "type", + "definition": "班级学生", + "usedBy": [ + "getClassStudents", + "classes/components" + ] + }, + { + "name": "ClassScheduleItem", + "type": "type", + "definition": "班级课表项", + "usedBy": [ + "getClassSchedule", + "classes/components" + ] + }, + { + "name": "CreateClassScheduleItemInput", + "type": "type", + "definition": "创建课表项输入", + "usedBy": [ + "createClassScheduleItem" + ] + }, + { + "name": "UpdateClassScheduleItemInput", + "type": "type", + "definition": "更新课表项输入", + "usedBy": [ + "updateClassScheduleItem" + ] + }, + { + "name": "StudentEnrolledClass", + "type": "type", + "definition": "学生已注册班级", + "usedBy": [ + "getStudentClasses", + "dashboard" + ] + }, + { + "name": "StudentScheduleItem", + "type": "type", + "definition": "学生课表项", + "usedBy": [ + "getStudentSchedule", + "dashboard" + ] + }, + { + "name": "ScoreStats", + "type": "type", + "definition": "成绩统计", + "usedBy": [ + "classes/components" + ] + }, + { + "name": "ClassHomeworkAssignmentStats", + "type": "type", + "definition": "班级作业统计", + "usedBy": [ + "classes/components" + ] + }, + { + "name": "ClassHomeworkInsights", + "type": "type", + "definition": "班级作业洞察", + "usedBy": [ + "getClassHomeworkInsights", + "classes/components" + ] + }, + { + "name": "GradeHomeworkClassSummary", + "type": "type", + "definition": "年级作业班级汇总", + "usedBy": [ + "classes/components" + ] + }, + { + "name": "GradeHomeworkInsights", + "type": "type", + "definition": "年级作业洞察", + "usedBy": [ + "getGradeHomeworkInsights", + "classes/components" + ] + } ], "components": [ - {"name": "StudentsTable", "purpose": "学生表格"}, - {"name": "StudentsFilters", "purpose": "学生筛选器"}, - {"name": "ScheduleView", "purpose": "课表视图"}, - {"name": "MyClassesGrid", "purpose": "我的班级网格"}, - {"name": "AdminClassesClient", "purpose": "管理员班级客户端"}, - {"name": "ScheduleFilters", "purpose": "课表筛选器"}, - {"name": "GradeClassesClient", "purpose": "年级班级客户端"}, - {"name": "transformAssignmentsToChartData", "file": "class-detail/transformAssignmentsToChartData", "type": "function", "purpose": "转换作业为图表数据"}, - {"name": "ClassSubmissionTrendChart", "file": "class-detail/ClassSubmissionTrendChart", "purpose": "班级提交趋势图表"}, - {"name": "ClassTrendsWidget", "file": "class-detail/ClassTrendsWidget", "purpose": "班级趋势组件"}, - {"name": "ClassStudentsWidget", "file": "class-detail/ClassStudentsWidget", "purpose": "班级学生组件"}, - {"name": "ClassScheduleGrid", "file": "class-detail/ClassScheduleGrid", "purpose": "班级课表网格"}, - {"name": "ClassScheduleWidget", "file": "class-detail/ClassScheduleWidget", "purpose": "班级课表组件"}, - {"name": "ClassQuickActions", "file": "class-detail/ClassQuickActions", "purpose": "班级快捷操作"}, - {"name": "EditClassDialog", "file": "class-detail/EditClassDialog", "purpose": "编辑班级对话框"}, - {"name": "ClassOverviewStats", "file": "class-detail/ClassOverviewStats", "purpose": "班级概览统计"}, - {"name": "ClassHeader", "file": "class-detail/ClassHeader", "purpose": "班级头部"}, - {"name": "ClassAssignmentsWidget", "file": "class-detail/ClassAssignmentsWidget", "purpose": "班级作业组件"} + { + "name": "StudentsTable", + "purpose": "学生表格" + }, + { + "name": "StudentsFilters", + "purpose": "学生筛选器" + }, + { + "name": "ScheduleView", + "purpose": "课表视图" + }, + { + "name": "MyClassesGrid", + "purpose": "我的班级网格" + }, + { + "name": "AdminClassesClient", + "purpose": "管理员班级客户端" + }, + { + "name": "ScheduleFilters", + "purpose": "课表筛选器" + }, + { + "name": "GradeClassesClient", + "purpose": "年级班级客户端" + }, + { + "name": "transformAssignmentsToChartData", + "file": "class-detail/transformAssignmentsToChartData", + "type": "function", + "purpose": "转换作业为图表数据" + }, + { + "name": "ClassSubmissionTrendChart", + "file": "class-detail/ClassSubmissionTrendChart", + "purpose": "班级提交趋势图表" + }, + { + "name": "ClassTrendsWidget", + "file": "class-detail/ClassTrendsWidget", + "purpose": "班级趋势组件" + }, + { + "name": "ClassStudentsWidget", + "file": "class-detail/ClassStudentsWidget", + "purpose": "班级学生组件" + }, + { + "name": "ClassScheduleGrid", + "file": "class-detail/ClassScheduleGrid", + "purpose": "班级课表网格" + }, + { + "name": "ClassScheduleWidget", + "file": "class-detail/ClassScheduleWidget", + "purpose": "班级课表组件" + }, + { + "name": "ClassQuickActions", + "file": "class-detail/ClassQuickActions", + "purpose": "班级快捷操作" + }, + { + "name": "EditClassDialog", + "file": "class-detail/EditClassDialog", + "purpose": "编辑班级对话框" + }, + { + "name": "ClassOverviewStats", + "file": "class-detail/ClassOverviewStats", + "purpose": "班级概览统计" + }, + { + "name": "ClassHeader", + "file": "class-detail/ClassHeader", + "purpose": "班级头部" + }, + { + "name": "ClassAssignmentsWidget", + "file": "class-detail/ClassAssignmentsWidget", + "purpose": "班级作业组件" + } ] } }, @@ -857,45 +4012,232 @@ "description": "学校基础数据管理:学校、年级、部门、学年的CRUD。学校CRUD actions在写操作成功后调用logAudit()记录操作日志", "exports": { "actions": [ - {"name": "createSchoolAction", "permission": "SCHOOL_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建学校", "auditLog": "school.create"}, - {"name": "updateSchoolAction", "permission": "SCHOOL_MANAGE", "signature": "(schoolId, prevState, formData) => Promise>", "purpose": "更新学校", "auditLog": "school.update"}, - {"name": "deleteSchoolAction", "permission": "SCHOOL_MANAGE", "signature": "(schoolId) => Promise>", "purpose": "删除学校", "auditLog": "school.delete"}, - {"name": "createGradeAction", "permission": "GRADE_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建年级"}, - {"name": "updateGradeAction", "permission": "GRADE_MANAGE", "signature": "(gradeId, prevState, formData) => Promise>", "purpose": "更新年级"}, - {"name": "deleteGradeAction", "permission": "GRADE_MANAGE", "signature": "(gradeId) => Promise>", "purpose": "删除年级"}, - {"name": "createDepartmentAction", "permission": "SCHOOL_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建部门"}, - {"name": "updateDepartmentAction", "permission": "SCHOOL_MANAGE", "signature": "(departmentId, prevState, formData) => Promise>", "purpose": "更新部门"}, - {"name": "deleteDepartmentAction", "permission": "SCHOOL_MANAGE", "signature": "(departmentId) => Promise>", "purpose": "删除部门"}, - {"name": "createAcademicYearAction", "permission": "SCHOOL_MANAGE", "signature": "(prevState, formData) => Promise>", "purpose": "创建学年"}, - {"name": "updateAcademicYearAction", "permission": "SCHOOL_MANAGE", "signature": "(academicYearId, prevState, formData) => Promise>", "purpose": "更新学年"}, - {"name": "deleteAcademicYearAction", "permission": "SCHOOL_MANAGE", "signature": "(academicYearId) => Promise>", "purpose": "删除学年"} + { + "name": "createSchoolAction", + "permission": "SCHOOL_MANAGE", + "signature": "(prevState, formData) => Promise>", + "purpose": "创建学校", + "auditLog": "school.create" + }, + { + "name": "updateSchoolAction", + "permission": "SCHOOL_MANAGE", + "signature": "(schoolId, prevState, formData) => Promise>", + "purpose": "更新学校", + "auditLog": "school.update" + }, + { + "name": "deleteSchoolAction", + "permission": "SCHOOL_MANAGE", + "signature": "(schoolId) => Promise>", + "purpose": "删除学校", + "auditLog": "school.delete" + }, + { + "name": "createGradeAction", + "permission": "GRADE_MANAGE", + "signature": "(prevState, formData) => Promise>", + "purpose": "创建年级" + }, + { + "name": "updateGradeAction", + "permission": "GRADE_MANAGE", + "signature": "(gradeId, prevState, formData) => Promise>", + "purpose": "更新年级" + }, + { + "name": "deleteGradeAction", + "permission": "GRADE_MANAGE", + "signature": "(gradeId) => Promise>", + "purpose": "删除年级" + }, + { + "name": "createDepartmentAction", + "permission": "SCHOOL_MANAGE", + "signature": "(prevState, formData) => Promise>", + "purpose": "创建部门" + }, + { + "name": "updateDepartmentAction", + "permission": "SCHOOL_MANAGE", + "signature": "(departmentId, prevState, formData) => Promise>", + "purpose": "更新部门" + }, + { + "name": "deleteDepartmentAction", + "permission": "SCHOOL_MANAGE", + "signature": "(departmentId) => Promise>", + "purpose": "删除部门" + }, + { + "name": "createAcademicYearAction", + "permission": "SCHOOL_MANAGE", + "signature": "(prevState, formData) => Promise>", + "purpose": "创建学年" + }, + { + "name": "updateAcademicYearAction", + "permission": "SCHOOL_MANAGE", + "signature": "(academicYearId, prevState, formData) => Promise>", + "purpose": "更新学年" + }, + { + "name": "deleteAcademicYearAction", + "permission": "SCHOOL_MANAGE", + "signature": "(academicYearId) => Promise>", + "purpose": "删除学年" + } ], "dataAccess": [ - {"name": "getSchools", "signature": "() => Promise", "usedBy": ["admin学校管理", "onboarding"]}, - {"name": "getGrades", "signature": "() => Promise", "usedBy": ["admin年级管理", "exams", "onboarding"]}, - {"name": "getDepartments", "signature": "() => Promise", "usedBy": ["admin部门管理"]}, - {"name": "getAcademicYears", "signature": "() => Promise", "usedBy": ["admin学年管理"]}, - {"name": "getStaffOptions", "signature": "() => Promise", "usedBy": ["school组件"]}, - {"name": "getGradesForStaff", "signature": "(staffId) => Promise", "usedBy": ["grade_head视图"]} + { + "name": "getSchools", + "signature": "() => Promise", + "usedBy": [ + "admin学校管理", + "onboarding" + ] + }, + { + "name": "getGrades", + "signature": "() => Promise", + "usedBy": [ + "admin年级管理", + "exams", + "onboarding" + ] + }, + { + "name": "getDepartments", + "signature": "() => Promise", + "usedBy": [ + "admin部门管理" + ] + }, + { + "name": "getAcademicYears", + "signature": "() => Promise", + "usedBy": [ + "admin学年管理" + ] + }, + { + "name": "getStaffOptions", + "signature": "() => Promise", + "usedBy": [ + "school组件" + ] + }, + { + "name": "getGradesForStaff", + "signature": "(staffId) => Promise", + "usedBy": [ + "grade_head视图" + ] + } ], "schema": [ - {"name": "UpsertDepartmentSchema", "type": "const", "description": "zod schema 部门upsert", "usedBy": ["createDepartmentAction", "updateDepartmentAction"]}, - {"name": "UpsertAcademicYearSchema", "type": "const", "description": "zod schema 学年upsert", "usedBy": ["createAcademicYearAction", "updateAcademicYearAction"]}, - {"name": "UpsertSchoolSchema", "type": "const", "description": "zod schema 学校upsert", "usedBy": ["createSchoolAction", "updateSchoolAction"]}, - {"name": "UpsertGradeSchema", "type": "const", "description": "zod schema 年级upsert", "usedBy": ["createGradeAction", "updateGradeAction"]} + { + "name": "UpsertDepartmentSchema", + "type": "const", + "description": "zod schema 部门upsert", + "usedBy": [ + "createDepartmentAction", + "updateDepartmentAction" + ] + }, + { + "name": "UpsertAcademicYearSchema", + "type": "const", + "description": "zod schema 学年upsert", + "usedBy": [ + "createAcademicYearAction", + "updateAcademicYearAction" + ] + }, + { + "name": "UpsertSchoolSchema", + "type": "const", + "description": "zod schema 学校upsert", + "usedBy": [ + "createSchoolAction", + "updateSchoolAction" + ] + }, + { + "name": "UpsertGradeSchema", + "type": "const", + "description": "zod schema 年级upsert", + "usedBy": [ + "createGradeAction", + "updateGradeAction" + ] + } ], "types": [ - {"name": "DepartmentListItem", "type": "type", "definition": "部门列表项", "usedBy": ["getDepartments", "school/components"]}, - {"name": "AcademicYearListItem", "type": "type", "definition": "学年列表项", "usedBy": ["getAcademicYears", "school/components"]}, - {"name": "SchoolListItem", "type": "type", "definition": "学校列表项", "usedBy": ["getSchools", "school/components"]}, - {"name": "StaffOption", "type": "type", "definition": "员工选项", "usedBy": ["getStaffOptions", "school/components"]}, - {"name": "GradeListItem", "type": "type", "definition": "年级列表项", "usedBy": ["getGrades", "school/components", "exams"]} + { + "name": "DepartmentListItem", + "type": "type", + "definition": "部门列表项", + "usedBy": [ + "getDepartments", + "school/components" + ] + }, + { + "name": "AcademicYearListItem", + "type": "type", + "definition": "学年列表项", + "usedBy": [ + "getAcademicYears", + "school/components" + ] + }, + { + "name": "SchoolListItem", + "type": "type", + "definition": "学校列表项", + "usedBy": [ + "getSchools", + "school/components" + ] + }, + { + "name": "StaffOption", + "type": "type", + "definition": "员工选项", + "usedBy": [ + "getStaffOptions", + "school/components" + ] + }, + { + "name": "GradeListItem", + "type": "type", + "definition": "年级列表项", + "usedBy": [ + "getGrades", + "school/components", + "exams" + ] + } ], "components": [ - {"name": "SchoolsClient", "purpose": "学校管理客户端"}, - {"name": "GradesClient", "purpose": "年级管理客户端"}, - {"name": "DepartmentsClient", "purpose": "部门管理客户端"}, - {"name": "AcademicYearClient", "purpose": "学年管理客户端"} + { + "name": "SchoolsClient", + "purpose": "学校管理客户端" + }, + { + "name": "GradesClient", + "purpose": "年级管理客户端" + }, + { + "name": "DepartmentsClient", + "purpose": "部门管理客户端" + }, + { + "name": "AcademicYearClient", + "purpose": "学年管理客户端" + } ] } }, @@ -904,34 +4246,161 @@ "description": "各角色仪表盘数据聚合与展示", "exports": { "dataAccess": [ - {"name": "getAdminDashboardData", "signature": "(scope?: DataScope) => Promise", "deps": ["shared/db", "DataScope"], "usedBy": ["admin/dashboard/page.tsx"]} + { + "name": "getAdminDashboardData", + "signature": "(scope?: DataScope) => Promise", + "deps": [ + "shared/db", + "DataScope" + ], + "usedBy": [ + "admin/dashboard/page.tsx" + ] + } ], "types": [ - {"name": "StudentDashboardProps", "definition": "{ studentName, enrolledClassCount, dueSoonCount, overdueCount, gradedCount, todayScheduleItems, upcomingAssignments, grades }", "deps": ["homework/types.StudentDashboardGradeProps"], "usedBy": ["student-dashboard-view.tsx"]}, - {"name": "TeacherDashboardData", "definition": "{ classes, schedule, assignments, submissions, teacherName, gradeTrends }", "deps": ["homework/data-access.getTeacherGradeTrends", "classes/data-access.getTeacherClasses"], "usedBy": ["teacher-dashboard-view.tsx"]}, - {"name": "AdminDashboardData", "definition": "{ activeSessionsCount, userCount, userRoleCounts, classCount, ... }", "usedBy": ["admin/dashboard/page.tsx"]}, - {"name": "AdminDashboardUserRoleCount", "type": "type", "definition": "管理员仪表盘用户角色计数", "usedBy": ["admin-dashboard/AdminDashboardView"]}, - {"name": "AdminDashboardRecentUser", "type": "type", "definition": "管理员仪表盘最近用户", "usedBy": ["admin-dashboard/AdminDashboardView"]}, - {"name": "StudentTodayScheduleItem", "type": "type", "definition": "学生今日课表项", "usedBy": ["student-dashboard/StudentTodayScheduleCard"]}, - {"name": "TeacherTodayScheduleItem", "type": "type", "definition": "教师今日课表项", "usedBy": ["teacher-dashboard/TeacherSchedule"]} + { + "name": "StudentDashboardProps", + "definition": "{ studentName, enrolledClassCount, dueSoonCount, overdueCount, gradedCount, todayScheduleItems, upcomingAssignments, grades }", + "deps": [ + "homework/types.StudentDashboardGradeProps" + ], + "usedBy": [ + "student-dashboard-view.tsx" + ] + }, + { + "name": "TeacherDashboardData", + "definition": "{ classes, schedule, assignments, submissions, teacherName, gradeTrends }", + "deps": [ + "homework/data-access.getTeacherGradeTrends", + "classes/data-access.getTeacherClasses" + ], + "usedBy": [ + "teacher-dashboard-view.tsx" + ] + }, + { + "name": "AdminDashboardData", + "definition": "{ activeSessionsCount, userCount, userRoleCounts, classCount, ... }", + "usedBy": [ + "admin/dashboard/page.tsx" + ] + }, + { + "name": "AdminDashboardUserRoleCount", + "type": "type", + "definition": "管理员仪表盘用户角色计数", + "usedBy": [ + "admin-dashboard/AdminDashboardView" + ] + }, + { + "name": "AdminDashboardRecentUser", + "type": "type", + "definition": "管理员仪表盘最近用户", + "usedBy": [ + "admin-dashboard/AdminDashboardView" + ] + }, + { + "name": "StudentTodayScheduleItem", + "type": "type", + "definition": "学生今日课表项", + "usedBy": [ + "student-dashboard/StudentTodayScheduleCard" + ] + }, + { + "name": "TeacherTodayScheduleItem", + "type": "type", + "definition": "教师今日课表项", + "usedBy": [ + "teacher-dashboard/TeacherSchedule" + ] + } ], "components": [ - {"name": "AdminDashboardView", "file": "admin-dashboard/AdminDashboardView", "purpose": "管理员仪表盘视图"}, - {"name": "StudentDashboard", "file": "student-dashboard/StudentDashboard", "purpose": "学生仪表盘(注意:非StudentDashboardView)"}, - {"name": "StudentDashboardHeader", "file": "student-dashboard/StudentDashboardHeader", "purpose": "学生仪表盘头部"}, - {"name": "StudentGradesCard", "file": "student-dashboard/StudentGradesCard", "purpose": "学生成绩卡片"}, - {"name": "StudentStatsGrid", "file": "student-dashboard/StudentStatsGrid", "purpose": "学生统计网格"}, - {"name": "StudentTodayScheduleCard", "file": "student-dashboard/StudentTodayScheduleCard", "purpose": "学生今日课表卡片"}, - {"name": "StudentUpcomingAssignmentsCard", "file": "student-dashboard/StudentUpcomingAssignmentsCard", "purpose": "学生即将到来作业卡片"}, - {"name": "TeacherDashboardView", "file": "teacher-dashboard/TeacherDashboardView", "purpose": "教师仪表盘视图"}, - {"name": "TeacherClassesCard", "file": "teacher-dashboard/TeacherClassesCard", "purpose": "教师班级卡片"}, - {"name": "TeacherDashboardHeader", "file": "teacher-dashboard/TeacherDashboardHeader", "purpose": "教师仪表盘头部"}, - {"name": "TeacherGradeTrends", "file": "teacher-dashboard/TeacherGradeTrends", "purpose": "教师年级趋势"}, - {"name": "TeacherHomeworkCard", "file": "teacher-dashboard/TeacherHomeworkCard", "purpose": "教师作业卡片"}, - {"name": "TeacherQuickActions", "file": "teacher-dashboard/TeacherQuickActions", "purpose": "教师快捷操作"}, - {"name": "TeacherSchedule", "file": "teacher-dashboard/TeacherSchedule", "purpose": "教师课表"}, - {"name": "TeacherStats", "file": "teacher-dashboard/TeacherStats", "purpose": "教师统计"}, - {"name": "RecentSubmissions", "file": "teacher-dashboard/RecentSubmissions", "purpose": "最近提交"} + { + "name": "AdminDashboardView", + "file": "admin-dashboard/AdminDashboardView", + "purpose": "管理员仪表盘视图" + }, + { + "name": "StudentDashboard", + "file": "student-dashboard/StudentDashboard", + "purpose": "学生仪表盘(注意:非StudentDashboardView)" + }, + { + "name": "StudentDashboardHeader", + "file": "student-dashboard/StudentDashboardHeader", + "purpose": "学生仪表盘头部" + }, + { + "name": "StudentGradesCard", + "file": "student-dashboard/StudentGradesCard", + "purpose": "学生成绩卡片" + }, + { + "name": "StudentStatsGrid", + "file": "student-dashboard/StudentStatsGrid", + "purpose": "学生统计网格" + }, + { + "name": "StudentTodayScheduleCard", + "file": "student-dashboard/StudentTodayScheduleCard", + "purpose": "学生今日课表卡片" + }, + { + "name": "StudentUpcomingAssignmentsCard", + "file": "student-dashboard/StudentUpcomingAssignmentsCard", + "purpose": "学生即将到来作业卡片" + }, + { + "name": "TeacherDashboardView", + "file": "teacher-dashboard/TeacherDashboardView", + "purpose": "教师仪表盘视图" + }, + { + "name": "TeacherClassesCard", + "file": "teacher-dashboard/TeacherClassesCard", + "purpose": "教师班级卡片" + }, + { + "name": "TeacherDashboardHeader", + "file": "teacher-dashboard/TeacherDashboardHeader", + "purpose": "教师仪表盘头部" + }, + { + "name": "TeacherGradeTrends", + "file": "teacher-dashboard/TeacherGradeTrends", + "purpose": "教师年级趋势" + }, + { + "name": "TeacherHomeworkCard", + "file": "teacher-dashboard/TeacherHomeworkCard", + "purpose": "教师作业卡片" + }, + { + "name": "TeacherQuickActions", + "file": "teacher-dashboard/TeacherQuickActions", + "purpose": "教师快捷操作" + }, + { + "name": "TeacherSchedule", + "file": "teacher-dashboard/TeacherSchedule", + "purpose": "教师课表" + }, + { + "name": "TeacherStats", + "file": "teacher-dashboard/TeacherStats", + "purpose": "教师统计" + }, + { + "name": "RecentSubmissions", + "file": "teacher-dashboard/RecentSubmissions", + "purpose": "最近提交" + } ] } }, @@ -940,17 +4409,61 @@ "description": "应用布局框架:侧边栏、顶栏、导航配置", "exports": { "components": [ - {"name": "AppSidebar", "purpose": "根据权限渲染侧边栏导航", "internalDeps": ["usePermission", "NAV_CONFIG"]}, - {"name": "SiteHeader", "purpose": "顶部导航栏(集成 GlobalSearch 全局搜索:Cmd/Ctrl+K 唤起、300ms 防抖、↑/↓ 导航、Enter 跳转;NotificationDropdown 通知下拉)", "internalDeps": ["useSession", "signOut", "shared/components/global-search.GlobalSearch", "messaging/components/notification-dropdown.NotificationDropdown"]}, - {"name": "SidebarProvider", "props": "{ children, sidebar }", "purpose": "侧边栏上下文Provider"}, - {"name": "useSidebar", "type": "hook", "purpose": "侧边栏状态Hook"} + { + "name": "AppSidebar", + "purpose": "根据权限渲染侧边栏导航", + "internalDeps": [ + "usePermission", + "NAV_CONFIG" + ] + }, + { + "name": "SiteHeader", + "purpose": "顶部导航栏(集成 GlobalSearch 全局搜索:Cmd/Ctrl+K 唤起、300ms 防抖、↑/↓ 导航、Enter 跳转;NotificationDropdown 通知下拉)", + "internalDeps": [ + "useSession", + "signOut", + "shared/components/global-search.GlobalSearch", + "messaging/components/notification-dropdown.NotificationDropdown" + ] + }, + { + "name": "SidebarProvider", + "props": "{ children, sidebar }", + "purpose": "侧边栏上下文Provider" + }, + { + "name": "useSidebar", + "type": "hook", + "purpose": "侧边栏状态Hook" + } ], "types": [ - {"name": "Role", "type": "type", "definition": "\"admin\" | \"teacher\" | \"student\" | \"parent\"", "usedBy": ["NAV_CONFIG", "usePermission"]}, - {"name": "NavItem", "type": "type", "definition": "{ title, href, icon?, permission? }", "usedBy": ["NAV_CONFIG", "AppSidebar"]} + { + "name": "Role", + "type": "type", + "definition": "\"admin\" | \"teacher\" | \"student\" | \"parent\"", + "usedBy": [ + "NAV_CONFIG", + "usePermission" + ] + }, + { + "name": "NavItem", + "type": "type", + "definition": "{ title, href, icon?, permission? }", + "usedBy": [ + "NAV_CONFIG", + "AppSidebar" + ] + } ], "config": [ - {"name": "NAV_CONFIG", "type": "Record", "note": "每个NavItem含permission字段用于权限过滤。admin角色菜单包含Audit Logs项(icon: ScrollText, href: /admin/audit-logs, permission: AUDIT_LOG_READ),含子项Operation Logs与Login Logs。admin角色菜单的School Management子菜单包含Import Users项(href: /admin/users/import, permission: USER_MANAGE)。admin角色菜单包含Scheduling项(icon: CalendarClock, href: /admin/scheduling/rules, permission: SCHEDULE_ADJUST),含子项Rules(/admin/scheduling/rules, permission: SCHEDULE_ADJUST)、Auto Schedule(/admin/scheduling/auto, permission: SCHEDULE_AUTO)、Change Requests(/admin/scheduling/changes, permission: SCHEDULE_ADJUST)。teacher角色菜单包含Grades项(icon: GraduationCap, permission: GRADE_RECORD_READ),含子项All Grades(/teacher/grades)、Batch Entry(/teacher/grades/entry, permission: GRADE_RECORD_MANAGE)、Statistics(/teacher/grades/stats)。teacher角色菜单包含Schedule Changes项(icon: CalendarClock, href: /teacher/schedule-changes, permission: SCHEDULE_ADJUST)。teacher角色菜单包含Diagnostic项(icon: Stethoscope, href: /teacher/diagnostic, permission: DIAGNOSTIC_READ)。student角色菜单包含My Grades项(icon: GraduationCap, href: /student/grades, permission: GRADE_RECORD_READ)。student角色菜单包含Diagnostic项(icon: Stethoscope, href: /student/diagnostic, permission: DIAGNOSTIC_READ)。parent角色菜单包含Dashboard项(icon: LayoutDashboard, href: /parent/dashboard,无permission字段仅需登录)、Grades项(icon: GraduationCap, href: /parent/grades, permission: GRADE_RECORD_READ)、Announcements项(icon: Megaphone, href: /announcements, permission: ANNOUNCEMENT_READ)"} + { + "name": "NAV_CONFIG", + "type": "Record", + "note": "每个NavItem含permission字段用于权限过滤。admin角色菜单包含Audit Logs项(icon: ScrollText, href: /admin/audit-logs, permission: AUDIT_LOG_READ),含子项Operation Logs与Login Logs。admin角色菜单的School Management子菜单包含Import Users项(href: /admin/users/import, permission: USER_MANAGE)。admin角色菜单包含Scheduling项(icon: CalendarClock, href: /admin/scheduling/rules, permission: SCHEDULE_ADJUST),含子项Rules(/admin/scheduling/rules, permission: SCHEDULE_ADJUST)、Auto Schedule(/admin/scheduling/auto, permission: SCHEDULE_AUTO)、Change Requests(/admin/scheduling/changes, permission: SCHEDULE_ADJUST)。teacher角色菜单包含Grades项(icon: GraduationCap, permission: GRADE_RECORD_READ),含子项All Grades(/teacher/grades)、Batch Entry(/teacher/grades/entry, permission: GRADE_RECORD_MANAGE)、Statistics(/teacher/grades/stats)。teacher角色菜单包含Schedule Changes项(icon: CalendarClock, href: /teacher/schedule-changes, permission: SCHEDULE_ADJUST)。teacher角色菜单包含Diagnostic项(icon: Stethoscope, href: /teacher/diagnostic, permission: DIAGNOSTIC_READ)。student角色菜单包含My Grades项(icon: GraduationCap, href: /student/grades, permission: GRADE_RECORD_READ)。student角色菜单包含Diagnostic项(icon: Stethoscope, href: /student/diagnostic, permission: DIAGNOSTIC_READ)。parent角色菜单包含Dashboard项(icon: LayoutDashboard, href: /parent/dashboard,无permission字段仅需登录)、Grades项(icon: GraduationCap, href: /parent/grades, permission: GRADE_RECORD_READ)、Announcements项(icon: Megaphone, href: /announcements, permission: ANNOUNCEMENT_READ)" + } ] } }, @@ -959,23 +4472,109 @@ "description": "系统设置:AI Provider配置、用户偏好、密码安全(修改密码、强度校验)", "exports": { "actions": [ - {"name": "getAiProviderSummaries", "permission": "AI_CONFIGURE", "signature": "() => Promise", "purpose": "获取AI Provider列表"}, - {"name": "upsertAiProviderAction", "permission": "AI_CONFIGURE", "signature": "(data) => Promise>", "purpose": "创建/更新AI Provider", "deps": ["shared/lib/ai (encrypt/decrypt)"]}, - {"name": "testAiProviderAction", "permission": "AI_CONFIGURE", "signature": "(data) => Promise>", "purpose": "测试AI Provider连通性", "deps": ["shared/lib/ai.testAiProviderConfig"]}, - {"name": "changePasswordAction", "file": "actions-password.ts", "permission": "requireAuth()", "signature": "(prevState: ActionState, formData: FormData) => Promise>", "purpose": "修改当前用户密码(校验当前密码、新密码策略、速率限制 PASSWORD_CHANGE: 5次/分钟)", "deps": ["requireAuth", "validatePassword", "rateLimit", "bcryptjs (hash/compare)", "shared/db (users, passwordSecurity)"], "usedBy": ["components/password-change-form.tsx"]} + { + "name": "getAiProviderSummaries", + "permission": "AI_CONFIGURE", + "signature": "() => Promise", + "purpose": "获取AI Provider列表" + }, + { + "name": "upsertAiProviderAction", + "permission": "AI_CONFIGURE", + "signature": "(data) => Promise>", + "purpose": "创建/更新AI Provider", + "deps": [ + "shared/lib/ai (encrypt/decrypt)" + ] + }, + { + "name": "testAiProviderAction", + "permission": "AI_CONFIGURE", + "signature": "(data) => Promise>", + "purpose": "测试AI Provider连通性", + "deps": [ + "shared/lib/ai.testAiProviderConfig" + ] + }, + { + "name": "changePasswordAction", + "file": "actions-password.ts", + "permission": "requireAuth()", + "signature": "(prevState: ActionState, formData: FormData) => Promise>", + "purpose": "修改当前用户密码(校验当前密码、新密码策略、速率限制 PASSWORD_CHANGE: 5次/分钟)", + "deps": [ + "requireAuth", + "validatePassword", + "rateLimit", + "bcryptjs (hash/compare)", + "shared/db (users, passwordSecurity)" + ], + "usedBy": [ + "components/password-change-form.tsx" + ] + } ], "types": [ - {"name": "AiProviderSummary", "type": "type", "definition": "AI Provider摘要", "usedBy": ["getAiProviderSummaries", "settings/components"]} + { + "name": "AiProviderSummary", + "type": "type", + "definition": "AI Provider摘要", + "usedBy": [ + "getAiProviderSummaries", + "settings/components" + ] + } ], "components": [ - {"name": "AiProviderSettingsCard", "purpose": "AI Provider设置卡片"}, - {"name": "AdminSettingsView", "purpose": "管理员设置视图(General/Appearance/Security/Notifications tab,Security 含 PasswordChangeForm,Notifications 含 NotificationPreferencesForm)"}, - {"name": "ProfileSettingsForm", "purpose": "个人资料设置表单"}, - {"name": "ThemePreferencesCard", "purpose": "主题偏好卡片"}, - {"name": "StudentSettingsView", "purpose": "学生设置视图(含 Notifications tab)"}, - {"name": "TeacherSettingsView", "purpose": "教师设置视图(含 Notifications tab)"}, - {"name": "PasswordChangeForm", "purpose": "密码修改表单(当前密码/新密码/确认密码 + 强度指示器 + 需求提示)", "deps": ["changePasswordAction", "getPasswordStrength", "PASSWORD_REQUIREMENT_HINTS"]}, - {"name": "NotificationPreferencesForm", "file": "components/notification-preferences-form.tsx", "purpose": "通知偏好设置表单(Switch 切换 email/sms/push 通道 + 5 个分类开关:作业/成绩/公告/消息/考勤;隐藏 checkbox 与 Switch 同步,useActionState 调用 updateNotificationPreferencesAction)", "deps": ["updateNotificationPreferencesAction", "shared/components/ui/switch", "shared/components/ui/card", "react.useActionState"], "usedBy": ["AdminSettingsView", "TeacherSettingsView", "StudentSettingsView"]} + { + "name": "AiProviderSettingsCard", + "purpose": "AI Provider设置卡片" + }, + { + "name": "AdminSettingsView", + "purpose": "管理员设置视图(General/Appearance/Security/Notifications tab,Security 含 PasswordChangeForm,Notifications 含 NotificationPreferencesForm)" + }, + { + "name": "ProfileSettingsForm", + "purpose": "个人资料设置表单" + }, + { + "name": "ThemePreferencesCard", + "purpose": "主题偏好卡片" + }, + { + "name": "StudentSettingsView", + "purpose": "学生设置视图(含 Notifications tab)" + }, + { + "name": "TeacherSettingsView", + "purpose": "教师设置视图(含 Notifications tab)" + }, + { + "name": "PasswordChangeForm", + "purpose": "密码修改表单(当前密码/新密码/确认密码 + 强度指示器 + 需求提示)", + "deps": [ + "changePasswordAction", + "getPasswordStrength", + "PASSWORD_REQUIREMENT_HINTS" + ] + }, + { + "name": "NotificationPreferencesForm", + "file": "components/notification-preferences-form.tsx", + "purpose": "通知偏好设置表单(Switch 切换 email/sms/push 通道 + 5 个分类开关:作业/成绩/公告/消息/考勤;隐藏 checkbox 与 Switch 同步,useActionState 调用 updateNotificationPreferencesAction)", + "deps": [ + "updateNotificationPreferencesAction", + "shared/components/ui/switch", + "shared/components/ui/card", + "react.useActionState" + ], + "usedBy": [ + "AdminSettingsView", + "TeacherSettingsView", + "StudentSettingsView" + ] + } ] } }, @@ -984,29 +4583,185 @@ "description": "用户个人资料管理 + 用户批量导入/导出(Excel)", "exports": { "actions": [ - { "name": "updateUserProfile", "signature": "(data: UpdateUserProfileInput) => Promise", "file": "actions.ts", "permission": "requireAuth()", "deps": ["shared.db", "shared.db.schema.users", "shared.lib.auth-guard.requireAuth"] }, - { "name": "UpdateUserProfileInput", "type": "type", "file": "actions.ts", "definition": "{ name?, phone?, address?, gender?, age? }" }, - { "name": "downloadUserTemplateAction", "signature": "() => Promise>", "file": "actions.ts", "permission": "USER_MANAGE", "purpose": "生成用户导入模板(返回 base64 编码的 Excel)", "deps": ["requirePermission", "import-export.generateUserImportTemplate"], "usedBy": ["components/user-import-dialog.tsx"] }, - { "name": "importUsersAction", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "permission": "USER_MANAGE", "purpose": "导入用户:接收文件,解析+验证+批量创建(默认密码 123456)", "deps": ["requirePermission", "shared.lib.excel.parseExcel", "import-export.parseUserImportData", "import-export.batchImportUsers"], "usedBy": ["components/user-import-dialog.tsx"] }, - { "name": "exportUsersAction", "signature": "(role?: string) => Promise>", "file": "actions.ts", "permission": "USER_MANAGE", "purpose": "导出用户列表(返回 base64 编码的 Excel)", "deps": ["requirePermission", "import-export.exportUsersToExcel"], "usedBy": ["待扩展"] } + { + "name": "updateUserProfile", + "signature": "(data: UpdateUserProfileInput) => Promise", + "file": "actions.ts", + "permission": "requireAuth()", + "deps": [ + "shared.db", + "shared.db.schema.users", + "shared.lib.auth-guard.requireAuth" + ] + }, + { + "name": "UpdateUserProfileInput", + "type": "type", + "file": "actions.ts", + "definition": "{ name?, phone?, address?, gender?, age? }" + }, + { + "name": "downloadUserTemplateAction", + "signature": "() => Promise>", + "file": "actions.ts", + "permission": "USER_MANAGE", + "purpose": "生成用户导入模板(返回 base64 编码的 Excel)", + "deps": [ + "requirePermission", + "import-export.generateUserImportTemplate" + ], + "usedBy": [ + "components/user-import-dialog.tsx" + ] + }, + { + "name": "importUsersAction", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "permission": "USER_MANAGE", + "purpose": "导入用户:接收文件,解析+验证+批量创建(默认密码 123456)", + "deps": [ + "requirePermission", + "shared.lib.excel.parseExcel", + "import-export.parseUserImportData", + "import-export.batchImportUsers" + ], + "usedBy": [ + "components/user-import-dialog.tsx" + ] + }, + { + "name": "exportUsersAction", + "signature": "(role?: string) => Promise>", + "file": "actions.ts", + "permission": "USER_MANAGE", + "purpose": "导出用户列表(返回 base64 编码的 Excel)", + "deps": [ + "requirePermission", + "import-export.exportUsersToExcel" + ], + "usedBy": [ + "待扩展" + ] + } ], "dataAccess": [ - { "name": "getUserProfile", "signature": "(userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.users"] }, - { "name": "UserProfile", "type": "type", "file": "data-access.ts", "definition": "{ id, name, email, image, role, phone, address, gender, age, onboardedAt, createdAt, updatedAt }" } + { + "name": "getUserProfile", + "signature": "(userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.users" + ] + }, + { + "name": "UserProfile", + "type": "type", + "file": "data-access.ts", + "definition": "{ id, name, email, image, role, phone, address, gender, age, onboardedAt, createdAt, updatedAt }" + } ], "importExport": [ - { "name": "generateUserImportTemplate", "signature": "() => Promise", "file": "import-export.ts", "purpose": "生成用户导入模板(列:姓名/邮箱/角色/手机/班级邀请码,含示例行)", "deps": ["shared.lib.excel.generateTemplate"], "usedBy": ["actions.downloadUserTemplateAction"] }, - { "name": "parseUserImportData", "signature": "(rows: Record[]) => UserImportValidation", "file": "import-export.ts", "purpose": "解析并验证导入行(校验姓名/邮箱格式/角色枚举/邀请码仅 student)", "deps": [], "usedBy": ["actions.importUsersAction"] }, - { "name": "batchImportUsers", "signature": "(records: UserImportRecord[]) => Promise", "file": "import-export.ts", "purpose": "批量创建用户(默认密码 123456 bcrypt 哈希,自动创建 usersToRoles,student 通过邀请码自动加入班级)", "deps": ["shared.db", "shared.db.schema.users", "shared.db.schema.roles", "shared.db.schema.usersToRoles", "shared.db.schema.classes", "shared.db.schema.classEnrollments", "bcryptjs", "@paralleldrive/cuid2"], "usedBy": ["actions.importUsersAction"] }, - { "name": "exportUsersToExcel", "signature": "(params: { scope: DataScope; role?: string }) => Promise", "file": "import-export.ts", "purpose": "导出用户列表到 Excel(含姓名/邮箱/手机/性别/年龄/角色/创建时间)", "deps": ["shared.db", "shared.db.schema.users", "shared.db.schema.roles", "shared.db.schema.usersToRoles", "shared.lib.excel.exportToExcel"], "usedBy": ["actions.exportUsersAction", "app/api/export/route.ts"] } + { + "name": "generateUserImportTemplate", + "signature": "() => Promise", + "file": "import-export.ts", + "purpose": "生成用户导入模板(列:姓名/邮箱/角色/手机/班级邀请码,含示例行)", + "deps": [ + "shared.lib.excel.generateTemplate" + ], + "usedBy": [ + "actions.downloadUserTemplateAction" + ] + }, + { + "name": "parseUserImportData", + "signature": "(rows: Record[]) => UserImportValidation", + "file": "import-export.ts", + "purpose": "解析并验证导入行(校验姓名/邮箱格式/角色枚举/邀请码仅 student)", + "deps": [], + "usedBy": [ + "actions.importUsersAction" + ] + }, + { + "name": "batchImportUsers", + "signature": "(records: UserImportRecord[]) => Promise", + "file": "import-export.ts", + "purpose": "批量创建用户(默认密码 123456 bcrypt 哈希,自动创建 usersToRoles,student 通过邀请码自动加入班级)", + "deps": [ + "shared.db", + "shared.db.schema.users", + "shared.db.schema.roles", + "shared.db.schema.usersToRoles", + "shared.db.schema.classes", + "shared.db.schema.classEnrollments", + "bcryptjs", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "actions.importUsersAction" + ] + }, + { + "name": "exportUsersToExcel", + "signature": "(params: { scope: DataScope; role?: string }) => Promise", + "file": "import-export.ts", + "purpose": "导出用户列表到 Excel(含姓名/邮箱/手机/性别/年龄/角色/创建时间)", + "deps": [ + "shared.db", + "shared.db.schema.users", + "shared.db.schema.roles", + "shared.db.schema.usersToRoles", + "shared.lib.excel.exportToExcel" + ], + "usedBy": [ + "actions.exportUsersAction", + "app/api/export/route.ts" + ] + } ], "types": [ - { "name": "UserImportRecord", "type": "type", "file": "import-export.ts", "definition": "{ name, email, role, phone?, invitationCode? }", "usedBy": ["parseUserImportData", "batchImportUsers"] }, - { "name": "UserImportValidation", "type": "type", "file": "import-export.ts", "definition": "{ valid: UserImportRecord[], invalid: Array<{ row, record, errors }> }", "usedBy": ["parseUserImportData"] }, - { "name": "UserImportResult", "type": "type", "file": "import-export.ts", "definition": "{ successCount, failedCount, errors: Array<{ row, email, error }> }", "usedBy": ["batchImportUsers", "importUsersAction"] } + { + "name": "UserImportRecord", + "type": "type", + "file": "import-export.ts", + "definition": "{ name, email, role, phone?, invitationCode? }", + "usedBy": [ + "parseUserImportData", + "batchImportUsers" + ] + }, + { + "name": "UserImportValidation", + "type": "type", + "file": "import-export.ts", + "definition": "{ valid: UserImportRecord[], invalid: Array<{ row, record, errors }> }", + "usedBy": [ + "parseUserImportData" + ] + }, + { + "name": "UserImportResult", + "type": "type", + "file": "import-export.ts", + "definition": "{ successCount, failedCount, errors: Array<{ row, email, error }> }", + "usedBy": [ + "batchImportUsers", + "importUsersAction" + ] + } ], "components": [ - { "name": "UserImportDialog", "file": "components/user-import-dialog.tsx", "purpose": "用户批量导入对话框(4 状态:idle/preview/importing/done;下载模板→上传预览→确认导入→结果展示)", "usedBy": ["app/(dashboard)/admin/users/import/page.tsx"] } + { + "name": "UserImportDialog", + "file": "components/user-import-dialog.tsx", + "purpose": "用户批量导入对话框(4 状态:idle/preview/importing/done;下载模板→上传预览→确认导入→结果展示)", + "usedBy": [ + "app/(dashboard)/admin/users/import/page.tsx" + ] + } ] } }, @@ -1015,40 +4770,281 @@ "description": "操作日志、登录日志与数据变更日志查询,支持 Excel 导出", "exports": { "dataAccess": [ - { "name": "getAuditLogs", "signature": "(params?: AuditLogQueryParams) => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.auditLogs"], "usedBy": ["app/(dashboard)/admin/audit-logs/page.tsx"] }, - { "name": "getLoginLogs", "signature": "(params?: LoginLogQueryParams) => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.loginLogs"], "usedBy": ["app/(dashboard)/admin/audit-logs/login-logs/page.tsx"] }, - { "name": "getAuditModuleOptions", "signature": "() => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.auditLogs"], "usedBy": ["app/(dashboard)/admin/audit-logs/page.tsx"] }, - { "name": "getDataChangeLogs", "signature": "(params?: DataChangeLogQueryParams) => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.dataChangeLogs"], "usedBy": ["getDataChangeLogsAction"] }, - { "name": "getDataChangeStats", "signature": "() => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.dataChangeLogs"], "usedBy": ["getDataChangeLogsAction"] }, - { "name": "getDataChangeTableOptions", "signature": "() => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.dataChangeLogs"], "usedBy": ["getDataChangeLogsAction"] }, - { "name": "getDataChangeLogsForExport", "signature": "(params?: DataChangeLogQueryParams) => Promise", "file": "data-access.ts", "deps": ["getDataChangeLogs"], "usedBy": ["exportDataChangeLogsAction"] }, - { "name": "getAuditLogsForExport", "signature": "(params?: AuditLogQueryParams) => Promise", "file": "data-access.ts", "deps": ["getAuditLogs"], "usedBy": ["exportAuditLogsAction"] }, - { "name": "getLoginLogsForExport", "signature": "(params?: LoginLogQueryParams) => Promise", "file": "data-access.ts", "deps": ["getLoginLogs"], "usedBy": ["exportLoginLogsAction"] } + { + "name": "getAuditLogs", + "signature": "(params?: AuditLogQueryParams) => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.auditLogs" + ], + "usedBy": [ + "app/(dashboard)/admin/audit-logs/page.tsx" + ] + }, + { + "name": "getLoginLogs", + "signature": "(params?: LoginLogQueryParams) => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.loginLogs" + ], + "usedBy": [ + "app/(dashboard)/admin/audit-logs/login-logs/page.tsx" + ] + }, + { + "name": "getAuditModuleOptions", + "signature": "() => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.auditLogs" + ], + "usedBy": [ + "app/(dashboard)/admin/audit-logs/page.tsx" + ] + }, + { + "name": "getDataChangeLogs", + "signature": "(params?: DataChangeLogQueryParams) => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.dataChangeLogs" + ], + "usedBy": [ + "getDataChangeLogsAction" + ] + }, + { + "name": "getDataChangeStats", + "signature": "() => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.dataChangeLogs" + ], + "usedBy": [ + "getDataChangeLogsAction" + ] + }, + { + "name": "getDataChangeTableOptions", + "signature": "() => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.dataChangeLogs" + ], + "usedBy": [ + "getDataChangeLogsAction" + ] + }, + { + "name": "getDataChangeLogsForExport", + "signature": "(params?: DataChangeLogQueryParams) => Promise", + "file": "data-access.ts", + "deps": [ + "getDataChangeLogs" + ], + "usedBy": [ + "exportDataChangeLogsAction" + ] + }, + { + "name": "getAuditLogsForExport", + "signature": "(params?: AuditLogQueryParams) => Promise", + "file": "data-access.ts", + "deps": [ + "getAuditLogs" + ], + "usedBy": [ + "exportAuditLogsAction" + ] + }, + { + "name": "getLoginLogsForExport", + "signature": "(params?: LoginLogQueryParams) => Promise", + "file": "data-access.ts", + "deps": [ + "getLoginLogs" + ], + "usedBy": [ + "exportLoginLogsAction" + ] + } ], "actions": [ - { "name": "getDataChangeLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: DataChangeLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "获取数据变更日志(分页结果 + tableOptions + stats 三者并行加载)", "deps": ["requirePermission", "data-access.getDataChangeLogs", "data-access.getDataChangeTableOptions", "data-access.getDataChangeStats"], "usedBy": ["待扩展"] }, - { "name": "exportAuditLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: AuditLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "导出操作日志为 Excel", "deps": ["requirePermission", "data-access.getAuditLogsForExport", "shared.lib.excel.exportToExcel"], "usedBy": ["待扩展"] }, - { "name": "exportLoginLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: LoginLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "导出登录日志为 Excel", "deps": ["requirePermission", "data-access.getLoginLogsForExport", "shared.lib.excel.exportToExcel"], "usedBy": ["待扩展"] }, - { "name": "exportDataChangeLogsAction", "permission": "AUDIT_LOG_READ", "signature": "(params?: DataChangeLogQueryParams) => Promise>", "file": "actions.ts", "purpose": "导出数据变更日志为 Excel", "deps": ["requirePermission", "data-access.getDataChangeLogsForExport", "shared.lib.excel.exportToExcel"], "usedBy": ["待扩展"] } + { + "name": "getDataChangeLogsAction", + "permission": "AUDIT_LOG_READ", + "signature": "(params?: DataChangeLogQueryParams) => Promise>", + "file": "actions.ts", + "purpose": "获取数据变更日志(分页结果 + tableOptions + stats 三者并行加载)", + "deps": [ + "requirePermission", + "data-access.getDataChangeLogs", + "data-access.getDataChangeTableOptions", + "data-access.getDataChangeStats" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "exportAuditLogsAction", + "permission": "AUDIT_LOG_READ", + "signature": "(params?: AuditLogQueryParams) => Promise>", + "file": "actions.ts", + "purpose": "导出操作日志为 Excel", + "deps": [ + "requirePermission", + "data-access.getAuditLogsForExport", + "shared.lib.excel.exportToExcel" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "exportLoginLogsAction", + "permission": "AUDIT_LOG_READ", + "signature": "(params?: LoginLogQueryParams) => Promise>", + "file": "actions.ts", + "purpose": "导出登录日志为 Excel", + "deps": [ + "requirePermission", + "data-access.getLoginLogsForExport", + "shared.lib.excel.exportToExcel" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "exportDataChangeLogsAction", + "permission": "AUDIT_LOG_READ", + "signature": "(params?: DataChangeLogQueryParams) => Promise>", + "file": "actions.ts", + "purpose": "导出数据变更日志为 Excel", + "deps": [ + "requirePermission", + "data-access.getDataChangeLogsForExport", + "shared.lib.excel.exportToExcel" + ], + "usedBy": [ + "待扩展" + ] + } ], "types": [ - { "name": "AuditLog", "type": "interface", "file": "types.ts", "definition": "{ id, userId, userName, action, module, targetId, targetType, detail, ipAddress, userAgent, status, createdAt }" }, - { "name": "LoginLog", "type": "interface", "file": "types.ts", "definition": "{ id, userId, userEmail, action, status, ipAddress, userAgent, errorMessage, createdAt }" }, - { "name": "AuditLogQueryParams", "type": "type", "file": "types.ts", "definition": "{ userId?, module?, action?, status?, page?, pageSize?, startDate?, endDate? }" }, - { "name": "LoginLogQueryParams", "type": "type", "file": "types.ts", "definition": "{ userId?, action?, status?, page?, pageSize?, startDate?, endDate? }" }, - { "name": "PaginatedResult", "type": "interface", "file": "types.ts", "definition": "{ items: T[], total, page, pageSize, totalPages }" }, - { "name": "DataChangeAction", "type": "type", "file": "types.ts", "definition": "'create' | 'update' | 'delete'", "usedBy": ["data-access", "shared/lib/change-logger", "DataChangeLog"] }, - { "name": "DataChangeLog", "type": "interface", "file": "types.ts", "definition": "{ id, tableName, recordId, action, oldValue, newValue, changedBy, changedByName, ipAddress, createdAt }", "usedBy": ["audit/data-access", "audit/actions"] }, - { "name": "DataChangeStat", "type": "interface", "file": "types.ts", "definition": "{ tableName: string, count: number }", "usedBy": ["getDataChangeStats", "getDataChangeLogsAction"] }, - { "name": "DataChangeLogQueryParams", "type": "type", "file": "types.ts", "definition": "{ tableName?, recordId?, action?, userId?, page?, pageSize?, startDate?, endDate? }", "usedBy": ["getDataChangeLogs", "getDataChangeLogsForExport", "getDataChangeLogsAction", "exportDataChangeLogsAction"] } + { + "name": "AuditLog", + "type": "interface", + "file": "types.ts", + "definition": "{ id, userId, userName, action, module, targetId, targetType, detail, ipAddress, userAgent, status, createdAt }" + }, + { + "name": "LoginLog", + "type": "interface", + "file": "types.ts", + "definition": "{ id, userId, userEmail, action, status, ipAddress, userAgent, errorMessage, createdAt }" + }, + { + "name": "AuditLogQueryParams", + "type": "type", + "file": "types.ts", + "definition": "{ userId?, module?, action?, status?, page?, pageSize?, startDate?, endDate? }" + }, + { + "name": "LoginLogQueryParams", + "type": "type", + "file": "types.ts", + "definition": "{ userId?, action?, status?, page?, pageSize?, startDate?, endDate? }" + }, + { + "name": "PaginatedResult", + "type": "interface", + "file": "types.ts", + "definition": "{ items: T[], total, page, pageSize, totalPages }" + }, + { + "name": "DataChangeAction", + "type": "type", + "file": "types.ts", + "definition": "'create' | 'update' | 'delete'", + "usedBy": [ + "data-access", + "shared/lib/change-logger", + "DataChangeLog" + ] + }, + { + "name": "DataChangeLog", + "type": "interface", + "file": "types.ts", + "definition": "{ id, tableName, recordId, action, oldValue, newValue, changedBy, changedByName, ipAddress, createdAt }", + "usedBy": [ + "audit/data-access", + "audit/actions" + ] + }, + { + "name": "DataChangeStat", + "type": "interface", + "file": "types.ts", + "definition": "{ tableName: string, count: number }", + "usedBy": [ + "getDataChangeStats", + "getDataChangeLogsAction" + ] + }, + { + "name": "DataChangeLogQueryParams", + "type": "type", + "file": "types.ts", + "definition": "{ tableName?, recordId?, action?, userId?, page?, pageSize?, startDate?, endDate? }", + "usedBy": [ + "getDataChangeLogs", + "getDataChangeLogsForExport", + "getDataChangeLogsAction", + "exportDataChangeLogsAction" + ] + } ], "components": [ - { "name": "AuditLogTable", "file": "components/audit-log-table.tsx", "purpose": "操作日志表格(分页)" }, - { "name": "AuditLogFilters", "file": "components/audit-log-filters.tsx", "purpose": "操作日志筛选器(模块/操作/状态/日期)" }, - { "name": "AuditLogView", "file": "components/audit-log-view.tsx", "purpose": "操作日志视图(筛选+表格+分页)" }, - { "name": "LoginLogTable", "file": "components/login-log-table.tsx", "purpose": "登录日志表格(分页)" }, - { "name": "LoginLogFilters", "file": "components/login-log-filters.tsx", "purpose": "登录日志筛选器(操作/状态/日期)" }, - { "name": "LoginLogView", "file": "components/login-log-view.tsx", "purpose": "登录日志视图(筛选+表格+分页)" } + { + "name": "AuditLogTable", + "file": "components/audit-log-table.tsx", + "purpose": "操作日志表格(分页)" + }, + { + "name": "AuditLogFilters", + "file": "components/audit-log-filters.tsx", + "purpose": "操作日志筛选器(模块/操作/状态/日期)" + }, + { + "name": "AuditLogView", + "file": "components/audit-log-view.tsx", + "purpose": "操作日志视图(筛选+表格+分页)" + }, + { + "name": "LoginLogTable", + "file": "components/login-log-table.tsx", + "purpose": "登录日志表格(分页)" + }, + { + "name": "LoginLogFilters", + "file": "components/login-log-filters.tsx", + "purpose": "登录日志筛选器(操作/状态/日期)" + }, + { + "name": "LoginLogView", + "file": "components/login-log-view.tsx", + "purpose": "登录日志视图(筛选+表格+分页)" + } ] } }, @@ -1057,34 +5053,209 @@ "description": "通知公告系统:创建、编辑、发布、归档、删除公告,所有登录用户可查看已发布公告", "exports": { "actions": [ - { "name": "createAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建公告(草稿/已发布)", "deps": ["requirePermission", "shared/db"], "usedBy": ["announcement-form.tsx"] }, - { "name": "updateAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新公告", "deps": ["requirePermission", "shared/db"], "usedBy": ["announcement-form.tsx"] }, - { "name": "deleteAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除公告", "deps": ["requirePermission", "shared/db"], "usedBy": ["announcement-detail.tsx"] }, - { "name": "publishAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string) => Promise>", "purpose": "发布公告", "deps": ["requirePermission", "shared/db"], "usedBy": ["announcement-detail.tsx"] }, - { "name": "archiveAnnouncementAction", "permission": "ANNOUNCEMENT_MANAGE", "signature": "(id: string) => Promise>", "purpose": "归档公告", "deps": ["requirePermission", "shared/db"], "usedBy": ["announcement-detail.tsx"] }, - { "name": "getAnnouncementsAction", "permission": "requireAuth", "signature": "(params?: GetAnnouncementsParams) => Promise>", "purpose": "获取公告列表(所有登录用户可读)", "deps": ["requireAuth", "data-access.getAnnouncements"], "usedBy": ["待扩展"] } + { + "name": "createAnnouncementAction", + "permission": "ANNOUNCEMENT_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "创建公告(草稿/已发布)", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "announcement-form.tsx" + ] + }, + { + "name": "updateAnnouncementAction", + "permission": "ANNOUNCEMENT_MANAGE", + "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "更新公告", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "announcement-form.tsx" + ] + }, + { + "name": "deleteAnnouncementAction", + "permission": "ANNOUNCEMENT_MANAGE", + "signature": "(id: string) => Promise>", + "purpose": "删除公告", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "announcement-detail.tsx" + ] + }, + { + "name": "publishAnnouncementAction", + "permission": "ANNOUNCEMENT_MANAGE", + "signature": "(id: string) => Promise>", + "purpose": "发布公告", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "announcement-detail.tsx" + ] + }, + { + "name": "archiveAnnouncementAction", + "permission": "ANNOUNCEMENT_MANAGE", + "signature": "(id: string) => Promise>", + "purpose": "归档公告", + "deps": [ + "requirePermission", + "shared/db" + ], + "usedBy": [ + "announcement-detail.tsx" + ] + }, + { + "name": "getAnnouncementsAction", + "permission": "requireAuth", + "signature": "(params?: GetAnnouncementsParams) => Promise>", + "purpose": "获取公告列表(所有登录用户可读)", + "deps": [ + "requireAuth", + "data-access.getAnnouncements" + ], + "usedBy": [ + "待扩展" + ] + } ], "dataAccess": [ - { "name": "getAnnouncements", "signature": "(params?: { status?, type?, page?, pageSize? }) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.announcements"], "usedBy": ["admin/announcements/page.tsx", "announcements/page.tsx"] }, - { "name": "getAnnouncementById", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.announcements"], "usedBy": ["admin/announcements/[id]/page.tsx"] } + { + "name": "getAnnouncements", + "signature": "(params?: { status?, type?, page?, pageSize? }) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.announcements" + ], + "usedBy": [ + "admin/announcements/page.tsx", + "announcements/page.tsx" + ] + }, + { + "name": "getAnnouncementById", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.announcements" + ], + "usedBy": [ + "admin/announcements/[id]/page.tsx" + ] + } ], "schemas": [ - { "name": "CreateAnnouncementSchema", "type": "zod", "file": "schema.ts", "definition": "{ title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt? }", "usedBy": ["createAnnouncementAction"] }, - { "name": "UpdateAnnouncementSchema", "type": "zod", "file": "schema.ts", "definition": "{ title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt? }", "usedBy": ["updateAnnouncementAction"] } + { + "name": "CreateAnnouncementSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt? }", + "usedBy": [ + "createAnnouncementAction" + ] + }, + { + "name": "UpdateAnnouncementSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ title, content, type?, status?, targetGradeId?, targetClassId?, publishedAt? }", + "usedBy": [ + "updateAnnouncementAction" + ] + } ], "types": [ - { "name": "Announcement", "type": "interface", "file": "types.ts", "definition": "{ id, title, content, type, status, targetGradeId, targetClassId, authorId, authorName, publishedAt, createdAt, updatedAt }", "usedBy": ["announcements/components", "页面"] }, - { "name": "AnnouncementListItem", "type": "type", "file": "types.ts", "definition": "= Announcement", "usedBy": ["列表页"] }, - { "name": "AnnouncementStatus", "type": "type", "file": "types.ts", "definition": "\"draft\" | \"published\" | \"archived\"", "usedBy": ["data-access", "components"] }, - { "name": "AnnouncementType", "type": "type", "file": "types.ts", "definition": "\"school\" | \"grade\" | \"class\"", "usedBy": ["data-access", "components"] }, - { "name": "GetAnnouncementsParams", "type": "interface", "file": "types.ts", "definition": "{ status?, type?, page?, pageSize? }", "usedBy": ["getAnnouncements", "getAnnouncementsAction"] } + { + "name": "Announcement", + "type": "interface", + "file": "types.ts", + "definition": "{ id, title, content, type, status, targetGradeId, targetClassId, authorId, authorName, publishedAt, createdAt, updatedAt }", + "usedBy": [ + "announcements/components", + "页面" + ] + }, + { + "name": "AnnouncementListItem", + "type": "type", + "file": "types.ts", + "definition": "= Announcement", + "usedBy": [ + "列表页" + ] + }, + { + "name": "AnnouncementStatus", + "type": "type", + "file": "types.ts", + "definition": "\"draft\" | \"published\" | \"archived\"", + "usedBy": [ + "data-access", + "components" + ] + }, + { + "name": "AnnouncementType", + "type": "type", + "file": "types.ts", + "definition": "\"school\" | \"grade\" | \"class\"", + "usedBy": [ + "data-access", + "components" + ] + }, + { + "name": "GetAnnouncementsParams", + "type": "interface", + "file": "types.ts", + "definition": "{ status?, type?, page?, pageSize? }", + "usedBy": [ + "getAnnouncements", + "getAnnouncementsAction" + ] + } ], "components": [ - { "name": "AnnouncementList", "file": "components/announcement-list.tsx", "purpose": "公告列表(支持状态筛选)" }, - { "name": "AnnouncementCard", "file": "components/announcement-card.tsx", "purpose": "单条公告卡片" }, - { "name": "AnnouncementForm", "file": "components/announcement-form.tsx", "purpose": "创建/编辑表单" }, - { "name": "AnnouncementDetail", "file": "components/announcement-detail.tsx", "purpose": "详情查看(含发布/归档/删除操作)" }, - { "name": "AdminAnnouncementsView", "file": "components/admin-announcements-view.tsx", "purpose": "管理端公告视图(列表+创建对话框)" } + { + "name": "AnnouncementList", + "file": "components/announcement-list.tsx", + "purpose": "公告列表(支持状态筛选)" + }, + { + "name": "AnnouncementCard", + "file": "components/announcement-card.tsx", + "purpose": "单条公告卡片" + }, + { + "name": "AnnouncementForm", + "file": "components/announcement-form.tsx", + "purpose": "创建/编辑表单" + }, + { + "name": "AnnouncementDetail", + "file": "components/announcement-detail.tsx", + "purpose": "详情查看(含发布/归档/删除操作)" + }, + { + "name": "AdminAnnouncementsView", + "file": "components/admin-announcements-view.tsx", + "purpose": "管理端公告视图(列表+创建对话框)" + } ] } }, @@ -1093,32 +5264,234 @@ "description": "文件上传与管理:通过 API 路由处理文件上传(保存到 public/uploads/YYYY-MM/),记录文件元数据到 DB,支持按关联资源(exam/textbook/question/announcement)多态查询、下载与删除", "exports": { "dataAccess": [ - { "name": "createFileAttachment", "signature": "(data: CreateFileAttachmentInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.fileAttachments"], "usedBy": ["app/api/upload/route.ts"] }, - { "name": "getFileAttachment", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.fileAttachments"], "usedBy": ["app/api/files/[id]/route.ts"] }, - { "name": "getFileAttachmentsByTarget", "signature": "(targetType: string, targetId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.fileAttachments"], "usedBy": ["按关联资源查询文件列表"] }, - { "name": "getFileAttachmentsByUploader", "signature": "(uploaderId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.fileAttachments"], "usedBy": ["按上传者查询文件列表"] }, - { "name": "getAllFileAttachments", "signature": "(limit?: number) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.fileAttachments"], "usedBy": ["app/(dashboard)/admin/files/page.tsx"] }, - { "name": "deleteFileAttachment", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.fileAttachments"], "usedBy": ["app/api/files/[id]/route.ts"] }, - { "name": "deleteFileAttachments", "signature": "(ids: string[]) => Promise", "file": "data-access.ts", "purpose": "批量删除文件附件记录(仅删 DB 行,磁盘文件由调用方处理;失败时回退到逐条删除)", "deps": ["shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.inArray"], "usedBy": ["app/api/files/batch-delete/route.ts"] }, - { "name": "getFileAttachmentsWithFilters", "signature": "(params: FileAttachmentQueryParams) => Promise", "file": "data-access.ts", "purpose": "按 mimeType(精确或前缀匹配)与 search(originalName/filename 模糊匹配)筛选文件列表,支持 limit/offset 分页", "deps": ["shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.like", "drizzle-orm.or", "drizzle-orm.and"], "usedBy": ["app/(dashboard)/admin/files/page.tsx"] }, - { "name": "getFileStats", "signature": "() => Promise", "file": "data-access.ts", "purpose": "获取文件统计(总数、总大小、按 mimeType 分组的 count/size)", "deps": ["shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.count", "drizzle-orm.sql"], "usedBy": ["app/(dashboard)/admin/files/page.tsx"] }, - { "name": "getFileAttachmentsByIds", "signature": "(ids: string[]) => Promise", "file": "data-access.ts", "purpose": "按 ID 列表批量查询文件(用于批量删除前获取磁盘路径)", "deps": ["shared.db", "shared.db.schema.fileAttachments", "drizzle-orm.inArray"], "usedBy": ["app/api/files/batch-delete/route.ts"] } + { + "name": "createFileAttachment", + "signature": "(data: CreateFileAttachmentInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments" + ], + "usedBy": [ + "app/api/upload/route.ts" + ] + }, + { + "name": "getFileAttachment", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments" + ], + "usedBy": [ + "app/api/files/[id]/route.ts" + ] + }, + { + "name": "getFileAttachmentsByTarget", + "signature": "(targetType: string, targetId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments" + ], + "usedBy": [ + "按关联资源查询文件列表" + ] + }, + { + "name": "getFileAttachmentsByUploader", + "signature": "(uploaderId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments" + ], + "usedBy": [ + "按上传者查询文件列表" + ] + }, + { + "name": "getAllFileAttachments", + "signature": "(limit?: number) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments" + ], + "usedBy": [ + "app/(dashboard)/admin/files/page.tsx" + ] + }, + { + "name": "deleteFileAttachment", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments" + ], + "usedBy": [ + "app/api/files/[id]/route.ts" + ] + }, + { + "name": "deleteFileAttachments", + "signature": "(ids: string[]) => Promise", + "file": "data-access.ts", + "purpose": "批量删除文件附件记录(仅删 DB 行,磁盘文件由调用方处理;失败时回退到逐条删除)", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments", + "drizzle-orm.inArray" + ], + "usedBy": [ + "app/api/files/batch-delete/route.ts" + ] + }, + { + "name": "getFileAttachmentsWithFilters", + "signature": "(params: FileAttachmentQueryParams) => Promise", + "file": "data-access.ts", + "purpose": "按 mimeType(精确或前缀匹配)与 search(originalName/filename 模糊匹配)筛选文件列表,支持 limit/offset 分页", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments", + "drizzle-orm.like", + "drizzle-orm.or", + "drizzle-orm.and" + ], + "usedBy": [ + "app/(dashboard)/admin/files/page.tsx" + ] + }, + { + "name": "getFileStats", + "signature": "() => Promise", + "file": "data-access.ts", + "purpose": "获取文件统计(总数、总大小、按 mimeType 分组的 count/size)", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments", + "drizzle-orm.count", + "drizzle-orm.sql" + ], + "usedBy": [ + "app/(dashboard)/admin/files/page.tsx" + ] + }, + { + "name": "getFileAttachmentsByIds", + "signature": "(ids: string[]) => Promise", + "file": "data-access.ts", + "purpose": "按 ID 列表批量查询文件(用于批量删除前获取磁盘路径)", + "deps": [ + "shared.db", + "shared.db.schema.fileAttachments", + "drizzle-orm.inArray" + ], + "usedBy": [ + "app/api/files/batch-delete/route.ts" + ] + } ], "types": [ - { "name": "FileAttachment", "type": "interface", "file": "types.ts", "definition": "{ id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType, targetId, createdAt }", "usedBy": ["files/components", "data-access", "API 路由"] }, - { "name": "FileUploadResult", "type": "interface", "file": "types.ts", "definition": "{ id, url, filename, originalName, size, mimeType }", "usedBy": ["app/api/upload/route.ts 响应", "file-upload.tsx 回调"] }, - { "name": "FileTargetType", "type": "type", "file": "types.ts", "definition": "\"exam\" | \"textbook\" | \"question\" | \"announcement\"", "usedBy": ["types.FileAttachment.targetType", "file-upload.tsx"] }, - { "name": "CreateFileAttachmentInput", "type": "interface", "file": "types.ts", "definition": "{ id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType?, targetId? }", "usedBy": ["createFileAttachment"] }, - { "name": "FileAttachmentQueryParams", "type": "interface", "file": "types.ts", "definition": "{ mimeType?, search?, limit?, offset? }", "usedBy": ["getFileAttachmentsWithFilters"] }, - { "name": "FileStats", "type": "interface", "file": "types.ts", "definition": "{ totalCount, totalSize, byType: Array<{ mimeType, count, size }> }", "usedBy": ["getFileStats"] }, - { "name": "BatchDeleteResult", "type": "interface", "file": "types.ts", "definition": "{ success, deletedCount, failedIds: string[] }", "usedBy": ["deleteFileAttachments", "app/api/files/batch-delete/route.ts"] } + { + "name": "FileAttachment", + "type": "interface", + "file": "types.ts", + "definition": "{ id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType, targetId, createdAt }", + "usedBy": [ + "files/components", + "data-access", + "API 路由" + ] + }, + { + "name": "FileUploadResult", + "type": "interface", + "file": "types.ts", + "definition": "{ id, url, filename, originalName, size, mimeType }", + "usedBy": [ + "app/api/upload/route.ts 响应", + "file-upload.tsx 回调" + ] + }, + { + "name": "FileTargetType", + "type": "type", + "file": "types.ts", + "definition": "\"exam\" | \"textbook\" | \"question\" | \"announcement\"", + "usedBy": [ + "types.FileAttachment.targetType", + "file-upload.tsx" + ] + }, + { + "name": "CreateFileAttachmentInput", + "type": "interface", + "file": "types.ts", + "definition": "{ id, filename, originalName, mimeType, size, storagePath, url, uploaderId, targetType?, targetId? }", + "usedBy": [ + "createFileAttachment" + ] + }, + { + "name": "FileAttachmentQueryParams", + "type": "interface", + "file": "types.ts", + "definition": "{ mimeType?, search?, limit?, offset? }", + "usedBy": [ + "getFileAttachmentsWithFilters" + ] + }, + { + "name": "FileStats", + "type": "interface", + "file": "types.ts", + "definition": "{ totalCount, totalSize, byType: Array<{ mimeType, count, size }> }", + "usedBy": [ + "getFileStats" + ] + }, + { + "name": "BatchDeleteResult", + "type": "interface", + "file": "types.ts", + "definition": "{ success, deletedCount, failedIds: string[] }", + "usedBy": [ + "deleteFileAttachments", + "app/api/files/batch-delete/route.ts" + ] + } ], "components": [ - { "name": "FileUpload", "file": "components/file-upload.tsx", "purpose": "文件上传组件(拖拽+点击上传,进度条,文件类型校验,调用 /api/upload)" }, - { "name": "FileList", "file": "components/file-list.tsx", "purpose": "文件列表展示(图标、文件名、大小、下载链接、删除按钮)" }, - { "name": "FilePreview", "file": "components/file-preview.tsx", "purpose": "文件预览(图片直接预览,PDF iframe,其他下载)" }, - { "name": "FileIcon", "file": "components/file-icon.tsx", "purpose": "根据 MIME 类型显示不同图标与颜色" }, - { "name": "AdminFilesView", "file": "components/admin-files-view.tsx", "purpose": "管理端文件视图(上传+列表+删除)" } + { + "name": "FileUpload", + "file": "components/file-upload.tsx", + "purpose": "文件上传组件(拖拽+点击上传,进度条,文件类型校验,调用 /api/upload)" + }, + { + "name": "FileList", + "file": "components/file-list.tsx", + "purpose": "文件列表展示(图标、文件名、大小、下载链接、删除按钮)" + }, + { + "name": "FilePreview", + "file": "components/file-preview.tsx", + "purpose": "文件预览(图片直接预览,PDF iframe,其他下载)" + }, + { + "name": "FileIcon", + "file": "components/file-icon.tsx", + "purpose": "根据 MIME 类型显示不同图标与颜色" + }, + { + "name": "AdminFilesView", + "file": "components/admin-files-view.tsx", + "purpose": "管理端文件视图(上传+列表+删除)" + } ] } }, @@ -1127,82 +5500,683 @@ "description": "成绩分析模块:成绩录入(单条+批量)、查询(按班级/科目/考试/学期过滤)、统计报表(均分、中位数、标准差、及格率、优秀率、排名)、Excel 导出(成绩明细+统计汇总/班级多科目横向对比)、趋势对比分析(成绩趋势、班级对比、科目对比、分数分布、排名趋势)", "exports": { "dataAccess": [ - { "name": "getGradeRecords", "signature": "(params: GradeQueryParams & { scope: DataScope; currentUserId?: string }) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classes", "shared.db.schema.classEnrollments", "shared.db.schema.subjects", "shared.db.schema.users"], "usedBy": ["grades/actions.getGradeRecordsAction"] }, - { "name": "getGradeRecordById", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords"], "usedBy": ["grades/actions.getGradeRecordByIdAction"] }, - { "name": "createGradeRecord", "signature": "(data: CreateGradeRecordInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords"], "usedBy": ["grades/actions.createGradeRecordAction"] }, - { "name": "batchCreateGradeRecords", "signature": "(data: BatchCreateGradeRecordInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords"], "usedBy": ["grades/actions.batchCreateGradeRecordsAction"] }, - { "name": "updateGradeRecord", "signature": "(id: string, data: UpdateGradeRecordInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords"], "usedBy": ["grades/actions.updateGradeRecordAction"] }, - { "name": "deleteGradeRecord", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords"], "usedBy": ["grades/actions.deleteGradeRecordAction"] }, - { "name": "getClassGradeStats", "signature": "(classId: string, subjectId?: string, examId?: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords"], "usedBy": ["grades/data-access.getClassGradeStatsWithMeta"] }, - { "name": "getClassGradeStatsWithMeta", "signature": "(classId: string, subjectId?: string, examId?: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classes", "shared.db.schema.subjects"], "usedBy": ["grades/actions.getClassGradeStatsAction"] }, - { "name": "getStudentGradeSummary", "signature": "(studentId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.subjects"], "usedBy": ["grades/actions.getStudentGradeSummaryAction"] }, - { "name": "getClassRanking", "signature": "(classId: string, subjectId?: string, examId?: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.users"], "usedBy": ["grades/actions.getClassRankingAction"] }, - { "name": "getClassStudentsForEntry", "signature": "(classId: string) => Promise<{ id: string; name: string }[]>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.classEnrollments", "shared.db.schema.users"], "usedBy": ["grades/components/batch-grade-entry"] }, - { "name": "getGradeTrend", "signature": "(params: { studentId; subjectId?; semester?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classEnrollments"], "usedBy": ["grades/actions-analytics.getGradeTrendAction", "teacher/grades/analytics"] }, - { "name": "getClassComparison", "signature": "(params: { gradeId; subjectId; examId?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classes"], "usedBy": ["grades/actions-analytics.getClassComparisonAction", "teacher/grades/analytics"] }, - { "name": "getSubjectComparison", "signature": "(params: { classId; examId?; semester?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.subjects"], "usedBy": ["grades/actions-analytics.getSubjectComparisonAction", "teacher/grades/analytics"] }, - { "name": "getGradeDistribution", "signature": "(params: { classId; subjectId?; examId?; scope: DataScope }) => Promise", "file": "data-access-analytics.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords"], "usedBy": ["grades/actions-analytics.getGradeDistributionAction", "teacher/grades/analytics"] }, - { "name": "getRankingTrend", "signature": "(studentId: string, subjectId?, semester?) => Promise", "file": "data-access-ranking.ts", "deps": ["shared.db", "shared.db.schema.gradeRecords", "shared.db.schema.classEnrollments"], "usedBy": ["grades/actions-analytics.getRankingTrendAction"] } + { + "name": "getGradeRecords", + "signature": "(params: GradeQueryParams & { scope: DataScope; currentUserId?: string }) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.classes", + "shared.db.schema.classEnrollments", + "shared.db.schema.subjects", + "shared.db.schema.users" + ], + "usedBy": [ + "grades/actions.getGradeRecordsAction" + ] + }, + { + "name": "getGradeRecordById", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords" + ], + "usedBy": [ + "grades/actions.getGradeRecordByIdAction" + ] + }, + { + "name": "createGradeRecord", + "signature": "(data: CreateGradeRecordInput, recordedBy: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords" + ], + "usedBy": [ + "grades/actions.createGradeRecordAction" + ] + }, + { + "name": "batchCreateGradeRecords", + "signature": "(data: BatchCreateGradeRecordInput, recordedBy: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords" + ], + "usedBy": [ + "grades/actions.batchCreateGradeRecordsAction" + ] + }, + { + "name": "updateGradeRecord", + "signature": "(id: string, data: UpdateGradeRecordInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords" + ], + "usedBy": [ + "grades/actions.updateGradeRecordAction" + ] + }, + { + "name": "deleteGradeRecord", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords" + ], + "usedBy": [ + "grades/actions.deleteGradeRecordAction" + ] + }, + { + "name": "getClassGradeStats", + "signature": "(classId: string, subjectId?: string, examId?: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords" + ], + "usedBy": [ + "grades/data-access.getClassGradeStatsWithMeta" + ] + }, + { + "name": "getClassGradeStatsWithMeta", + "signature": "(classId: string, subjectId?: string, examId?: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.classes", + "shared.db.schema.subjects" + ], + "usedBy": [ + "grades/actions.getClassGradeStatsAction" + ] + }, + { + "name": "getStudentGradeSummary", + "signature": "(studentId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.subjects" + ], + "usedBy": [ + "grades/actions.getStudentGradeSummaryAction" + ] + }, + { + "name": "getClassRanking", + "signature": "(classId: string, subjectId?: string, examId?: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.users" + ], + "usedBy": [ + "grades/actions.getClassRankingAction" + ] + }, + { + "name": "getClassStudentsForEntry", + "signature": "(classId: string) => Promise<{ id: string; name: string }[]>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.classEnrollments", + "shared.db.schema.users" + ], + "usedBy": [ + "grades/components/batch-grade-entry" + ] + }, + { + "name": "getGradeTrend", + "signature": "(params: { studentId; subjectId?; semester?; scope: DataScope }) => Promise", + "file": "data-access-analytics.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.classEnrollments" + ], + "usedBy": [ + "grades/actions-analytics.getGradeTrendAction", + "teacher/grades/analytics" + ] + }, + { + "name": "getClassComparison", + "signature": "(params: { gradeId; subjectId; examId?; scope: DataScope }) => Promise", + "file": "data-access-analytics.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.classes" + ], + "usedBy": [ + "grades/actions-analytics.getClassComparisonAction", + "teacher/grades/analytics" + ] + }, + { + "name": "getSubjectComparison", + "signature": "(params: { classId; examId?; semester?; scope: DataScope }) => Promise", + "file": "data-access-analytics.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.subjects" + ], + "usedBy": [ + "grades/actions-analytics.getSubjectComparisonAction", + "teacher/grades/analytics" + ] + }, + { + "name": "getGradeDistribution", + "signature": "(params: { classId; subjectId?; examId?; scope: DataScope }) => Promise", + "file": "data-access-analytics.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords" + ], + "usedBy": [ + "grades/actions-analytics.getGradeDistributionAction", + "teacher/grades/analytics" + ] + }, + { + "name": "getRankingTrend", + "signature": "(studentId: string, subjectId?, semester?) => Promise", + "file": "data-access-ranking.ts", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "shared.db.schema.classEnrollments" + ], + "usedBy": [ + "grades/actions-analytics.getRankingTrendAction" + ] + } ], "actions": [ - { "name": "createGradeRecordAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "usedBy": ["grades/components/grade-record-form"] }, - { "name": "batchCreateGradeRecordsAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "usedBy": ["grades/components/batch-grade-entry"] }, - { "name": "updateGradeRecordAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "usedBy": ["grades/components/grade-record-list"] }, - { "name": "deleteGradeRecordAction", "signature": "(prevState, formData) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_MANAGE", "usedBy": ["grades/components/grade-record-list"] }, - { "name": "getGradeRecordsAction", "signature": "(params) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": ["teacher/grades/page"] }, - { "name": "getClassGradeStatsAction", "signature": "(classId, subjectId?, examId?) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": ["teacher/grades/stats/page"] }, - { "name": "getStudentGradeSummaryAction", "signature": "(studentId?) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": ["student/grades/page", "parent/grades/page"] }, - { "name": "getClassRankingAction", "signature": "(classId, subjectId?, examId?) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": ["teacher/grades/stats/page"] }, - { "name": "getGradeRecordByIdAction", "signature": "(id) => Promise", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "usedBy": ["grades/components/grade-record-list"] }, - { "name": "exportGradesAction", "signature": "(params: { classId: string; subjectId?: string; examId?: string; reportType?: \"detail\" | \"class\" }) => Promise>", "file": "actions.ts", "permission": "GRADE_RECORD_READ", "purpose": "导出成绩到 Excel(detail=成绩明细+统计汇总,class=班级多科目横向对比总表),返回 base64 buffer", "deps": ["requirePermission", "export.exportGradeRecordsToExcel", "export.exportClassGradeReportToExcel", "export.formatDateForFile"], "usedBy": ["grades/components/export-button.tsx"] }, - { "name": "getGradeTrendAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取成绩趋势(按学生/科目/学期,返回归一化分数趋势点)", "usedBy": ["teacher/grades/analytics"] }, - { "name": "getClassComparisonAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取班级对比(同年级各班的均分/及格率/优秀率)", "usedBy": ["teacher/grades/analytics"] }, - { "name": "getSubjectComparisonAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取科目对比(同班级各科目雷达图数据)", "usedBy": ["teacher/grades/analytics"] }, - { "name": "getGradeDistributionAction", "signature": "(params) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取分数分布(90-100/80-89/70-79/60-69/<60 各区间人数)", "usedBy": ["teacher/grades/analytics"] }, - { "name": "getRankingTrendAction", "signature": "(studentId, subjectId?, semester?) => Promise", "file": "actions-analytics.ts", "permission": "GRADE_RECORD_READ", "purpose": "获取排名趋势(学生历次考试排名变化,含 DataScope 二次校验)", "usedBy": ["待扩展"] } + { + "name": "createGradeRecordAction", + "signature": "(prevState, formData) => Promise>", + "file": "actions.ts", + "permission": "GRADE_RECORD_MANAGE", + "usedBy": [ + "grades/components/grade-record-form" + ] + }, + { + "name": "batchCreateGradeRecordsAction", + "signature": "(prevState, formData) => Promise>", + "file": "actions.ts", + "permission": "GRADE_RECORD_MANAGE", + "usedBy": [ + "grades/components/batch-grade-entry" + ] + }, + { + "name": "updateGradeRecordAction", + "signature": "(prevState, formData) => Promise>", + "file": "actions.ts", + "permission": "GRADE_RECORD_MANAGE", + "usedBy": [ + "grades/components/grade-record-list" + ] + }, + { + "name": "deleteGradeRecordAction", + "signature": "(prevState, formData) => Promise>", + "file": "actions.ts", + "permission": "GRADE_RECORD_MANAGE", + "usedBy": [ + "grades/components/grade-record-list" + ] + }, + { + "name": "getGradeRecordsAction", + "signature": "(params) => Promise", + "file": "actions.ts", + "permission": "GRADE_RECORD_READ", + "usedBy": [ + "teacher/grades/page" + ] + }, + { + "name": "getClassGradeStatsAction", + "signature": "(classId, subjectId?, examId?) => Promise", + "file": "actions.ts", + "permission": "GRADE_RECORD_READ", + "usedBy": [ + "teacher/grades/stats/page" + ] + }, + { + "name": "getStudentGradeSummaryAction", + "signature": "(studentId?) => Promise", + "file": "actions.ts", + "permission": "GRADE_RECORD_READ", + "usedBy": [ + "student/grades/page", + "parent/grades/page" + ] + }, + { + "name": "getClassRankingAction", + "signature": "(classId, subjectId?, examId?) => Promise", + "file": "actions.ts", + "permission": "GRADE_RECORD_READ", + "usedBy": [ + "teacher/grades/stats/page" + ] + }, + { + "name": "getGradeRecordByIdAction", + "signature": "(id) => Promise", + "file": "actions.ts", + "permission": "GRADE_RECORD_READ", + "usedBy": [ + "grades/components/grade-record-list" + ] + }, + { + "name": "exportGradesAction", + "signature": "(params: { classId: string; subjectId?: string; examId?: string; reportType?: \"detail\" | \"class\" }) => Promise>", + "file": "actions.ts", + "permission": "GRADE_RECORD_READ", + "purpose": "导出成绩到 Excel(detail=成绩明细+统计汇总,class=班级多科目横向对比总表),返回 base64 buffer", + "deps": [ + "requirePermission", + "export.exportGradeRecordsToExcel", + "export.exportClassGradeReportToExcel", + "export.formatDateForFile" + ], + "usedBy": [ + "grades/components/export-button.tsx" + ] + }, + { + "name": "getGradeTrendAction", + "signature": "(params) => Promise", + "file": "actions-analytics.ts", + "permission": "GRADE_RECORD_READ", + "purpose": "获取成绩趋势(按学生/科目/学期,返回归一化分数趋势点)", + "usedBy": [ + "teacher/grades/analytics" + ] + }, + { + "name": "getClassComparisonAction", + "signature": "(params) => Promise", + "file": "actions-analytics.ts", + "permission": "GRADE_RECORD_READ", + "purpose": "获取班级对比(同年级各班的均分/及格率/优秀率)", + "usedBy": [ + "teacher/grades/analytics" + ] + }, + { + "name": "getSubjectComparisonAction", + "signature": "(params) => Promise", + "file": "actions-analytics.ts", + "permission": "GRADE_RECORD_READ", + "purpose": "获取科目对比(同班级各科目雷达图数据)", + "usedBy": [ + "teacher/grades/analytics" + ] + }, + { + "name": "getGradeDistributionAction", + "signature": "(params) => Promise", + "file": "actions-analytics.ts", + "permission": "GRADE_RECORD_READ", + "purpose": "获取分数分布(90-100/80-89/70-79/60-69/<60 各区间人数)", + "usedBy": [ + "teacher/grades/analytics" + ] + }, + { + "name": "getRankingTrendAction", + "signature": "(studentId, subjectId?, semester?) => Promise", + "file": "actions-analytics.ts", + "permission": "GRADE_RECORD_READ", + "purpose": "获取排名趋势(学生历次考试排名变化,含 DataScope 二次校验)", + "usedBy": [ + "待扩展" + ] + } ], "schemas": [ - { "name": "CreateGradeRecordSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": ["createGradeRecordAction"] }, - { "name": "BatchCreateGradeRecordSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": ["batchCreateGradeRecordsAction"] }, - { "name": "UpdateGradeRecordSchema", "type": "ZodSchema", "file": "schema.ts", "usedBy": ["updateGradeRecordAction"] } + { + "name": "CreateGradeRecordSchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "createGradeRecordAction" + ] + }, + { + "name": "BatchCreateGradeRecordSchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "batchCreateGradeRecordsAction" + ] + }, + { + "name": "UpdateGradeRecordSchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "updateGradeRecordAction" + ] + } ], "types": [ - { "name": "GradeRecord", "type": "interface", "file": "types.ts", "usedBy": ["data-access", "actions"] }, - { "name": "GradeRecordListItem", "type": "interface", "file": "types.ts", "usedBy": ["data-access", "actions", "components/grade-record-list"] }, - { "name": "GradeStats", "type": "interface", "file": "types.ts", "definition": "{ count, average, median, stdDev, passRate, excellentRate, maxScore, minScore }", "usedBy": ["data-access", "components/grade-stats-card"] }, - { "name": "ClassGradeStats", "type": "interface", "file": "types.ts", "usedBy": ["data-access", "actions", "components/class-grade-report"] }, - { "name": "StudentGradeSummary", "type": "interface", "file": "types.ts", "usedBy": ["data-access", "actions", "components/student-grade-summary"] }, - { "name": "ClassRankingItem", "type": "interface", "file": "types.ts", "usedBy": ["data-access", "actions", "components/class-grade-report"] }, - { "name": "GradeRecordType", "type": "type", "file": "types.ts", "definition": "\"exam\" | \"quiz\" | \"assignment\" | \"monthly\" | \"midterm\" | \"final\"", "usedBy": ["types.GradeRecord.type"] }, - { "name": "GradeRecordSemester", "type": "type", "file": "types.ts", "definition": "\"1\" | \"2\"", "usedBy": ["types.GradeRecord.semester"] }, - { "name": "GradeQueryParams", "type": "interface", "file": "types.ts", "usedBy": ["data-access.getGradeRecords"] }, - { "name": "GradeTrendPoint", "type": "interface", "file": "types.ts", "definition": "{ date, title, score, fullScore, normalizedScore, type }", "usedBy": ["data-access-analytics.getGradeTrend", "grade-trend-chart"] }, - { "name": "GradeTrendResult", "type": "interface", "file": "types.ts", "definition": "{ label, points: GradeTrendPoint[], averageScore }", "usedBy": ["data-access-analytics.getGradeTrend", "grade-trend-chart"] }, - { "name": "ClassComparisonItem", "type": "interface", "file": "types.ts", "definition": "{ classId, className, averageScore, passRate, excellentRate, studentCount }", "usedBy": ["data-access-analytics.getClassComparison", "class-comparison-chart"] }, - { "name": "SubjectComparisonItem", "type": "interface", "file": "types.ts", "definition": "{ subjectId, subjectName, averageScore, passRate, excellentRate }", "usedBy": ["data-access-analytics.getSubjectComparison", "subject-comparison-chart"] }, - { "name": "GradeDistributionBucket", "type": "interface", "file": "types.ts", "definition": "{ label, min, max, count, percentage }", "usedBy": ["data-access-analytics.getGradeDistribution", "grade-distribution-chart"] }, - { "name": "GradeDistributionResult", "type": "interface", "file": "types.ts", "definition": "{ buckets: GradeDistributionBucket[], totalCount }", "usedBy": ["data-access-analytics.getGradeDistribution", "grade-distribution-chart"] }, - { "name": "RankingTrendPoint", "type": "interface", "file": "types.ts", "definition": "{ title, date, rank, totalStudents, score }", "usedBy": ["data-access-ranking.getRankingTrend"] }, - { "name": "RankingTrendResult", "type": "interface", "file": "types.ts", "definition": "{ studentName, points: RankingTrendPoint[] }", "usedBy": ["data-access-ranking.getRankingTrend"] } + { + "name": "GradeRecord", + "type": "interface", + "file": "types.ts", + "usedBy": [ + "data-access", + "actions" + ] + }, + { + "name": "GradeRecordListItem", + "type": "interface", + "file": "types.ts", + "usedBy": [ + "data-access", + "actions", + "components/grade-record-list" + ] + }, + { + "name": "GradeStats", + "type": "interface", + "file": "types.ts", + "definition": "{ count, average, median, stdDev, passRate, excellentRate, maxScore, minScore }", + "usedBy": [ + "data-access", + "components/grade-stats-card" + ] + }, + { + "name": "ClassGradeStats", + "type": "interface", + "file": "types.ts", + "usedBy": [ + "data-access", + "actions", + "components/class-grade-report" + ] + }, + { + "name": "StudentGradeSummary", + "type": "interface", + "file": "types.ts", + "usedBy": [ + "data-access", + "actions", + "components/student-grade-summary" + ] + }, + { + "name": "ClassRankingItem", + "type": "interface", + "file": "types.ts", + "usedBy": [ + "data-access", + "actions", + "components/class-grade-report" + ] + }, + { + "name": "GradeRecordType", + "type": "type", + "file": "types.ts", + "definition": "\"exam\" | \"quiz\" | \"assignment\" | \"monthly\" | \"midterm\" | \"final\"", + "usedBy": [ + "types.GradeRecord.type" + ] + }, + { + "name": "GradeRecordSemester", + "type": "type", + "file": "types.ts", + "definition": "\"1\" | \"2\"", + "usedBy": [ + "types.GradeRecord.semester" + ] + }, + { + "name": "GradeQueryParams", + "type": "interface", + "file": "types.ts", + "usedBy": [ + "data-access.getGradeRecords" + ] + }, + { + "name": "GradeTrendPoint", + "type": "interface", + "file": "types.ts", + "definition": "{ date, title, score, fullScore, normalizedScore, type }", + "usedBy": [ + "data-access-analytics.getGradeTrend", + "grade-trend-chart" + ] + }, + { + "name": "GradeTrendResult", + "type": "interface", + "file": "types.ts", + "definition": "{ label, points: GradeTrendPoint[], averageScore }", + "usedBy": [ + "data-access-analytics.getGradeTrend", + "grade-trend-chart" + ] + }, + { + "name": "ClassComparisonItem", + "type": "interface", + "file": "types.ts", + "definition": "{ classId, className, averageScore, passRate, excellentRate, studentCount }", + "usedBy": [ + "data-access-analytics.getClassComparison", + "class-comparison-chart" + ] + }, + { + "name": "SubjectComparisonItem", + "type": "interface", + "file": "types.ts", + "definition": "{ subjectId, subjectName, averageScore, passRate, excellentRate }", + "usedBy": [ + "data-access-analytics.getSubjectComparison", + "subject-comparison-chart" + ] + }, + { + "name": "GradeDistributionBucket", + "type": "interface", + "file": "types.ts", + "definition": "{ label, min, max, count, percentage }", + "usedBy": [ + "data-access-analytics.getGradeDistribution", + "grade-distribution-chart" + ] + }, + { + "name": "GradeDistributionResult", + "type": "interface", + "file": "types.ts", + "definition": "{ buckets: GradeDistributionBucket[], totalCount }", + "usedBy": [ + "data-access-analytics.getGradeDistribution", + "grade-distribution-chart" + ] + }, + { + "name": "RankingTrendPoint", + "type": "interface", + "file": "types.ts", + "definition": "{ title, date, rank, totalStudents, score }", + "usedBy": [ + "data-access-ranking.getRankingTrend" + ] + }, + { + "name": "RankingTrendResult", + "type": "interface", + "file": "types.ts", + "definition": "{ studentName, points: RankingTrendPoint[] }", + "usedBy": [ + "data-access-ranking.getRankingTrend" + ] + } ], "importExport": [ - { "name": "exportGradeRecordsToExcel", "signature": "(params: { classId: string; subjectId?: string; examId?: string; scope: DataScope }) => Promise", "file": "export.ts", "purpose": "导出成绩单(Sheet1 成绩明细,Sheet2 统计汇总:均分/中位数/最高分/最低分/标准差/及格率/优秀率/参考人数)", "deps": ["shared.lib.excel.exportToExcel", "data-access.getGradeRecords", "data-access.getClassGradeStats"], "usedBy": ["actions.exportGradesAction", "app/api/export/route.ts"] }, - { "name": "exportClassGradeReportToExcel", "signature": "(params: { classId: string; scope: DataScope }) => Promise", "file": "export.ts", "purpose": "导出班级成绩总表(多科目横向对比,含总分/平均分/排名列)", "deps": ["shared.db", "shared.db.schema.classes", "shared.db.schema.subjects", "shared.db.schema.gradeRecords", "shared.db.schema.users", "shared.lib.excel.exportToExcel", "data-access.getGradeRecords"], "usedBy": ["actions.exportGradesAction"] }, - { "name": "formatDateForFile", "signature": "(d?: Date) => string", "file": "export.ts", "purpose": "格式化日期为 YYYY-MM-DD 用于文件名", "deps": [], "usedBy": ["actions.exportGradesAction"] } + { + "name": "exportGradeRecordsToExcel", + "signature": "(params: { classId: string; subjectId?: string; examId?: string; scope: DataScope }) => Promise", + "file": "export.ts", + "purpose": "导出成绩单(Sheet1 成绩明细,Sheet2 统计汇总:均分/中位数/最高分/最低分/标准差/及格率/优秀率/参考人数)", + "deps": [ + "shared.lib.excel.exportToExcel", + "data-access.getGradeRecords", + "data-access.getClassGradeStats" + ], + "usedBy": [ + "actions.exportGradesAction", + "app/api/export/route.ts" + ] + }, + { + "name": "exportClassGradeReportToExcel", + "signature": "(params: { classId: string; scope: DataScope }) => Promise", + "file": "export.ts", + "purpose": "导出班级成绩总表(多科目横向对比,含总分/平均分/排名列)", + "deps": [ + "shared.db", + "shared.db.schema.classes", + "shared.db.schema.subjects", + "shared.db.schema.gradeRecords", + "shared.db.schema.users", + "shared.lib.excel.exportToExcel", + "data-access.getGradeRecords" + ], + "usedBy": [ + "actions.exportGradesAction" + ] + }, + { + "name": "formatDateForFile", + "signature": "(d?: Date) => string", + "file": "export.ts", + "purpose": "格式化日期为 YYYY-MM-DD 用于文件名", + "deps": [], + "usedBy": [ + "actions.exportGradesAction" + ] + } ], "components": [ - { "name": "GradeRecordForm", "file": "components/grade-record-form.tsx", "purpose": "单条成绩录入表单(选择学生、班级、科目、考试,输入分数、满分、类型、学期、备注)" }, - { "name": "BatchGradeEntry", "file": "components/batch-grade-entry.tsx", "purpose": "批量录入界面(选择班级+科目+考试,表格形式录入每个学生分数)" }, - { "name": "GradeRecordList", "file": "components/grade-record-list.tsx", "purpose": "成绩列表(含查询筛选、删除对话框)" }, - { "name": "GradeStatsCard", "file": "components/grade-stats-card.tsx", "purpose": "统计卡片(均分、中位数、标准差、及格率、优秀率、最高分、最低分)" }, - { "name": "GradeQueryFilters", "file": "components/grade-query-filters.tsx", "purpose": "查询筛选器(班级、科目、考试类型、学期)" }, - { "name": "StudentGradeSummary", "file": "components/student-grade-summary.tsx", "purpose": "学生成绩汇总视图(按科目分组展示成绩趋势)" }, - { "name": "ClassGradeReport", "file": "components/class-grade-report.tsx", "purpose": "班级成绩报表(含统计+排名)" }, - { "name": "ExportButton", "file": "components/export-button.tsx", "purpose": "成绩导出按钮(DropdownMenu 选择 detail/class 报表类型,调用 exportGradesAction 并触发浏览器下载)", "usedBy": ["teacher/grades/page.tsx", "teacher/grades/stats/page.tsx"] }, - { "name": "GradeTrendChart", "file": "components/grade-trend-chart.tsx", "purpose": "成绩趋势折线图(recharts LineChart,归一化分数 0-100)", "deps": ["recharts", "shared/components/ui/chart"] }, - { "name": "ClassComparisonChart", "file": "components/class-comparison-chart.tsx", "purpose": "班级对比柱状图(recharts BarChart,均分/及格率/优秀率)", "deps": ["recharts", "shared/components/ui/chart"] }, - { "name": "SubjectComparisonChart", "file": "components/subject-comparison-chart.tsx", "purpose": "科目对比雷达图(recharts RadarChart)", "deps": ["recharts", "shared/components/ui/chart"] }, - { "name": "GradeDistributionChart", "file": "components/grade-distribution-chart.tsx", "purpose": "分数分布柱状图(recharts BarChart,彩色区间 90-100/80-89/70-79/60-69/<60)", "deps": ["recharts", "shared/components/ui/chart"] } + { + "name": "GradeRecordForm", + "file": "components/grade-record-form.tsx", + "purpose": "单条成绩录入表单(选择学生、班级、科目、考试,输入分数、满分、类型、学期、备注)" + }, + { + "name": "BatchGradeEntry", + "file": "components/batch-grade-entry.tsx", + "purpose": "批量录入界面(选择班级+科目+考试,表格形式录入每个学生分数)" + }, + { + "name": "GradeRecordList", + "file": "components/grade-record-list.tsx", + "purpose": "成绩列表(含查询筛选、删除对话框)" + }, + { + "name": "GradeStatsCard", + "file": "components/grade-stats-card.tsx", + "purpose": "统计卡片(均分、中位数、标准差、及格率、优秀率、最高分、最低分)" + }, + { + "name": "GradeQueryFilters", + "file": "components/grade-query-filters.tsx", + "purpose": "查询筛选器(班级、科目、考试类型、学期)" + }, + { + "name": "StudentGradeSummary", + "file": "components/student-grade-summary.tsx", + "purpose": "学生成绩汇总视图(按科目分组展示成绩趋势)" + }, + { + "name": "ClassGradeReport", + "file": "components/class-grade-report.tsx", + "purpose": "班级成绩报表(含统计+排名)" + }, + { + "name": "ExportButton", + "file": "components/export-button.tsx", + "purpose": "成绩导出按钮(DropdownMenu 选择 detail/class 报表类型,调用 exportGradesAction 并触发浏览器下载)", + "usedBy": [ + "teacher/grades/page.tsx", + "teacher/grades/stats/page.tsx" + ] + }, + { + "name": "GradeTrendChart", + "file": "components/grade-trend-chart.tsx", + "purpose": "成绩趋势折线图(recharts LineChart,归一化分数 0-100)", + "deps": [ + "recharts", + "shared/components/ui/chart" + ] + }, + { + "name": "ClassComparisonChart", + "file": "components/class-comparison-chart.tsx", + "purpose": "班级对比柱状图(recharts BarChart,均分/及格率/优秀率)", + "deps": [ + "recharts", + "shared/components/ui/chart" + ] + }, + { + "name": "SubjectComparisonChart", + "file": "components/subject-comparison-chart.tsx", + "purpose": "科目对比雷达图(recharts RadarChart)", + "deps": [ + "recharts", + "shared/components/ui/chart" + ] + }, + { + "name": "GradeDistributionChart", + "file": "components/grade-distribution-chart.tsx", + "purpose": "分数分布柱状图(recharts BarChart,彩色区间 90-100/80-89/70-79/60-69/<60)", + "deps": [ + "recharts", + "shared/components/ui/chart" + ] + } ] } }, @@ -1211,50 +6185,411 @@ "description": "课程计划管理:创建、编辑、删除课程计划(含周计划条目),管理员可管理全部,教师/学生/年级主任/教务主任可查看", "exports": { "actions": [ - { "name": "createCoursePlanAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建课程计划", "deps": ["requirePermission", "shared/db", "data-access.createCoursePlan"], "usedBy": ["course-plan-form.tsx"] }, - { "name": "updateCoursePlanAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新课程计划", "deps": ["requirePermission", "shared/db", "data-access.updateCoursePlan"], "usedBy": ["course-plan-form.tsx"] }, - { "name": "deleteCoursePlanAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除课程计划", "deps": ["requirePermission", "shared/db", "data-access.deleteCoursePlan"], "usedBy": ["course-plan-detail.tsx"] }, - { "name": "getCoursePlansAction", "permission": "COURSE_PLAN_READ", "signature": "(params?: GetCoursePlansParams) => Promise>", "purpose": "获取课程计划列表", "deps": ["requirePermission", "data-access.getCoursePlans"], "usedBy": ["待扩展"] }, - { "name": "getCoursePlanAction", "permission": "COURSE_PLAN_READ", "signature": "(id: string) => Promise>", "purpose": "获取课程计划详情(含周计划条目)", "deps": ["requirePermission", "data-access.getCoursePlanById"], "usedBy": ["待扩展"] }, - { "name": "createCoursePlanItemAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建周计划条目", "deps": ["requirePermission", "shared/db", "data-access.createCoursePlanItem"], "usedBy": ["course-plan-item-editor.tsx"] }, - { "name": "updateCoursePlanItemAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新周计划条目", "deps": ["requirePermission", "shared/db", "data-access.updateCoursePlanItem"], "usedBy": ["course-plan-item-editor.tsx"] }, - { "name": "deleteCoursePlanItemAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除周计划条目", "deps": ["requirePermission", "shared/db", "data-access.deleteCoursePlanItem"], "usedBy": ["course-plan-item-editor.tsx"] }, - { "name": "toggleCoursePlanItemCompletedAction", "permission": "COURSE_PLAN_MANAGE", "signature": "(id: string, completed: boolean) => Promise>", "purpose": "切换周计划条目完成状态", "deps": ["requirePermission", "shared/db", "data-access.updateCoursePlanItem"], "usedBy": ["course-plan-detail.tsx"] } + { + "name": "createCoursePlanAction", + "permission": "COURSE_PLAN_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "创建课程计划", + "deps": [ + "requirePermission", + "shared/db", + "data-access.createCoursePlan" + ], + "usedBy": [ + "course-plan-form.tsx" + ] + }, + { + "name": "updateCoursePlanAction", + "permission": "COURSE_PLAN_MANAGE", + "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "更新课程计划", + "deps": [ + "requirePermission", + "shared/db", + "data-access.updateCoursePlan" + ], + "usedBy": [ + "course-plan-form.tsx" + ] + }, + { + "name": "deleteCoursePlanAction", + "permission": "COURSE_PLAN_MANAGE", + "signature": "(id: string) => Promise>", + "purpose": "删除课程计划", + "deps": [ + "requirePermission", + "shared/db", + "data-access.deleteCoursePlan" + ], + "usedBy": [ + "course-plan-detail.tsx" + ] + }, + { + "name": "getCoursePlansAction", + "permission": "COURSE_PLAN_READ", + "signature": "(params?: GetCoursePlansParams) => Promise>", + "purpose": "获取课程计划列表", + "deps": [ + "requirePermission", + "data-access.getCoursePlans" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getCoursePlanAction", + "permission": "COURSE_PLAN_READ", + "signature": "(id: string) => Promise>", + "purpose": "获取课程计划详情(含周计划条目)", + "deps": [ + "requirePermission", + "data-access.getCoursePlanById" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "createCoursePlanItemAction", + "permission": "COURSE_PLAN_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "创建周计划条目", + "deps": [ + "requirePermission", + "shared/db", + "data-access.createCoursePlanItem" + ], + "usedBy": [ + "course-plan-item-editor.tsx" + ] + }, + { + "name": "updateCoursePlanItemAction", + "permission": "COURSE_PLAN_MANAGE", + "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "更新周计划条目", + "deps": [ + "requirePermission", + "shared/db", + "data-access.updateCoursePlanItem" + ], + "usedBy": [ + "course-plan-item-editor.tsx" + ] + }, + { + "name": "deleteCoursePlanItemAction", + "permission": "COURSE_PLAN_MANAGE", + "signature": "(id: string) => Promise>", + "purpose": "删除周计划条目", + "deps": [ + "requirePermission", + "shared/db", + "data-access.deleteCoursePlanItem" + ], + "usedBy": [ + "course-plan-item-editor.tsx" + ] + }, + { + "name": "toggleCoursePlanItemCompletedAction", + "permission": "COURSE_PLAN_MANAGE", + "signature": "(id: string, completed: boolean) => Promise>", + "purpose": "切换周计划条目完成状态", + "deps": [ + "requirePermission", + "shared/db", + "data-access.updateCoursePlanItem" + ], + "usedBy": [ + "course-plan-detail.tsx" + ] + } ], "dataAccess": [ - { "name": "getCoursePlans", "signature": "(params?: GetCoursePlansParams) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlans", "shared.db.schema.classes", "shared.db.schema.subjects", "shared.db.schema.users"], "usedBy": ["admin/course-plans/page.tsx", "teacher/course-plans/page.tsx"] }, - { "name": "getCoursePlanById", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlans", "shared.db.schema.coursePlanItems", "shared.db.schema.classes", "shared.db.schema.subjects", "shared.db.schema.users"], "usedBy": ["admin/course-plans/[id]/page.tsx", "teacher/course-plans/[id]/page.tsx"] }, - { "name": "createCoursePlan", "signature": "(data: CreateCoursePlanInput, createdBy: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlans", "@paralleldrive/cuid2"], "usedBy": ["createCoursePlanAction"] }, - { "name": "updateCoursePlan", "signature": "(id: string, data: Partial) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlans"], "usedBy": ["updateCoursePlanAction"] }, - { "name": "deleteCoursePlan", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlans"], "usedBy": ["deleteCoursePlanAction"] }, - { "name": "createCoursePlanItem", "signature": "(data: CreateCoursePlanItemInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlanItems", "@paralleldrive/cuid2"], "usedBy": ["createCoursePlanItemAction"] }, - { "name": "updateCoursePlanItem", "signature": "(id: string, data: Partial) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlanItems"], "usedBy": ["updateCoursePlanItemAction", "toggleCoursePlanItemCompletedAction"] }, - { "name": "deleteCoursePlanItem", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlanItems"], "usedBy": ["deleteCoursePlanItemAction"] }, - { "name": "reorderCoursePlanItems", "signature": "(planId: string, items: ReorderCoursePlanItemInput[]) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.coursePlanItems"], "usedBy": ["待扩展"] }, - { "name": "getSubjectOptions", "signature": "() => Promise<{id:string;name:string}[]>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.subjects"], "usedBy": ["admin/course-plans/create/page.tsx", "admin/course-plans/[id]/edit/page.tsx"] } + { + "name": "getCoursePlans", + "signature": "(params?: GetCoursePlansParams) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlans", + "shared.db.schema.classes", + "shared.db.schema.subjects", + "shared.db.schema.users" + ], + "usedBy": [ + "admin/course-plans/page.tsx", + "teacher/course-plans/page.tsx" + ] + }, + { + "name": "getCoursePlanById", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlans", + "shared.db.schema.coursePlanItems", + "shared.db.schema.classes", + "shared.db.schema.subjects", + "shared.db.schema.users" + ], + "usedBy": [ + "admin/course-plans/[id]/page.tsx", + "teacher/course-plans/[id]/page.tsx" + ] + }, + { + "name": "createCoursePlan", + "signature": "(data: CreateCoursePlanInput, createdBy: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlans", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "createCoursePlanAction" + ] + }, + { + "name": "updateCoursePlan", + "signature": "(id: string, data: Partial) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlans" + ], + "usedBy": [ + "updateCoursePlanAction" + ] + }, + { + "name": "deleteCoursePlan", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlans" + ], + "usedBy": [ + "deleteCoursePlanAction" + ] + }, + { + "name": "createCoursePlanItem", + "signature": "(data: CreateCoursePlanItemInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlanItems", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "createCoursePlanItemAction" + ] + }, + { + "name": "updateCoursePlanItem", + "signature": "(id: string, data: Partial) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlanItems" + ], + "usedBy": [ + "updateCoursePlanItemAction", + "toggleCoursePlanItemCompletedAction" + ] + }, + { + "name": "deleteCoursePlanItem", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlanItems" + ], + "usedBy": [ + "deleteCoursePlanItemAction" + ] + }, + { + "name": "reorderCoursePlanItems", + "signature": "(planId: string, items: ReorderCoursePlanItemInput[]) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.coursePlanItems" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getSubjectOptions", + "signature": "() => Promise<{id:string;name:string}[]>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.subjects" + ], + "usedBy": [ + "admin/course-plans/create/page.tsx", + "admin/course-plans/[id]/edit/page.tsx" + ] + } ], "schemas": [ - { "name": "CreateCoursePlanSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, subjectId, teacherId, academicYearId?, semester?, totalHours?, weeklyHours?, startDate?, endDate?, syllabus?, objectives?, status? }", "usedBy": ["createCoursePlanAction"] }, - { "name": "UpdateCoursePlanSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId?, subjectId?, teacherId?, academicYearId?, semester?, totalHours?, completedHours?, weeklyHours?, startDate?, endDate?, syllabus?, objectives?, status? }", "usedBy": ["updateCoursePlanAction"] }, - { "name": "CreateCoursePlanItemSchema", "type": "zod", "file": "schema.ts", "definition": "{ planId, week, topic, content?, hours?, textbookChapter?, notes? }", "usedBy": ["createCoursePlanItemAction"] }, - { "name": "UpdateCoursePlanItemSchema", "type": "zod", "file": "schema.ts", "definition": "{ week?, topic?, content?, hours?, textbookChapter?, notes?, isCompleted?, completedAt? }", "usedBy": ["updateCoursePlanItemAction"] } + { + "name": "CreateCoursePlanSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ classId, subjectId, teacherId, academicYearId?, semester?, totalHours?, weeklyHours?, startDate?, endDate?, syllabus?, objectives?, status? }", + "usedBy": [ + "createCoursePlanAction" + ] + }, + { + "name": "UpdateCoursePlanSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ classId?, subjectId?, teacherId?, academicYearId?, semester?, totalHours?, completedHours?, weeklyHours?, startDate?, endDate?, syllabus?, objectives?, status? }", + "usedBy": [ + "updateCoursePlanAction" + ] + }, + { + "name": "CreateCoursePlanItemSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ planId, week, topic, content?, hours?, textbookChapter?, notes? }", + "usedBy": [ + "createCoursePlanItemAction" + ] + }, + { + "name": "UpdateCoursePlanItemSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ week?, topic?, content?, hours?, textbookChapter?, notes?, isCompleted?, completedAt? }", + "usedBy": [ + "updateCoursePlanItemAction" + ] + } ], "types": [ - { "name": "CoursePlan", "type": "interface", "file": "types.ts", "definition": "{ id, classId, subjectId, teacherId, academicYearId, semester, totalHours, completedHours, weeklyHours, startDate, endDate, syllabus, objectives, status, createdBy, createdAt, updatedAt }", "usedBy": ["course-plans/components", "data-access"] }, - { "name": "CoursePlanItem", "type": "interface", "file": "types.ts", "definition": "{ id, planId, week, topic, content, hours, textbookChapter, notes, isCompleted, completedAt, createdAt, updatedAt }", "usedBy": ["course-plans/components", "data-access"] }, - { "name": "CoursePlanListItem", "type": "interface", "file": "types.ts", "definition": "= CoursePlan & { className, subjectName, teacherName }", "usedBy": ["列表页", "data-access"] }, - { "name": "CoursePlanWithItems", "type": "interface", "file": "types.ts", "definition": "= CoursePlanListItem & { items: CoursePlanItem[] }", "usedBy": ["详情页", "data-access"] }, - { "name": "CoursePlanStatus", "type": "type", "file": "types.ts", "definition": "\"planning\" | \"active\" | \"completed\" | \"paused\"", "usedBy": ["data-access", "components"] }, - { "name": "CoursePlanSemester", "type": "type", "file": "types.ts", "definition": "\"1\" | \"2\"", "usedBy": ["data-access", "components"] }, - { "name": "GetCoursePlansParams", "type": "interface", "file": "types.ts", "definition": "{ classId?, teacherId?, subjectId?, status? }", "usedBy": ["getCoursePlans", "getCoursePlansAction"] }, - { "name": "ReorderCoursePlanItemInput", "type": "interface", "file": "types.ts", "definition": "{ id, week }", "usedBy": ["reorderCoursePlanItems"] } + { + "name": "CoursePlan", + "type": "interface", + "file": "types.ts", + "definition": "{ id, classId, subjectId, teacherId, academicYearId, semester, totalHours, completedHours, weeklyHours, startDate, endDate, syllabus, objectives, status, createdBy, createdAt, updatedAt }", + "usedBy": [ + "course-plans/components", + "data-access" + ] + }, + { + "name": "CoursePlanItem", + "type": "interface", + "file": "types.ts", + "definition": "{ id, planId, week, topic, content, hours, textbookChapter, notes, isCompleted, completedAt, createdAt, updatedAt }", + "usedBy": [ + "course-plans/components", + "data-access" + ] + }, + { + "name": "CoursePlanListItem", + "type": "interface", + "file": "types.ts", + "definition": "= CoursePlan & { className, subjectName, teacherName }", + "usedBy": [ + "列表页", + "data-access" + ] + }, + { + "name": "CoursePlanWithItems", + "type": "interface", + "file": "types.ts", + "definition": "= CoursePlanListItem & { items: CoursePlanItem[] }", + "usedBy": [ + "详情页", + "data-access" + ] + }, + { + "name": "CoursePlanStatus", + "type": "type", + "file": "types.ts", + "definition": "\"planning\" | \"active\" | \"completed\" | \"paused\"", + "usedBy": [ + "data-access", + "components" + ] + }, + { + "name": "CoursePlanSemester", + "type": "type", + "file": "types.ts", + "definition": "\"1\" | \"2\"", + "usedBy": [ + "data-access", + "components" + ] + }, + { + "name": "GetCoursePlansParams", + "type": "interface", + "file": "types.ts", + "definition": "{ classId?, teacherId?, subjectId?, status? }", + "usedBy": [ + "getCoursePlans", + "getCoursePlansAction" + ] + }, + { + "name": "ReorderCoursePlanItemInput", + "type": "interface", + "file": "types.ts", + "definition": "{ id, week }", + "usedBy": [ + "reorderCoursePlanItems" + ] + } ], "components": [ - { "name": "CoursePlanProgress", "file": "components/course-plan-progress.tsx", "purpose": "进度条组件(completedHours/totalHours 百分比)" }, - { "name": "CoursePlanList", "file": "components/course-plan-list.tsx", "purpose": "课程计划列表(支持状态筛选,URL同步)" }, - { "name": "CoursePlanForm", "file": "components/course-plan-form.tsx", "purpose": "创建/编辑表单(班级、科目、教师、学年、学期、状态、课时、日期、大纲、目标)" }, - { "name": "CoursePlanItemEditor", "file": "components/course-plan-item-editor.tsx", "purpose": "周计划条目编辑器(Dialog,支持创建/编辑/删除/切换完成)" }, - { "name": "CoursePlanDetail", "file": "components/course-plan-detail.tsx", "purpose": "详情视图(计划信息、进度、大纲、目标、周计划表格、删除确认)" } + { + "name": "CoursePlanProgress", + "file": "components/course-plan-progress.tsx", + "purpose": "进度条组件(completedHours/totalHours 百分比)" + }, + { + "name": "CoursePlanList", + "file": "components/course-plan-list.tsx", + "purpose": "课程计划列表(支持状态筛选,URL同步)" + }, + { + "name": "CoursePlanForm", + "file": "components/course-plan-form.tsx", + "purpose": "创建/编辑表单(班级、科目、教师、学年、学期、状态、课时、日期、大纲、目标)" + }, + { + "name": "CoursePlanItemEditor", + "file": "components/course-plan-item-editor.tsx", + "purpose": "周计划条目编辑器(Dialog,支持创建/编辑/删除/切换完成)" + }, + { + "name": "CoursePlanDetail", + "file": "components/course-plan-detail.tsx", + "purpose": "详情视图(计划信息、进度、大纲、目标、周计划表格、删除确认)" + } ] } }, @@ -1263,27 +6598,169 @@ "description": "家长端仪表盘:聚合家长关联子女的学习数据(课表、作业、成绩、班级),支持多子女切换查看。家长通过 parentStudentRelations 表关联子女,DataScope 解析为 children 类型", "exports": { "dataAccess": [ - { "name": "getChildren", "signature": "(parentId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.parentStudentRelations", "react.cache"], "usedBy": ["getParentDashboardData (内部)", "parent/children/[studentId]/page.tsx"] }, - { "name": "getChildBasicInfo", "signature": "(studentId: string, relation?: string | null) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.users", "shared.db.schema.grades", "shared.db.schema.classEnrollments", "shared.db.schema.classes", "react.cache"], "usedBy": ["getChildDashboardData (内部)"] }, - { "name": "getChildDashboardData", "signature": "(studentId: string, relation?: string | null) => Promise", "file": "data-access.ts", "deps": ["getChildBasicInfo", "classes/data-access.getStudentClasses", "classes/data-access.getStudentSchedule", "homework/data-access.getStudentHomeworkAssignments", "homework/data-access.getStudentDashboardGrades", "grades/data-access.getStudentGradeSummary", "react.cache"], "usedBy": ["parent/children/[studentId]/page.tsx", "getParentDashboardData (内部)"] }, - { "name": "getParentDashboardData", "signature": "(parentId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.users", "getChildren", "getChildDashboardData", "react.cache"], "usedBy": ["parent/dashboard/page.tsx"] } + { + "name": "getChildren", + "signature": "(parentId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.parentStudentRelations", + "react.cache" + ], + "usedBy": [ + "getParentDashboardData (内部)", + "parent/children/[studentId]/page.tsx" + ] + }, + { + "name": "getChildBasicInfo", + "signature": "(studentId: string, relation?: string | null) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.users", + "shared.db.schema.grades", + "shared.db.schema.classEnrollments", + "shared.db.schema.classes", + "react.cache" + ], + "usedBy": [ + "getChildDashboardData (内部)" + ] + }, + { + "name": "getChildDashboardData", + "signature": "(studentId: string, relation?: string | null) => Promise", + "file": "data-access.ts", + "deps": [ + "getChildBasicInfo", + "classes/data-access.getStudentClasses", + "classes/data-access.getStudentSchedule", + "homework/data-access.getStudentHomeworkAssignments", + "homework/data-access.getStudentDashboardGrades", + "grades/data-access.getStudentGradeSummary", + "react.cache" + ], + "usedBy": [ + "parent/children/[studentId]/page.tsx", + "getParentDashboardData (内部)" + ] + }, + { + "name": "getParentDashboardData", + "signature": "(parentId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.users", + "getChildren", + "getChildDashboardData", + "react.cache" + ], + "usedBy": [ + "parent/dashboard/page.tsx" + ] + } ], "types": [ - { "name": "ParentChildRelation", "type": "type", "file": "types.ts", "definition": "{ id, parentId, studentId, relation: string | null, createdAt: string }", "usedBy": ["getChildren", "getParentDashboardData"] }, - { "name": "ChildBasicInfo", "type": "type", "file": "types.ts", "definition": "{ id, name: string | null, email, image: string | null, gradeName: string | null, className: string | null, classId: string | null, relation: string | null }", "usedBy": ["getChildBasicInfo", "ChildDashboardData.basicInfo"] }, - { "name": "ChildScheduleItem", "type": "type", "file": "types.ts", "definition": "{ id, classId, className, course, startTime, endTime, location: string | null }", "usedBy": ["ChildDashboardData.todaySchedule", "child-schedule-card.tsx"] }, - { "name": "ChildHomeworkSummary", "type": "type", "file": "types.ts", "definition": "{ pendingCount, submittedCount, gradedCount, overdueCount, recentAssignments: StudentHomeworkAssignmentListItem[] }", "usedBy": ["ChildDashboardData.homeworkSummary", "child-homework-summary.tsx"] }, - { "name": "ChildDashboardData", "type": "type", "file": "types.ts", "definition": "{ basicInfo: ChildBasicInfo, enrolledClasses: StudentEnrolledClass[], todaySchedule: ChildScheduleItem[], homeworkSummary: ChildHomeworkSummary, gradeTrend: StudentDashboardGradeProps, gradeSummary: StudentGradeSummary | null }", "usedBy": ["getChildDashboardData", "ParentDashboardData.children", "所有 child-* 组件"] }, - { "name": "ParentDashboardData", "type": "type", "file": "types.ts", "definition": "{ parentName: string | null, children: ChildDashboardData[] }", "usedBy": ["getParentDashboardData", "parent-dashboard.tsx"] } + { + "name": "ParentChildRelation", + "type": "type", + "file": "types.ts", + "definition": "{ id, parentId, studentId, relation: string | null, createdAt: string }", + "usedBy": [ + "getChildren", + "getParentDashboardData" + ] + }, + { + "name": "ChildBasicInfo", + "type": "type", + "file": "types.ts", + "definition": "{ id, name: string | null, email, image: string | null, gradeName: string | null, className: string | null, classId: string | null, relation: string | null }", + "usedBy": [ + "getChildBasicInfo", + "ChildDashboardData.basicInfo" + ] + }, + { + "name": "ChildScheduleItem", + "type": "type", + "file": "types.ts", + "definition": "{ id, classId, className, course, startTime, endTime, location: string | null }", + "usedBy": [ + "ChildDashboardData.todaySchedule", + "child-schedule-card.tsx" + ] + }, + { + "name": "ChildHomeworkSummary", + "type": "type", + "file": "types.ts", + "definition": "{ pendingCount, submittedCount, gradedCount, overdueCount, recentAssignments: StudentHomeworkAssignmentListItem[] }", + "usedBy": [ + "ChildDashboardData.homeworkSummary", + "child-homework-summary.tsx" + ] + }, + { + "name": "ChildDashboardData", + "type": "type", + "file": "types.ts", + "definition": "{ basicInfo: ChildBasicInfo, enrolledClasses: StudentEnrolledClass[], todaySchedule: ChildScheduleItem[], homeworkSummary: ChildHomeworkSummary, gradeTrend: StudentDashboardGradeProps, gradeSummary: StudentGradeSummary | null }", + "usedBy": [ + "getChildDashboardData", + "ParentDashboardData.children", + "所有 child-* 组件" + ] + }, + { + "name": "ParentDashboardData", + "type": "type", + "file": "types.ts", + "definition": "{ parentName: string | null, children: ChildDashboardData[] }", + "usedBy": [ + "getParentDashboardData", + "parent-dashboard.tsx" + ] + } ], "components": [ - { "name": "ParentDashboard", "file": "components/parent-dashboard.tsx", "purpose": "主容器组件(问候语、子女卡片网格、空状态)" }, - { "name": "ChildCard", "file": "components/child-card.tsx", "purpose": "子女卡片(头像、姓名、班级、待完成/逾期/平均分统计,点击跳转详情)" }, - { "name": "ChildDetailHeader", "file": "components/child-detail-header.tsx", "purpose": "子女详情页头部(返回按钮、头像、姓名、班级、年级、关系)" }, - { "name": "ChildDetailPanel", "file": "components/child-detail-panel.tsx", "purpose": "子女详情面板容器(组合 homework/grade/schedule 三个子组件)" }, - { "name": "ChildHomeworkSummary", "file": "components/child-homework-summary.tsx", "purpose": "子女作业概览(pending/submitted/graded/overdue 统计 + 最近作业列表)" }, - { "name": "ChildGradeSummary", "file": "components/child-grade-summary.tsx", "purpose": "子女成绩概览(Recharts 折线图趋势 + 最新分数 + 班级排名 + 最近成绩列表,use client)" }, - { "name": "ChildScheduleCard", "file": "components/child-schedule-card.tsx", "purpose": "子女今日课表卡片(课程、时间、地点、班级)" } + { + "name": "ParentDashboard", + "file": "components/parent-dashboard.tsx", + "purpose": "主容器组件(问候语、子女卡片网格、空状态)" + }, + { + "name": "ChildCard", + "file": "components/child-card.tsx", + "purpose": "子女卡片(头像、姓名、班级、待完成/逾期/平均分统计,点击跳转详情)" + }, + { + "name": "ChildDetailHeader", + "file": "components/child-detail-header.tsx", + "purpose": "子女详情页头部(返回按钮、头像、姓名、班级、年级、关系)" + }, + { + "name": "ChildDetailPanel", + "file": "components/child-detail-panel.tsx", + "purpose": "子女详情面板容器(组合 homework/grade/schedule 三个子组件)" + }, + { + "name": "ChildHomeworkSummary", + "file": "components/child-homework-summary.tsx", + "purpose": "子女作业概览(pending/submitted/graded/overdue 统计 + 最近作业列表)" + }, + { + "name": "ChildGradeSummary", + "file": "components/child-grade-summary.tsx", + "purpose": "子女成绩概览(Recharts 折线图趋势 + 最新分数 + 班级排名 + 最近成绩列表,use client)" + }, + { + "name": "ChildScheduleCard", + "file": "components/child-schedule-card.tsx", + "purpose": "子女今日课表卡片(课程、时间、地点、班级)" + } ] } }, @@ -1292,61 +6769,529 @@ "description": "站内消息系统:用户间私信收发(支持回复链)、站内通知(多态类型:message/announcement/homework/grade),SiteHeader 通知下拉菜单展示未读数", "exports": { "actions": [ - { "name": "sendMessageAction", "permission": "MESSAGE_SEND", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "发送消息(同时为收件人创建通知;支持 parentMessageId 回复)", "deps": ["requirePermission", "shared/db", "data-access.createMessage", "data-access.createNotification", "revalidatePath"], "usedBy": ["message-compose.tsx"] }, - { "name": "markMessageAsReadAction", "permission": "MESSAGE_READ", "signature": "(id: string) => Promise>", "purpose": "标记消息已读(设置 readAt)", "deps": ["requirePermission", "data-access.markMessageAsRead", "revalidatePath"], "usedBy": ["message-detail.tsx", "messages/[id]/page.tsx"] }, - { "name": "deleteMessageAction", "permission": "MESSAGE_DELETE", "signature": "(id: string) => Promise>", "purpose": "删除消息(仅发送者或接收者可删)", "deps": ["requirePermission", "data-access.deleteMessage", "revalidatePath"], "usedBy": ["message-detail.tsx"] }, - { "name": "getMessagesAction", "permission": "MESSAGE_READ", "signature": "(params?: { type?, page?, pageSize? }) => Promise>>", "purpose": "获取消息列表(收件箱/已发送,分页)", "deps": ["requirePermission", "data-access.getMessages"], "usedBy": ["message-list.tsx"] }, - { "name": "getMessageDetailAction", "permission": "MESSAGE_READ", "signature": "(id: string) => Promise>", "purpose": "获取消息详情(含回复线程)", "deps": ["requirePermission", "data-access.getMessageById", "data-access.getMessageThread"], "usedBy": ["message-detail.tsx"] }, - { "name": "getRecipientsAction", "permission": "MESSAGE_SEND", "signature": "() => Promise>", "purpose": "获取可发送对象列表(按 DataScope 过滤)", "deps": ["requirePermission", "data-access.getRecipients"], "usedBy": ["messages/compose/page.tsx"] }, - { "name": "getNotificationsAction", "permission": "requireAuth", "signature": "(params?: { page?, pageSize? }) => Promise>>", "purpose": "获取当前用户通知列表(分页)", "deps": ["requireAuth", "data-access.getNotifications"], "usedBy": ["notification-dropdown.tsx", "notification-list.tsx"] }, - { "name": "markNotificationAsReadAction", "permission": "requireAuth", "signature": "(id: string) => Promise>", "purpose": "标记单条通知已读", "deps": ["requireAuth", "data-access.markNotificationAsRead", "revalidatePath"], "usedBy": ["notification-dropdown.tsx", "notification-list.tsx"] }, - { "name": "markAllNotificationsAsReadAction", "permission": "requireAuth", "signature": "() => Promise>", "purpose": "标记所有通知已读", "deps": ["requireAuth", "data-access.markAllNotificationsAsRead", "revalidatePath"], "usedBy": ["notification-dropdown.tsx", "notification-list.tsx"] }, - { "name": "getNotificationPreferencesAction", "permission": "requireAuth", "signature": "() => Promise>", "purpose": "获取当前用户的通知偏好设置(首次访问自动创建默认记录)", "deps": ["requireAuth", "notification-preferences.getNotificationPreferences"], "usedBy": ["settings/page.tsx", "settings/components/notification-preferences-form.tsx"] }, - { "name": "updateNotificationPreferencesAction", "permission": "requireAuth", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新当前用户的通知偏好设置(upsert 语义,未提供字段保留原值)", "deps": ["requireAuth", "notification-preferences.upsertNotificationPreferences", "revalidatePath"], "usedBy": ["settings/components/notification-preferences-form.tsx"] } + { + "name": "sendMessageAction", + "permission": "MESSAGE_SEND", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "发送消息(同时为收件人创建通知;支持 parentMessageId 回复)", + "deps": [ + "requirePermission", + "shared/db", + "data-access.createMessage", + "data-access.createNotification", + "revalidatePath" + ], + "usedBy": [ + "message-compose.tsx" + ] + }, + { + "name": "markMessageAsReadAction", + "permission": "MESSAGE_READ", + "signature": "(id: string) => Promise>", + "purpose": "标记消息已读(设置 readAt)", + "deps": [ + "requirePermission", + "data-access.markMessageAsRead", + "revalidatePath" + ], + "usedBy": [ + "message-detail.tsx", + "messages/[id]/page.tsx" + ] + }, + { + "name": "deleteMessageAction", + "permission": "MESSAGE_DELETE", + "signature": "(id: string) => Promise>", + "purpose": "删除消息(仅发送者或接收者可删)", + "deps": [ + "requirePermission", + "data-access.deleteMessage", + "revalidatePath" + ], + "usedBy": [ + "message-detail.tsx" + ] + }, + { + "name": "getMessagesAction", + "permission": "MESSAGE_READ", + "signature": "(params?: { type?, page?, pageSize? }) => Promise>>", + "purpose": "获取消息列表(收件箱/已发送,分页)", + "deps": [ + "requirePermission", + "data-access.getMessages" + ], + "usedBy": [ + "message-list.tsx" + ] + }, + { + "name": "getMessageDetailAction", + "permission": "MESSAGE_READ", + "signature": "(id: string) => Promise>", + "purpose": "获取消息详情(含回复线程)", + "deps": [ + "requirePermission", + "data-access.getMessageById", + "data-access.getMessageThread" + ], + "usedBy": [ + "message-detail.tsx" + ] + }, + { + "name": "getRecipientsAction", + "permission": "MESSAGE_SEND", + "signature": "() => Promise>", + "purpose": "获取可发送对象列表(按 DataScope 过滤)", + "deps": [ + "requirePermission", + "data-access.getRecipients" + ], + "usedBy": [ + "messages/compose/page.tsx" + ] + }, + { + "name": "getNotificationsAction", + "permission": "requireAuth", + "signature": "(params?: { page?, pageSize? }) => Promise>>", + "purpose": "获取当前用户通知列表(分页)", + "deps": [ + "requireAuth", + "data-access.getNotifications" + ], + "usedBy": [ + "notification-dropdown.tsx", + "notification-list.tsx" + ] + }, + { + "name": "markNotificationAsReadAction", + "permission": "requireAuth", + "signature": "(id: string) => Promise>", + "purpose": "标记单条通知已读", + "deps": [ + "requireAuth", + "data-access.markNotificationAsRead", + "revalidatePath" + ], + "usedBy": [ + "notification-dropdown.tsx", + "notification-list.tsx" + ] + }, + { + "name": "markAllNotificationsAsReadAction", + "permission": "requireAuth", + "signature": "() => Promise>", + "purpose": "标记所有通知已读", + "deps": [ + "requireAuth", + "data-access.markAllNotificationsAsRead", + "revalidatePath" + ], + "usedBy": [ + "notification-dropdown.tsx", + "notification-list.tsx" + ] + }, + { + "name": "getNotificationPreferencesAction", + "permission": "requireAuth", + "signature": "() => Promise>", + "purpose": "获取当前用户的通知偏好设置(首次访问自动创建默认记录)", + "deps": [ + "requireAuth", + "notification-preferences.getNotificationPreferences" + ], + "usedBy": [ + "settings/page.tsx", + "settings/components/notification-preferences-form.tsx" + ] + }, + { + "name": "updateNotificationPreferencesAction", + "permission": "requireAuth", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "更新当前用户的通知偏好设置(upsert 语义,未提供字段保留原值)", + "deps": [ + "requireAuth", + "notification-preferences.upsertNotificationPreferences", + "revalidatePath" + ], + "usedBy": [ + "settings/components/notification-preferences-form.tsx" + ] + } ], "dataAccess": [ - { "name": "getMessages", "signature": "(userId: string, params?: { type?, page?, pageSize? }) => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messages", "shared.db.schema.users"], "usedBy": ["getMessagesAction"] }, - { "name": "getMessageById", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messages", "shared.db.schema.users"], "usedBy": ["getMessageDetailAction", "messages/[id]/page.tsx"] }, - { "name": "getMessageThread", "signature": "(rootId: string, userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messages", "shared.db.schema.users"], "usedBy": ["getMessageDetailAction"] }, - { "name": "createMessage", "signature": "(input: CreateMessageInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messages", "@paralleldrive/cuid2"], "usedBy": ["sendMessageAction"] }, - { "name": "markMessageAsRead", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messages"], "usedBy": ["markMessageAsReadAction", "messages/[id]/page.tsx"] }, - { "name": "deleteMessage", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messages"], "usedBy": ["deleteMessageAction"] }, - { "name": "getUnreadMessageCount", "signature": "(userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messages"], "usedBy": ["待扩展"] }, - { "name": "getNotifications", "signature": "(userId: string, params?: { page?, pageSize? }) => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messageNotifications"], "usedBy": ["getNotificationsAction"] }, - { "name": "createNotification", "signature": "(input: CreateNotificationInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messageNotifications", "@paralleldrive/cuid2"], "usedBy": ["sendMessageAction (内部调用)"] }, - { "name": "markNotificationAsRead", "signature": "(id: string, userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messageNotifications"], "usedBy": ["markNotificationAsReadAction"] }, - { "name": "markAllNotificationsAsRead", "signature": "(userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messageNotifications"], "usedBy": ["markAllNotificationsAsReadAction"] }, - { "name": "getUnreadNotificationCount", "signature": "(userId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.messageNotifications"], "usedBy": ["待扩展"] }, - { "name": "getRecipients", "signature": "(ctx: AuthContext) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.users", "shared.db.schema.classEnrollments", "shared.db.schema.classes", "shared.db.schema.grades"], "usedBy": ["getRecipientsAction", "messages/compose/page.tsx"] } + { + "name": "getMessages", + "signature": "(userId: string, params?: { type?, page?, pageSize? }) => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messages", + "shared.db.schema.users" + ], + "usedBy": [ + "getMessagesAction" + ] + }, + { + "name": "getMessageById", + "signature": "(id: string, userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messages", + "shared.db.schema.users" + ], + "usedBy": [ + "getMessageDetailAction", + "messages/[id]/page.tsx" + ] + }, + { + "name": "getMessageThread", + "signature": "(rootId: string, userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messages", + "shared.db.schema.users" + ], + "usedBy": [ + "getMessageDetailAction" + ] + }, + { + "name": "createMessage", + "signature": "(input: CreateMessageInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messages", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "sendMessageAction" + ] + }, + { + "name": "markMessageAsRead", + "signature": "(id: string, userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messages" + ], + "usedBy": [ + "markMessageAsReadAction", + "messages/[id]/page.tsx" + ] + }, + { + "name": "deleteMessage", + "signature": "(id: string, userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messages" + ], + "usedBy": [ + "deleteMessageAction" + ] + }, + { + "name": "getUnreadMessageCount", + "signature": "(userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messages" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getNotifications", + "signature": "(userId: string, params?: { page?, pageSize? }) => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messageNotifications" + ], + "usedBy": [ + "getNotificationsAction" + ] + }, + { + "name": "createNotification", + "signature": "(input: CreateNotificationInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messageNotifications", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "sendMessageAction (内部调用)" + ] + }, + { + "name": "markNotificationAsRead", + "signature": "(id: string, userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messageNotifications" + ], + "usedBy": [ + "markNotificationAsReadAction" + ] + }, + { + "name": "markAllNotificationsAsRead", + "signature": "(userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messageNotifications" + ], + "usedBy": [ + "markAllNotificationsAsReadAction" + ] + }, + { + "name": "getUnreadNotificationCount", + "signature": "(userId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.messageNotifications" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getRecipients", + "signature": "(ctx: AuthContext) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.users", + "shared.db.schema.classEnrollments", + "shared.db.schema.classes", + "shared.db.schema.grades" + ], + "usedBy": [ + "getRecipientsAction", + "messages/compose/page.tsx" + ] + } ], "notificationPreferences": [ - { "name": "getNotificationPreferences", "signature": "(userId: string) => Promise", "file": "notification-preferences.ts", "purpose": "获取用户通知偏好(React cache 包装;若不存在则自动创建默认记录,并发冲突回退到查询)", "deps": ["shared.db", "shared.db.schema.notificationPreferences", "react.cache", "@paralleldrive/cuid2"], "usedBy": ["getNotificationPreferencesAction", "settings/page.tsx"] }, - { "name": "upsertNotificationPreferences", "signature": "(userId: string, input: UpdateNotificationPreferencesInput) => Promise", "file": "notification-preferences.ts", "purpose": "更新或插入用户通知偏好(存在则部分更新,不存在则插入;未提供字段保留原值)", "deps": ["shared.db", "shared.db.schema.notificationPreferences", "@paralleldrive/cuid2"], "usedBy": ["updateNotificationPreferencesAction"] } + { + "name": "getNotificationPreferences", + "signature": "(userId: string) => Promise", + "file": "notification-preferences.ts", + "purpose": "获取用户通知偏好(React cache 包装;若不存在则自动创建默认记录,并发冲突回退到查询)", + "deps": [ + "shared.db", + "shared.db.schema.notificationPreferences", + "react.cache", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "getNotificationPreferencesAction", + "settings/page.tsx" + ] + }, + { + "name": "upsertNotificationPreferences", + "signature": "(userId: string, input: UpdateNotificationPreferencesInput) => Promise", + "file": "notification-preferences.ts", + "purpose": "更新或插入用户通知偏好(存在则部分更新,不存在则插入;未提供字段保留原值)", + "deps": [ + "shared.db", + "shared.db.schema.notificationPreferences", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "updateNotificationPreferencesAction" + ] + } ], "schemas": [ - { "name": "SendMessageSchema", "type": "zod", "file": "schema.ts", "definition": "{ receiverId: string, subject?: string, content: string, parentMessageId?: string }", "usedBy": ["sendMessageAction"] } + { + "name": "SendMessageSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ receiverId: string, subject?: string, content: string, parentMessageId?: string }", + "usedBy": [ + "sendMessageAction" + ] + } ], "types": [ - { "name": "Message", "type": "type", "file": "types.ts", "definition": "{ id, senderId, receiverId, subject: string | null, content, isRead, readAt: string | null, parentMessageId: string | null, createdAt, senderName, receiverName }", "usedBy": ["messaging/components", "页面"] }, - { "name": "MessageListItem", "type": "type", "file": "types.ts", "definition": "消息列表项类型(同 Message 精简版)", "usedBy": ["列表页"] }, - { "name": "MessageThread", "type": "type", "file": "types.ts", "definition": "{ root: Message, replies: Message[] }", "usedBy": ["详情页"] }, - { "name": "Notification", "type": "type", "file": "types.ts", "definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, createdAt }", "usedBy": ["notification-dropdown", "notification-list"] }, - { "name": "NotificationListItem", "type": "type", "file": "types.ts", "definition": "通知列表项类型(同 Notification)", "usedBy": ["列表页"] }, - { "name": "NotificationType", "type": "type", "file": "types.ts", "definition": "'message' | 'announcement' | 'homework' | 'grade'", "usedBy": ["data-access", "components"] }, - { "name": "MessageType", "type": "type", "file": "types.ts", "definition": "'inbox' | 'sent'", "usedBy": ["getMessages 参数"] }, - { "name": "CreateMessageInput", "type": "type", "file": "types.ts", "definition": "{ senderId, receiverId, subject?, content, parentMessageId? }", "usedBy": ["createMessage"] }, - { "name": "CreateNotificationInput", "type": "type", "file": "types.ts", "definition": "{ userId, type: NotificationType, title, content?, link? }", "usedBy": ["createNotification"] }, - { "name": "RecipientOption", "type": "type", "file": "types.ts", "definition": "{ id: string, name: string }", "usedBy": ["compose 页面下拉选项"] }, - { "name": "PaginatedResult", "type": "type", "file": "types.ts", "definition": "{ items: T[], total: number, page: number, pageSize: number, totalPages: number }", "usedBy": ["getMessages", "getNotifications"] }, - { "name": "NotificationPreferences", "type": "interface", "file": "types.ts", "definition": "{ id, userId, emailEnabled, smsEnabled, pushEnabled, homeworkNotifications, gradeNotifications, announcementNotifications, messageNotifications, attendanceNotifications, createdAt, updatedAt }", "usedBy": ["notification-preferences", "getNotificationPreferencesAction", "updateNotificationPreferencesAction", "settings/components/notification-preferences-form"] }, - { "name": "UpdateNotificationPreferencesInput", "type": "interface", "file": "types.ts", "definition": "{ emailEnabled?, smsEnabled?, pushEnabled?, homeworkNotifications?, gradeNotifications?, announcementNotifications?, messageNotifications?, attendanceNotifications? }", "usedBy": ["upsertNotificationPreferences", "updateNotificationPreferencesAction"] } + { + "name": "Message", + "type": "type", + "file": "types.ts", + "definition": "{ id, senderId, receiverId, subject: string | null, content, isRead, readAt: string | null, parentMessageId: string | null, createdAt, senderName, receiverName }", + "usedBy": [ + "messaging/components", + "页面" + ] + }, + { + "name": "MessageListItem", + "type": "type", + "file": "types.ts", + "definition": "消息列表项类型(同 Message 精简版)", + "usedBy": [ + "列表页" + ] + }, + { + "name": "MessageThread", + "type": "type", + "file": "types.ts", + "definition": "{ root: Message, replies: Message[] }", + "usedBy": [ + "详情页" + ] + }, + { + "name": "Notification", + "type": "type", + "file": "types.ts", + "definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, createdAt }", + "usedBy": [ + "notification-dropdown", + "notification-list" + ] + }, + { + "name": "NotificationListItem", + "type": "type", + "file": "types.ts", + "definition": "通知列表项类型(同 Notification)", + "usedBy": [ + "列表页" + ] + }, + { + "name": "NotificationType", + "type": "type", + "file": "types.ts", + "definition": "'message' | 'announcement' | 'homework' | 'grade'", + "usedBy": [ + "data-access", + "components" + ] + }, + { + "name": "MessageType", + "type": "type", + "file": "types.ts", + "definition": "'inbox' | 'sent'", + "usedBy": [ + "getMessages 参数" + ] + }, + { + "name": "CreateMessageInput", + "type": "type", + "file": "types.ts", + "definition": "{ senderId, receiverId, subject?, content, parentMessageId? }", + "usedBy": [ + "createMessage" + ] + }, + { + "name": "CreateNotificationInput", + "type": "type", + "file": "types.ts", + "definition": "{ userId, type: NotificationType, title, content?, link? }", + "usedBy": [ + "createNotification" + ] + }, + { + "name": "RecipientOption", + "type": "type", + "file": "types.ts", + "definition": "{ id: string, name: string }", + "usedBy": [ + "compose 页面下拉选项" + ] + }, + { + "name": "PaginatedResult", + "type": "type", + "file": "types.ts", + "definition": "{ items: T[], total: number, page: number, pageSize: number, totalPages: number }", + "usedBy": [ + "getMessages", + "getNotifications" + ] + }, + { + "name": "NotificationPreferences", + "type": "interface", + "file": "types.ts", + "definition": "{ id, userId, emailEnabled, smsEnabled, pushEnabled, homeworkNotifications, gradeNotifications, announcementNotifications, messageNotifications, attendanceNotifications, createdAt, updatedAt }", + "usedBy": [ + "notification-preferences", + "getNotificationPreferencesAction", + "updateNotificationPreferencesAction", + "settings/components/notification-preferences-form" + ] + }, + { + "name": "UpdateNotificationPreferencesInput", + "type": "interface", + "file": "types.ts", + "definition": "{ emailEnabled?, smsEnabled?, pushEnabled?, homeworkNotifications?, gradeNotifications?, announcementNotifications?, messageNotifications?, attendanceNotifications? }", + "usedBy": [ + "upsertNotificationPreferences", + "updateNotificationPreferencesAction" + ] + } ], "components": [ - { "name": "MessageList", "file": "components/message-list.tsx", "purpose": "消息列表(收件箱/已发送 Tab 切换,已读/未读标记,usePermission 控制写消息按钮)" }, - { "name": "MessageDetail", "file": "components/message-detail.tsx", "purpose": "消息详情(含回复线程、回复/删除操作,AlertDialog 删除确认,usePermission 控制按钮可见性)" }, - { "name": "MessageCompose", "file": "components/message-compose.tsx", "purpose": "写消息表单(收件人 Select、主题 Input、内容 Textarea,支持回复模式)" }, - { "name": "NotificationDropdown", "file": "components/notification-dropdown.tsx", "purpose": "SiteHeader 通知下拉菜单(Bell 图标 + 未读数 Badge,滚动列表,标记已读,查看全部链接)" }, - { "name": "NotificationList", "file": "components/notification-list.tsx", "purpose": "通知完整列表(全部标记已读、单条标记已读、查看链接)" } + { + "name": "MessageList", + "file": "components/message-list.tsx", + "purpose": "消息列表(收件箱/已发送 Tab 切换,已读/未读标记,usePermission 控制写消息按钮)" + }, + { + "name": "MessageDetail", + "file": "components/message-detail.tsx", + "purpose": "消息详情(含回复线程、回复/删除操作,AlertDialog 删除确认,usePermission 控制按钮可见性)" + }, + { + "name": "MessageCompose", + "file": "components/message-compose.tsx", + "purpose": "写消息表单(收件人 Select、主题 Input、内容 Textarea,支持回复模式)" + }, + { + "name": "NotificationDropdown", + "file": "components/notification-dropdown.tsx", + "purpose": "SiteHeader 通知下拉菜单(Bell 图标 + 未读数 Badge,滚动列表,标记已读,查看全部链接)" + }, + { + "name": "NotificationList", + "file": "components/notification-list.tsx", + "purpose": "通知完整列表(全部标记已读、单条标记已读、查看链接)" + } ] } }, @@ -1355,32 +7300,215 @@ "description": "通知渠道集成层:基于用户通知偏好(notification_preferences)将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口,dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。", "exports": { "actions": [ - { "name": "sendNotificationAction", "permission": "MESSAGE_SEND", "signature": "(payload: NotificationPayload) => Promise>", "purpose": "发送通知给指定用户(按偏好多渠道分发)", "deps": ["requirePermission", "dispatcher.sendNotification"], "usedBy": ["待扩展"] }, - { "name": "sendClassNotificationAction", "permission": "MESSAGE_SEND", "signature": "(classId: string, payload: Omit) => Promise>", "purpose": "发送班级通知(批量发送给班级所有学生;教师只能给自己所教班级发送)", "deps": ["requirePermission", "db.schema.classEnrollments", "db.schema.classes", "dispatcher.sendBatchNotifications"], "usedBy": ["待扩展"] } + { + "name": "sendNotificationAction", + "permission": "MESSAGE_SEND", + "signature": "(payload: NotificationPayload) => Promise>", + "purpose": "发送通知给指定用户(按偏好多渠道分发)", + "deps": [ + "requirePermission", + "dispatcher.sendNotification" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "sendClassNotificationAction", + "permission": "MESSAGE_SEND", + "signature": "(classId: string, payload: Omit) => Promise>", + "purpose": "发送班级通知(批量发送给班级所有学生;教师只能给自己所教班级发送)", + "deps": [ + "requirePermission", + "db.schema.classEnrollments", + "db.schema.classes", + "dispatcher.sendBatchNotifications" + ], + "usedBy": [ + "待扩展" + ] + } ], "dispatcher": [ - { "name": "sendNotification", "signature": "(payload: NotificationPayload) => Promise", "file": "dispatcher.ts", "purpose": "发送单条通知:读取用户偏好+联系方式,按偏好选择渠道并行发送,记录日志", "deps": ["data-access.getUserNotificationPreferences", "data-access.getUserContactInfo", "data-access.logNotificationSendBatch", "channels.sms-channel.createSmsSender", "channels.wechat-channel.createWechatSender", "channels.email-channel.createEmailSender", "channels.in-app-channel.createInAppSender"], "usedBy": ["sendNotificationAction", "sendClassNotificationAction"] }, - { "name": "sendBatchNotifications", "signature": "(payloads: NotificationPayload[]) => Promise", "file": "dispatcher.ts", "purpose": "批量发送通知(每个用户独立选择渠道,并行发送)", "deps": ["sendNotification"], "usedBy": ["sendClassNotificationAction"] } + { + "name": "sendNotification", + "signature": "(payload: NotificationPayload) => Promise", + "file": "dispatcher.ts", + "purpose": "发送单条通知:读取用户偏好+联系方式,按偏好选择渠道并行发送,记录日志", + "deps": [ + "data-access.getUserNotificationPreferences", + "data-access.getUserContactInfo", + "data-access.logNotificationSendBatch", + "channels.sms-channel.createSmsSender", + "channels.wechat-channel.createWechatSender", + "channels.email-channel.createEmailSender", + "channels.in-app-channel.createInAppSender" + ], + "usedBy": [ + "sendNotificationAction", + "sendClassNotificationAction" + ] + }, + { + "name": "sendBatchNotifications", + "signature": "(payloads: NotificationPayload[]) => Promise", + "file": "dispatcher.ts", + "purpose": "批量发送通知(每个用户独立选择渠道,并行发送)", + "deps": [ + "sendNotification" + ], + "usedBy": [ + "sendClassNotificationAction" + ] + } ], "dataAccess": [ - { "name": "getUserNotificationPreferences", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "获取用户通知偏好(复用 messaging.notification-preferences.getNotificationPreferences)", "deps": ["messaging.notification-preferences.getNotificationPreferences"], "usedBy": ["dispatcher.sendNotification"] }, - { "name": "getUserContactInfo", "signature": "(userId: string) => Promise", "file": "data-access.ts", "purpose": "获取用户联系方式(phone/email;wechatOpenId 暂不支持,users 表无此字段)", "deps": ["shared.db", "shared.db.schema.users", "react.cache"], "usedBy": ["dispatcher.sendNotification"] }, - { "name": "logNotificationSend", "signature": "(result: ChannelSendResult) => void", "file": "data-access.ts", "purpose": "记录单条发送日志(当前使用 console.info;未来可扩展 notification_logs 表)", "deps": [], "usedBy": ["logNotificationSendBatch"] }, - { "name": "logNotificationSendBatch", "signature": "(results: ChannelSendResult[]) => void", "file": "data-access.ts", "purpose": "批量记录发送日志", "deps": ["logNotificationSend"], "usedBy": ["dispatcher.sendNotification", "dispatcher.sendBatchNotifications"] } + { + "name": "getUserNotificationPreferences", + "signature": "(userId: string) => Promise", + "file": "data-access.ts", + "purpose": "获取用户通知偏好(复用 messaging.notification-preferences.getNotificationPreferences)", + "deps": [ + "messaging.notification-preferences.getNotificationPreferences" + ], + "usedBy": [ + "dispatcher.sendNotification" + ] + }, + { + "name": "getUserContactInfo", + "signature": "(userId: string) => Promise", + "file": "data-access.ts", + "purpose": "获取用户联系方式(phone/email;wechatOpenId 暂不支持,users 表无此字段)", + "deps": [ + "shared.db", + "shared.db.schema.users", + "react.cache" + ], + "usedBy": [ + "dispatcher.sendNotification" + ] + }, + { + "name": "logNotificationSend", + "signature": "(result: ChannelSendResult) => void", + "file": "data-access.ts", + "purpose": "记录单条发送日志(当前使用 console.info;未来可扩展 notification_logs 表)", + "deps": [], + "usedBy": [ + "logNotificationSendBatch" + ] + }, + { + "name": "logNotificationSendBatch", + "signature": "(results: ChannelSendResult[]) => void", + "file": "data-access.ts", + "purpose": "批量记录发送日志", + "deps": [ + "logNotificationSend" + ], + "usedBy": [ + "dispatcher.sendNotification", + "dispatcher.sendBatchNotifications" + ] + } ], "channels": [ - { "name": "createSmsSender", "file": "channels/sms-channel.ts", "purpose": "创建 SMS 渠道发送器(aliyun/tencent/mock,根据 SMS_PROVIDER 环境变量选择;SDK 动态 import)", "deps": ["环境变量: SMS_PROVIDER, SMS_ACCESS_KEY_ID, SMS_ACCESS_KEY_SECRET, SMS_SIGN_NAME, SMS_TEMPLATE_CODE"] }, - { "name": "createWechatSender", "file": "channels/wechat-channel.ts", "purpose": "创建微信渠道发送器(配置完整用真实发送器,否则 Mock;access_token 带缓存)", "deps": ["环境变量: WECHAT_APP_ID, WECHAT_APP_SECRET, WECHAT_TEMPLATE_ID"] }, - { "name": "createEmailSender", "file": "channels/email-channel.ts", "purpose": "创建邮件渠道发送器(配置 EMAIL_HOST 用 Nodemailer SMTP,否则 Mock;HTML 模板按 type 着色)", "deps": ["环境变量: EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, EMAIL_FROM"] }, - { "name": "createInAppSender", "file": "channels/in-app-channel.ts", "purpose": "创建站内消息渠道发送器(复用 messaging.data-access.createNotification 写入 message_notifications 表;总是启用)", "deps": ["messaging.data-access.createNotification"] } + { + "name": "createSmsSender", + "file": "channels/sms-channel.ts", + "purpose": "创建 SMS 渠道发送器(aliyun/tencent/mock,根据 SMS_PROVIDER 环境变量选择;SDK 动态 import)", + "deps": [ + "环境变量: SMS_PROVIDER, SMS_ACCESS_KEY_ID, SMS_ACCESS_KEY_SECRET, SMS_SIGN_NAME, SMS_TEMPLATE_CODE" + ] + }, + { + "name": "createWechatSender", + "file": "channels/wechat-channel.ts", + "purpose": "创建微信渠道发送器(配置完整用真实发送器,否则 Mock;access_token 带缓存)", + "deps": [ + "环境变量: WECHAT_APP_ID, WECHAT_APP_SECRET, WECHAT_TEMPLATE_ID" + ] + }, + { + "name": "createEmailSender", + "file": "channels/email-channel.ts", + "purpose": "创建邮件渠道发送器(配置 EMAIL_HOST 用 Nodemailer SMTP,否则 Mock;HTML 模板按 type 着色)", + "deps": [ + "环境变量: EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, EMAIL_FROM" + ] + }, + { + "name": "createInAppSender", + "file": "channels/in-app-channel.ts", + "purpose": "创建站内消息渠道发送器(复用 messaging.data-access.createNotification 写入 message_notifications 表;总是启用)", + "deps": [ + "messaging.data-access.createNotification" + ] + } ], "types": [ - { "name": "NotificationChannel", "type": "type", "file": "types.ts", "definition": "'in_app' | 'email' | 'sms' | 'wechat'", "usedBy": ["所有渠道文件", "dispatcher"] }, - { "name": "NotificationPayload", "type": "interface", "file": "types.ts", "definition": "{ userId, title, content, type: 'info'|'warning'|'error'|'success', metadata?, actionUrl? }", "usedBy": ["dispatcher", "actions", "所有渠道"] }, - { "name": "ChannelSendResult", "type": "interface", "file": "types.ts", "definition": "{ channel, success, messageId?, error?, sentAt }", "usedBy": ["dispatcher", "actions", "所有渠道"] }, - { "name": "NotificationChannelConfig", "type": "interface", "file": "types.ts", "definition": "{ enabled, sms?, wechat?, email? }", "usedBy": ["类型定义"] }, - { "name": "NotificationChannelSender", "type": "interface", "file": "channels/types.ts", "definition": "{ channel: NotificationChannel, send(payload, recipient), sendBatch(items) }", "usedBy": ["所有渠道实现", "dispatcher"] }, - { "name": "ChannelRecipient", "type": "interface", "file": "channels/types.ts", "definition": "{ userId, phone?, email?, wechatOpenId? }", "usedBy": ["所有渠道", "data-access.getUserContactInfo"] } + { + "name": "NotificationChannel", + "type": "type", + "file": "types.ts", + "definition": "'in_app' | 'email' | 'sms' | 'wechat'", + "usedBy": [ + "所有渠道文件", + "dispatcher" + ] + }, + { + "name": "NotificationPayload", + "type": "interface", + "file": "types.ts", + "definition": "{ userId, title, content, type: 'info'|'warning'|'error'|'success', metadata?, actionUrl? }", + "usedBy": [ + "dispatcher", + "actions", + "所有渠道" + ] + }, + { + "name": "ChannelSendResult", + "type": "interface", + "file": "types.ts", + "definition": "{ channel, success, messageId?, error?, sentAt }", + "usedBy": [ + "dispatcher", + "actions", + "所有渠道" + ] + }, + { + "name": "NotificationChannelConfig", + "type": "interface", + "file": "types.ts", + "definition": "{ enabled, sms?, wechat?, email? }", + "usedBy": [ + "类型定义" + ] + }, + { + "name": "NotificationChannelSender", + "type": "interface", + "file": "channels/types.ts", + "definition": "{ channel: NotificationChannel, send(payload, recipient), sendBatch(items) }", + "usedBy": [ + "所有渠道实现", + "dispatcher" + ] + }, + { + "name": "ChannelRecipient", + "type": "interface", + "file": "channels/types.ts", + "definition": "{ userId, phone?, email?, wechatOpenId? }", + "usedBy": [ + "所有渠道", + "data-access.getUserContactInfo" + ] + } ] } }, @@ -1389,57 +7517,479 @@ "description": "学生考勤管理:教师按班级/日期点名(单条/批量)、查询考勤记录、统计出勤率/迟到率,学生/家长查看本人/子女考勤汇总,管理员查看全校考勤记录。支持班级考勤规则配置。", "exports": { "actions": [ - { "name": "recordAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "创建单条考勤记录", "deps": ["requirePermission", "data-access.createAttendanceRecord", "revalidatePath"], "usedBy": ["attendance-record-list.tsx"] }, - { "name": "batchRecordAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "批量点名(班级+日期,表格形式录入每个学生状态)", "deps": ["requirePermission", "data-access.batchCreateAttendanceRecords", "revalidatePath"], "usedBy": ["attendance-sheet.tsx"] }, - { "name": "updateAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "更新考勤记录(状态、备注)", "deps": ["requirePermission", "data-access.updateAttendanceRecord", "revalidatePath"], "usedBy": ["attendance-record-list.tsx"] }, - { "name": "deleteAttendanceAction", "permission": "ATTENDANCE_MANAGE", "signature": "(id: string) => Promise>", "purpose": "删除考勤记录", "deps": ["requirePermission", "data-access.deleteAttendanceRecord", "revalidatePath"], "usedBy": ["attendance-record-list.tsx"] }, - { "name": "getAttendanceAction", "permission": "ATTENDANCE_READ", "signature": "(params?: AttendanceQueryParams) => Promise>", "purpose": "分页查询考勤记录(按 scope 过滤)", "deps": ["requirePermission", "data-access.getAttendanceRecords"], "usedBy": ["teacher/attendance/page.tsx", "admin/attendance/page.tsx"] }, - { "name": "getStudentAttendanceAction", "permission": "ATTENDANCE_READ", "signature": "(studentId: string) => Promise>", "purpose": "获取学生考勤汇总(含 DataScope 二次校验:class_members 仅查自己,children 仅查子女)", "deps": ["requirePermission", "data-access-stats.getStudentAttendanceSummary"], "usedBy": ["student/attendance/page.tsx", "parent/attendance/page.tsx"] }, - { "name": "getClassAttendanceStatsAction", "permission": "ATTENDANCE_READ", "signature": "(classId: string, startDate?: string, endDate?: string) => Promise>", "purpose": "获取班级考勤统计", "deps": ["requirePermission", "data-access-stats.getClassAttendanceStats"], "usedBy": ["teacher/attendance/stats/page.tsx"] }, - { "name": "getClassAttendanceForDateAction", "permission": "ATTENDANCE_READ", "signature": "(classId: string, date: string) => Promise>", "purpose": "获取班级指定日期考勤(用于点名页加载已有记录)", "deps": ["requirePermission", "data-access.getClassAttendanceForDate"], "usedBy": ["attendance-sheet.tsx"] }, - { "name": "saveAttendanceRulesAction", "permission": "ATTENDANCE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "保存班级考勤规则(upsert)", "deps": ["requirePermission", "data-access.upsertAttendanceRules", "revalidatePath"], "usedBy": ["attendance-rules-form.tsx"] }, - { "name": "getAttendanceRulesAction", "permission": "ATTENDANCE_READ", "signature": "(classId?: string) => Promise>", "purpose": "获取班级考勤规则", "deps": ["requirePermission", "data-access.getAttendanceRules"], "usedBy": ["attendance-rules-form.tsx"] } + { + "name": "recordAttendanceAction", + "permission": "ATTENDANCE_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "创建单条考勤记录", + "deps": [ + "requirePermission", + "data-access.createAttendanceRecord", + "revalidatePath" + ], + "usedBy": [ + "attendance-record-list.tsx" + ] + }, + { + "name": "batchRecordAttendanceAction", + "permission": "ATTENDANCE_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "批量点名(班级+日期,表格形式录入每个学生状态)", + "deps": [ + "requirePermission", + "data-access.batchCreateAttendanceRecords", + "revalidatePath" + ], + "usedBy": [ + "attendance-sheet.tsx" + ] + }, + { + "name": "updateAttendanceAction", + "permission": "ATTENDANCE_MANAGE", + "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "更新考勤记录(状态、备注)", + "deps": [ + "requirePermission", + "data-access.updateAttendanceRecord", + "revalidatePath" + ], + "usedBy": [ + "attendance-record-list.tsx" + ] + }, + { + "name": "deleteAttendanceAction", + "permission": "ATTENDANCE_MANAGE", + "signature": "(id: string) => Promise>", + "purpose": "删除考勤记录", + "deps": [ + "requirePermission", + "data-access.deleteAttendanceRecord", + "revalidatePath" + ], + "usedBy": [ + "attendance-record-list.tsx" + ] + }, + { + "name": "getAttendanceAction", + "permission": "ATTENDANCE_READ", + "signature": "(params?: AttendanceQueryParams) => Promise>", + "purpose": "分页查询考勤记录(按 scope 过滤)", + "deps": [ + "requirePermission", + "data-access.getAttendanceRecords" + ], + "usedBy": [ + "teacher/attendance/page.tsx", + "admin/attendance/page.tsx" + ] + }, + { + "name": "getStudentAttendanceAction", + "permission": "ATTENDANCE_READ", + "signature": "(studentId: string) => Promise>", + "purpose": "获取学生考勤汇总(含 DataScope 二次校验:class_members 仅查自己,children 仅查子女)", + "deps": [ + "requirePermission", + "data-access-stats.getStudentAttendanceSummary" + ], + "usedBy": [ + "student/attendance/page.tsx", + "parent/attendance/page.tsx" + ] + }, + { + "name": "getClassAttendanceStatsAction", + "permission": "ATTENDANCE_READ", + "signature": "(classId: string, startDate?: string, endDate?: string) => Promise>", + "purpose": "获取班级考勤统计", + "deps": [ + "requirePermission", + "data-access-stats.getClassAttendanceStats" + ], + "usedBy": [ + "teacher/attendance/stats/page.tsx" + ] + }, + { + "name": "getClassAttendanceForDateAction", + "permission": "ATTENDANCE_READ", + "signature": "(classId: string, date: string) => Promise>", + "purpose": "获取班级指定日期考勤(用于点名页加载已有记录)", + "deps": [ + "requirePermission", + "data-access.getClassAttendanceForDate" + ], + "usedBy": [ + "attendance-sheet.tsx" + ] + }, + { + "name": "saveAttendanceRulesAction", + "permission": "ATTENDANCE_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "保存班级考勤规则(upsert)", + "deps": [ + "requirePermission", + "data-access.upsertAttendanceRules", + "revalidatePath" + ], + "usedBy": [ + "attendance-rules-form.tsx" + ] + }, + { + "name": "getAttendanceRulesAction", + "permission": "ATTENDANCE_READ", + "signature": "(classId?: string) => Promise>", + "purpose": "获取班级考勤规则", + "deps": [ + "requirePermission", + "data-access.getAttendanceRules" + ], + "usedBy": [ + "attendance-rules-form.tsx" + ] + } ], "dataAccess": [ - { "name": "getAttendanceRecords", "signature": "(params: AttendanceQueryParams & { scope: DataScope; currentUserId?: string }) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords", "shared.db.schema.users", "shared.db.schema.classes", "types.DataScope"], "usedBy": ["getAttendanceAction"] }, - { "name": "getClassAttendanceForDate", "signature": "(classId: string, date: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords", "shared.db.schema.users", "shared.db.schema.classes"], "usedBy": ["getClassAttendanceForDateAction"] }, - { "name": "createAttendanceRecord", "signature": "(data: RecordAttendanceInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords", "@paralleldrive/cuid2"], "usedBy": ["recordAttendanceAction"] }, - { "name": "batchCreateAttendanceRecords", "signature": "(data: BatchRecordAttendanceInput, recordedBy: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords", "@paralleldrive/cuid2"], "usedBy": ["batchRecordAttendanceAction"] }, - { "name": "updateAttendanceRecord", "signature": "(id: string, data: UpdateAttendanceInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords"], "usedBy": ["updateAttendanceAction"] }, - { "name": "deleteAttendanceRecord", "signature": "(id: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords"], "usedBy": ["deleteAttendanceAction"] }, - { "name": "getClassStudentsForAttendance", "signature": "(classId: string) => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.classEnrollments", "shared.db.schema.users"], "usedBy": ["attendance-sheet.tsx"] }, - { "name": "getAttendanceRules", "signature": "(classId?: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRules"], "usedBy": ["getAttendanceRulesAction"] }, - { "name": "upsertAttendanceRules", "signature": "(data: AttendanceRuleInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.attendanceRules", "@paralleldrive/cuid2"], "usedBy": ["saveAttendanceRulesAction"] }, - { "name": "getStudentAttendanceSummary", "signature": "(studentId: string, startDate?: string, endDate?: string) => Promise", "file": "data-access-stats.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords", "shared.db.schema.classes", "shared.db.schema.users"], "usedBy": ["getStudentAttendanceAction", "student/attendance/page.tsx", "parent/attendance/page.tsx"] }, - { "name": "getClassAttendanceStats", "signature": "(classId: string, startDate?: string, endDate?: string) => Promise", "file": "data-access-stats.ts", "deps": ["shared.db", "shared.db.schema.attendanceRecords", "shared.db.schema.classes", "shared.db.schema.users"], "usedBy": ["getClassAttendanceStatsAction", "teacher/attendance/stats/page.tsx"] } + { + "name": "getAttendanceRecords", + "signature": "(params: AttendanceQueryParams & { scope: DataScope; currentUserId?: string }) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords", + "shared.db.schema.users", + "shared.db.schema.classes", + "types.DataScope" + ], + "usedBy": [ + "getAttendanceAction" + ] + }, + { + "name": "getClassAttendanceForDate", + "signature": "(classId: string, date: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords", + "shared.db.schema.users", + "shared.db.schema.classes" + ], + "usedBy": [ + "getClassAttendanceForDateAction" + ] + }, + { + "name": "createAttendanceRecord", + "signature": "(data: RecordAttendanceInput, recordedBy: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "recordAttendanceAction" + ] + }, + { + "name": "batchCreateAttendanceRecords", + "signature": "(data: BatchRecordAttendanceInput, recordedBy: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "batchRecordAttendanceAction" + ] + }, + { + "name": "updateAttendanceRecord", + "signature": "(id: string, data: UpdateAttendanceInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords" + ], + "usedBy": [ + "updateAttendanceAction" + ] + }, + { + "name": "deleteAttendanceRecord", + "signature": "(id: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords" + ], + "usedBy": [ + "deleteAttendanceAction" + ] + }, + { + "name": "getClassStudentsForAttendance", + "signature": "(classId: string) => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.classEnrollments", + "shared.db.schema.users" + ], + "usedBy": [ + "attendance-sheet.tsx" + ] + }, + { + "name": "getAttendanceRules", + "signature": "(classId?: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRules" + ], + "usedBy": [ + "getAttendanceRulesAction" + ] + }, + { + "name": "upsertAttendanceRules", + "signature": "(data: AttendanceRuleInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRules", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "saveAttendanceRulesAction" + ] + }, + { + "name": "getStudentAttendanceSummary", + "signature": "(studentId: string, startDate?: string, endDate?: string) => Promise", + "file": "data-access-stats.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords", + "shared.db.schema.classes", + "shared.db.schema.users" + ], + "usedBy": [ + "getStudentAttendanceAction", + "student/attendance/page.tsx", + "parent/attendance/page.tsx" + ] + }, + { + "name": "getClassAttendanceStats", + "signature": "(classId: string, startDate?: string, endDate?: string) => Promise", + "file": "data-access-stats.ts", + "deps": [ + "shared.db", + "shared.db.schema.attendanceRecords", + "shared.db.schema.classes", + "shared.db.schema.users" + ], + "usedBy": [ + "getClassAttendanceStatsAction", + "teacher/attendance/stats/page.tsx" + ] + } ], "schemas": [ - { "name": "AttendanceStatusEnum", "type": "zod", "file": "schema.ts", "definition": "enum('present','absent','late','early_leave','excused')", "usedBy": ["RecordAttendanceSchema", "BatchRecordAttendanceSchema", "UpdateAttendanceSchema"] }, - { "name": "RecordAttendanceSchema", "type": "zod", "file": "schema.ts", "definition": "{ studentId, classId, scheduleId?, date, status, remark? }", "usedBy": ["recordAttendanceAction"] }, - { "name": "BatchRecordAttendanceSchema", "type": "zod", "file": "schema.ts", "definition": "{ records: [{ studentId, classId, scheduleId?, date, status, remark? }] }", "usedBy": ["batchRecordAttendanceAction"] }, - { "name": "UpdateAttendanceSchema", "type": "zod", "file": "schema.ts", "definition": "{ status?, remark?, scheduleId? }", "usedBy": ["updateAttendanceAction"] }, - { "name": "AttendanceRuleSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, lateThresholdMinutes?, earlyLeaveThresholdMinutes?, enableAutoMark? }", "usedBy": ["saveAttendanceRulesAction"] } + { + "name": "AttendanceStatusEnum", + "type": "zod", + "file": "schema.ts", + "definition": "enum('present','absent','late','early_leave','excused')", + "usedBy": [ + "RecordAttendanceSchema", + "BatchRecordAttendanceSchema", + "UpdateAttendanceSchema" + ] + }, + { + "name": "RecordAttendanceSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ studentId, classId, scheduleId?, date, status, remark? }", + "usedBy": [ + "recordAttendanceAction" + ] + }, + { + "name": "BatchRecordAttendanceSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ records: [{ studentId, classId, scheduleId?, date, status, remark? }] }", + "usedBy": [ + "batchRecordAttendanceAction" + ] + }, + { + "name": "UpdateAttendanceSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ status?, remark?, scheduleId? }", + "usedBy": [ + "updateAttendanceAction" + ] + }, + { + "name": "AttendanceRuleSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ classId, lateThresholdMinutes?, earlyLeaveThresholdMinutes?, enableAutoMark? }", + "usedBy": [ + "saveAttendanceRulesAction" + ] + } ], "types": [ - { "name": "AttendanceStatus", "type": "type", "file": "types.ts", "definition": "'present' | 'absent' | 'late' | 'early_leave' | 'excused'", "usedBy": ["attendance/data-access", "attendance/components"] }, - { "name": "AttendanceRecord", "type": "type", "file": "types.ts", "definition": "考勤记录完整类型", "usedBy": ["attendance/data-access"] }, - { "name": "AttendanceListItem", "type": "type", "file": "types.ts", "definition": "{ id, studentId, studentName, classId, className, scheduleId, date, status, remark, recordedBy, recorderName, createdAt }", "usedBy": ["attendance/components", "页面"] }, - { "name": "AttendanceStats", "type": "type", "file": "types.ts", "definition": "{ total, present, absent, late, earlyLeave, excused, presentRate, lateRate }", "usedBy": ["attendance-stats-card.tsx"] }, - { "name": "StudentAttendanceSummary", "type": "type", "file": "types.ts", "definition": "{ studentId, studentName, stats: AttendanceStats, recentRecords: AttendanceListItem[] }", "usedBy": ["student-attendance-view.tsx"] }, - { "name": "ClassAttendanceSummary", "type": "type", "file": "types.ts", "definition": "{ classId, className, date, stats: AttendanceStats, studentRecords: AttendanceListItem[] }", "usedBy": ["teacher/attendance/stats"] }, - { "name": "AttendanceRule", "type": "type", "file": "types.ts", "definition": "{ id, classId, lateThresholdMinutes, earlyLeaveThresholdMinutes, enableAutoMark, createdAt, updatedAt }", "usedBy": ["attendance-rules-form.tsx"] }, - { "name": "AttendanceQueryParams", "type": "type", "file": "types.ts", "definition": "{ classId?, studentId?, date?, startDate?, endDate?, status?, page?, pageSize? }", "usedBy": ["getAttendanceRecords", "getAttendanceAction"] }, - { "name": "PaginatedAttendanceResult", "type": "type", "file": "types.ts", "definition": "{ items: AttendanceListItem[], total, page, pageSize, totalPages }", "usedBy": ["getAttendanceRecords", "getAttendanceAction"] }, - { "name": "ATTENDANCE_STATUS_LABELS", "type": "const", "file": "types.ts", "definition": "状态中文标签常量", "usedBy": ["attendance/components"] }, - { "name": "ATTENDANCE_STATUS_COLORS", "type": "const", "file": "types.ts", "definition": "状态颜色常量(用于 Badge)", "usedBy": ["attendance/components"] } + { + "name": "AttendanceStatus", + "type": "type", + "file": "types.ts", + "definition": "'present' | 'absent' | 'late' | 'early_leave' | 'excused'", + "usedBy": [ + "attendance/data-access", + "attendance/components" + ] + }, + { + "name": "AttendanceRecord", + "type": "type", + "file": "types.ts", + "definition": "考勤记录完整类型", + "usedBy": [ + "attendance/data-access" + ] + }, + { + "name": "AttendanceListItem", + "type": "type", + "file": "types.ts", + "definition": "{ id, studentId, studentName, classId, className, scheduleId, date, status, remark, recordedBy, recorderName, createdAt }", + "usedBy": [ + "attendance/components", + "页面" + ] + }, + { + "name": "AttendanceStats", + "type": "type", + "file": "types.ts", + "definition": "{ total, present, absent, late, earlyLeave, excused, presentRate, lateRate }", + "usedBy": [ + "attendance-stats-card.tsx" + ] + }, + { + "name": "StudentAttendanceSummary", + "type": "type", + "file": "types.ts", + "definition": "{ studentId, studentName, stats: AttendanceStats, recentRecords: AttendanceListItem[] }", + "usedBy": [ + "student-attendance-view.tsx" + ] + }, + { + "name": "ClassAttendanceSummary", + "type": "type", + "file": "types.ts", + "definition": "{ classId, className, date, stats: AttendanceStats, studentRecords: AttendanceListItem[] }", + "usedBy": [ + "teacher/attendance/stats" + ] + }, + { + "name": "AttendanceRule", + "type": "type", + "file": "types.ts", + "definition": "{ id, classId, lateThresholdMinutes, earlyLeaveThresholdMinutes, enableAutoMark, createdAt, updatedAt }", + "usedBy": [ + "attendance-rules-form.tsx" + ] + }, + { + "name": "AttendanceQueryParams", + "type": "type", + "file": "types.ts", + "definition": "{ classId?, studentId?, date?, startDate?, endDate?, status?, page?, pageSize? }", + "usedBy": [ + "getAttendanceRecords", + "getAttendanceAction" + ] + }, + { + "name": "PaginatedAttendanceResult", + "type": "type", + "file": "types.ts", + "definition": "{ items: AttendanceListItem[], total, page, pageSize, totalPages }", + "usedBy": [ + "getAttendanceRecords", + "getAttendanceAction" + ] + }, + { + "name": "ATTENDANCE_STATUS_LABELS", + "type": "const", + "file": "types.ts", + "definition": "状态中文标签常量", + "usedBy": [ + "attendance/components" + ] + }, + { + "name": "ATTENDANCE_STATUS_COLORS", + "type": "const", + "file": "types.ts", + "definition": "状态颜色常量(用于 Badge)", + "usedBy": [ + "attendance/components" + ] + } ], "components": [ - { "name": "AttendanceSheet", "file": "components/attendance-sheet.tsx", "purpose": "批量点名表单(班级/日期选择器 + 学生表格 + 每行状态 Select + 全部标记到场按钮)" }, - { "name": "AttendanceRecordList", "file": "components/attendance-record-list.tsx", "purpose": "考勤记录列表表格(含删除确认对话框)" }, - { "name": "AttendanceStatsCard", "file": "components/attendance-stats-card.tsx", "purpose": "统计卡片(总数、到场、缺勤、迟到、早退、请假、出勤率、迟到率)" }, - { "name": "AttendanceFilters", "file": "components/attendance-filters.tsx", "purpose": "URL 同步筛选器(班级、状态、日期)" }, - { "name": "StudentAttendanceView", "file": "components/student-attendance-view.tsx", "purpose": "学生/家长视图(统计卡片 + 最近记录表格)" }, - { "name": "AttendanceRulesForm", "file": "components/attendance-rules-form.tsx", "purpose": "考勤规则配置表单(班级选择器、迟到/早退阈值、自动标记勾选)" } + { + "name": "AttendanceSheet", + "file": "components/attendance-sheet.tsx", + "purpose": "批量点名表单(班级/日期选择器 + 学生表格 + 每行状态 Select + 全部标记到场按钮)" + }, + { + "name": "AttendanceRecordList", + "file": "components/attendance-record-list.tsx", + "purpose": "考勤记录列表表格(含删除确认对话框)" + }, + { + "name": "AttendanceStatsCard", + "file": "components/attendance-stats-card.tsx", + "purpose": "统计卡片(总数、到场、缺勤、迟到、早退、请假、出勤率、迟到率)" + }, + { + "name": "AttendanceFilters", + "file": "components/attendance-filters.tsx", + "purpose": "URL 同步筛选器(班级、状态、日期)" + }, + { + "name": "StudentAttendanceView", + "file": "components/student-attendance-view.tsx", + "purpose": "学生/家长视图(统计卡片 + 最近记录表格)" + }, + { + "name": "AttendanceRulesForm", + "file": "components/attendance-rules-form.tsx", + "purpose": "考勤规则配置表单(班级选择器、迟到/早退阈值、自动标记勾选)" + } ] } }, @@ -1448,61 +7998,498 @@ "description": "排课与调课:管理员配置班级排课规则(每日课时、连续课时、午休、上下学时间、避免背靠背、科目均衡),自动排课引擎按规则生成周课表,调课/代课申请与审批流程,课表冲突检测。", "exports": { "actions": [ - { "name": "saveSchedulingRulesAction", "permission": "SCHEDULE_ADJUST", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "保存班级排课规则(upsert,classId 为空时为全局规则)", "deps": ["requirePermission", "data-access.upsertSchedulingRules", "revalidatePath"], "usedBy": ["scheduling-rules-form.tsx"] }, - { "name": "autoScheduleAction", "permission": "SCHEDULE_AUTO", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "根据规则与科目分配生成预览课表(不落库)", "deps": ["requirePermission", "data-access.getSchedulingRules", "data-access.getClassSubjectsForScheduling", "data-access.getClassroomsForScheduling", "auto-scheduler.autoSchedule", "auto-scheduler.buildDefaultTimeSlots", "revalidatePath"], "usedBy": ["auto-schedule-panel.tsx"] }, - { "name": "applyAutoScheduleAction", "permission": "SCHEDULE_AUTO", "signature": "(classId: string, schedules: Array<{ weekday, startTime, endTime, course, location }>) => Promise>", "purpose": "将生成的课表写入 classSchedule 表(先删除该班旧课表再插入新课表,事务)", "deps": ["requirePermission", "shared.db", "shared.db.schema.classSchedule", "@paralleldrive/cuid2", "revalidatePath"], "usedBy": ["auto-schedule-panel.tsx"] }, - { "name": "requestScheduleChangeAction", "permission": "SCHEDULE_ADJUST", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "purpose": "提交调课/代课申请(status=pending)", "deps": ["requirePermission", "data-access.createScheduleChange", "revalidatePath"], "usedBy": ["schedule-change-form.tsx"] }, - { "name": "approveScheduleChangeAction", "permission": "SCHEDULE_AUTO", "signature": "(changeId: string) => Promise", "purpose": "审批通过调课申请(status=approved)", "deps": ["requirePermission", "data-access.updateScheduleChangeStatus", "revalidatePath"], "usedBy": ["schedule-change-list.tsx"] }, - { "name": "rejectScheduleChangeAction", "permission": "SCHEDULE_AUTO", "signature": "(changeId: string, reason?: string) => Promise", "purpose": "驳回调课申请(status=rejected)", "deps": ["requirePermission", "data-access.updateScheduleChangeStatus", "revalidatePath"], "usedBy": ["schedule-change-list.tsx"] }, - { "name": "getScheduleChangesAction", "permission": "SCHEDULE_ADJUST", "signature": "(params: ScheduleChangeQueryParams) => Promise>", "purpose": "查询调课申请列表(可按 classId/status/requesterId 过滤)", "deps": ["requirePermission", "data-access.getScheduleChanges"], "usedBy": ["admin/scheduling/changes/page.tsx", "teacher/schedule-changes/page.tsx"] }, - { "name": "getClassConflictsAction", "permission": "SCHEDULE_ADJUST", "signature": "(classId: string) => Promise>", "purpose": "检测班级课表时间重叠冲突", "deps": ["requirePermission", "data-access.getClassConflicts"], "usedBy": ["schedule-conflicts-view.tsx"] } + { + "name": "saveSchedulingRulesAction", + "permission": "SCHEDULE_ADJUST", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "保存班级排课规则(upsert,classId 为空时为全局规则)", + "deps": [ + "requirePermission", + "data-access.upsertSchedulingRules", + "revalidatePath" + ], + "usedBy": [ + "scheduling-rules-form.tsx" + ] + }, + { + "name": "autoScheduleAction", + "permission": "SCHEDULE_AUTO", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "根据规则与科目分配生成预览课表(不落库)", + "deps": [ + "requirePermission", + "data-access.getSchedulingRules", + "data-access.getClassSubjectsForScheduling", + "data-access.getClassroomsForScheduling", + "auto-scheduler.autoSchedule", + "auto-scheduler.buildDefaultTimeSlots", + "revalidatePath" + ], + "usedBy": [ + "auto-schedule-panel.tsx" + ] + }, + { + "name": "applyAutoScheduleAction", + "permission": "SCHEDULE_AUTO", + "signature": "(classId: string, schedules: Array<{ weekday, startTime, endTime, course, location }>) => Promise>", + "purpose": "将生成的课表写入 classSchedule 表(先删除该班旧课表再插入新课表,事务)", + "deps": [ + "requirePermission", + "shared.db", + "shared.db.schema.classSchedule", + "@paralleldrive/cuid2", + "revalidatePath" + ], + "usedBy": [ + "auto-schedule-panel.tsx" + ] + }, + { + "name": "requestScheduleChangeAction", + "permission": "SCHEDULE_ADJUST", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "purpose": "提交调课/代课申请(status=pending)", + "deps": [ + "requirePermission", + "data-access.createScheduleChange", + "revalidatePath" + ], + "usedBy": [ + "schedule-change-form.tsx" + ] + }, + { + "name": "approveScheduleChangeAction", + "permission": "SCHEDULE_AUTO", + "signature": "(changeId: string) => Promise", + "purpose": "审批通过调课申请(status=approved)", + "deps": [ + "requirePermission", + "data-access.updateScheduleChangeStatus", + "revalidatePath" + ], + "usedBy": [ + "schedule-change-list.tsx" + ] + }, + { + "name": "rejectScheduleChangeAction", + "permission": "SCHEDULE_AUTO", + "signature": "(changeId: string, reason?: string) => Promise", + "purpose": "驳回调课申请(status=rejected)", + "deps": [ + "requirePermission", + "data-access.updateScheduleChangeStatus", + "revalidatePath" + ], + "usedBy": [ + "schedule-change-list.tsx" + ] + }, + { + "name": "getScheduleChangesAction", + "permission": "SCHEDULE_ADJUST", + "signature": "(params: ScheduleChangeQueryParams) => Promise>", + "purpose": "查询调课申请列表(可按 classId/status/requesterId 过滤)", + "deps": [ + "requirePermission", + "data-access.getScheduleChanges" + ], + "usedBy": [ + "admin/scheduling/changes/page.tsx", + "teacher/schedule-changes/page.tsx" + ] + }, + { + "name": "getClassConflictsAction", + "permission": "SCHEDULE_ADJUST", + "signature": "(classId: string) => Promise>", + "purpose": "检测班级课表时间重叠冲突", + "deps": [ + "requirePermission", + "data-access.getClassConflicts" + ], + "usedBy": [ + "schedule-conflicts-view.tsx" + ] + } ], "dataAccess": [ - { "name": "getSchedulingRules", "signature": "(classId?: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.schedulingRules"], "usedBy": ["saveSchedulingRulesAction", "autoScheduleAction", "admin/scheduling/rules/page.tsx"] }, - { "name": "upsertSchedulingRules", "signature": "(data: SchedulingRuleInput) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.schedulingRules", "@paralleldrive/cuid2"], "usedBy": ["saveSchedulingRulesAction"] }, - { "name": "getScheduleChanges", "signature": "(params: ScheduleChangeQueryParams) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.scheduleChanges", "shared.db.schema.classes", "shared.db.schema.users"], "usedBy": ["getScheduleChangesAction", "admin/scheduling/changes/page.tsx", "teacher/schedule-changes/page.tsx"] }, - { "name": "createScheduleChange", "signature": "(data: ScheduleChangeInput, requestedBy: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.scheduleChanges", "@paralleldrive/cuid2"], "usedBy": ["requestScheduleChangeAction"] }, - { "name": "updateScheduleChangeStatus", "signature": "(id: string, status: 'approved'|'rejected'|'completed', approverId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.scheduleChanges"], "usedBy": ["approveScheduleChangeAction", "rejectScheduleChangeAction"] }, - { "name": "getClassConflicts", "signature": "(classId: string) => Promise", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.classSchedule"], "usedBy": ["getClassConflictsAction"] }, - { "name": "getAdminClassesForScheduling", "signature": "() => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.classes"], "usedBy": ["admin/scheduling/rules/page.tsx", "admin/scheduling/auto/page.tsx", "admin/scheduling/changes/page.tsx", "teacher/schedule-changes/page.tsx"] }, - { "name": "getTeachersForScheduling", "signature": "() => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.users", "shared.db.schema.classSubjectTeachers"], "usedBy": ["teacher/schedule-changes/page.tsx"] }, - { "name": "getClassroomsForScheduling", "signature": "() => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.classrooms"], "usedBy": ["autoScheduleAction"] }, - { "name": "getClassSubjectsForScheduling", "signature": "(classId: string) => Promise>", "file": "data-access.ts", "deps": ["shared.db", "shared.db.schema.classSubjectTeachers", "shared.db.schema.subjects"], "usedBy": ["autoScheduleAction"] } + { + "name": "getSchedulingRules", + "signature": "(classId?: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.schedulingRules" + ], + "usedBy": [ + "saveSchedulingRulesAction", + "autoScheduleAction", + "admin/scheduling/rules/page.tsx" + ] + }, + { + "name": "upsertSchedulingRules", + "signature": "(data: SchedulingRuleInput) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.schedulingRules", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "saveSchedulingRulesAction" + ] + }, + { + "name": "getScheduleChanges", + "signature": "(params: ScheduleChangeQueryParams) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.scheduleChanges", + "shared.db.schema.classes", + "shared.db.schema.users" + ], + "usedBy": [ + "getScheduleChangesAction", + "admin/scheduling/changes/page.tsx", + "teacher/schedule-changes/page.tsx" + ] + }, + { + "name": "createScheduleChange", + "signature": "(data: ScheduleChangeInput, requestedBy: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.scheduleChanges", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "requestScheduleChangeAction" + ] + }, + { + "name": "updateScheduleChangeStatus", + "signature": "(id: string, status: 'approved'|'rejected'|'completed', approverId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.scheduleChanges" + ], + "usedBy": [ + "approveScheduleChangeAction", + "rejectScheduleChangeAction" + ] + }, + { + "name": "getClassConflicts", + "signature": "(classId: string) => Promise", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.classSchedule" + ], + "usedBy": [ + "getClassConflictsAction" + ] + }, + { + "name": "getAdminClassesForScheduling", + "signature": "() => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.classes" + ], + "usedBy": [ + "admin/scheduling/rules/page.tsx", + "admin/scheduling/auto/page.tsx", + "admin/scheduling/changes/page.tsx", + "teacher/schedule-changes/page.tsx" + ] + }, + { + "name": "getTeachersForScheduling", + "signature": "() => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.users", + "shared.db.schema.classSubjectTeachers" + ], + "usedBy": [ + "teacher/schedule-changes/page.tsx" + ] + }, + { + "name": "getClassroomsForScheduling", + "signature": "() => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.classrooms" + ], + "usedBy": [ + "autoScheduleAction" + ] + }, + { + "name": "getClassSubjectsForScheduling", + "signature": "(classId: string) => Promise>", + "file": "data-access.ts", + "deps": [ + "shared.db", + "shared.db.schema.classSubjectTeachers", + "shared.db.schema.subjects" + ], + "usedBy": [ + "autoScheduleAction" + ] + } ], "autoScheduler": [ - { "name": "autoSchedule", "signature": "(params: AutoScheduleParams) => AutoScheduleResult", "file": "auto-scheduler.ts", "purpose": "贪心+冲突检测排课算法:按科目每周课时降序,为每节课选择第一个满足约束的时段(午休、每日窗口、班级/教师/教室冲突、每日最大课时、避免背靠背)", "deps": ["findOptimalSlot", "validateSchedule"], "usedBy": ["autoScheduleAction"] }, - { "name": "findOptimalSlot", "signature": "(args) => TimeSlot | null", "file": "auto-scheduler.ts", "purpose": "在候选时段中找到第一个满足所有约束的时段", "usedBy": ["autoSchedule"] }, - { "name": "validateSchedule", "signature": "(schedules: GeneratedSchedule[], rules: SchedulingRule) => ScheduleConflict[]", "file": "auto-scheduler.ts", "purpose": "校验生成的课表是否违反规则,返回冲突列表", "usedBy": ["autoSchedule"] }, - { "name": "buildDefaultTimeSlots", "signature": "(morningStart, afternoonEnd, lunchBreakStart, lunchBreakEnd) => TimeSlot[]", "file": "auto-scheduler.ts", "purpose": "根据上下学时间和午休时间构建默认时段(周一至周五,上午4节+下午4节)", "usedBy": ["autoScheduleAction"] } + { + "name": "autoSchedule", + "signature": "(params: AutoScheduleParams) => AutoScheduleResult", + "file": "auto-scheduler.ts", + "purpose": "贪心+冲突检测排课算法:按科目每周课时降序,为每节课选择第一个满足约束的时段(午休、每日窗口、班级/教师/教室冲突、每日最大课时、避免背靠背)", + "deps": [ + "findOptimalSlot", + "validateSchedule" + ], + "usedBy": [ + "autoScheduleAction" + ] + }, + { + "name": "findOptimalSlot", + "signature": "(args) => TimeSlot | null", + "file": "auto-scheduler.ts", + "purpose": "在候选时段中找到第一个满足所有约束的时段", + "usedBy": [ + "autoSchedule" + ] + }, + { + "name": "validateSchedule", + "signature": "(schedules: GeneratedSchedule[], rules: SchedulingRule) => ScheduleConflict[]", + "file": "auto-scheduler.ts", + "purpose": "校验生成的课表是否违反规则,返回冲突列表", + "usedBy": [ + "autoSchedule" + ] + }, + { + "name": "buildDefaultTimeSlots", + "signature": "(morningStart, afternoonEnd, lunchBreakStart, lunchBreakEnd) => TimeSlot[]", + "file": "auto-scheduler.ts", + "purpose": "根据上下学时间和午休时间构建默认时段(周一至周五,上午4节+下午4节)", + "usedBy": [ + "autoScheduleAction" + ] + } ], "schemas": [ - { "name": "SchedulingRuleSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, maxDailyHours?, maxContinuousHours?, lunchBreakStart?, lunchBreakEnd?, morningStart?, afternoonEnd?, avoidBackToBack?, balancedSubjects? }", "usedBy": ["saveSchedulingRulesAction"] }, - { "name": "ScheduleChangeSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, originalScheduleId?, originalTeacherId?, substituteTeacherId?, originalDate?, newDate?, newStartTime?, newEndTime?, reason }", "usedBy": ["requestScheduleChangeAction"] }, - { "name": "AutoScheduleParamsSchema", "type": "zod", "file": "schema.ts", "definition": "{ classId, rules: SchedulingRuleSchema, subjects: [{ subjectId, subjectName, weeklyHours, teacherId? }], teachers: [{ id, name }], classrooms: [{ id, name }], timeSlots: [{ weekday, startTime, endTime }] }", "usedBy": ["autoScheduleAction"] }, - { "name": "ScheduleChangeStatusEnum", "type": "zod", "file": "schema.ts", "definition": "enum('pending','approved','rejected','completed')", "usedBy": ["ScheduleChangeSchema"] }, - { "name": "ApproveScheduleChangeSchema", "type": "zod", "file": "schema.ts", "definition": "{ changeId, reason? }", "usedBy": ["approveScheduleChangeAction"] } + { + "name": "SchedulingRuleSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ classId, maxDailyHours?, maxContinuousHours?, lunchBreakStart?, lunchBreakEnd?, morningStart?, afternoonEnd?, avoidBackToBack?, balancedSubjects? }", + "usedBy": [ + "saveSchedulingRulesAction" + ] + }, + { + "name": "ScheduleChangeSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ classId, originalScheduleId?, originalTeacherId?, substituteTeacherId?, originalDate?, newDate?, newStartTime?, newEndTime?, reason }", + "usedBy": [ + "requestScheduleChangeAction" + ] + }, + { + "name": "AutoScheduleParamsSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ classId, rules: SchedulingRuleSchema, subjects: [{ subjectId, subjectName, weeklyHours, teacherId? }], teachers: [{ id, name }], classrooms: [{ id, name }], timeSlots: [{ weekday, startTime, endTime }] }", + "usedBy": [ + "autoScheduleAction" + ] + }, + { + "name": "ScheduleChangeStatusEnum", + "type": "zod", + "file": "schema.ts", + "definition": "enum('pending','approved','rejected','completed')", + "usedBy": [ + "ScheduleChangeSchema" + ] + }, + { + "name": "ApproveScheduleChangeSchema", + "type": "zod", + "file": "schema.ts", + "definition": "{ changeId, reason? }", + "usedBy": [ + "approveScheduleChangeAction" + ] + } ], "types": [ - { "name": "ScheduleChangeStatus", "type": "type", "file": "types.ts", "definition": "'pending' | 'approved' | 'rejected' | 'completed'", "usedBy": ["scheduling/data-access", "scheduling/components"] }, - { "name": "SchedulingRule", "type": "type", "file": "types.ts", "definition": "{ id, classId, maxDailyHours, maxContinuousHours, lunchBreakStart, lunchBreakEnd, morningStart, afternoonEnd, avoidBackToBack, balancedSubjects, createdAt, updatedAt }", "usedBy": ["scheduling-rules-form.tsx", "auto-scheduler.ts"] }, - { "name": "ScheduleChange", "type": "type", "file": "types.ts", "definition": "调课申请完整类型", "usedBy": ["scheduling/data-access"] }, - { "name": "ScheduleChangeListItem", "type": "type", "file": "types.ts", "definition": "{ ...ScheduleChange, className, originalTeacherName, substituteTeacherName, requesterName, approverName }", "usedBy": ["schedule-change-list.tsx", "页面"] }, - { "name": "TimeSlot", "type": "type", "file": "types.ts", "definition": "{ weekday, startTime, endTime }", "usedBy": ["auto-scheduler.ts"] }, - { "name": "ScheduleConflict", "type": "type", "file": "types.ts", "definition": "{ type: 'teacher_overlap'|'classroom_overlap'|'class_overlap'|'rule_violation', description, scheduleIds }", "usedBy": ["auto-schedule-result.tsx", "schedule-conflicts-view.tsx"] }, - { "name": "AutoScheduleResult", "type": "type", "file": "types.ts", "definition": "{ success, scheduledCount, conflictCount, conflicts, schedules: GeneratedSchedule[] }", "usedBy": ["auto-schedule-panel.tsx", "auto-schedule-result.tsx"] }, - { "name": "GeneratedSchedule", "type": "type", "file": "types.ts", "definition": "{ classId, weekday, startTime, endTime, course, location, teacherId, subjectId }", "usedBy": ["auto-scheduler.ts", "auto-schedule-result.tsx"] }, - { "name": "AutoScheduleParams", "type": "type", "file": "types.ts", "definition": "{ classId, rules, subjects, teachers, classrooms, timeSlots }", "usedBy": ["auto-scheduler.ts", "autoScheduleAction"] }, - { "name": "ScheduleChangeQueryParams", "type": "type", "file": "types.ts", "definition": "{ classId?, status?, requesterId? }", "usedBy": ["getScheduleChanges", "getScheduleChangesAction"] }, - { "name": "SCHEDULE_CHANGE_STATUS_LABELS", "type": "const", "file": "types.ts", "definition": "状态英文标签常量", "usedBy": ["schedule-change-list.tsx"] }, - { "name": "SCHEDULE_CHANGE_STATUS_COLORS", "type": "const", "file": "types.ts", "definition": "状态颜色常量(用于 Badge)", "usedBy": ["schedule-change-list.tsx"] } + { + "name": "ScheduleChangeStatus", + "type": "type", + "file": "types.ts", + "definition": "'pending' | 'approved' | 'rejected' | 'completed'", + "usedBy": [ + "scheduling/data-access", + "scheduling/components" + ] + }, + { + "name": "SchedulingRule", + "type": "type", + "file": "types.ts", + "definition": "{ id, classId, maxDailyHours, maxContinuousHours, lunchBreakStart, lunchBreakEnd, morningStart, afternoonEnd, avoidBackToBack, balancedSubjects, createdAt, updatedAt }", + "usedBy": [ + "scheduling-rules-form.tsx", + "auto-scheduler.ts" + ] + }, + { + "name": "ScheduleChange", + "type": "type", + "file": "types.ts", + "definition": "调课申请完整类型", + "usedBy": [ + "scheduling/data-access" + ] + }, + { + "name": "ScheduleChangeListItem", + "type": "type", + "file": "types.ts", + "definition": "{ ...ScheduleChange, className, originalTeacherName, substituteTeacherName, requesterName, approverName }", + "usedBy": [ + "schedule-change-list.tsx", + "页面" + ] + }, + { + "name": "TimeSlot", + "type": "type", + "file": "types.ts", + "definition": "{ weekday, startTime, endTime }", + "usedBy": [ + "auto-scheduler.ts" + ] + }, + { + "name": "ScheduleConflict", + "type": "type", + "file": "types.ts", + "definition": "{ type: 'teacher_overlap'|'classroom_overlap'|'class_overlap'|'rule_violation', description, scheduleIds }", + "usedBy": [ + "auto-schedule-result.tsx", + "schedule-conflicts-view.tsx" + ] + }, + { + "name": "AutoScheduleResult", + "type": "type", + "file": "types.ts", + "definition": "{ success, scheduledCount, conflictCount, conflicts, schedules: GeneratedSchedule[] }", + "usedBy": [ + "auto-schedule-panel.tsx", + "auto-schedule-result.tsx" + ] + }, + { + "name": "GeneratedSchedule", + "type": "type", + "file": "types.ts", + "definition": "{ classId, weekday, startTime, endTime, course, location, teacherId, subjectId }", + "usedBy": [ + "auto-scheduler.ts", + "auto-schedule-result.tsx" + ] + }, + { + "name": "AutoScheduleParams", + "type": "type", + "file": "types.ts", + "definition": "{ classId, rules, subjects, teachers, classrooms, timeSlots }", + "usedBy": [ + "auto-scheduler.ts", + "autoScheduleAction" + ] + }, + { + "name": "ScheduleChangeQueryParams", + "type": "type", + "file": "types.ts", + "definition": "{ classId?, status?, requesterId? }", + "usedBy": [ + "getScheduleChanges", + "getScheduleChangesAction" + ] + }, + { + "name": "SCHEDULE_CHANGE_STATUS_LABELS", + "type": "const", + "file": "types.ts", + "definition": "状态英文标签常量", + "usedBy": [ + "schedule-change-list.tsx" + ] + }, + { + "name": "SCHEDULE_CHANGE_STATUS_COLORS", + "type": "const", + "file": "types.ts", + "definition": "状态颜色常量(用于 Badge)", + "usedBy": [ + "schedule-change-list.tsx" + ] + } ], "components": [ - { "name": "SchedulingRulesForm", "file": "components/scheduling-rules-form.tsx", "purpose": "排课规则配置表单(班级选择器、每日最大课时、连续课时、午休时间、上下学时间、避免背靠背、科目均衡)" }, - { "name": "AutoSchedulePanel", "file": "components/auto-schedule-panel.tsx", "purpose": "自动排课面板(班级选择→预览→应用流程,调用 autoScheduleAction 和 applyAutoScheduleAction)" }, - { "name": "AutoScheduleResultView", "file": "components/auto-schedule-result.tsx", "purpose": "排课结果预览(课表表格 + 冲突/警告列表)" }, - { "name": "ScheduleChangeForm", "file": "components/schedule-change-form.tsx", "purpose": "调课/代课申请表单(班级、原任课教师、代课教师、原日期、新日期、新时间、原因)" }, - { "name": "ScheduleChangeList", "file": "components/schedule-change-list.tsx", "purpose": "调课申请列表表格(含审批/驳回对话框,canApprove 控制是否显示审批按钮)" }, - { "name": "ScheduleConflictsView", "file": "components/schedule-conflicts-view.tsx", "purpose": "冲突检测视图(班级选择器 + 检测按钮 + 冲突结果列表)" } + { + "name": "SchedulingRulesForm", + "file": "components/scheduling-rules-form.tsx", + "purpose": "排课规则配置表单(班级选择器、每日最大课时、连续课时、午休时间、上下学时间、避免背靠背、科目均衡)" + }, + { + "name": "AutoSchedulePanel", + "file": "components/auto-schedule-panel.tsx", + "purpose": "自动排课面板(班级选择→预览→应用流程,调用 autoScheduleAction 和 applyAutoScheduleAction)" + }, + { + "name": "AutoScheduleResultView", + "file": "components/auto-schedule-result.tsx", + "purpose": "排课结果预览(课表表格 + 冲突/警告列表)" + }, + { + "name": "ScheduleChangeForm", + "file": "components/schedule-change-form.tsx", + "purpose": "调课/代课申请表单(班级、原任课教师、代课教师、原日期、新日期、新时间、原因)" + }, + { + "name": "ScheduleChangeList", + "file": "components/schedule-change-list.tsx", + "purpose": "调课申请列表表格(含审批/驳回对话框,canApprove 控制是否显示审批按钮)" + }, + { + "name": "ScheduleConflictsView", + "file": "components/schedule-conflicts-view.tsx", + "purpose": "冲突检测视图(班级选择器 + 检测按钮 + 冲突结果列表)" + } ] } }, @@ -1511,35 +8498,170 @@ "description": "考试监考模块:监考模式考试实时监控、防作弊事件采集、教师监考面板、学生端防作弊监控、考试模式配置", "exports": { "actions": [ - {"name": "recordProctoringEventAction", "permission": "requireAuth()", "signature": "(prevState: ActionState<{id:string}> | null, formData: FormData) => Promise>", "purpose": "学生端上报监考事件(含 submission 归属校验)", "deps": ["requireAuth", "shared/db", "data-access.recordProctoringEvent"], "usedBy": ["anti-cheat-monitor.tsx"]}, - {"name": "getProctoringDashboardAction", "permission": "EXAM_PROCTOR", "signature": "(examId: string) => Promise>", "purpose": "获取监考面板数据(摘要+学生状态+最近事件)", "deps": ["requirePermission(EXAM_PROCTOR)", "data-access.getExamForProctoring,getExamProctoringSummary,getStudentProctoringStatuses,getRecentProctoringEvents"], "usedBy": ["proctoring-dashboard.tsx"]} + { + "name": "recordProctoringEventAction", + "permission": "requireAuth()", + "signature": "(prevState: ActionState<{id:string}> | null, formData: FormData) => Promise>", + "purpose": "学生端上报监考事件(含 submission 归属校验)", + "deps": [ + "requireAuth", + "shared/db", + "data-access.recordProctoringEvent" + ], + "usedBy": [ + "anti-cheat-monitor.tsx" + ] + }, + { + "name": "getProctoringDashboardAction", + "permission": "EXAM_PROCTOR", + "signature": "(examId: string) => Promise>", + "purpose": "获取监考面板数据(摘要+学生状态+最近事件)", + "deps": [ + "requirePermission(EXAM_PROCTOR)", + "data-access.getExamForProctoring,getExamProctoringSummary,getStudentProctoringStatuses,getRecentProctoringEvents" + ], + "usedBy": [ + "proctoring-dashboard.tsx" + ] + } ], "dataAccess": [ - {"name": "recordProctoringEvent", "signature": "(input: RecordProctoringEventInput) => Promise", "purpose": "记录一条监考事件", "usedBy": ["actions.recordProctoringEventAction", "api/proctoring/event/route.ts"]}, - {"name": "getProctoringEvents", "signature": "(examId: string, filters?: GetProctoringEventsFilters) => Promise", "purpose": "查询考试监考事件(含学生姓名、考试标题)", "usedBy": ["待扩展"]}, - {"name": "getProctoringEventsBySubmission", "signature": "(submissionId: string) => Promise", "purpose": "查询提交的监考事件", "usedBy": ["待扩展"]}, - {"name": "getExamProctoringSummary", "signature": "(examId: string) => Promise", "purpose": "获取考试监考摘要", "usedBy": ["actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx"]}, - {"name": "getStudentProctoringStatuses", "signature": "(examId: string) => Promise", "purpose": "获取所有学生监考状态", "usedBy": ["actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx"]}, - {"name": "getExamForProctoring", "signature": "(examId: string) => Promise<{id,title,examMode,config} | null>", "purpose": "获取考试信息(含 examMode 设置)", "usedBy": ["actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx"]}, - {"name": "getRecentProctoringEvents", "signature": "(examId: string, limit?: number) => Promise", "purpose": "获取最近 N 条监考事件", "usedBy": ["actions.getProctoringDashboardAction", "teacher/exams/[id]/proctoring/page.tsx"]} + { + "name": "recordProctoringEvent", + "signature": "(input: RecordProctoringEventInput) => Promise", + "purpose": "记录一条监考事件", + "usedBy": [ + "actions.recordProctoringEventAction", + "api/proctoring/event/route.ts" + ] + }, + { + "name": "getProctoringEvents", + "signature": "(examId: string, filters?: GetProctoringEventsFilters) => Promise", + "purpose": "查询考试监考事件(含学生姓名、考试标题)", + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getProctoringEventsBySubmission", + "signature": "(submissionId: string) => Promise", + "purpose": "查询提交的监考事件", + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getExamProctoringSummary", + "signature": "(examId: string) => Promise", + "purpose": "获取考试监考摘要", + "usedBy": [ + "actions.getProctoringDashboardAction", + "teacher/exams/[id]/proctoring/page.tsx" + ] + }, + { + "name": "getStudentProctoringStatuses", + "signature": "(examId: string) => Promise", + "purpose": "获取所有学生监考状态", + "usedBy": [ + "actions.getProctoringDashboardAction", + "teacher/exams/[id]/proctoring/page.tsx" + ] + }, + { + "name": "getExamForProctoring", + "signature": "(examId: string) => Promise<{id,title,examMode,config} | null>", + "purpose": "获取考试信息(含 examMode 设置)", + "usedBy": [ + "actions.getProctoringDashboardAction", + "teacher/exams/[id]/proctoring/page.tsx" + ] + }, + { + "name": "getRecentProctoringEvents", + "signature": "(examId: string, limit?: number) => Promise", + "purpose": "获取最近 N 条监考事件", + "usedBy": [ + "actions.getProctoringDashboardAction", + "teacher/exams/[id]/proctoring/page.tsx" + ] + } ], "types": [ - {"name": "ProctoringEventType", "type": "type", "definition": "\"tab_switch\" | \"window_blur\" | \"copy_attempt\" | \"paste_attempt\" | \"right_click\" | \"devtools_open\" | \"fullscreen_exit\" | \"idle_timeout\""}, - {"name": "ExamMode", "type": "type", "definition": "\"homework\" | \"timed\" | \"proctored\""}, - {"name": "ProctoringEvent", "type": "interface", "definition": "{ id, submissionId, studentId, examId, eventType, eventDetail?, occurredAt, createdAt }"}, - {"name": "ProctoringEventWithDetails", "type": "interface", "definition": "ProctoringEvent & { studentName, examTitle }"}, - {"name": "ExamProctoringSummary", "type": "interface", "definition": "{ examId, examTitle, examMode, totalStudents, startedStudents, submittedStudents, totalEvents, abnormalStudents, eventsByType }"}, - {"name": "StudentProctoringStatus", "type": "interface", "definition": "{ studentId, studentName, submissionId, submissionStatus, eventCount, lastEventAt, isAbnormal, eventsByType }"}, - {"name": "ProctoringDashboardData", "type": "interface", "definition": "{ summary, students, recentEvents }"}, - {"name": "ExamModeConfig", "type": "interface", "definition": "{ examMode, durationMinutes, shuffleQuestions, allowLateStart, lateStartGraceMinutes, antiCheatEnabled }"}, - {"name": "PROCTORING_EVENT_LABELS", "type": "const", "description": "事件类型中文标签常量"}, - {"name": "EXAM_MODE_LABELS", "type": "const", "description": "考试模式中文标签常量"}, - {"name": "ABNORMAL_EVENT_THRESHOLD", "type": "const", "description": "异常学生事件数阈值(3)"} + { + "name": "ProctoringEventType", + "type": "type", + "definition": "\"tab_switch\" | \"window_blur\" | \"copy_attempt\" | \"paste_attempt\" | \"right_click\" | \"devtools_open\" | \"fullscreen_exit\" | \"idle_timeout\"" + }, + { + "name": "ExamMode", + "type": "type", + "definition": "\"homework\" | \"timed\" | \"proctored\"" + }, + { + "name": "ProctoringEvent", + "type": "interface", + "definition": "{ id, submissionId, studentId, examId, eventType, eventDetail?, occurredAt, createdAt }" + }, + { + "name": "ProctoringEventWithDetails", + "type": "interface", + "definition": "ProctoringEvent & { studentName, examTitle }" + }, + { + "name": "ExamProctoringSummary", + "type": "interface", + "definition": "{ examId, examTitle, examMode, totalStudents, startedStudents, submittedStudents, totalEvents, abnormalStudents, eventsByType }" + }, + { + "name": "StudentProctoringStatus", + "type": "interface", + "definition": "{ studentId, studentName, submissionId, submissionStatus, eventCount, lastEventAt, isAbnormal, eventsByType }" + }, + { + "name": "ProctoringDashboardData", + "type": "interface", + "definition": "{ summary, students, recentEvents }" + }, + { + "name": "ExamModeConfig", + "type": "interface", + "definition": "{ examMode, durationMinutes, shuffleQuestions, allowLateStart, lateStartGraceMinutes, antiCheatEnabled }" + }, + { + "name": "PROCTORING_EVENT_LABELS", + "type": "const", + "description": "事件类型中文标签常量" + }, + { + "name": "EXAM_MODE_LABELS", + "type": "const", + "description": "考试模式中文标签常量" + }, + { + "name": "ABNORMAL_EVENT_THRESHOLD", + "type": "const", + "description": "异常学生事件数阈值(3)" + } ], "components": [ - {"name": "ProctoringDashboard", "file": "components/proctoring-dashboard.tsx", "purpose": "教师监考面板(实时学生状态、异常事件统计、异常学生高亮、10 秒轮询、usePermission 权限控制)"}, - {"name": "AntiCheatMonitor", "file": "components/anti-cheat-monitor.tsx", "purpose": "学生端防作弊监控(visibilitychange/blur/copy/paste/contextmenu/keydown/fullscreenchange 监听、空闲超时检测、强制全屏、警告提示、事件上报)"}, - {"name": "ExamModeConfig", "file": "components/exam-mode-config.tsx", "purpose": "考试模式配置(react-hook-form Controller,作业/限时/监考模式选择,限时设置时长,监考设置防作弊选项)"} + { + "name": "ProctoringDashboard", + "file": "components/proctoring-dashboard.tsx", + "purpose": "教师监考面板(实时学生状态、异常事件统计、异常学生高亮、10 秒轮询、usePermission 权限控制)" + }, + { + "name": "AntiCheatMonitor", + "file": "components/anti-cheat-monitor.tsx", + "purpose": "学生端防作弊监控(visibilitychange/blur/copy/paste/contextmenu/keydown/fullscreenchange 监听、空闲超时检测、强制全屏、警告提示、事件上报)" + }, + { + "name": "ExamModeConfig", + "file": "components/exam-mode-config.tsx", + "purpose": "考试模式配置(react-hook-form Controller,作业/限时/监考模式选择,限时设置时长,监考设置防作弊选项)" + } ] } }, @@ -1548,44 +8670,429 @@ "description": "学情诊断报告模块:基于知识点掌握度(knowledgePointMastery)生成个人/班级诊断报告,掌握度雷达图(学生 vs 班级平均),强项/弱项分析,知识点掌握度热力图,需重点关注学生列表,报告发布/删除管理", "exports": { "dataAccess": [ - { "name": "getStudentMastery", "signature": "(studentId: string) => Promise", "file": "data-access.ts", "purpose": "获取学生在所有知识点的掌握度(含知识点名称,按掌握度降序)", "deps": ["shared.db", "shared.db.schema.knowledgePointMastery", "shared.db.schema.knowledgePoints"], "usedBy": ["data-access.getStudentMasterySummary", "teacher/diagnostic/student/[studentId]/page.tsx"] }, - { "name": "getStudentMasterySummary", "signature": "(studentId: string) => Promise", "file": "data-access.ts", "purpose": "获取学生掌握度摘要(平均掌握度、强项≥80%、弱项<60%)", "deps": ["shared.db", "shared.db.schema.users", "data-access.getStudentMastery"], "usedBy": ["data-access-reports.generateDiagnosticReport", "teacher/diagnostic/student/[studentId]/page.tsx", "student/diagnostic/page.tsx"] }, - { "name": "updateMasteryFromSubmission", "signature": "(submissionId: string) => Promise", "file": "data-access.ts", "purpose": "从提交答案更新掌握度(按知识点聚合正确率,onDuplicateKeyUpdate upsert)", "deps": ["shared.db", "shared.db.schema.examSubmissions", "shared.db.schema.submissionAnswers", "shared.db.schema.questionsToKnowledgePoints", "shared.db.schema.knowledgePointMastery"], "usedBy": ["待扩展(作业/考试提交后触发)"] }, - { "name": "getClassMasterySummary", "signature": "(classId: string) => Promise", "file": "data-access.ts", "purpose": "获取班级掌握度摘要(学生数、平均掌握度、知识点统计、需重点关注学生)", "deps": ["shared.db", "shared.db.schema.classes", "shared.db.schema.classEnrollments", "shared.db.schema.users", "shared.db.schema.knowledgePointMastery", "shared.db.schema.knowledgePoints"], "usedBy": ["data-access-reports.generateClassDiagnosticReport", "teacher/diagnostic/class/[classId]/page.tsx"] }, - { "name": "getKnowledgePointStats", "signature": "(classId?: string, gradeId?: string) => Promise", "file": "data-access.ts", "purpose": "获取知识点统计(按班级或年级聚合平均掌握度、掌握人数、未掌握人数)", "deps": ["shared.db", "shared.db.schema.classEnrollments", "shared.db.schema.users", "shared.db.schema.knowledgePointMastery", "shared.db.schema.knowledgePoints"], "usedBy": ["teacher/diagnostic/student/[studentId]/page.tsx (班级平均对比)"] }, - { "name": "generateDiagnosticReport", "signature": "(studentId: string, period: string, generatedBy: string) => Promise", "file": "data-access-reports.ts", "purpose": "生成个人诊断报告(计算 overallScore、强项/弱项列表、复习建议,status=draft)", "deps": ["shared.db", "shared.db.schema.learningDiagnosticReports", "data-access.getStudentMasterySummary", "@paralleldrive/cuid2"], "usedBy": ["actions.generateStudentReportAction"] }, - { "name": "generateClassDiagnosticReport", "signature": "(classId: string, period: string, generatedBy: string) => Promise", "file": "data-access-reports.ts", "purpose": "生成班级诊断报告(聚合班级掌握度,识别薄弱知识点,status=draft,studentId 存生成者 ID)", "deps": ["shared.db", "shared.db.schema.learningDiagnosticReports", "data-access.getClassMasterySummary", "@paralleldrive/cuid2"], "usedBy": ["actions.generateClassReportAction"] }, - { "name": "getDiagnosticReports", "signature": "(filters: DiagnosticReportQueryParams) => Promise", "file": "data-access-reports.ts", "purpose": "查询诊断报告列表(可按 studentId/reportType/status/period 过滤,含学生名和生成者名)", "deps": ["shared.db", "shared.db.schema.learningDiagnosticReports", "shared.db.schema.users"], "usedBy": ["actions.getDiagnosticReportsAction", "teacher/diagnostic/page.tsx", "teacher/diagnostic/student/[studentId]/page.tsx", "student/diagnostic/page.tsx"] }, - { "name": "getDiagnosticReportById", "signature": "(id: string) => Promise", "file": "data-access-reports.ts", "purpose": "获取报告详情(含学生名和生成者名)", "deps": ["shared.db", "shared.db.schema.learningDiagnosticReports", "shared.db.schema.users"], "usedBy": ["actions.getDiagnosticReportByIdAction"] }, - { "name": "publishDiagnosticReport", "signature": "(id: string) => Promise", "file": "data-access-reports.ts", "purpose": "发布诊断报告(status=published)", "deps": ["shared.db", "shared.db.schema.learningDiagnosticReports"], "usedBy": ["actions.publishReportAction"] }, - { "name": "deleteDiagnosticReport", "signature": "(id: string) => Promise", "file": "data-access-reports.ts", "purpose": "删除诊断报告", "deps": ["shared.db", "shared.db.schema.learningDiagnosticReports"], "usedBy": ["actions.deleteReportAction"] } + { + "name": "getStudentMastery", + "signature": "(studentId: string) => Promise", + "file": "data-access.ts", + "purpose": "获取学生在所有知识点的掌握度(含知识点名称,按掌握度降序)", + "deps": [ + "shared.db", + "shared.db.schema.knowledgePointMastery", + "shared.db.schema.knowledgePoints" + ], + "usedBy": [ + "data-access.getStudentMasterySummary", + "teacher/diagnostic/student/[studentId]/page.tsx" + ] + }, + { + "name": "getStudentMasterySummary", + "signature": "(studentId: string) => Promise", + "file": "data-access.ts", + "purpose": "获取学生掌握度摘要(平均掌握度、强项≥80%、弱项<60%)", + "deps": [ + "shared.db", + "shared.db.schema.users", + "data-access.getStudentMastery" + ], + "usedBy": [ + "data-access-reports.generateDiagnosticReport", + "teacher/diagnostic/student/[studentId]/page.tsx", + "student/diagnostic/page.tsx" + ] + }, + { + "name": "updateMasteryFromSubmission", + "signature": "(submissionId: string) => Promise", + "file": "data-access.ts", + "purpose": "从提交答案更新掌握度(按知识点聚合正确率,onDuplicateKeyUpdate upsert)", + "deps": [ + "shared.db", + "shared.db.schema.examSubmissions", + "shared.db.schema.submissionAnswers", + "shared.db.schema.questionsToKnowledgePoints", + "shared.db.schema.knowledgePointMastery" + ], + "usedBy": [ + "待扩展(作业/考试提交后触发)" + ] + }, + { + "name": "getClassMasterySummary", + "signature": "(classId: string) => Promise", + "file": "data-access.ts", + "purpose": "获取班级掌握度摘要(学生数、平均掌握度、知识点统计、需重点关注学生)", + "deps": [ + "shared.db", + "shared.db.schema.classes", + "shared.db.schema.classEnrollments", + "shared.db.schema.users", + "shared.db.schema.knowledgePointMastery", + "shared.db.schema.knowledgePoints" + ], + "usedBy": [ + "data-access-reports.generateClassDiagnosticReport", + "teacher/diagnostic/class/[classId]/page.tsx" + ] + }, + { + "name": "getKnowledgePointStats", + "signature": "(classId?: string, gradeId?: string) => Promise", + "file": "data-access.ts", + "purpose": "获取知识点统计(按班级或年级聚合平均掌握度、掌握人数、未掌握人数)", + "deps": [ + "shared.db", + "shared.db.schema.classEnrollments", + "shared.db.schema.users", + "shared.db.schema.knowledgePointMastery", + "shared.db.schema.knowledgePoints" + ], + "usedBy": [ + "teacher/diagnostic/student/[studentId]/page.tsx (班级平均对比)" + ] + }, + { + "name": "generateDiagnosticReport", + "signature": "(studentId: string, period: string, generatedBy: string) => Promise", + "file": "data-access-reports.ts", + "purpose": "生成个人诊断报告(计算 overallScore、强项/弱项列表、复习建议,status=draft)", + "deps": [ + "shared.db", + "shared.db.schema.learningDiagnosticReports", + "data-access.getStudentMasterySummary", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "actions.generateStudentReportAction" + ] + }, + { + "name": "generateClassDiagnosticReport", + "signature": "(classId: string, period: string, generatedBy: string) => Promise", + "file": "data-access-reports.ts", + "purpose": "生成班级诊断报告(聚合班级掌握度,识别薄弱知识点,status=draft,studentId 存生成者 ID)", + "deps": [ + "shared.db", + "shared.db.schema.learningDiagnosticReports", + "data-access.getClassMasterySummary", + "@paralleldrive/cuid2" + ], + "usedBy": [ + "actions.generateClassReportAction" + ] + }, + { + "name": "getDiagnosticReports", + "signature": "(filters: DiagnosticReportQueryParams) => Promise", + "file": "data-access-reports.ts", + "purpose": "查询诊断报告列表(可按 studentId/reportType/status/period 过滤,含学生名和生成者名)", + "deps": [ + "shared.db", + "shared.db.schema.learningDiagnosticReports", + "shared.db.schema.users" + ], + "usedBy": [ + "actions.getDiagnosticReportsAction", + "teacher/diagnostic/page.tsx", + "teacher/diagnostic/student/[studentId]/page.tsx", + "student/diagnostic/page.tsx" + ] + }, + { + "name": "getDiagnosticReportById", + "signature": "(id: string) => Promise", + "file": "data-access-reports.ts", + "purpose": "获取报告详情(含学生名和生成者名)", + "deps": [ + "shared.db", + "shared.db.schema.learningDiagnosticReports", + "shared.db.schema.users" + ], + "usedBy": [ + "actions.getDiagnosticReportByIdAction" + ] + }, + { + "name": "publishDiagnosticReport", + "signature": "(id: string) => Promise", + "file": "data-access-reports.ts", + "purpose": "发布诊断报告(status=published)", + "deps": [ + "shared.db", + "shared.db.schema.learningDiagnosticReports" + ], + "usedBy": [ + "actions.publishReportAction" + ] + }, + { + "name": "deleteDiagnosticReport", + "signature": "(id: string) => Promise", + "file": "data-access-reports.ts", + "purpose": "删除诊断报告", + "deps": [ + "shared.db", + "shared.db.schema.learningDiagnosticReports" + ], + "usedBy": [ + "actions.deleteReportAction" + ] + } ], "actions": [ - { "name": "generateStudentReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "生成学生个人诊断报告(formData: studentId, period)", "deps": ["requirePermission", "data-access-reports.generateDiagnosticReport", "revalidatePath"], "usedBy": ["components/student-diagnostic-view.tsx"] }, - { "name": "generateClassReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "生成班级诊断报告(formData: classId, period)", "deps": ["requirePermission", "data-access-reports.generateClassDiagnosticReport", "revalidatePath"], "usedBy": ["components/class-diagnostic-view.tsx"] }, - { "name": "publishReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "发布诊断报告(formData: id)", "deps": ["requirePermission", "data-access-reports.publishDiagnosticReport", "revalidatePath"], "usedBy": ["components/report-list.tsx"] }, - { "name": "deleteReportAction", "permission": "DIAGNOSTIC_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "删除诊断报告(formData: id)", "deps": ["requirePermission", "data-access-reports.deleteDiagnosticReport", "revalidatePath"], "usedBy": ["components/report-list.tsx"] }, - { "name": "getDiagnosticReportsAction", "permission": "DIAGNOSTIC_READ", "signature": "(params: DiagnosticReportQueryParams) => Promise>", "file": "actions.ts", "purpose": "查询诊断报告列表(读权限)", "deps": ["requirePermission", "data-access-reports.getDiagnosticReports"], "usedBy": ["待扩展"] }, - { "name": "getDiagnosticReportByIdAction", "permission": "DIAGNOSTIC_READ", "signature": "(id: string) => Promise>", "file": "actions.ts", "purpose": "获取诊断报告详情(读权限)", "deps": ["requirePermission", "data-access-reports.getDiagnosticReportById"], "usedBy": ["待扩展"] } + { + "name": "generateStudentReportAction", + "permission": "DIAGNOSTIC_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "生成学生个人诊断报告(formData: studentId, period)", + "deps": [ + "requirePermission", + "data-access-reports.generateDiagnosticReport", + "revalidatePath" + ], + "usedBy": [ + "components/student-diagnostic-view.tsx" + ] + }, + { + "name": "generateClassReportAction", + "permission": "DIAGNOSTIC_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "生成班级诊断报告(formData: classId, period)", + "deps": [ + "requirePermission", + "data-access-reports.generateClassDiagnosticReport", + "revalidatePath" + ], + "usedBy": [ + "components/class-diagnostic-view.tsx" + ] + }, + { + "name": "publishReportAction", + "permission": "DIAGNOSTIC_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "发布诊断报告(formData: id)", + "deps": [ + "requirePermission", + "data-access-reports.publishDiagnosticReport", + "revalidatePath" + ], + "usedBy": [ + "components/report-list.tsx" + ] + }, + { + "name": "deleteReportAction", + "permission": "DIAGNOSTIC_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "删除诊断报告(formData: id)", + "deps": [ + "requirePermission", + "data-access-reports.deleteDiagnosticReport", + "revalidatePath" + ], + "usedBy": [ + "components/report-list.tsx" + ] + }, + { + "name": "getDiagnosticReportsAction", + "permission": "DIAGNOSTIC_READ", + "signature": "(params: DiagnosticReportQueryParams) => Promise>", + "file": "actions.ts", + "purpose": "查询诊断报告列表(读权限)", + "deps": [ + "requirePermission", + "data-access-reports.getDiagnosticReports" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getDiagnosticReportByIdAction", + "permission": "DIAGNOSTIC_READ", + "signature": "(id: string) => Promise>", + "file": "actions.ts", + "purpose": "获取诊断报告详情(读权限)", + "deps": [ + "requirePermission", + "data-access-reports.getDiagnosticReportById" + ], + "usedBy": [ + "待扩展" + ] + } ], "types": [ - { "name": "DiagnosticReportType", "type": "type", "file": "types.ts", "definition": "\"individual\" | \"class\" | \"grade\"", "usedBy": ["types.DiagnosticReport.reportType", "actions", "components/report-list.tsx"] }, - { "name": "DiagnosticReportStatus", "type": "type", "file": "types.ts", "definition": "\"draft\" | \"published\" | \"archived\"", "usedBy": ["types.DiagnosticReport.status", "actions", "components/report-list.tsx"] }, - { "name": "KnowledgePointMastery", "type": "interface", "file": "types.ts", "definition": "{ id, studentId, knowledgePointId, masteryLevel(0-100), totalQuestions, correctQuestions, lastAssessedAt, createdAt, updatedAt }", "usedBy": ["data-access", "types.MasteryWithKnowledgePoint"] }, - { "name": "MasteryWithKnowledgePoint", "type": "interface", "file": "types.ts", "definition": "KnowledgePointMastery & { knowledgePointName, knowledgePointDescription }", "usedBy": ["data-access.getStudentMastery", "types.StudentMasterySummary"] }, - { "name": "StudentMasterySummary", "type": "interface", "file": "types.ts", "definition": "{ studentId, studentName, averageMastery, totalKnowledgePoints, strengths(≥80), weaknesses(<60), allMastery }", "usedBy": ["data-access.getStudentMasterySummary", "data-access-reports.generateDiagnosticReport", "components/student-diagnostic-view.tsx"] }, - { "name": "DiagnosticReport", "type": "interface", "file": "types.ts", "definition": "{ id, studentId, generatedBy, reportType, period, summary, strengths[], weaknesses[], recommendations[], overallScore, status, createdAt, updatedAt }", "usedBy": ["data-access-reports", "types.DiagnosticReportWithDetails"] }, - { "name": "DiagnosticReportWithDetails", "type": "interface", "file": "types.ts", "definition": "DiagnosticReport & { studentName, generatedByName }", "usedBy": ["data-access-reports.getDiagnosticReports", "actions", "components/report-list.tsx", "components/student-diagnostic-view.tsx"] }, - { "name": "ClassMasterySummary", "type": "interface", "file": "types.ts", "definition": "{ classId, className, studentCount, averageMastery, knowledgePointStats[], studentsNeedingAttention[] }", "usedBy": ["data-access.getClassMasterySummary", "data-access-reports.generateClassDiagnosticReport", "components/class-diagnostic-view.tsx"] }, - { "name": "KnowledgePointStat", "type": "interface", "file": "types.ts", "definition": "{ knowledgePointId, knowledgePointName, averageMastery, masteredCount(≥80), notMasteredCount(<60), totalStudents }", "usedBy": ["data-access.getKnowledgePointStats", "types.ClassMasterySummary", "components/class-diagnostic-view.tsx"] }, - { "name": "DiagnosticReportQueryParams", "type": "interface", "file": "types.ts", "definition": "{ studentId?, reportType?, status?, period? }", "usedBy": ["data-access-reports.getDiagnosticReports", "actions.getDiagnosticReportsAction"] }, - { "name": "MasteryRadarPoint", "type": "interface", "file": "types.ts", "definition": "{ knowledgePoint, student(0-100), classAverage?(0-100) }", "usedBy": ["components/mastery-radar-chart.tsx", "components/student-diagnostic-view.tsx", "teacher/diagnostic/student/[studentId]/page.tsx"] } + { + "name": "DiagnosticReportType", + "type": "type", + "file": "types.ts", + "definition": "\"individual\" | \"class\" | \"grade\"", + "usedBy": [ + "types.DiagnosticReport.reportType", + "actions", + "components/report-list.tsx" + ] + }, + { + "name": "DiagnosticReportStatus", + "type": "type", + "file": "types.ts", + "definition": "\"draft\" | \"published\" | \"archived\"", + "usedBy": [ + "types.DiagnosticReport.status", + "actions", + "components/report-list.tsx" + ] + }, + { + "name": "KnowledgePointMastery", + "type": "interface", + "file": "types.ts", + "definition": "{ id, studentId, knowledgePointId, masteryLevel(0-100), totalQuestions, correctQuestions, lastAssessedAt, createdAt, updatedAt }", + "usedBy": [ + "data-access", + "types.MasteryWithKnowledgePoint" + ] + }, + { + "name": "MasteryWithKnowledgePoint", + "type": "interface", + "file": "types.ts", + "definition": "KnowledgePointMastery & { knowledgePointName, knowledgePointDescription }", + "usedBy": [ + "data-access.getStudentMastery", + "types.StudentMasterySummary" + ] + }, + { + "name": "StudentMasterySummary", + "type": "interface", + "file": "types.ts", + "definition": "{ studentId, studentName, averageMastery, totalKnowledgePoints, strengths(≥80), weaknesses(<60), allMastery }", + "usedBy": [ + "data-access.getStudentMasterySummary", + "data-access-reports.generateDiagnosticReport", + "components/student-diagnostic-view.tsx" + ] + }, + { + "name": "DiagnosticReport", + "type": "interface", + "file": "types.ts", + "definition": "{ id, studentId, generatedBy, reportType, period, summary, strengths[], weaknesses[], recommendations[], overallScore, status, createdAt, updatedAt }", + "usedBy": [ + "data-access-reports", + "types.DiagnosticReportWithDetails" + ] + }, + { + "name": "DiagnosticReportWithDetails", + "type": "interface", + "file": "types.ts", + "definition": "DiagnosticReport & { studentName, generatedByName }", + "usedBy": [ + "data-access-reports.getDiagnosticReports", + "actions", + "components/report-list.tsx", + "components/student-diagnostic-view.tsx" + ] + }, + { + "name": "ClassMasterySummary", + "type": "interface", + "file": "types.ts", + "definition": "{ classId, className, studentCount, averageMastery, knowledgePointStats[], studentsNeedingAttention[] }", + "usedBy": [ + "data-access.getClassMasterySummary", + "data-access-reports.generateClassDiagnosticReport", + "components/class-diagnostic-view.tsx" + ] + }, + { + "name": "KnowledgePointStat", + "type": "interface", + "file": "types.ts", + "definition": "{ knowledgePointId, knowledgePointName, averageMastery, masteredCount(≥80), notMasteredCount(<60), totalStudents }", + "usedBy": [ + "data-access.getKnowledgePointStats", + "types.ClassMasterySummary", + "components/class-diagnostic-view.tsx" + ] + }, + { + "name": "DiagnosticReportQueryParams", + "type": "interface", + "file": "types.ts", + "definition": "{ studentId?, reportType?, status?, period? }", + "usedBy": [ + "data-access-reports.getDiagnosticReports", + "actions.getDiagnosticReportsAction" + ] + }, + { + "name": "MasteryRadarPoint", + "type": "interface", + "file": "types.ts", + "definition": "{ knowledgePoint, student(0-100), classAverage?(0-100) }", + "usedBy": [ + "components/mastery-radar-chart.tsx", + "components/student-diagnostic-view.tsx", + "teacher/diagnostic/student/[studentId]/page.tsx" + ] + } ], "components": [ - { "name": "MasteryRadarChart", "file": "components/mastery-radar-chart.tsx", "purpose": "知识点掌握度雷达图(recharts RadarChart,学生 vs 班级平均对比,无数据时显示 EmptyState)", "deps": ["recharts", "shared/components/ui/card", "shared/components/ui/chart", "shared/components/ui/empty-state"] }, - { "name": "StudentDiagnosticView", "file": "components/student-diagnostic-view.tsx", "purpose": "学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示)", "deps": ["usePermission", "actions.generateStudentReportAction", "components/mastery-radar-chart", "shared/components/ui/*"] }, - { "name": "ClassDiagnosticView", "file": "components/class-diagnostic-view.tsx", "purpose": "班级诊断视图(概览卡片、知识点掌握度热力图[绿/黄/橙/红]、知识点排名表、需重点关注学生表[链接到学生视图]、生成班级报告表单[DIAGNOSTIC_MANAGE])", "deps": ["usePermission", "actions.generateClassReportAction", "shared/components/ui/*"] }, - { "name": "ReportList", "file": "components/report-list.tsx", "purpose": "诊断报告列表(reportType/status 过滤器[URL searchParams]、报告表格、发布/删除操作[DIAGNOSTIC_MANAGE]、确认对话框)", "deps": ["usePermission", "actions.publishReportAction", "actions.deleteReportAction", "shared/components/ui/*"] } + { + "name": "MasteryRadarChart", + "file": "components/mastery-radar-chart.tsx", + "purpose": "知识点掌握度雷达图(recharts RadarChart,学生 vs 班级平均对比,无数据时显示 EmptyState)", + "deps": [ + "recharts", + "shared/components/ui/card", + "shared/components/ui/chart", + "shared/components/ui/empty-state" + ] + }, + { + "name": "StudentDiagnosticView", + "file": "components/student-diagnostic-view.tsx", + "purpose": "学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示)", + "deps": [ + "usePermission", + "actions.generateStudentReportAction", + "components/mastery-radar-chart", + "shared/components/ui/*" + ] + }, + { + "name": "ClassDiagnosticView", + "file": "components/class-diagnostic-view.tsx", + "purpose": "班级诊断视图(概览卡片、知识点掌握度热力图[绿/黄/橙/红]、知识点排名表、需重点关注学生表[链接到学生视图]、生成班级报告表单[DIAGNOSTIC_MANAGE])", + "deps": [ + "usePermission", + "actions.generateClassReportAction", + "shared/components/ui/*" + ] + }, + { + "name": "ReportList", + "file": "components/report-list.tsx", + "purpose": "诊断报告列表(reportType/status 过滤器[URL searchParams]、报告表格、发布/删除操作[DIAGNOSTIC_MANAGE]、确认对话框)", + "deps": [ + "usePermission", + "actions.publishReportAction", + "actions.deleteReportAction", + "shared/components/ui/*" + ] + } ] } }, @@ -1594,334 +9101,2792 @@ "description": "选课管理模块:选修课程 CRUD、选课开放/关闭、学生选课/退课、抽签模式批量录取、FCFS 即时录取、DataScope 行级过滤(admin 全部、teacher 所教、grade_head 所管年级、student 可选课程)", "exports": { "actions": [ - {"name": "createElectiveCourseAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "创建选修课程(formData: name, subjectId?, teacherId, gradeId?, description?, capacity?, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, selectionMode?, credit?)", "deps": ["requirePermission(ELECTIVE_MANAGE)", "data-access.createElectiveCourse", "revalidatePath"], "usedBy": ["admin/elective/create/page.tsx"]}, - {"name": "updateElectiveCourseAction", "permission": "ELECTIVE_MANAGE", "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "更新选修课程", "deps": ["requirePermission(ELECTIVE_MANAGE)", "data-access.getElectiveCourseById", "data-access.updateElectiveCourse", "revalidatePath"], "usedBy": ["admin/elective/[id]/edit/page.tsx"]}, - {"name": "deleteElectiveCourseAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "删除选修课程(formData: courseId)", "deps": ["requirePermission(ELECTIVE_MANAGE)", "data-access.deleteElectiveCourse", "revalidatePath"], "usedBy": ["components/elective-course-list.tsx"]}, - {"name": "openSelectionAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "开放选课(formData: courseId)", "deps": ["requirePermission(ELECTIVE_MANAGE)", "data-access.openSelection", "revalidatePath"], "usedBy": ["components/elective-course-list.tsx"]}, - {"name": "closeSelectionAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "关闭选课(formData: courseId)", "deps": ["requirePermission(ELECTIVE_MANAGE)", "data-access.closeSelection", "revalidatePath"], "usedBy": ["components/elective-course-list.tsx"]}, - {"name": "runLotteryAction", "permission": "ELECTIVE_MANAGE", "signature": "(prevState: ActionState<{enrolled:number,waitlist:number}> | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "执行抽签录取(formData: courseId)", "deps": ["requirePermission(ELECTIVE_MANAGE)", "data-access-operations.runLottery", "revalidatePath"], "usedBy": ["components/elective-course-list.tsx"]}, - {"name": "selectCourseAction", "permission": "ELECTIVE_SELECT", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "学生选课(formData: courseId, priority?)", "deps": ["requirePermission(ELECTIVE_SELECT)", "data-access-operations.selectCourse", "revalidatePath"], "usedBy": ["components/student-selection-view.tsx"]}, - {"name": "dropCourseAction", "permission": "ELECTIVE_SELECT", "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", "file": "actions.ts", "purpose": "学生退课(formData: courseId)", "deps": ["requirePermission(ELECTIVE_SELECT)", "data-access-operations.dropCourse", "revalidatePath"], "usedBy": ["components/student-selection-view.tsx"]}, - {"name": "getElectiveCoursesAction", "permission": "ELECTIVE_READ", "signature": "(params?: GetElectiveCoursesParams) => Promise>", "file": "actions.ts", "purpose": "查询选修课程列表(按 DataScope 过滤)", "deps": ["requirePermission(ELECTIVE_READ)", "data-access.getElectiveCourses (scope, currentUserId)"], "usedBy": ["admin/elective/page.tsx", "teacher/elective/page.tsx"]}, - {"name": "getStudentSelectionsAction", "permission": "ELECTIVE_READ", "signature": "(studentId: string) => Promise>", "file": "actions.ts", "purpose": "查询学生选课记录(含 DataScope 二次校验:class_members 仅自己,children 仅子女)", "deps": ["requirePermission(ELECTIVE_READ)", "data-access-selections.getStudentSelections"], "usedBy": ["待扩展"]}, - {"name": "getAvailableCoursesAction", "permission": "ELECTIVE_SELECT", "signature": "() => Promise>", "file": "actions.ts", "purpose": "获取学生可选课程(status=open 且匹配年级)", "deps": ["requirePermission(ELECTIVE_SELECT)", "data-access-selections.getAvailableCoursesForStudent"], "usedBy": ["待扩展"]} + { + "name": "createElectiveCourseAction", + "permission": "ELECTIVE_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "创建选修课程(formData: name, subjectId?, teacherId, gradeId?, description?, capacity?, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, selectionMode?, credit?)", + "deps": [ + "requirePermission(ELECTIVE_MANAGE)", + "data-access.createElectiveCourse", + "revalidatePath" + ], + "usedBy": [ + "admin/elective/create/page.tsx" + ] + }, + { + "name": "updateElectiveCourseAction", + "permission": "ELECTIVE_MANAGE", + "signature": "(id: string, prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "更新选修课程", + "deps": [ + "requirePermission(ELECTIVE_MANAGE)", + "data-access.getElectiveCourseById", + "data-access.updateElectiveCourse", + "revalidatePath" + ], + "usedBy": [ + "admin/elective/[id]/edit/page.tsx" + ] + }, + { + "name": "deleteElectiveCourseAction", + "permission": "ELECTIVE_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "删除选修课程(formData: courseId)", + "deps": [ + "requirePermission(ELECTIVE_MANAGE)", + "data-access.deleteElectiveCourse", + "revalidatePath" + ], + "usedBy": [ + "components/elective-course-list.tsx" + ] + }, + { + "name": "openSelectionAction", + "permission": "ELECTIVE_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "开放选课(formData: courseId)", + "deps": [ + "requirePermission(ELECTIVE_MANAGE)", + "data-access.openSelection", + "revalidatePath" + ], + "usedBy": [ + "components/elective-course-list.tsx" + ] + }, + { + "name": "closeSelectionAction", + "permission": "ELECTIVE_MANAGE", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "关闭选课(formData: courseId)", + "deps": [ + "requirePermission(ELECTIVE_MANAGE)", + "data-access.closeSelection", + "revalidatePath" + ], + "usedBy": [ + "components/elective-course-list.tsx" + ] + }, + { + "name": "runLotteryAction", + "permission": "ELECTIVE_MANAGE", + "signature": "(prevState: ActionState<{enrolled:number,waitlist:number}> | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "执行抽签录取(formData: courseId)", + "deps": [ + "requirePermission(ELECTIVE_MANAGE)", + "data-access-operations.runLottery", + "revalidatePath" + ], + "usedBy": [ + "components/elective-course-list.tsx" + ] + }, + { + "name": "selectCourseAction", + "permission": "ELECTIVE_SELECT", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "学生选课(formData: courseId, priority?)", + "deps": [ + "requirePermission(ELECTIVE_SELECT)", + "data-access-operations.selectCourse", + "revalidatePath" + ], + "usedBy": [ + "components/student-selection-view.tsx" + ] + }, + { + "name": "dropCourseAction", + "permission": "ELECTIVE_SELECT", + "signature": "(prevState: ActionState | null, formData: FormData) => Promise>", + "file": "actions.ts", + "purpose": "学生退课(formData: courseId)", + "deps": [ + "requirePermission(ELECTIVE_SELECT)", + "data-access-operations.dropCourse", + "revalidatePath" + ], + "usedBy": [ + "components/student-selection-view.tsx" + ] + }, + { + "name": "getElectiveCoursesAction", + "permission": "ELECTIVE_READ", + "signature": "(params?: GetElectiveCoursesParams) => Promise>", + "file": "actions.ts", + "purpose": "查询选修课程列表(按 DataScope 过滤)", + "deps": [ + "requirePermission(ELECTIVE_READ)", + "data-access.getElectiveCourses (scope, currentUserId)" + ], + "usedBy": [ + "admin/elective/page.tsx", + "teacher/elective/page.tsx" + ] + }, + { + "name": "getStudentSelectionsAction", + "permission": "ELECTIVE_READ", + "signature": "(studentId: string) => Promise>", + "file": "actions.ts", + "purpose": "查询学生选课记录(含 DataScope 二次校验:class_members 仅自己,children 仅子女)", + "deps": [ + "requirePermission(ELECTIVE_READ)", + "data-access-selections.getStudentSelections" + ], + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getAvailableCoursesAction", + "permission": "ELECTIVE_SELECT", + "signature": "() => Promise>", + "file": "actions.ts", + "purpose": "获取学生可选课程(status=open 且匹配年级)", + "deps": [ + "requirePermission(ELECTIVE_SELECT)", + "data-access-selections.getAvailableCoursesForStudent" + ], + "usedBy": [ + "待扩展" + ] + } ], "dataAccess": [ - {"name": "getElectiveCourses", "file": "data-access.ts", "signature": "(params?: GetElectiveCoursesParams & { scope?: DataScope; currentUserId?: string }) => Promise", "purpose": "查询选修课程列表(按 scope 行级过滤:owned/class_taught 按 teacherId,grade_managed 按 gradeIds)", "usedBy": ["actions.getElectiveCoursesAction", "admin/elective/page.tsx", "teacher/elective/page.tsx"]}, - {"name": "getElectiveCourseById", "file": "data-access.ts", "signature": "(id: string) => Promise", "purpose": "获取课程详情", "usedBy": ["actions.updateElectiveCourseAction", "admin/elective/[id]/edit/page.tsx"]}, - {"name": "createElectiveCourse", "file": "data-access.ts", "signature": "(data: CreateElectiveCourseInput, teacherId: string) => Promise", "purpose": "创建选修课程(status=draft, enrolledCount=0)", "usedBy": ["actions.createElectiveCourseAction"]}, - {"name": "updateElectiveCourse", "file": "data-access.ts", "signature": "(id: string, data: Partial) => Promise", "purpose": "更新选修课程字段", "usedBy": ["actions.updateElectiveCourseAction"]}, - {"name": "deleteElectiveCourse", "file": "data-access.ts", "signature": "(id: string) => Promise", "purpose": "删除选修课程", "usedBy": ["actions.deleteElectiveCourseAction"]}, - {"name": "openSelection", "file": "data-access.ts", "signature": "(courseId: string) => Promise", "purpose": "开放选课(status=open)", "usedBy": ["actions.openSelectionAction"]}, - {"name": "closeSelection", "file": "data-access.ts", "signature": "(courseId: string) => Promise", "purpose": "关闭选课(status=closed)", "usedBy": ["actions.closeSelectionAction"]}, - {"name": "getSubjectOptions", "file": "data-access.ts", "signature": "() => Promise<{id, name}[]>", "purpose": "获取学科选项(按 order, name 排序)", "usedBy": ["admin/elective/create/page.tsx", "admin/elective/[id]/edit/page.tsx"]}, - {"name": "getCourseSelections", "file": "data-access-selections.ts", "signature": "(courseId: string) => Promise", "purpose": "查询课程所有选课记录(按 priority, selectedAt 排序)", "usedBy": ["待扩展"]}, - {"name": "getStudentSelections", "file": "data-access-selections.ts", "signature": "(studentId: string) => Promise", "purpose": "查询学生选课记录(按 selectedAt 降序)", "usedBy": ["actions.getStudentSelectionsAction", "student/elective/page.tsx"]}, - {"name": "getStudentGradeId", "file": "data-access-selections.ts", "signature": "(studentId: string) => Promise", "purpose": "获取学生所在年级 ID(通过 classEnrollments active 记录)", "usedBy": ["data-access-selections.getAvailableCoursesForStudent"]}, - {"name": "getAvailableCoursesForStudent", "file": "data-access-selections.ts", "signature": "(studentId: string, gradeId?: string | null) => Promise", "purpose": "获取学生可选课程(status=open 且 gradeId 匹配或为空)", "usedBy": ["actions.getAvailableCoursesAction", "student/elective/page.tsx"]}, - {"name": "runLottery", "file": "data-access-operations.ts", "signature": "(courseId: string) => Promise<{enrolled: number, waitlist: number}>", "purpose": "抽签录取(随机打乱 selected 记录,前 capacity 名 enrolled,其余 waitlist,课程 status=closed)", "usedBy": ["actions.runLotteryAction"]}, - {"name": "selectCourse", "file": "data-access-operations.ts", "signature": "(courseId: string, studentId: string, priority?: number) => Promise<{status: CourseSelectionStatus, message: string}>", "purpose": "学生选课(校验课程状态/时间窗口/重复选课;FCFS 模式即时 enrolled/waitlist,lottery 模式 selected)", "usedBy": ["actions.selectCourseAction"]}, - {"name": "dropCourse", "file": "data-access-operations.ts", "signature": "(courseId: string, studentId: string) => Promise", "purpose": "学生退课(status=dropped;FCFS 模式自动递补 waitlist 首位)", "usedBy": ["actions.dropCourseAction"]} + { + "name": "getElectiveCourses", + "file": "data-access.ts", + "signature": "(params?: GetElectiveCoursesParams & { scope?: DataScope; currentUserId?: string }) => Promise", + "purpose": "查询选修课程列表(按 scope 行级过滤:owned/class_taught 按 teacherId,grade_managed 按 gradeIds)", + "usedBy": [ + "actions.getElectiveCoursesAction", + "admin/elective/page.tsx", + "teacher/elective/page.tsx" + ] + }, + { + "name": "getElectiveCourseById", + "file": "data-access.ts", + "signature": "(id: string) => Promise", + "purpose": "获取课程详情", + "usedBy": [ + "actions.updateElectiveCourseAction", + "admin/elective/[id]/edit/page.tsx" + ] + }, + { + "name": "createElectiveCourse", + "file": "data-access.ts", + "signature": "(data: CreateElectiveCourseInput, teacherId: string) => Promise", + "purpose": "创建选修课程(status=draft, enrolledCount=0)", + "usedBy": [ + "actions.createElectiveCourseAction" + ] + }, + { + "name": "updateElectiveCourse", + "file": "data-access.ts", + "signature": "(id: string, data: Partial) => Promise", + "purpose": "更新选修课程字段", + "usedBy": [ + "actions.updateElectiveCourseAction" + ] + }, + { + "name": "deleteElectiveCourse", + "file": "data-access.ts", + "signature": "(id: string) => Promise", + "purpose": "删除选修课程", + "usedBy": [ + "actions.deleteElectiveCourseAction" + ] + }, + { + "name": "openSelection", + "file": "data-access.ts", + "signature": "(courseId: string) => Promise", + "purpose": "开放选课(status=open)", + "usedBy": [ + "actions.openSelectionAction" + ] + }, + { + "name": "closeSelection", + "file": "data-access.ts", + "signature": "(courseId: string) => Promise", + "purpose": "关闭选课(status=closed)", + "usedBy": [ + "actions.closeSelectionAction" + ] + }, + { + "name": "getSubjectOptions", + "file": "data-access.ts", + "signature": "() => Promise<{id, name}[]>", + "purpose": "获取学科选项(按 order, name 排序)", + "usedBy": [ + "admin/elective/create/page.tsx", + "admin/elective/[id]/edit/page.tsx" + ] + }, + { + "name": "getCourseSelections", + "file": "data-access-selections.ts", + "signature": "(courseId: string) => Promise", + "purpose": "查询课程所有选课记录(按 priority, selectedAt 排序)", + "usedBy": [ + "待扩展" + ] + }, + { + "name": "getStudentSelections", + "file": "data-access-selections.ts", + "signature": "(studentId: string) => Promise", + "purpose": "查询学生选课记录(按 selectedAt 降序)", + "usedBy": [ + "actions.getStudentSelectionsAction", + "student/elective/page.tsx" + ] + }, + { + "name": "getStudentGradeId", + "file": "data-access-selections.ts", + "signature": "(studentId: string) => Promise", + "purpose": "获取学生所在年级 ID(通过 classEnrollments active 记录)", + "usedBy": [ + "data-access-selections.getAvailableCoursesForStudent" + ] + }, + { + "name": "getAvailableCoursesForStudent", + "file": "data-access-selections.ts", + "signature": "(studentId: string, gradeId?: string | null) => Promise", + "purpose": "获取学生可选课程(status=open 且 gradeId 匹配或为空)", + "usedBy": [ + "actions.getAvailableCoursesAction", + "student/elective/page.tsx" + ] + }, + { + "name": "runLottery", + "file": "data-access-operations.ts", + "signature": "(courseId: string) => Promise<{enrolled: number, waitlist: number}>", + "purpose": "抽签录取(随机打乱 selected 记录,前 capacity 名 enrolled,其余 waitlist,课程 status=closed)", + "usedBy": [ + "actions.runLotteryAction" + ] + }, + { + "name": "selectCourse", + "file": "data-access-operations.ts", + "signature": "(courseId: string, studentId: string, priority?: number) => Promise<{status: CourseSelectionStatus, message: string}>", + "purpose": "学生选课(校验课程状态/时间窗口/重复选课;FCFS 模式即时 enrolled/waitlist,lottery 模式 selected)", + "usedBy": [ + "actions.selectCourseAction" + ] + }, + { + "name": "dropCourse", + "file": "data-access-operations.ts", + "signature": "(courseId: string, studentId: string) => Promise", + "purpose": "学生退课(status=dropped;FCFS 模式自动递补 waitlist 首位)", + "usedBy": [ + "actions.dropCourseAction" + ] + } ], "types": [ - {"name": "ElectiveCourseStatus", "type": "type", "file": "types.ts", "definition": "\"draft\" | \"open\" | \"closed\" | \"cancelled\""}, - {"name": "ElectiveSelectionMode", "type": "type", "file": "types.ts", "definition": "\"fcfs\" | \"lottery\""}, - {"name": "CourseSelectionStatus", "type": "type", "file": "types.ts", "definition": "\"selected\" | \"enrolled\" | \"waitlist\" | \"dropped\" | \"rejected\""}, - {"name": "ElectiveCourse", "type": "interface", "file": "types.ts", "definition": "{ id, name, subjectId?, teacherId, gradeId?, description?, capacity, enrolledCount, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, status, selectionMode, credit, createdAt, updatedAt }"}, - {"name": "ElectiveCourseWithDetails", "type": "interface", "file": "types.ts", "definition": "ElectiveCourse & { teacherName?, subjectName?, gradeName? }"}, - {"name": "CourseSelection", "type": "interface", "file": "types.ts", "definition": "{ id, courseId, studentId, status, priority?, selectedAt, enrolledAt?, droppedAt?, lotteryRank?, createdAt, updatedAt }"}, - {"name": "CourseSelectionWithDetails", "type": "interface", "file": "types.ts", "definition": "CourseSelection & { courseName?, studentName?, courseCapacity?, courseEnrolledCount?, courseStatus? }"}, - {"name": "GetElectiveCoursesParams", "type": "interface", "file": "types.ts", "definition": "{ status?, gradeId?, subjectId?, teacherId? }"}, - {"name": "ELECTIVE_STATUS_LABELS", "type": "const", "file": "types.ts", "description": "课程状态标签常量"}, - {"name": "ELECTIVE_STATUS_COLORS", "type": "const", "file": "types.ts", "description": "课程状态颜色常量(Badge variant)"}, - {"name": "SELECTION_MODE_LABELS", "type": "const", "file": "types.ts", "description": "选课模式标签常量"}, - {"name": "COURSE_SELECTION_STATUS_LABELS", "type": "const", "file": "types.ts", "description": "选课状态标签常量"}, - {"name": "COURSE_SELECTION_STATUS_COLORS", "type": "const", "file": "types.ts", "description": "选课状态颜色常量(Badge variant)"} + { + "name": "ElectiveCourseStatus", + "type": "type", + "file": "types.ts", + "definition": "\"draft\" | \"open\" | \"closed\" | \"cancelled\"" + }, + { + "name": "ElectiveSelectionMode", + "type": "type", + "file": "types.ts", + "definition": "\"fcfs\" | \"lottery\"" + }, + { + "name": "CourseSelectionStatus", + "type": "type", + "file": "types.ts", + "definition": "\"selected\" | \"enrolled\" | \"waitlist\" | \"dropped\" | \"rejected\"" + }, + { + "name": "ElectiveCourse", + "type": "interface", + "file": "types.ts", + "definition": "{ id, name, subjectId?, teacherId, gradeId?, description?, capacity, enrolledCount, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, status, selectionMode, credit, createdAt, updatedAt }" + }, + { + "name": "ElectiveCourseWithDetails", + "type": "interface", + "file": "types.ts", + "definition": "ElectiveCourse & { teacherName?, subjectName?, gradeName? }" + }, + { + "name": "CourseSelection", + "type": "interface", + "file": "types.ts", + "definition": "{ id, courseId, studentId, status, priority?, selectedAt, enrolledAt?, droppedAt?, lotteryRank?, createdAt, updatedAt }" + }, + { + "name": "CourseSelectionWithDetails", + "type": "interface", + "file": "types.ts", + "definition": "CourseSelection & { courseName?, studentName?, courseCapacity?, courseEnrolledCount?, courseStatus? }" + }, + { + "name": "GetElectiveCoursesParams", + "type": "interface", + "file": "types.ts", + "definition": "{ status?, gradeId?, subjectId?, teacherId? }" + }, + { + "name": "ELECTIVE_STATUS_LABELS", + "type": "const", + "file": "types.ts", + "description": "课程状态标签常量" + }, + { + "name": "ELECTIVE_STATUS_COLORS", + "type": "const", + "file": "types.ts", + "description": "课程状态颜色常量(Badge variant)" + }, + { + "name": "SELECTION_MODE_LABELS", + "type": "const", + "file": "types.ts", + "description": "选课模式标签常量" + }, + { + "name": "COURSE_SELECTION_STATUS_LABELS", + "type": "const", + "file": "types.ts", + "description": "选课状态标签常量" + }, + { + "name": "COURSE_SELECTION_STATUS_COLORS", + "type": "const", + "file": "types.ts", + "description": "选课状态颜色常量(Badge variant)" + } ], "schemas": [ - {"name": "ElectiveCourseStatusEnum", "file": "schema.ts", "definition": "z.enum([\"draft\",\"open\",\"closed\",\"cancelled\"])"}, - {"name": "ElectiveSelectionModeEnum", "file": "schema.ts", "definition": "z.enum([\"fcfs\",\"lottery\"])"}, - {"name": "CourseSelectionStatusEnum", "file": "schema.ts", "definition": "z.enum([\"selected\",\"enrolled\",\"waitlist\",\"dropped\",\"rejected\"])"}, - {"name": "CreateElectiveCourseSchema", "file": "schema.ts", "purpose": "创建课程校验(name 必填,teacherId 必填,capacity 1-500 默认 30,selectionMode 默认 fcfs,credit 默认 1.0)"}, - {"name": "UpdateElectiveCourseSchema", "file": "schema.ts", "purpose": "更新课程校验(所有字段可选,含 status)"}, - {"name": "SelectCourseSchema", "file": "schema.ts", "purpose": "选课校验(courseId 必填,priority 1-10 可选)"}, - {"name": "DropCourseSchema", "file": "schema.ts", "purpose": "退课校验(courseId 必填)"}, - {"name": "RunLotterySchema", "file": "schema.ts", "purpose": "抽签校验(courseId 必填)"} + { + "name": "ElectiveCourseStatusEnum", + "file": "schema.ts", + "definition": "z.enum([\"draft\",\"open\",\"closed\",\"cancelled\"])" + }, + { + "name": "ElectiveSelectionModeEnum", + "file": "schema.ts", + "definition": "z.enum([\"fcfs\",\"lottery\"])" + }, + { + "name": "CourseSelectionStatusEnum", + "file": "schema.ts", + "definition": "z.enum([\"selected\",\"enrolled\",\"waitlist\",\"dropped\",\"rejected\"])" + }, + { + "name": "CreateElectiveCourseSchema", + "file": "schema.ts", + "purpose": "创建课程校验(name 必填,teacherId 必填,capacity 1-500 默认 30,selectionMode 默认 fcfs,credit 默认 1.0)" + }, + { + "name": "UpdateElectiveCourseSchema", + "file": "schema.ts", + "purpose": "更新课程校验(所有字段可选,含 status)" + }, + { + "name": "SelectCourseSchema", + "file": "schema.ts", + "purpose": "选课校验(courseId 必填,priority 1-10 可选)" + }, + { + "name": "DropCourseSchema", + "file": "schema.ts", + "purpose": "退课校验(courseId 必填)" + }, + { + "name": "RunLotterySchema", + "file": "schema.ts", + "purpose": "抽签校验(courseId 必填)" + } ], "components": [ - {"name": "ElectiveCourseList", "file": "components/elective-course-list.tsx", "purpose": "课程卡片列表(管理员/教师视图,含编辑/开放/关闭/抽签/删除操作按钮,usePermission 控制权限)", "deps": ["usePermission", "actions.deleteElectiveCourseAction", "actions.openSelectionAction", "actions.closeSelectionAction", "actions.runLotteryAction", "shared/components/ui/*"]}, - {"name": "ElectiveCourseForm", "file": "components/elective-course-form.tsx", "purpose": "课程创建/编辑表单(name, subjectId, teacherId, gradeId, description, capacity, classroom, schedule, dates, selectionMode, credit)", "deps": ["react-hook-form", "actions.createElectiveCourseAction", "actions.updateElectiveCourseAction", "shared/components/ui/*"]}, - {"name": "StudentSelectionView", "file": "components/student-selection-view.tsx", "purpose": "学生选课视图(可选课程列表 + 我的选课记录,含选课/退课按钮)", "deps": ["usePermission", "actions.selectCourseAction", "actions.dropCourseAction", "shared/components/ui/*"]} + { + "name": "ElectiveCourseList", + "file": "components/elective-course-list.tsx", + "purpose": "课程卡片列表(管理员/教师视图,含编辑/开放/关闭/抽签/删除操作按钮,usePermission 控制权限)", + "deps": [ + "usePermission", + "actions.deleteElectiveCourseAction", + "actions.openSelectionAction", + "actions.closeSelectionAction", + "actions.runLotteryAction", + "shared/components/ui/*" + ] + }, + { + "name": "ElectiveCourseForm", + "file": "components/elective-course-form.tsx", + "purpose": "课程创建/编辑表单(name, subjectId, teacherId, gradeId, description, capacity, classroom, schedule, dates, selectionMode, credit)", + "deps": [ + "react-hook-form", + "actions.createElectiveCourseAction", + "actions.updateElectiveCourseAction", + "shared/components/ui/*" + ] + }, + { + "name": "StudentSelectionView", + "file": "components/student-selection-view.tsx", + "purpose": "学生选课视图(可选课程列表 + 我的选课记录,含选课/退课按钮)", + "deps": [ + "usePermission", + "actions.selectCourseAction", + "actions.dropCourseAction", + "shared/components/ui/*" + ] + } ] } } }, - "dependencyMatrix": { - "shared": {"dependsOn": []}, - "auth": {"dependsOn": ["shared"], "uses": {"shared": ["db", "schema", "permissions"]}}, - "exams": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard", "types", "ai"], "auth": ["auth"]}}, - "homework": {"dependsOn": ["shared", "auth", "exams"], "uses": {"shared": ["db", "auth-guard", "types"], "auth": ["auth"], "exams": ["data-access.getExams"]}}, - "questions": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard", "types"], "auth": ["auth"]}}, - "textbooks": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard", "types"], "auth": ["auth"]}}, - "classes": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard", "types"], "auth": ["auth"]}}, - "school": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard", "types"], "auth": ["auth"]}}, - "dashboard": {"dependsOn": ["shared", "auth", "homework", "classes"], "uses": {"shared": ["db", "types"], "auth": ["auth"], "homework": ["data-access.getTeacherGradeTrends", "data-access.getStudentDashboardGrades"], "classes": ["data-access.getTeacherClasses", "data-access.getStudentClasses", "data-access.getStudentSchedule"]}}, - "layout": {"dependsOn": ["shared", "auth", "messaging"], "uses": {"shared": ["hooks.usePermission", "components.global-search.GlobalSearch"], "auth": ["useSession"], "messaging": ["components.notification-dropdown"]}}, - "settings": {"dependsOn": ["shared", "auth", "messaging"], "uses": {"shared": ["db", "auth-guard", "ai", "types", "components.ui.switch"], "auth": ["auth"], "messaging": ["notification-preferences.getNotificationPreferences", "actions.getNotificationPreferencesAction", "actions.updateNotificationPreferencesAction"]}}, - "users": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requireAuth", "auth-guard.requirePermission", "db.schema.users", "db.schema.roles", "db.schema.usersToRoles", "db.schema.classes", "db.schema.classEnrollments", "types.permissions", "types.action-state", "lib.excel"], "auth": ["auth"]}}, - "audit": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.auditLogs", "db.schema.loginLogs", "db.schema.dataChangeLogs", "types.permissions", "lib.excel"], "auth": ["auth"]}}, - "announcements": {"dependsOn": ["shared", "auth", "school"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.requireAuth", "db.schema.announcements", "types.permissions"], "auth": ["auth"], "school": ["data-access.getGrades"]}}, - "files": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requireAuth", "auth-guard.requirePermission", "types.permissions", "lib.file-storage", "lib.storage-provider"], "auth": ["auth"]}}, - "course-plans": {"dependsOn": ["shared", "auth", "school", "classes"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.coursePlans", "db.schema.coursePlanItems", "db.schema.classes", "db.schema.subjects", "db.schema.users", "types.permissions", "types.action-state"], "auth": ["auth"], "school": ["data-access.getAcademicYears"], "classes": ["data-access.getAdminClasses", "data-access.getStaffOptions"]}}, - "grades": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "types.permissions", "types.action-state", "db.schema.gradeRecords", "db.schema.classes", "db.schema.classEnrollments", "db.schema.subjects", "db.schema.users", "lib.excel"], "auth": ["auth"]}}, - "parent": {"dependsOn": ["shared", "auth", "homework", "classes", "grades"], "uses": {"shared": ["db", "auth-guard.requireAuth", "db.schema.parentStudentRelations", "db.schema.users", "db.schema.grades", "db.schema.classEnrollments", "db.schema.classes", "types"], "auth": ["auth"], "homework": ["data-access.getStudentHomeworkAssignments", "data-access.getStudentDashboardGrades"], "classes": ["data-access.getStudentClasses", "data-access.getStudentSchedule"], "grades": ["data-access.getStudentGradeSummary"]}}, - "messaging": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.requireAuth", "db.schema.messages", "db.schema.messageNotifications", "db.schema.notificationPreferences", "db.schema.users", "db.schema.classEnrollments", "db.schema.classes", "db.schema.grades", "types.permissions", "types.action-state"], "auth": ["auth"]}}, - "notifications": {"dependsOn": ["shared", "auth", "messaging"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.users", "db.schema.classEnrollments", "db.schema.classes", "types.permissions", "types.action-state"], "auth": ["auth"], "messaging": ["notification-preferences.getNotificationPreferences", "data-access.createNotification"]}}, - "attendance": {"dependsOn": ["shared", "auth", "classes"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.attendanceRecords", "db.schema.attendanceRules", "db.schema.classEnrollments", "db.schema.users", "db.schema.classes", "types.permissions", "types.action-state", "types.DataScope"], "auth": ["auth"], "classes": ["data-access.getTeacherClasses", "data-access.getAdminClasses"]}}, - "scheduling": {"dependsOn": ["shared", "auth", "classes"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.schedulingRules", "db.schema.scheduleChanges", "db.schema.classSchedule", "db.schema.classes", "db.schema.users", "db.schema.classSubjectTeachers", "db.schema.subjects", "db.schema.classrooms", "types.permissions", "types.action-state"], "auth": ["auth"], "classes": []}}, - "diagnostic": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.knowledgePointMastery", "db.schema.learningDiagnosticReports", "db.schema.knowledgePoints", "db.schema.questionsToKnowledgePoints", "db.schema.examSubmissions", "db.schema.submissionAnswers", "db.schema.classEnrollments", "db.schema.classes", "db.schema.users", "types.permissions", "types.action-state", "hooks.usePermission", "components.ui.*"], "auth": ["auth"]}}, - "elective": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.electiveCourses", "db.schema.courseSelections", "db.schema.users", "db.schema.subjects", "db.schema.grades", "db.schema.classes", "db.schema.classEnrollments", "types.permissions", "types.action-state", "types.DataScope", "hooks.usePermission", "components.ui.*"], "auth": ["auth"]}} - }, - "parameterFlowChains": { - "userId": { - "origin": "auth.ts JWT callback 从 users 表查询", - "flow": [ - "auth.ts → session.user.id (JWT存储)", - "session.user.id → Server Components (通过auth())", - "session.user.id → Client Components (通过useSession())", - "getAuthContext().userId → 所有Server Actions", - "auth-guard.ts → 查询 usersToRoles (获取角色)", - "auth-guard.ts → 查询 classSubjectTeachers/grades (获取DataScope)", - "exams/actions.ts → 作为 creatorId 写入 exams 表", - "homework/actions.ts → 作为 creatorId 写入 homeworkAssignments 表", - "classes/data-access.ts → getTeacherClasses(teacherId), getGradeManagedClasses(userId)", - "grades/actions.ts → 作为 recordedBy 写入 gradeRecords 表", - "attendance/actions.ts → 作为 recordedBy 写入 attendanceRecords 表", - "elective/actions.ts → 作为 teacherId 默认值写入 electiveCourses 表(createElectiveCourseAction)", - "elective/actions.ts → 作为 studentId 查询学生选课(selectCourseAction/dropCourseAction/getStudentSelectionsAction)", - "elective/data-access.ts → getElectiveCourses({ scope, currentUserId }) 按 teacherId 过滤(class_taught/owned)" - ] + "dbTables": { + "_meta": { + "total": 54, + "orm": "Drizzle ORM 0.45", + "database": "MySQL", + "idStrategy": "CUID2 (varchar length 128)", + "source": "src/shared/db/schema.ts" }, - "examId": { - "origin": "exams/actions.ts createExamAction 产生 (CUID2)", - "flow": [ - "createExamAction → 写入 exams.id", - "exams/data-access.getExamById(id) → 读取考试详情", - "updateExamAction/deleteExamAction/duplicateExamAction → 定位考试", - "homework/actions.ts createHomeworkAssignmentAction → sourceExamId 参数", - "homeworkAssignments.sourceExamId → 外键关联源考试", - "homework/data-access.getHomeworkAssignmentAnalytics → 追溯作业来源" - ] + "usersAndAuth": { + "description": "用户与认证 (Auth.js v5 + RBAC)", + "tables": { + "users": { + "owner": "users", + "description": "用户主表(含未成年人信息保护字段)" + }, + "accounts": { + "owner": "auth", + "description": "OAuth 账户" + }, + "sessions": { + "owner": "auth", + "description": "Auth.js 会话" + }, + "verificationTokens": { + "owner": "auth", + "description": "验证令牌" + }, + "roles": { + "owner": "auth", + "description": "角色(admin/teacher/student/parent/grade_head/teaching_head)" + }, + "usersToRoles": { + "owner": "auth", + "description": "用户-角色多对多" + }, + "rolePermissions": { + "owner": "auth", + "description": "角色-权限(细粒度 RBAC)" + }, + "passwordSecurity": { + "owner": "auth", + "description": "密码安全策略(失败次数/锁定/必须改密)" + } + } }, - "classId": { - "origin": "classes/actions.ts createTeacherClassAction/createAdminClassAction 产生", - "flow": [ - "createTeacherClassAction → 写入 classes.id", - "classes/data-access.getClassStudents(classId) → 学生列表", - "classes/data-access.getClassSchedule(classId) → 课表", - "classes/data-access.getClassHomeworkInsights(classId) → 作业洞察", - "homework/data-access.getHomeworkAssignments({ classId }) → 过滤作业", - "auth-guard.ts → classSubjectTeachers 查询 → DataScope.class_taught.classIds", - "grades/data-access.getGradeRecords({ classId }) → 过滤成绩列表", - "grades/data-access.getClassGradeStats(classId) → 班级成绩统计", - "grades/data-access.getClassRanking(classId) → 班级排名", - "attendance/data-access.getAttendanceRecords({ classId }) → 过滤考勤记录", - "attendance/data-access.getClassAttendanceForDate(classId, date) → 班级指定日期考勤", - "attendance/data-access-stats.getClassAttendanceStats(classId) → 班级考勤统计" - ] + "knowledgeStructure": { + "description": "知识点树结构", + "tables": { + "knowledgePoints": { + "owner": "textbooks", + "description": "知识点(树结构,parentId 自引用)" + } + } }, - "permission": { - "origin": "shared/types/permissions.ts Permissions 常量定义(50 个权限点,含 FILE_UPLOAD/FILE_READ/FILE_DELETE、GRADE_RECORD_MANAGE/GRADE_RECORD_READ、ATTENDANCE_MANAGE/ATTENDANCE_READ、MESSAGE_SEND/MESSAGE_READ/MESSAGE_DELETE、SCHEDULE_AUTO/SCHEDULE_ADJUST、ELECTIVE_MANAGE/ELECTIVE_READ/ELECTIVE_SELECT)", - "flow": [ - "shared/lib/permissions.ts ROLE_PERMISSIONS → 角色到权限映射(admin/teacher 拥有全部 FILE_* 及 GRADE_RECORD_MANAGE/READ,student/parent/grade_head/teaching_head 拥有 GRADE_RECORD_READ;admin/teacher 拥有 ATTENDANCE_MANAGE+ATTENDANCE_READ,student/parent/grade_head/teaching_head 拥有 ATTENDANCE_READ;admin/teacher/parent/grade_head/teaching_head 拥有 MESSAGE_SEND/READ/DELETE,student 拥有 MESSAGE_READ/DELETE 但无 MESSAGE_SEND;admin 拥有 SCHEDULE_AUTO+SCHEDULE_ADJUST,teacher 无排课权限;admin/teacher 拥有 ELECTIVE_MANAGE+ELECTIVE_READ,student 拥有 ELECTIVE_SELECT+ELECTIVE_READ,grade_head/teaching_head 拥有 ELECTIVE_READ)", - "auth.ts JWT callback → resolvePermissions(roleNames) → token.permissions", - "proxy.ts middleware → token.permissions → 路由权限检查", - "auth-guard.ts requirePermission(permission) → Server Action权限断言(如 /api/files/[id] DELETE 使用 FILE_DELETE;grades/actions.ts 使用 GRADE_RECORD_MANAGE/READ;messaging/actions.ts 使用 MESSAGE_SEND/READ/DELETE;attendance/actions.ts 使用 ATTENDANCE_MANAGE/READ;scheduling/actions.ts 使用 SCHEDULE_AUTO/SCHEDULE_ADJUST;elective/actions.ts 使用 ELECTIVE_MANAGE/READ/SELECT)", - "auth-guard.ts requireAuth() → 仅校验登录(如 /api/upload POST、/api/files/[id] GET、messaging 通知读取 actions)", - "use-permission.ts hasPermission(permission) → 客户端条件渲染(如 file-list.tsx 删除按钮可见性;message-list/detail.tsx 写消息/删除按钮可见性;elective-course-list.tsx 操作按钮可见性)", - "layout/config/navigation.ts NavItem.permission → 侧边栏菜单过滤(Grades 菜单项使用 GRADE_RECORD_READ;Messages 菜单项使用 MESSAGE_READ;Attendance 菜单项 teacher 使用 ATTENDANCE_MANAGE,student/parent 使用 ATTENDANCE_READ;Scheduling 菜单项 admin 使用 SCHEDULE_ADJUST/SCHEDULE_AUTO,teacher Schedule Changes 使用 SCHEDULE_ADJUST;Electives 菜单项 admin/teacher 使用 ELECTIVE_MANAGE,student 使用 ELECTIVE_SELECT)" - ] + "questionBank": { + "description": "题库核心", + "tables": { + "questions": { + "owner": "questions", + "description": "题目(content JSON,支持复合题 parentId 自引用)" + }, + "questionsToKnowledgePoints": { + "owner": "questions", + "description": "题目-知识点多对多" + } + } }, - "dataScope": { - "origin": "auth-guard.ts resolveDataScope(userId, roles) 动态计算", - "flow": [ - "resolveDataScope → AuthContext.dataScope", - "exams/data-access.getExams({ scope }) → 行级过滤", - "homework/data-access.getHomeworkAssignments({ scope }) → 行级过滤", - "dashboard/data-access.getAdminDashboardData(scope) → 统计过滤", - "exams/actions.ts update/delete → scope.type !== 'all' 时校验资源归属", - "grades/data-access.getGradeRecords({ scope }) → 行级过滤(class_taught 限制所教班级,class_members 限制学生本人,children 限制子女)", - "attendance/data-access.getAttendanceRecords({ scope }) → 行级过滤(class_taught 按教师班级过滤,children 按子女过滤,class_members 仅查自己,all 查全部)", - "attendance/actions.ts getStudentAttendanceAction → 对 class_members/children 进行 DataScope 二次校验", - "elective/data-access.getElectiveCourses({ scope, currentUserId }) → 行级过滤(owned/class_taught 按 teacherId 过滤,grade_managed 按 gradeIds 过滤,class_members/children 返回 null 由 getAvailableCoursesForStudent 处理)", - "elective/actions.ts getStudentSelectionsAction → 对 class_members/children 进行 DataScope 二次校验" - ] + "academicStructure": { + "description": "教学结构", + "tables": { + "subjects": { + "owner": "school", + "description": "科目" + }, + "textbooks": { + "owner": "textbooks", + "description": "教材" + }, + "chapters": { + "owner": "textbooks", + "description": "章节(支持嵌套 parentId)" + } + } + }, + "schoolManagement": { + "description": "学校管理", + "tables": { + "departments": { + "owner": "school", + "description": "部门" + }, + "classrooms": { + "owner": "school", + "description": "教室" + }, + "academicYears": { + "owner": "school", + "description": "学年" + }, + "schools": { + "owner": "school", + "description": "学校" + }, + "grades": { + "owner": "school", + "description": "年级(含 gradeHeadId/teachingHeadId)" + } + } + }, + "classes": { + "description": "班级/选课/课表", + "tables": { + "classes": { + "owner": "classes", + "description": "班级" + }, + "classSubjectTeachers": { + "owner": "classes", + "description": "班级-科目-教师" + }, + "classEnrollments": { + "owner": "classes", + "description": "班级选课(active/inactive)" + }, + "classSchedule": { + "owner": "scheduling", + "description": "课表(注意:三处写入口,见 knownIssues P0-6)" + } + } + }, + "exams": { + "description": "考试与提交", + "tables": { + "exams": { + "owner": "exams", + "description": "考试(含 examMode: homework/timed/proctored)" + }, + "examQuestions": { + "owner": "exams", + "description": "考试-题目多对多" + }, + "examSubmissions": { + "owner": "exams", + "description": "考试提交(started/submitted/graded)" + }, + "submissionAnswers": { + "owner": "exams", + "description": "提交答案" + } + } + }, + "homework": { + "description": "作业", + "tables": { + "homeworkAssignments": { + "owner": "homework", + "description": "作业(关联 sourceExamId)" + }, + "homeworkAssignmentQuestions": { + "owner": "homework", + "description": "作业-题目" + }, + "homeworkAssignmentTargets": { + "owner": "homework", + "description": "作业目标学生" + }, + "homeworkSubmissions": { + "owner": "homework", + "description": "作业提交(含 attemptNo/isLate)" + }, + "homeworkAnswers": { + "owner": "homework", + "description": "作业答案" + } + } + }, + "ai": { + "description": "AI 配置", + "tables": { + "aiProviders": { + "owner": "settings", + "description": "AI Provider(zhipu/openai/gemini/custom,apiKey 加密)" + } + } + }, + "announcements": { + "description": "公告", + "tables": { + "announcements": { + "owner": "announcements", + "description": "公告(school/grade/class,draft/published/archived)" + } + } + }, + "auditLogs": { + "description": "审计与登录日志", + "tables": { + "auditLogs": { + "owner": "audit", + "description": "审计日志(success/failure)" + }, + "loginLogs": { + "owner": "audit", + "description": "登录日志(signin/signout/signup)" + }, + "dataChangeLogs": { + "owner": "audit", + "description": "数据变更日志(create/update/delete)" + } + } + }, + "gradeRecords": { + "description": "成绩录入", + "tables": { + "gradeRecords": { + "owner": "grades", + "description": "成绩记录(exam/quiz/homework/other)" + } + } + }, + "files": { + "description": "文件附件", + "tables": { + "fileAttachments": { + "owner": "files", + "description": "文件附件(含 storagePath/url)" + } + } + }, + "coursePlans": { + "description": "课程计划", + "tables": { + "coursePlans": { + "owner": "course-plans", + "description": "课程计划(planning/active/completed/paused)" + }, + "coursePlanItems": { + "owner": "course-plans", + "description": "课程计划项(按周)" + } + } + }, + "messaging": { + "description": "消息与通知", + "tables": { + "messages": { + "owner": "messaging", + "description": "站内消息(含回复链 parentMessageId)" + }, + "messageNotifications": { + "owner": "notifications", + "description": "消息通知" + }, + "notificationPreferences": { + "owner": "notifications", + "description": "通知偏好(email/sms/push + 分类开关)" + } + } + }, + "parentStudent": { + "description": "家长-子女关联", + "tables": { + "parentStudentRelations": { + "owner": "parent", + "description": "家长-子女关系" + } + } + }, + "attendance": { + "description": "考勤", + "tables": { + "attendanceRecords": { + "owner": "attendance", + "description": "考勤记录(present/absent/late/early_leave/excused)" + }, + "attendanceRules": { + "owner": "attendance", + "description": "考勤规则(迟到/早退阈值)" + } + } + }, + "scheduling": { + "description": "排课", + "tables": { + "schedulingRules": { + "owner": "scheduling", + "description": "排课规则(每日最大课时/连续课时/午休等)" + }, + "scheduleChanges": { + "owner": "scheduling", + "description": "调课/代课申请(pending/approved/rejected/completed)" + } + } + }, + "elective": { + "description": "选课管理 (P2)", + "tables": { + "electiveCourses": { + "owner": "elective", + "description": "选修课程(draft/open/closed/cancelled,fcfs/lottery)" + }, + "courseSelections": { + "owner": "elective", + "description": "选课记录(selected/enrolled/waitlist/dropped/rejected)" + } + } + }, + "proctoring": { + "description": "考试监考 (P2)", + "tables": { + "examProctoringEvents": { + "owner": "proctoring", + "description": "监考事件(tab_switch/window_blur/copy_attempt 等)" + } + } + }, + "diagnostic": { + "description": "学情诊断 (P2)", + "tables": { + "knowledgePointMastery": { + "owner": "diagnostic", + "description": "知识点掌握度(学生-知识点)" + }, + "learningDiagnosticReports": { + "owner": "diagnostic", + "description": "学情诊断报告(individual/class/grade)" + } + } } }, - "routes": { + "dependencyMatrix": { + "shared": { + "dependsOn": [] + }, "auth": { - "/login": {"component": "LoginForm", "type": "client", "module": "auth"}, - "/register": {"component": "RegisterForm + registerAction", "type": "server", "module": "auth", "description": "注册页面(含未成年人信息保护、隐私政策/用户协议同意勾选)"}, - "/privacy": {"component": "PrivacyPage", "type": "server", "module": "auth", "description": "隐私政策页面(信息收集/使用/保护、用户权利、Cookie、未成年人保护条款、联系方式)"}, - "/terms": {"component": "TermsPage", "type": "server", "module": "auth", "description": "用户协议页面(服务说明、注册、行为规范、知识产权、免责、变更终止、法律适用)"} + "dependsOn": [ + "shared" + ], + "uses": { + "shared": [ + "db", + "schema", + "permissions" + ] + } }, - "admin": { - "/admin/dashboard": {"component": "AdminDashboardView", "type": "server", "dataAccess": ["dashboard/data-access.getAdminDashboardData"], "permission": "school:manage"}, - "/admin/school": {"component": "重定向", "type": "server", "redirect": "/admin/school/classes", "permission": "school:manage"}, - "/admin/school/schools": {"component": "SchoolsClient", "type": "client", "module": "school", "permission": "school:manage"}, - "/admin/school/grades": {"component": "GradesClient", "type": "client", "module": "school", "permission": "grade:manage"}, - "/admin/school/grades/insights": {"component": "年级作业洞察", "type": "server", "dataAccess": ["classes/data-access.getGradeHomeworkInsights"], "permission": "grade:manage"}, - "/admin/school/departments": {"component": "DepartmentsClient", "type": "client", "module": "school", "permission": "school:manage"}, - "/admin/school/classes": {"component": "AdminClassesClient", "type": "client", "module": "classes", "permission": "school:manage"}, - "/admin/school/academic-year": {"component": "AcademicYearClient", "type": "client", "module": "school", "permission": "school:manage"}, - "/admin/audit-logs": {"component": "AuditLogView", "type": "server", "module": "audit", "dataAccess": ["audit/data-access.getAuditLogs", "audit/data-access.getAuditModuleOptions"], "permission": "audit_log:read"}, - "/admin/audit-logs/login-logs": {"component": "LoginLogView", "type": "server", "module": "audit", "dataAccess": ["audit/data-access.getLoginLogs"], "permission": "audit_log:read"}, - "/admin/announcements": {"component": "AdminAnnouncementsView", "type": "server", "module": "announcements", "dataAccess": ["announcements/data-access.getAnnouncements", "school/data-access.getGrades"], "actions": ["createAnnouncementAction"], "permission": "announcement:manage"}, - "/admin/announcements/[id]": {"component": "AnnouncementForm (edit)", "type": "server", "module": "announcements", "dataAccess": ["announcements/data-access.getAnnouncementById", "school/data-access.getGrades"], "actions": ["updateAnnouncementAction"], "permission": "announcement:manage"}, - "/admin/files": {"component": "AdminFilesView", "type": "server", "module": "files", "dataAccess": ["files/data-access.getAllFileAttachments"], "permission": "file:read"}, - "/admin/course-plans": {"component": "CoursePlanList", "type": "client", "module": "course-plans", "dataAccess": ["course-plans/data-access.getCoursePlans"], "permission": "course_plan:manage"}, - "/admin/course-plans/create": {"component": "CoursePlanForm (create)", "type": "client", "module": "course-plans", "actions": ["createCoursePlanAction"], "dataAccess": ["classes/data-access.getAdminClasses", "course-plans/data-access.getSubjectOptions", "classes/data-access.getStaffOptions", "school/data-access.getAcademicYears"], "permission": "course_plan:manage"}, - "/admin/course-plans/[id]": {"component": "CoursePlanDetail", "type": "client", "module": "course-plans", "dataAccess": ["course-plans/data-access.getCoursePlanById"], "actions": ["deleteCoursePlanAction", "createCoursePlanItemAction", "updateCoursePlanItemAction", "deleteCoursePlanItemAction", "toggleCoursePlanItemCompletedAction"], "permission": "course_plan:manage"}, - "/admin/course-plans/[id]/edit": {"component": "CoursePlanForm (edit)", "type": "client", "module": "course-plans", "actions": ["updateCoursePlanAction"], "dataAccess": ["course-plans/data-access.getCoursePlanById", "classes/data-access.getAdminClasses", "course-plans/data-access.getSubjectOptions", "classes/data-access.getStaffOptions", "school/data-access.getAcademicYears"], "permission": "course_plan:manage"}, - "/admin/attendance": {"component": "AttendanceRecordList", "type": "server", "module": "attendance", "dataAccess": ["attendance/data-access.getAttendanceRecords (scope=all)", "classes/data-access.getAdminClasses"], "permission": "attendance:manage", "description": "管理员考勤总览(权限:requirePermission(ATTENDANCE_MANAGE))"}, - "/admin/users/import": {"component": "UserImportPage (含 UserImportDialog)", "type": "server", "module": "users", "actions": ["users/actions.downloadUserTemplateAction", "users/actions.importUsersAction"], "permission": "user:manage", "description": "用户批量导入页面(说明卡片+字段文档表+导入对话框;权限:requirePermission(USER_MANAGE))"}, - "/admin/scheduling/rules": {"component": "SchedulingRulesForm", "type": "server", "module": "scheduling", "dataAccess": ["scheduling/actions.getAdminClassesForScheduling", "scheduling/actions.getSchedulingRules"], "actions": ["saveSchedulingRulesAction"], "permission": "schedule:adjust", "description": "排课规则配置页面(权限:requirePermission(SCHEDULE_ADJUST))"}, - "/admin/scheduling/auto": {"component": "AutoSchedulePanel + AutoScheduleResultView", "type": "server", "module": "scheduling", "dataAccess": ["scheduling/actions.getAdminClassesForScheduling"], "actions": ["autoScheduleAction", "applyAutoScheduleAction"], "permission": "schedule:auto", "description": "自动排课页面(预览+应用;权限:requirePermission(SCHEDULE_AUTO))"}, - "/admin/scheduling/changes": {"component": "ScheduleChangeList + ScheduleConflictsView", "type": "server", "module": "scheduling", "dataAccess": ["scheduling/actions.getAdminClassesForScheduling", "scheduling/actions.getScheduleChanges"], "actions": ["approveScheduleChangeAction", "rejectScheduleChangeAction", "getClassConflictsAction"], "permission": "schedule:adjust", "description": "调课申请审批+冲突检测页面(权限:requirePermission(SCHEDULE_ADJUST);审批操作需 SCHEDULE_AUTO)"}, - "/admin/elective": {"component": "ElectiveCourseList", "type": "server", "module": "elective", "dataAccess": ["elective/data-access.getElectiveCourses (scope=all)"], "actions": ["deleteElectiveCourseAction", "openSelectionAction", "closeSelectionAction", "runLotteryAction"], "permission": "elective:manage", "description": "管理员选修课程列表(权限:requirePermission(ELECTIVE_MANAGE))"}, - "/admin/elective/create": {"component": "ElectiveCourseForm", "type": "client", "module": "elective", "actions": ["createElectiveCourseAction"], "dataAccess": ["elective/data-access.getSubjectOptions"], "permission": "elective:manage", "description": "创建选修课程(权限:requirePermission(ELECTIVE_MANAGE))"}, - "/admin/elective/[id]/edit": {"component": "ElectiveCourseForm (edit)", "type": "client", "module": "elective", "actions": ["updateElectiveCourseAction"], "dataAccess": ["elective/data-access.getElectiveCourseById", "elective/data-access.getSubjectOptions"], "permission": "elective:manage", "description": "编辑选修课程(权限:requirePermission(ELECTIVE_MANAGE))"} + "exams": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard", + "types", + "ai" + ], + "auth": [ + "auth" + ] + } }, - "teacher": { - "/teacher/dashboard": {"component": "TeacherDashboardView", "type": "server", "dataAccess": ["dashboard/data-access (teacher)", "homework/data-access.getTeacherGradeTrends", "classes/data-access.getTeacherClasses"], "permission": "exam:read"}, - "/teacher/exams/all": {"component": "ExamDataTable", "type": "server", "dataAccess": ["exams/data-access.getExams"], "permission": "exam:read"}, - "/teacher/exams/create": {"component": "ExamForm", "type": "client", "actions": ["createExamAction", "createAiExamAction", "previewAiExamAction"], "permission": "exam:create"}, - "/teacher/questions": {"component": "QuestionDataTable", "type": "server", "dataAccess": ["questions/data-access.getQuestions"], "permission": "question:read"}, - "/teacher/textbooks": {"component": "TextbookList", "type": "server", "dataAccess": ["textbooks/data-access.getTextbooks"], "permission": "textbook:read"}, - "/teacher/textbooks/[id]": {"component": "TextbookReader", "type": "client", "dataAccess": ["textbooks/data-access.getTextbookById", "getChaptersByTextbookId", "getKnowledgePointsByTextbookId"], "permission": "textbook:read"}, - "/teacher/classes/my": {"component": "ClassList", "type": "server", "dataAccess": ["classes/data-access.getTeacherClasses"], "permission": "class:read"}, - "/teacher/classes/schedule": {"component": "ClassSchedule", "type": "server", "dataAccess": ["classes/data-access.getClassSchedule"], "permission": "class:read"}, - "/teacher/classes/students": {"component": "ClassStudents", "type": "server", "dataAccess": ["classes/data-access.getClassStudents"], "permission": "class:read"}, - "/teacher/classes": {"component": "重定向", "type": "server", "redirect": "/teacher/classes/my", "permission": "class:read"}, - "/teacher/classes/my/[id]": {"component": "班级详情", "type": "client", "module": "classes", "dataAccess": ["classes/data-access.getClassStudents", "classes/data-access.getClassSchedule", "classes/data-access.getClassHomeworkInsights"], "permission": "class:read"}, - "/teacher/homework": {"component": "重定向", "type": "server", "redirect": "/teacher/homework/assignments", "permission": "homework:create"}, - "/teacher/homework/assignments": {"component": "作业列表", "type": "server", "module": "homework", "dataAccess": ["homework/data-access.getHomeworkAssignments"], "permission": "homework:create"}, - "/teacher/homework/assignments/create": {"component": "HomeworkAssignmentForm", "type": "client", "module": "homework", "actions": ["createHomeworkAssignmentAction"], "permission": "homework:create"}, - "/teacher/homework/assignments/[id]": {"component": "作业详情+错误分析", "type": "client", "module": "homework", "dataAccess": ["homework/data-access.getHomeworkAssignmentById", "homework/data-access.getHomeworkAssignmentAnalytics"], "permission": "homework:create"}, - "/teacher/homework/assignments/[id]/submissions": {"component": "作业提交列表", "type": "server", "module": "homework", "dataAccess": ["homework/data-access.getHomeworkSubmissions"], "permission": "homework:create"}, - "/teacher/homework/submissions": {"component": "批改列表", "type": "server", "module": "homework", "dataAccess": ["homework/data-access.getHomeworkAssignmentReviewList"], "permission": "homework:grade"}, - "/teacher/homework/submissions/[submissionId]": {"component": "HomeworkGradingView", "type": "client", "module": "homework", "actions": ["gradeHomeworkSubmissionAction"], "dataAccess": ["homework/data-access.getHomeworkSubmissionDetails"], "permission": "homework:grade"}, - "/teacher/exams": {"component": "重定向", "type": "server", "redirect": "/teacher/exams/all", "permission": "exam:read"}, - "/teacher/exams/[id]/build": {"component": "ExamAssembly", "type": "client", "module": "exams", "permission": "exam:update"}, - "/teacher/exams/grading": {"component": "重定向", "type": "server", "redirect": "/teacher/homework/submissions", "permission": "homework:grade"}, - "/teacher/exams/grading/[submissionId]": {"component": "重定向", "type": "server", "redirect": "/teacher/homework/submissions", "permission": "homework:grade"}, - "/teacher/grades": {"component": "成绩管理首页", "type": "server", "module": "grades", "dataAccess": ["grades/actions.getGradeRecordsAction"], "permission": "grade_record:read"}, - "/teacher/grades/entry": {"component": "批量成绩录入", "type": "server", "module": "grades", "actions": ["grades/actions.batchCreateGradeRecordsAction", "grades/actions.createGradeRecordAction"], "permission": "grade_record:manage"}, - "/teacher/grades/stats": {"component": "成绩统计报表", "type": "server", "module": "grades", "dataAccess": ["grades/actions.getClassGradeStatsAction", "grades/actions.getClassRankingAction"], "permission": "grade_record:read"}, - "/teacher/course-plans": {"component": "CoursePlanList (teacher)", "type": "client", "module": "course-plans", "dataAccess": ["course-plans/data-access.getCoursePlans (filtered by teacherId)"], "permission": "course_plan:read"}, - "/teacher/course-plans/[id]": {"component": "CoursePlanDetail (teacher, read-only)", "type": "client", "module": "course-plans", "dataAccess": ["course-plans/data-access.getCoursePlanById"], "permission": "course_plan:read"}, - "/teacher/attendance": {"component": "AttendanceRecordList + AttendanceFilters", "type": "server", "module": "attendance", "dataAccess": ["attendance/data-access.getAttendanceRecords", "classes/data-access.getTeacherClasses"], "permission": "attendance:manage", "description": "教师考勤记录列表(权限:requirePermission(ATTENDANCE_MANAGE))"}, - "/teacher/attendance/sheet": {"component": "AttendanceSheet", "type": "client", "module": "attendance", "actions": ["batchRecordAttendanceAction", "getClassAttendanceForDateAction"], "dataAccess": ["attendance/data-access.getClassStudentsForAttendance"], "permission": "attendance:manage", "description": "批量点名页面(权限:requirePermission(ATTENDANCE_MANAGE))"}, - "/teacher/attendance/stats": {"component": "AttendanceStatsCard", "type": "server", "module": "attendance", "dataAccess": ["attendance/data-access-stats.getClassAttendanceStats", "classes/data-access.getTeacherClasses"], "permission": "attendance:read", "description": "班级考勤统计(权限:requirePermission(ATTENDANCE_READ))"}, - "/teacher/schedule-changes": {"component": "ScheduleChangeForm + ScheduleChangeList", "type": "server", "module": "scheduling", "dataAccess": ["scheduling/actions.getAdminClassesForScheduling", "scheduling/actions.getTeachersForScheduling", "scheduling/actions.getScheduleChanges (requesterId=ctx.userId)"], "actions": ["requestScheduleChangeAction"], "permission": "schedule:adjust", "description": "教师调课/代课申请页面(提交申请+查看本人申请列表;权限:requirePermission(SCHEDULE_ADJUST);admin 角色查看全部申请)"}, - "/teacher/diagnostic": {"component": "ReportList", "type": "client", "module": "diagnostic", "dataAccess": ["diagnostic/data-access-reports.getDiagnosticReports"], "actions": ["publishReportAction", "deleteReportAction"], "permission": "diagnostic:read", "description": "学情诊断报告列表(reportType/status 过滤器;权限:requirePermission(DIAGNOSTIC_READ);DataScope.class_members 仅查看自己报告;发布/删除操作需 DIAGNOSTIC_MANAGE)"}, - "/teacher/diagnostic/student/[studentId]": {"component": "StudentDiagnosticView", "type": "client", "module": "diagnostic", "dataAccess": ["diagnostic/data-access.getStudentMasterySummary", "diagnostic/data-access.getKnowledgePointStats (班级平均对比)", "diagnostic/data-access-reports.getDiagnosticReports"], "actions": ["generateStudentReportAction"], "permission": "diagnostic:read", "description": "学生学情诊断视图(概览卡片+雷达图+强项/弱项+生成报告[DIAGNOSTIC_MANAGE]+最新报告;权限:getAuthContext + DataScope 二次校验,class_members 仅自己,children 仅子女)"}, - "/teacher/diagnostic/class/[classId]": {"component": "ClassDiagnosticView", "type": "client", "module": "diagnostic", "dataAccess": ["diagnostic/data-access.getClassMasterySummary"], "actions": ["generateClassReportAction"], "permission": "diagnostic:read", "description": "班级学情诊断视图(概览+知识点热力图+排名表+需重点关注学生+生成班级报告[DIAGNOSTIC_MANAGE];权限:getAuthContext + DataScope 校验,class_taught 必须包含 classId,class_members/children notFound)"}, - "/teacher/elective": {"component": "ElectiveCourseList (teacher)", "type": "server", "module": "elective", "dataAccess": ["elective/data-access.getElectiveCourses (scope=class_taught/owned, currentUserId)"], "actions": ["deleteElectiveCourseAction", "openSelectionAction", "closeSelectionAction", "runLotteryAction"], "permission": "elective:manage", "description": "教师选修课程列表(权限:requirePermission(ELECTIVE_MANAGE);DataScope.class_taught/owned 按 teacherId 过滤)"} + "homework": { + "dependsOn": [ + "shared", + "auth", + "exams" + ], + "uses": { + "shared": [ + "db", + "auth-guard", + "types" + ], + "auth": [ + "auth" + ], + "exams": [ + "data-access.getExams" + ] + } }, - "student": { - "/student/dashboard": {"component": "StudentDashboardView", "type": "server", "dataAccess": ["dashboard/data-access (student)", "homework/data-access.getStudentDashboardGrades", "classes/data-access.getStudentClasses"], "permission": "homework:submit"}, - "/student/learning/assignments": {"component": "学生作业列表", "type": "server", "module": "homework", "dataAccess": ["homework/data-access.getStudentHomeworkAssignments"], "permission": "homework:submit"}, - "/student/learning/assignments/[assignmentId]": {"component": "学生作答/复习", "type": "client", "module": "homework", "actions": ["startHomeworkSubmissionAction", "saveHomeworkAnswerAction", "submitHomeworkAction"], "dataAccess": ["homework/data-access.getStudentHomeworkTakeData"], "permission": "homework:submit"}, - "/student/learning/courses": {"component": "StudentCoursesView", "type": "server", "permission": "homework:submit"}, - "/student/learning/textbooks": {"component": "学生教材列表(只读)", "type": "server", "module": "textbooks", "dataAccess": ["textbooks/data-access.getTextbooks"], "permission": "textbook:read"}, - "/student/learning/textbooks/[id]": {"component": "学生教材阅读(只读)", "type": "client", "module": "textbooks", "dataAccess": ["textbooks/data-access.getTextbookById", "getChaptersByTextbookId", "getKnowledgePointsByTextbookId"], "permission": "textbook:read"}, - "/student/schedule": {"component": "学生课表", "type": "server", "module": "classes", "dataAccess": ["classes/data-access.getStudentSchedule"], "permission": "homework:submit"}, - "/student/grades": {"component": "我的成绩", "type": "server", "module": "grades", "dataAccess": ["grades/actions.getStudentGradeSummaryAction"], "permission": "grade_record:read"}, - "/student/attendance": {"component": "StudentAttendanceView", "type": "server", "module": "attendance", "dataAccess": ["attendance/data-access-stats.getStudentAttendanceSummary"], "permission": "attendance:read", "description": "学生考勤视图(统计卡片 + 最近记录;权限:requirePermission(ATTENDANCE_READ),DataScope.class_members 仅查自己)"}, - "/student/diagnostic": {"component": "StudentDiagnosticView", "type": "client", "module": "diagnostic", "dataAccess": ["diagnostic/data-access.getStudentMasterySummary (ctx.userId)", "diagnostic/data-access-reports.getDiagnosticReports (studentId=ctx.userId)"], "permission": "diagnostic:read", "description": "学生本人学情诊断视图(概览+雷达图+强项/弱项+最新报告;权限:requirePermission(DIAGNOSTIC_READ),DataScope.class_members 仅查自己)"}, - "/student/elective": {"component": "StudentSelectionView", "type": "server", "module": "elective", "dataAccess": ["elective/data-access-selections.getAvailableCoursesForStudent", "elective/data-access-selections.getStudentSelections"], "actions": ["selectCourseAction", "dropCourseAction"], "permission": "elective:select", "description": "学生选课页面(可选课程列表 + 我的选课记录;权限:requirePermission(ELECTIVE_SELECT))"} + "questions": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard", + "types" + ], + "auth": [ + "auth" + ] + } }, - "management": { - "/management/grade/classes": {"component": "GradeClassesClient", "type": "client", "module": "classes", "permission": "grade:manage"}, - "/management/grade/insights": {"component": "年级作业洞察", "type": "server", "dataAccess": ["classes/data-access.getGradeHomeworkInsights"], "permission": "grade:manage"} + "textbooks": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard", + "types" + ], + "auth": [ + "auth" + ] + } + }, + "classes": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard", + "types" + ], + "auth": [ + "auth" + ] + } + }, + "school": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard", + "types" + ], + "auth": [ + "auth" + ] + } + }, + "dashboard": { + "dependsOn": [ + "shared", + "auth", + "homework", + "classes" + ], + "uses": { + "shared": [ + "db", + "types" + ], + "auth": [ + "auth" + ], + "homework": [ + "data-access.getTeacherGradeTrends", + "data-access.getStudentDashboardGrades" + ], + "classes": [ + "data-access.getTeacherClasses", + "data-access.getStudentClasses", + "data-access.getStudentSchedule" + ] + } + }, + "layout": { + "dependsOn": [ + "shared", + "auth", + "messaging" + ], + "uses": { + "shared": [ + "hooks.usePermission", + "components.global-search.GlobalSearch" + ], + "auth": [ + "useSession" + ], + "messaging": [ + "components.notification-dropdown" + ] + } + }, + "settings": { + "dependsOn": [ + "shared", + "auth", + "messaging" + ], + "uses": { + "shared": [ + "db", + "auth-guard", + "ai", + "types", + "components.ui.switch" + ], + "auth": [ + "auth" + ], + "messaging": [ + "notification-preferences.getNotificationPreferences", + "actions.getNotificationPreferencesAction", + "actions.updateNotificationPreferencesAction" + ] + } + }, + "users": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requireAuth", + "auth-guard.requirePermission", + "db.schema.users", + "db.schema.roles", + "db.schema.usersToRoles", + "db.schema.classes", + "db.schema.classEnrollments", + "types.permissions", + "types.action-state", + "lib.excel" + ], + "auth": [ + "auth" + ] + } + }, + "audit": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "db.schema.auditLogs", + "db.schema.loginLogs", + "db.schema.dataChangeLogs", + "types.permissions", + "lib.excel" + ], + "auth": [ + "auth" + ] + } + }, + "announcements": { + "dependsOn": [ + "shared", + "auth", + "school" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "auth-guard.requireAuth", + "db.schema.announcements", + "types.permissions" + ], + "auth": [ + "auth" + ], + "school": [ + "data-access.getGrades" + ] + } + }, + "files": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requireAuth", + "auth-guard.requirePermission", + "types.permissions", + "lib.file-storage", + "lib.storage-provider" + ], + "auth": [ + "auth" + ] + } + }, + "course-plans": { + "dependsOn": [ + "shared", + "auth", + "school", + "classes" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "db.schema.coursePlans", + "db.schema.coursePlanItems", + "db.schema.classes", + "db.schema.subjects", + "db.schema.users", + "types.permissions", + "types.action-state" + ], + "auth": [ + "auth" + ], + "school": [ + "data-access.getAcademicYears" + ], + "classes": [ + "data-access.getAdminClasses", + "data-access.getStaffOptions" + ] + } + }, + "grades": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "types.permissions", + "types.action-state", + "db.schema.gradeRecords", + "db.schema.classes", + "db.schema.classEnrollments", + "db.schema.subjects", + "db.schema.users", + "lib.excel" + ], + "auth": [ + "auth" + ] + } }, "parent": { - "/parent/dashboard": {"component": "ParentDashboard", "type": "server", "module": "parent", "dataAccess": ["parent/data-access.getParentDashboardData"], "permission": "auth_required", "description": "家长仪表盘首页(问候语 + 子女卡片网格;权限:requireAuth())"}, - "/parent/children/[studentId]": {"component": "ChildDetailHeader + ChildDetailPanel", "type": "server", "module": "parent", "dataAccess": ["parent/data-access.getChildDashboardData"], "permission": "auth_required", "description": "子女详情页(头部 + 作业/成绩/课表面板;权限:requireAuth() + 二次校验 ctx.dataScope.childrenIds 包含 studentId)"}, - "/parent/grades": {"component": "子女成绩", "type": "server", "module": "grades", "dataAccess": ["grades/data-access.getStudentGradeSummary"], "permission": "grade_record:read", "description": "家长成绩视图(按 DataScope.children 过滤)"}, - "/parent/attendance": {"component": "StudentAttendanceView (per child)", "type": "server", "module": "attendance", "dataAccess": ["parent/data-access.getChildren", "attendance/data-access-stats.getStudentAttendanceSummary"], "permission": "attendance:read", "description": "家长考勤视图(遍历子女,每个子女展示 StudentAttendanceView;权限:requirePermission(ATTENDANCE_READ),DataScope.children 仅查子女)"} + "dependsOn": [ + "shared", + "auth", + "homework", + "classes", + "grades" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requireAuth", + "db.schema.parentStudentRelations", + "db.schema.users", + "db.schema.grades", + "db.schema.classEnrollments", + "db.schema.classes", + "types" + ], + "auth": [ + "auth" + ], + "homework": [ + "data-access.getStudentHomeworkAssignments", + "data-access.getStudentDashboardGrades" + ], + "classes": [ + "data-access.getStudentClasses", + "data-access.getStudentSchedule" + ], + "grades": [ + "data-access.getStudentGradeSummary" + ] + } + }, + "messaging": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "auth-guard.requireAuth", + "db.schema.messages", + "db.schema.messageNotifications", + "db.schema.notificationPreferences", + "db.schema.users", + "db.schema.classEnrollments", + "db.schema.classes", + "db.schema.grades", + "types.permissions", + "types.action-state" + ], + "auth": [ + "auth" + ] + } + }, + "notifications": { + "dependsOn": [ + "shared", + "auth", + "messaging" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "db.schema.users", + "db.schema.classEnrollments", + "db.schema.classes", + "types.permissions", + "types.action-state" + ], + "auth": [ + "auth" + ], + "messaging": [ + "notification-preferences.getNotificationPreferences", + "data-access.createNotification" + ] + } + }, + "attendance": { + "dependsOn": [ + "shared", + "auth", + "classes" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "db.schema.attendanceRecords", + "db.schema.attendanceRules", + "db.schema.classEnrollments", + "db.schema.users", + "db.schema.classes", + "types.permissions", + "types.action-state", + "types.DataScope" + ], + "auth": [ + "auth" + ], + "classes": [ + "data-access.getTeacherClasses", + "data-access.getAdminClasses" + ] + } + }, + "scheduling": { + "dependsOn": [ + "shared", + "auth", + "classes" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "auth-guard.getAuthContext", + "db.schema.schedulingRules", + "db.schema.scheduleChanges", + "db.schema.classSchedule", + "db.schema.classes", + "db.schema.users", + "db.schema.classSubjectTeachers", + "db.schema.subjects", + "db.schema.classrooms", + "types.permissions", + "types.action-state" + ], + "auth": [ + "auth" + ], + "classes": [] + } + }, + "diagnostic": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "auth-guard.getAuthContext", + "db.schema.knowledgePointMastery", + "db.schema.learningDiagnosticReports", + "db.schema.knowledgePoints", + "db.schema.questionsToKnowledgePoints", + "db.schema.examSubmissions", + "db.schema.submissionAnswers", + "db.schema.classEnrollments", + "db.schema.classes", + "db.schema.users", + "types.permissions", + "types.action-state", + "hooks.usePermission", + "components.ui.*" + ], + "auth": [ + "auth" + ] + } + }, + "elective": { + "dependsOn": [ + "shared", + "auth" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "db.schema.electiveCourses", + "db.schema.courseSelections", + "db.schema.users", + "db.schema.subjects", + "db.schema.grades", + "db.schema.classes", + "db.schema.classEnrollments", + "types.permissions", + "types.action-state", + "types.DataScope", + "hooks.usePermission", + "components.ui.*" + ], + "auth": [ + "auth" + ] + } + }, + "proctoring": { + "dependsOn": [ + "shared", + "auth", + "exams" + ], + "uses": { + "shared": [ + "db", + "auth-guard.requirePermission", + "auth-guard.requireAuth", + "auth-guard.getAuthContext", + "db.schema.examProctoringEvents", + "db.schema.examSubmissions", + "db.schema.exams", + "db.schema.users", + "types.permissions", + "types.action-state", + "hooks.usePermission", + "components.ui.*" + ], + "auth": [ + "auth" + ], + "exams": [ + "data-access (read exam & submission for proctoring context)" + ] + } + } + }, + "moduleDependencyGraph": { + "nodes": [ + "shared", + "auth", + "exams", + "homework", + "questions", + "textbooks", + "classes", + "school", + "dashboard", + "layout", + "settings", + "users", + "audit", + "announcements", + "files", + "course-plans", + "grades", + "parent", + "messaging", + "notifications", + "attendance", + "scheduling", + "proctoring", + "diagnostic", + "elective" + ], + "edges": [ + { + "from": "exams", + "to": "questions", + "type": "data-access", + "description": "引用题目(examQuestions 关联)" + }, + { + "from": "exams", + "to": "shared", + "type": "normal", + "description": "使用 db/auth-guard/ai" + }, + { + "from": "homework", + "to": "exams", + "type": "data-access", + "description": "引用试卷结构(sourceExamId)" + }, + { + "from": "homework", + "to": "questions", + "type": "data-access", + "description": "引用题目" + }, + { + "from": "grades", + "to": "classes", + "type": "data-access", + "description": "查询班级" + }, + { + "from": "grades", + "to": "exams", + "type": "data-access", + "description": "关联考试(examId)" + }, + { + "from": "grades", + "to": "subjects", + "type": "data-access", + "description": "查询科目" + }, + { + "from": "dashboard", + "to": "exams", + "type": "violation", + "description": "直接查询 exams 表(违规)" + }, + { + "from": "dashboard", + "to": "homework", + "type": "violation", + "description": "直接查询 homeworkAssignments/homeworkSubmissions 表(违规)" + }, + { + "from": "dashboard", + "to": "classes", + "type": "violation", + "description": "直接查询 classes 表(违规)" + }, + { + "from": "dashboard", + "to": "users", + "type": "violation", + "description": "直接查询 sessions/users/usersToRoles/roles 表(违规)" + }, + { + "from": "dashboard", + "to": "textbooks", + "type": "violation", + "description": "直接查询 textbooks/chapters 表(违规)" + }, + { + "from": "dashboard", + "to": "questions", + "type": "violation", + "description": "直接查询 questions 表(违规)" + }, + { + "from": "messaging", + "to": "notifications", + "type": "violation", + "description": "绕过 notifications dispatcher 直接调用 createNotification(违规)" + }, + { + "from": "classes", + "to": "homework", + "type": "violation", + "description": "classes/data-access.ts 混入 homework 洞察逻辑(违规)" + }, + { + "from": "classes", + "to": "scheduling", + "type": "violation", + "description": "classes/data-access.ts 混入 scheduling 课表逻辑(违规)" + }, + { + "from": "classes", + "to": "grades", + "type": "violation", + "description": "classes/data-access.ts 混入 grades 查询逻辑(违规)" + }, + { + "from": "proctoring", + "to": "exams", + "type": "data-access", + "description": "关联考试与提交(examSubmissions)" + }, + { + "from": "diagnostic", + "to": "questions", + "type": "data-access", + "description": "关联题目知识点(questionsToKnowledgePoints)" + }, + { + "from": "diagnostic", + "to": "exams", + "type": "data-access", + "description": "关联考试提交(examSubmissions/submissionAnswers)" + }, + { + "from": "elective", + "to": "school", + "type": "data-access", + "description": "关联年级(grades)/科目(subjects)" + }, + { + "from": "attendance", + "to": "classes", + "type": "data-access", + "description": "查询班级与课表" + }, + { + "from": "parent", + "to": "grades", + "type": "data-access", + "description": "查询子女成绩" + }, + { + "from": "parent", + "to": "attendance", + "type": "data-access", + "description": "查询子女考勤" + }, + { + "from": "parent", + "to": "homework", + "type": "data-access", + "description": "查询子女作业" + }, + { + "from": "scheduling", + "to": "classes", + "type": "data-access", + "description": "排课关联班级" + }, + { + "from": "course-plans", + "to": "classes", + "type": "data-access", + "description": "课程计划关联班级" + }, + { + "from": "course-plans", + "to": "school", + "type": "data-access", + "description": "关联科目" + }, + { + "from": "announcements", + "to": "school", + "type": "data-access", + "description": "关联年级/班级" + }, + { + "from": "shared", + "to": "auth", + "type": "violation", + "description": "shared/lib → @/auth 循环依赖(违规)" + }, + { + "from": "auth", + "to": "shared", + "type": "violation", + "description": "auth.ts → shared/lib 循环依赖(违规)" + }, + { + "from": "layout", + "to": "shared", + "type": "normal", + "description": "使用 shared 组件与配置" + }, + { + "from": "settings", + "to": "shared", + "type": "normal", + "description": "使用 shared/lib/ai" + }, + { + "from": "settings", + "to": "messaging", + "type": "normal", + "description": "通知偏好设置" + }, + { + "from": "audit", + "to": "shared", + "type": "normal", + "description": "使用 shared/lib/audit-logger" + }, + { + "from": "files", + "to": "shared", + "type": "normal", + "description": "使用 shared/db" + }, + { + "from": "users", + "to": "classes", + "type": "violation", + "description": "import-export.ts 跨模块写 classEnrollments(违规)" + } + ] + }, + "knownIssues": [ + { + "id": "P0-1", + "severity": "P0", + "title": "classes/data-access.ts 超过 1000 行", + "file": "src/modules/classes/data-access.ts", + "lines": 2104, + "problem": "混入 homework/scheduling/grades 逻辑,严重违反模块职责单一原则", + "suggestion": "按职责拆分为 class-query/schedule/homework-insights/grade-query" + }, + { + "id": "P0-2", + "severity": "P0", + "title": "homework/data-access.ts 超过 1000 行", + "file": "src/modules/homework/data-access.ts", + "lines": 1038, + "problem": "混入排名计算业务逻辑", + "suggestion": "分离排名逻辑到独立文件(如 data-access-ranking.ts)" + }, + { + "id": "P0-3", + "severity": "P0", + "title": "shared/lib ↔ auth 循环依赖", + "file": "src/shared/lib/{audit-logger,change-logger,auth-guard}.ts ↔ src/auth.ts", + "problem": "shared/lib 依赖 @/auth(src/auth.ts),auth.ts 又依赖 shared/lib,形成循环依赖", + "suggestion": "拆分 auth.ts,将 shared 依赖部分抽出为独立模块" + }, + { + "id": "P0-4", + "severity": "P0", + "title": "dashboard 跨模块直接查询 11 张表", + "file": "src/modules/dashboard/data-access.ts", + "problem": "getAdminDashboardData 直查 sessions/users/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions/usersToRoles/roles,严重违反模块封装", + "suggestion": "改为通过各模块 data-access 获取数据" + }, + { + "id": "P0-5", + "severity": "P0", + "title": "messaging 绕过 notifications 直接写通知", + "file": "src/modules/messaging/actions.ts", + "lines": "66-72", + "problem": "直接调用 createNotification,导致用户通知偏好失效、多渠道通知无效", + "suggestion": "改为通过 notifications dispatcher 统一分发" + }, + { + "id": "P0-6", + "severity": "P0", + "title": "classSchedule 表三处写入口", + "file": "src/modules/classes/data-access.ts, src/modules/scheduling/actions.ts, src/modules/scheduling/data-access.ts", + "problem": "三处独立写入 classSchedule 表,数据完整性高风险", + "suggestion": "统一写入口到 scheduling 模块" + }, + { + "id": "P1-1", + "severity": "P1", + "title": "跨模块直接 DB 查询普遍存在", + "problem": "classes(8+)/classEnrollments(6+)/users(6+)/subjects(6+)/exams(5+) 表被多个模块直接查询", + "suggestion": "建立模块间数据访问规范,通过对方 data-access 或导出查询函数" + }, + { + "id": "P1-2", + "severity": "P1", + "title": "actions 层混入数据访问逻辑", + "file": "src/modules/{exams,homework,questions,announcements}/actions.ts", + "problem": "actions.ts 中存在直接 db.insert/update/delete,应通过 data-access 层", + "suggestion": "将 DB 操作下沉到 data-access 层" + }, + { + "id": "P1-3", + "severity": "P1", + "title": "auth.ts 混合 5 类职责", + "file": "src/auth.ts", + "problem": "NextAuth 配置 + 密码安全 DB 操作 + 角色规范化 + IP 解析 + 回调函数混合", + "suggestion": "拆分为 auth-config/password-security/role-normalizer/ip-utils 等多文件" + }, + { + "id": "P1-4", + "severity": "P1", + "title": "users/import-export.ts 四重职责", + "file": "src/modules/users/import-export.ts", + "problem": "导入解析 + 导出 + 用户创建(含密码哈希) + 班级注册(跨模块写 classEnrollments)", + "suggestion": "按职责拆分为 import-parser/exporter/user-creator/enrollment" + }, + { + "id": "P1-5", + "severity": "P1", + "title": "proctoring 死代码", + "file": "src/modules/proctoring/components/exam-mode-config.tsx", + "problem": "组件已创建但未集成到考试表单,DB schema 有 examMode 字段但表单不收集", + "suggestion": "集成 proctoring/exam-mode-config 到考试表单" + }, + { + "id": "P2-1", + "severity": "P2", + "title": "schema.ts 54 张表混合(1111 行)", + "file": "src/shared/db/schema.ts", + "lines": 1111, + "problem": "所有表定义混合在单文件,虽可接受但需分节", + "suggestion": "按业务域分节(加注释分隔)或拆分为多文件" + } + ], + "routes": { + "auth": { + "/login": { + "component": "LoginForm", + "type": "client", + "module": "auth" + }, + "/register": { + "component": "RegisterForm + registerAction", + "type": "server", + "module": "auth", + "description": "注册页面(含未成年人信息保护、隐私政策/用户协议同意勾选)" + }, + "/privacy": { + "component": "PrivacyPage", + "type": "server", + "module": "auth", + "description": "隐私政策页面(信息收集/使用/保护、用户权利、Cookie、未成年人保护条款、联系方式)" + }, + "/terms": { + "component": "TermsPage", + "type": "server", + "module": "auth", + "description": "用户协议页面(服务说明、注册、行为规范、知识产权、免责、变更终止、法律适用)" + } + }, + "admin": { + "/admin/dashboard": { + "component": "AdminDashboardView", + "type": "server", + "dataAccess": [ + "dashboard/data-access.getAdminDashboardData" + ], + "permission": "school:manage" + }, + "/admin/school": { + "component": "重定向", + "type": "server", + "redirect": "/admin/school/classes", + "permission": "school:manage" + }, + "/admin/school/schools": { + "component": "SchoolsClient", + "type": "client", + "module": "school", + "permission": "school:manage" + }, + "/admin/school/grades": { + "component": "GradesClient", + "type": "client", + "module": "school", + "permission": "grade:manage" + }, + "/admin/school/grades/insights": { + "component": "年级作业洞察", + "type": "server", + "dataAccess": [ + "classes/data-access.getGradeHomeworkInsights" + ], + "permission": "grade:manage" + }, + "/admin/school/departments": { + "component": "DepartmentsClient", + "type": "client", + "module": "school", + "permission": "school:manage" + }, + "/admin/school/classes": { + "component": "AdminClassesClient", + "type": "client", + "module": "classes", + "permission": "school:manage" + }, + "/admin/school/academic-year": { + "component": "AcademicYearClient", + "type": "client", + "module": "school", + "permission": "school:manage" + }, + "/admin/audit-logs": { + "component": "AuditLogView", + "type": "server", + "module": "audit", + "dataAccess": [ + "audit/data-access.getAuditLogs", + "audit/data-access.getAuditModuleOptions" + ], + "permission": "audit_log:read" + }, + "/admin/audit-logs/login-logs": { + "component": "LoginLogView", + "type": "server", + "module": "audit", + "dataAccess": [ + "audit/data-access.getLoginLogs" + ], + "permission": "audit_log:read" + }, + "/admin/announcements": { + "component": "AdminAnnouncementsView", + "type": "server", + "module": "announcements", + "dataAccess": [ + "announcements/data-access.getAnnouncements", + "school/data-access.getGrades" + ], + "actions": [ + "createAnnouncementAction" + ], + "permission": "announcement:manage" + }, + "/admin/announcements/[id]": { + "component": "AnnouncementForm (edit)", + "type": "server", + "module": "announcements", + "dataAccess": [ + "announcements/data-access.getAnnouncementById", + "school/data-access.getGrades" + ], + "actions": [ + "updateAnnouncementAction" + ], + "permission": "announcement:manage" + }, + "/admin/files": { + "component": "AdminFilesView", + "type": "server", + "module": "files", + "dataAccess": [ + "files/data-access.getAllFileAttachments" + ], + "permission": "file:read" + }, + "/admin/course-plans": { + "component": "CoursePlanList", + "type": "client", + "module": "course-plans", + "dataAccess": [ + "course-plans/data-access.getCoursePlans" + ], + "permission": "course_plan:manage" + }, + "/admin/course-plans/create": { + "component": "CoursePlanForm (create)", + "type": "client", + "module": "course-plans", + "actions": [ + "createCoursePlanAction" + ], + "dataAccess": [ + "classes/data-access.getAdminClasses", + "course-plans/data-access.getSubjectOptions", + "classes/data-access.getStaffOptions", + "school/data-access.getAcademicYears" + ], + "permission": "course_plan:manage" + }, + "/admin/course-plans/[id]": { + "component": "CoursePlanDetail", + "type": "client", + "module": "course-plans", + "dataAccess": [ + "course-plans/data-access.getCoursePlanById" + ], + "actions": [ + "deleteCoursePlanAction", + "createCoursePlanItemAction", + "updateCoursePlanItemAction", + "deleteCoursePlanItemAction", + "toggleCoursePlanItemCompletedAction" + ], + "permission": "course_plan:manage" + }, + "/admin/course-plans/[id]/edit": { + "component": "CoursePlanForm (edit)", + "type": "client", + "module": "course-plans", + "actions": [ + "updateCoursePlanAction" + ], + "dataAccess": [ + "course-plans/data-access.getCoursePlanById", + "classes/data-access.getAdminClasses", + "course-plans/data-access.getSubjectOptions", + "classes/data-access.getStaffOptions", + "school/data-access.getAcademicYears" + ], + "permission": "course_plan:manage" + }, + "/admin/attendance": { + "component": "AttendanceRecordList", + "type": "server", + "module": "attendance", + "dataAccess": [ + "attendance/data-access.getAttendanceRecords (scope=all)", + "classes/data-access.getAdminClasses" + ], + "permission": "attendance:manage", + "description": "管理员考勤总览(权限:requirePermission(ATTENDANCE_MANAGE))" + }, + "/admin/users/import": { + "component": "UserImportPage (含 UserImportDialog)", + "type": "server", + "module": "users", + "actions": [ + "users/actions.downloadUserTemplateAction", + "users/actions.importUsersAction" + ], + "permission": "user:manage", + "description": "用户批量导入页面(说明卡片+字段文档表+导入对话框;权限:requirePermission(USER_MANAGE))" + }, + "/admin/scheduling/rules": { + "component": "SchedulingRulesForm", + "type": "server", + "module": "scheduling", + "dataAccess": [ + "scheduling/actions.getAdminClassesForScheduling", + "scheduling/actions.getSchedulingRules" + ], + "actions": [ + "saveSchedulingRulesAction" + ], + "permission": "schedule:adjust", + "description": "排课规则配置页面(权限:requirePermission(SCHEDULE_ADJUST))" + }, + "/admin/scheduling/auto": { + "component": "AutoSchedulePanel + AutoScheduleResultView", + "type": "server", + "module": "scheduling", + "dataAccess": [ + "scheduling/actions.getAdminClassesForScheduling" + ], + "actions": [ + "autoScheduleAction", + "applyAutoScheduleAction" + ], + "permission": "schedule:auto", + "description": "自动排课页面(预览+应用;权限:requirePermission(SCHEDULE_AUTO))" + }, + "/admin/scheduling/changes": { + "component": "ScheduleChangeList + ScheduleConflictsView", + "type": "server", + "module": "scheduling", + "dataAccess": [ + "scheduling/actions.getAdminClassesForScheduling", + "scheduling/actions.getScheduleChanges" + ], + "actions": [ + "approveScheduleChangeAction", + "rejectScheduleChangeAction", + "getClassConflictsAction" + ], + "permission": "schedule:adjust", + "description": "调课申请审批+冲突检测页面(权限:requirePermission(SCHEDULE_ADJUST);审批操作需 SCHEDULE_AUTO)" + }, + "/admin/elective": { + "component": "ElectiveCourseList", + "type": "server", + "module": "elective", + "dataAccess": [ + "elective/data-access.getElectiveCourses (scope=all)" + ], + "actions": [ + "deleteElectiveCourseAction", + "openSelectionAction", + "closeSelectionAction", + "runLotteryAction" + ], + "permission": "elective:manage", + "description": "管理员选修课程列表(权限:requirePermission(ELECTIVE_MANAGE))" + }, + "/admin/elective/create": { + "component": "ElectiveCourseForm", + "type": "client", + "module": "elective", + "actions": [ + "createElectiveCourseAction" + ], + "dataAccess": [ + "elective/data-access.getSubjectOptions" + ], + "permission": "elective:manage", + "description": "创建选修课程(权限:requirePermission(ELECTIVE_MANAGE))" + }, + "/admin/elective/[id]/edit": { + "component": "ElectiveCourseForm (edit)", + "type": "client", + "module": "elective", + "actions": [ + "updateElectiveCourseAction" + ], + "dataAccess": [ + "elective/data-access.getElectiveCourseById", + "elective/data-access.getSubjectOptions" + ], + "permission": "elective:manage", + "description": "编辑选修课程(权限:requirePermission(ELECTIVE_MANAGE))" + } + }, + "teacher": { + "/teacher/dashboard": { + "component": "TeacherDashboardView", + "type": "server", + "dataAccess": [ + "dashboard/data-access (teacher)", + "homework/data-access.getTeacherGradeTrends", + "classes/data-access.getTeacherClasses" + ], + "permission": "exam:read" + }, + "/teacher/exams/all": { + "component": "ExamDataTable", + "type": "server", + "dataAccess": [ + "exams/data-access.getExams" + ], + "permission": "exam:read" + }, + "/teacher/exams/create": { + "component": "ExamForm", + "type": "client", + "actions": [ + "createExamAction", + "createAiExamAction", + "previewAiExamAction" + ], + "permission": "exam:create" + }, + "/teacher/questions": { + "component": "QuestionDataTable", + "type": "server", + "dataAccess": [ + "questions/data-access.getQuestions" + ], + "permission": "question:read" + }, + "/teacher/textbooks": { + "component": "TextbookList", + "type": "server", + "dataAccess": [ + "textbooks/data-access.getTextbooks" + ], + "permission": "textbook:read" + }, + "/teacher/textbooks/[id]": { + "component": "TextbookReader", + "type": "client", + "dataAccess": [ + "textbooks/data-access.getTextbookById", + "getChaptersByTextbookId", + "getKnowledgePointsByTextbookId" + ], + "permission": "textbook:read" + }, + "/teacher/classes/my": { + "component": "ClassList", + "type": "server", + "dataAccess": [ + "classes/data-access.getTeacherClasses" + ], + "permission": "class:read" + }, + "/teacher/classes/schedule": { + "component": "ClassSchedule", + "type": "server", + "dataAccess": [ + "classes/data-access.getClassSchedule" + ], + "permission": "class:read" + }, + "/teacher/classes/students": { + "component": "ClassStudents", + "type": "server", + "dataAccess": [ + "classes/data-access.getClassStudents" + ], + "permission": "class:read" + }, + "/teacher/classes": { + "component": "重定向", + "type": "server", + "redirect": "/teacher/classes/my", + "permission": "class:read" + }, + "/teacher/classes/my/[id]": { + "component": "班级详情", + "type": "client", + "module": "classes", + "dataAccess": [ + "classes/data-access.getClassStudents", + "classes/data-access.getClassSchedule", + "classes/data-access.getClassHomeworkInsights" + ], + "permission": "class:read" + }, + "/teacher/homework": { + "component": "重定向", + "type": "server", + "redirect": "/teacher/homework/assignments", + "permission": "homework:create" + }, + "/teacher/homework/assignments": { + "component": "作业列表", + "type": "server", + "module": "homework", + "dataAccess": [ + "homework/data-access.getHomeworkAssignments" + ], + "permission": "homework:create" + }, + "/teacher/homework/assignments/create": { + "component": "HomeworkAssignmentForm", + "type": "client", + "module": "homework", + "actions": [ + "createHomeworkAssignmentAction" + ], + "permission": "homework:create" + }, + "/teacher/homework/assignments/[id]": { + "component": "作业详情+错误分析", + "type": "client", + "module": "homework", + "dataAccess": [ + "homework/data-access.getHomeworkAssignmentById", + "homework/data-access.getHomeworkAssignmentAnalytics" + ], + "permission": "homework:create" + }, + "/teacher/homework/assignments/[id]/submissions": { + "component": "作业提交列表", + "type": "server", + "module": "homework", + "dataAccess": [ + "homework/data-access.getHomeworkSubmissions" + ], + "permission": "homework:create" + }, + "/teacher/homework/submissions": { + "component": "批改列表", + "type": "server", + "module": "homework", + "dataAccess": [ + "homework/data-access.getHomeworkAssignmentReviewList" + ], + "permission": "homework:grade" + }, + "/teacher/homework/submissions/[submissionId]": { + "component": "HomeworkGradingView", + "type": "client", + "module": "homework", + "actions": [ + "gradeHomeworkSubmissionAction" + ], + "dataAccess": [ + "homework/data-access.getHomeworkSubmissionDetails" + ], + "permission": "homework:grade" + }, + "/teacher/exams": { + "component": "重定向", + "type": "server", + "redirect": "/teacher/exams/all", + "permission": "exam:read" + }, + "/teacher/exams/[id]/build": { + "component": "ExamAssembly", + "type": "client", + "module": "exams", + "permission": "exam:update" + }, + "/teacher/exams/grading": { + "component": "重定向", + "type": "server", + "redirect": "/teacher/homework/submissions", + "permission": "homework:grade" + }, + "/teacher/exams/grading/[submissionId]": { + "component": "重定向", + "type": "server", + "redirect": "/teacher/homework/submissions", + "permission": "homework:grade" + }, + "/teacher/grades": { + "component": "成绩管理首页", + "type": "server", + "module": "grades", + "dataAccess": [ + "grades/actions.getGradeRecordsAction" + ], + "permission": "grade_record:read" + }, + "/teacher/grades/entry": { + "component": "批量成绩录入", + "type": "server", + "module": "grades", + "actions": [ + "grades/actions.batchCreateGradeRecordsAction", + "grades/actions.createGradeRecordAction" + ], + "permission": "grade_record:manage" + }, + "/teacher/grades/stats": { + "component": "成绩统计报表", + "type": "server", + "module": "grades", + "dataAccess": [ + "grades/actions.getClassGradeStatsAction", + "grades/actions.getClassRankingAction" + ], + "permission": "grade_record:read" + }, + "/teacher/course-plans": { + "component": "CoursePlanList (teacher)", + "type": "client", + "module": "course-plans", + "dataAccess": [ + "course-plans/data-access.getCoursePlans (filtered by teacherId)" + ], + "permission": "course_plan:read" + }, + "/teacher/course-plans/[id]": { + "component": "CoursePlanDetail (teacher, read-only)", + "type": "client", + "module": "course-plans", + "dataAccess": [ + "course-plans/data-access.getCoursePlanById" + ], + "permission": "course_plan:read" + }, + "/teacher/attendance": { + "component": "AttendanceRecordList + AttendanceFilters", + "type": "server", + "module": "attendance", + "dataAccess": [ + "attendance/data-access.getAttendanceRecords", + "classes/data-access.getTeacherClasses" + ], + "permission": "attendance:manage", + "description": "教师考勤记录列表(权限:requirePermission(ATTENDANCE_MANAGE))" + }, + "/teacher/attendance/sheet": { + "component": "AttendanceSheet", + "type": "client", + "module": "attendance", + "actions": [ + "batchRecordAttendanceAction", + "getClassAttendanceForDateAction" + ], + "dataAccess": [ + "attendance/data-access.getClassStudentsForAttendance" + ], + "permission": "attendance:manage", + "description": "批量点名页面(权限:requirePermission(ATTENDANCE_MANAGE))" + }, + "/teacher/attendance/stats": { + "component": "AttendanceStatsCard", + "type": "server", + "module": "attendance", + "dataAccess": [ + "attendance/data-access-stats.getClassAttendanceStats", + "classes/data-access.getTeacherClasses" + ], + "permission": "attendance:read", + "description": "班级考勤统计(权限:requirePermission(ATTENDANCE_READ))" + }, + "/teacher/schedule-changes": { + "component": "ScheduleChangeForm + ScheduleChangeList", + "type": "server", + "module": "scheduling", + "dataAccess": [ + "scheduling/actions.getAdminClassesForScheduling", + "scheduling/actions.getTeachersForScheduling", + "scheduling/actions.getScheduleChanges (requesterId=ctx.userId)" + ], + "actions": [ + "requestScheduleChangeAction" + ], + "permission": "schedule:adjust", + "description": "教师调课/代课申请页面(提交申请+查看本人申请列表;权限:requirePermission(SCHEDULE_ADJUST);admin 角色查看全部申请)" + }, + "/teacher/diagnostic": { + "component": "ReportList", + "type": "client", + "module": "diagnostic", + "dataAccess": [ + "diagnostic/data-access-reports.getDiagnosticReports" + ], + "actions": [ + "publishReportAction", + "deleteReportAction" + ], + "permission": "diagnostic:read", + "description": "学情诊断报告列表(reportType/status 过滤器;权限:requirePermission(DIAGNOSTIC_READ);DataScope.class_members 仅查看自己报告;发布/删除操作需 DIAGNOSTIC_MANAGE)" + }, + "/teacher/diagnostic/student/[studentId]": { + "component": "StudentDiagnosticView", + "type": "client", + "module": "diagnostic", + "dataAccess": [ + "diagnostic/data-access.getStudentMasterySummary", + "diagnostic/data-access.getKnowledgePointStats (班级平均对比)", + "diagnostic/data-access-reports.getDiagnosticReports" + ], + "actions": [ + "generateStudentReportAction" + ], + "permission": "diagnostic:read", + "description": "学生学情诊断视图(概览卡片+雷达图+强项/弱项+生成报告[DIAGNOSTIC_MANAGE]+最新报告;权限:getAuthContext + DataScope 二次校验,class_members 仅自己,children 仅子女)" + }, + "/teacher/diagnostic/class/[classId]": { + "component": "ClassDiagnosticView", + "type": "client", + "module": "diagnostic", + "dataAccess": [ + "diagnostic/data-access.getClassMasterySummary" + ], + "actions": [ + "generateClassReportAction" + ], + "permission": "diagnostic:read", + "description": "班级学情诊断视图(概览+知识点热力图+排名表+需重点关注学生+生成班级报告[DIAGNOSTIC_MANAGE];权限:getAuthContext + DataScope 校验,class_taught 必须包含 classId,class_members/children notFound)" + }, + "/teacher/elective": { + "component": "ElectiveCourseList (teacher)", + "type": "server", + "module": "elective", + "dataAccess": [ + "elective/data-access.getElectiveCourses (scope=class_taught/owned, currentUserId)" + ], + "actions": [ + "deleteElectiveCourseAction", + "openSelectionAction", + "closeSelectionAction", + "runLotteryAction" + ], + "permission": "elective:manage", + "description": "教师选修课程列表(权限:requirePermission(ELECTIVE_MANAGE);DataScope.class_taught/owned 按 teacherId 过滤)" + } + }, + "student": { + "/student/dashboard": { + "component": "StudentDashboardView", + "type": "server", + "dataAccess": [ + "dashboard/data-access (student)", + "homework/data-access.getStudentDashboardGrades", + "classes/data-access.getStudentClasses" + ], + "permission": "homework:submit" + }, + "/student/learning/assignments": { + "component": "学生作业列表", + "type": "server", + "module": "homework", + "dataAccess": [ + "homework/data-access.getStudentHomeworkAssignments" + ], + "permission": "homework:submit" + }, + "/student/learning/assignments/[assignmentId]": { + "component": "学生作答/复习", + "type": "client", + "module": "homework", + "actions": [ + "startHomeworkSubmissionAction", + "saveHomeworkAnswerAction", + "submitHomeworkAction" + ], + "dataAccess": [ + "homework/data-access.getStudentHomeworkTakeData" + ], + "permission": "homework:submit" + }, + "/student/learning/courses": { + "component": "StudentCoursesView", + "type": "server", + "permission": "homework:submit" + }, + "/student/learning/textbooks": { + "component": "学生教材列表(只读)", + "type": "server", + "module": "textbooks", + "dataAccess": [ + "textbooks/data-access.getTextbooks" + ], + "permission": "textbook:read" + }, + "/student/learning/textbooks/[id]": { + "component": "学生教材阅读(只读)", + "type": "client", + "module": "textbooks", + "dataAccess": [ + "textbooks/data-access.getTextbookById", + "getChaptersByTextbookId", + "getKnowledgePointsByTextbookId" + ], + "permission": "textbook:read" + }, + "/student/schedule": { + "component": "学生课表", + "type": "server", + "module": "classes", + "dataAccess": [ + "classes/data-access.getStudentSchedule" + ], + "permission": "homework:submit" + }, + "/student/grades": { + "component": "我的成绩", + "type": "server", + "module": "grades", + "dataAccess": [ + "grades/actions.getStudentGradeSummaryAction" + ], + "permission": "grade_record:read" + }, + "/student/attendance": { + "component": "StudentAttendanceView", + "type": "server", + "module": "attendance", + "dataAccess": [ + "attendance/data-access-stats.getStudentAttendanceSummary" + ], + "permission": "attendance:read", + "description": "学生考勤视图(统计卡片 + 最近记录;权限:requirePermission(ATTENDANCE_READ),DataScope.class_members 仅查自己)" + }, + "/student/diagnostic": { + "component": "StudentDiagnosticView", + "type": "client", + "module": "diagnostic", + "dataAccess": [ + "diagnostic/data-access.getStudentMasterySummary (ctx.userId)", + "diagnostic/data-access-reports.getDiagnosticReports (studentId=ctx.userId)" + ], + "permission": "diagnostic:read", + "description": "学生本人学情诊断视图(概览+雷达图+强项/弱项+最新报告;权限:requirePermission(DIAGNOSTIC_READ),DataScope.class_members 仅查自己)" + }, + "/student/elective": { + "component": "StudentSelectionView", + "type": "server", + "module": "elective", + "dataAccess": [ + "elective/data-access-selections.getAvailableCoursesForStudent", + "elective/data-access-selections.getStudentSelections" + ], + "actions": [ + "selectCourseAction", + "dropCourseAction" + ], + "permission": "elective:select", + "description": "学生选课页面(可选课程列表 + 我的选课记录;权限:requirePermission(ELECTIVE_SELECT))" + } + }, + "management": { + "/management/grade/classes": { + "component": "GradeClassesClient", + "type": "client", + "module": "classes", + "permission": "grade:manage" + }, + "/management/grade/insights": { + "component": "年级作业洞察", + "type": "server", + "dataAccess": [ + "classes/data-access.getGradeHomeworkInsights" + ], + "permission": "grade:manage" + } + }, + "parent": { + "/parent/dashboard": { + "component": "ParentDashboard", + "type": "server", + "module": "parent", + "dataAccess": [ + "parent/data-access.getParentDashboardData" + ], + "permission": "auth_required", + "description": "家长仪表盘首页(问候语 + 子女卡片网格;权限:requireAuth())" + }, + "/parent/children/[studentId]": { + "component": "ChildDetailHeader + ChildDetailPanel", + "type": "server", + "module": "parent", + "dataAccess": [ + "parent/data-access.getChildDashboardData" + ], + "permission": "auth_required", + "description": "子女详情页(头部 + 作业/成绩/课表面板;权限:requireAuth() + 二次校验 ctx.dataScope.childrenIds 包含 studentId)" + }, + "/parent/grades": { + "component": "子女成绩", + "type": "server", + "module": "grades", + "dataAccess": [ + "grades/data-access.getStudentGradeSummary" + ], + "permission": "grade_record:read", + "description": "家长成绩视图(按 DataScope.children 过滤)" + }, + "/parent/attendance": { + "component": "StudentAttendanceView (per child)", + "type": "server", + "module": "attendance", + "dataAccess": [ + "parent/data-access.getChildren", + "attendance/data-access-stats.getStudentAttendanceSummary" + ], + "permission": "attendance:read", + "description": "家长考勤视图(遍历子女,每个子女展示 StudentAttendanceView;权限:requirePermission(ATTENDANCE_READ),DataScope.children 仅查子女)" + } }, "root": { - "/": {"component": "重定向", "type": "server", "redirect": "/dashboard"} + "/": { + "component": "重定向", + "type": "server", + "redirect": "/dashboard" + } }, "shared": { - "/dashboard": {"component": "角色路由分发", "type": "server", "redirect": "按permissions判断→/admin|/teacher|/student|/parent"}, - "/profile": {"component": "ProfilePage", "type": "server", "permission": "auth_required"}, - "/settings": {"component": "SettingsPage", "type": "server", "permission": "auth_required", "dataAccess": ["messaging/notification-preferences.getNotificationPreferences"], "description": "设置页面(按角色分发 AdminSettingsView/TeacherSettingsView/StudentSettingsView;含 General/Appearance/Security/Notifications tab,Notifications 渲染 NotificationPreferencesForm)"}, - "/announcements": {"component": "AnnouncementList (published only)", "type": "server", "module": "announcements", "dataAccess": ["announcements/data-access.getAnnouncements (status=published)"], "permission": "announcement:read"} + "/dashboard": { + "component": "角色路由分发", + "type": "server", + "redirect": "按permissions判断→/admin|/teacher|/student|/parent" + }, + "/profile": { + "component": "ProfilePage", + "type": "server", + "permission": "auth_required" + }, + "/settings": { + "component": "SettingsPage", + "type": "server", + "permission": "auth_required", + "dataAccess": [ + "messaging/notification-preferences.getNotificationPreferences" + ], + "description": "设置页面(按角色分发 AdminSettingsView/TeacherSettingsView/StudentSettingsView;含 General/Appearance/Security/Notifications tab,Notifications 渲染 NotificationPreferencesForm)" + }, + "/announcements": { + "component": "AnnouncementList (published only)", + "type": "server", + "module": "announcements", + "dataAccess": [ + "announcements/data-access.getAnnouncements (status=published)" + ], + "permission": "announcement:read" + } }, "messages": { - "/messages": {"component": "MessageList + NotificationList", "type": "server", "module": "messaging", "dataAccess": ["messaging/data-access.getMessages", "messaging/data-access.getNotifications"], "permission": "message:read", "description": "消息首页(收件箱/已发送列表 + 通知列表;权限:requirePermission(MESSAGE_READ))"}, - "/messages/[id]": {"component": "MessageDetail", "type": "server", "module": "messaging", "dataAccess": ["messaging/data-access.getMessageById", "messaging/data-access.getMessageThread"], "actions": ["markMessageAsReadAction (自动已读)"], "permission": "message:read", "description": "消息详情(含回复线程;权限:requirePermission(MESSAGE_READ))"}, - "/messages/compose": {"component": "MessageCompose", "type": "server", "module": "messaging", "dataAccess": ["messaging/data-access.getRecipients"], "permission": "message:send", "description": "写消息页面(支持 reply 模式 via searchParams: receiverId, subject, parentMessageId;权限:requirePermission(MESSAGE_SEND))"} + "/messages": { + "component": "MessageList + NotificationList", + "type": "server", + "module": "messaging", + "dataAccess": [ + "messaging/data-access.getMessages", + "messaging/data-access.getNotifications" + ], + "permission": "message:read", + "description": "消息首页(收件箱/已发送列表 + 通知列表;权限:requirePermission(MESSAGE_READ))" + }, + "/messages/[id]": { + "component": "MessageDetail", + "type": "server", + "module": "messaging", + "dataAccess": [ + "messaging/data-access.getMessageById", + "messaging/data-access.getMessageThread" + ], + "actions": [ + "markMessageAsReadAction (自动已读)" + ], + "permission": "message:read", + "description": "消息详情(含回复线程;权限:requirePermission(MESSAGE_READ))" + }, + "/messages/compose": { + "component": "MessageCompose", + "type": "server", + "module": "messaging", + "dataAccess": [ + "messaging/data-access.getRecipients" + ], + "permission": "message:send", + "description": "写消息页面(支持 reply 模式 via searchParams: receiverId, subject, parentMessageId;权限:requirePermission(MESSAGE_SEND))" + } } }, "apiRoutes": { - "/api/auth/[...nextauth]": {"methods": ["GET", "POST"], "handler": "auth.handlers", "auth": "public"}, - "/api/ai/chat": {"methods": ["POST"], "handler": "createAiChatCompletion", "auth": "AI_CHAT", "validation": "parseAiChatPayload (Zod)"}, - "/api/onboarding/complete": {"methods": ["POST"], "handler": "onboarding complete", "auth": "required", "validation": "Zod schema"}, - "/api/onboarding/status": {"methods": ["GET"], "handler": "onboarding status", "auth": "required"}, - "/api/upload": {"methods": ["POST"], "handler": "文件上传 (multipart/form-data)", "auth": "requireAuth", "module": "files", "validation": "isAllowedMimeType + MAX_FILE_SIZE (10MB)", "description": "保存文件到 public/uploads/YYYY-MM/cuid.ext,写入 fileAttachments 表,返回 FileUploadResult"}, - "/api/files/[id]": {"methods": ["GET", "DELETE"], "handler": "文件元数据查询/删除", "auth": "GET: requireAuth, DELETE: requirePermission(FILE_DELETE)", "module": "files", "description": "GET 返回文件元数据;DELETE 删除 DB 记录并 unlink 磁盘文件(静默失败)"}, - "/api/files/batch-delete": {"methods": ["POST"], "handler": "批量删除文件", "auth": "requirePermission(FILE_DELETE)", "module": "files", "validation": "JSON body { ids: string[] },空数组返回 400", "description": "先通过 getFileAttachmentsByIds 查出文件记录,并行调用 storageProvider.delete 删除磁盘文件(静默失败),再调用 deleteFileAttachments 删除 DB 记录(失败时回退到逐条删除);响应 { success, message, deletedCount, failedIds }"}, - "/api/search": {"methods": ["GET"], "handler": "全局全文检索", "auth": "requireAuth", "module": "shared.db (questions/textbooks/exams/announcements)", "validation": "query params: q (关键词), type=all|question|textbook|exam|announcement, page=1, pageSize=10 (上限 50)", "description": "并行查询 questions/textbooks/exams/announcements(公告仅 status=published),按 createdAt 降序排序后分页;question content 字段 CAST AS CHAR 模糊匹配;返回 { success, query, type, results: [{ id, title, snippet, type, href, createdAt }], total, page, pageSize }"}, - "/api/export": {"methods": ["POST"], "handler": "Excel 导出(grades/users/attendance)", "auth": "requireAuth", "module": "shared.lib.excel + users/grades", "validation": "JSON body { type, params }", "description": "按 type 分发到 exportGradeRecordsToExcel/exportUsersToExcel,返回 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 二进制流"}, - "/api/import": {"methods": ["POST"], "handler": "Excel 解析预览(不写 DB)", "auth": "requirePermission(USER_MANAGE)", "module": "shared.lib.excel", "validation": "multipart/form-data file,限 .xlsx/.xls,10MB 上限", "description": "接收 Excel 文件,调用 parseExcel 返回 sheets 预览数据(实际导入由 users/actions.importUsersAction 完成)"} + "/api/auth/[...nextauth]": { + "methods": [ + "GET", + "POST" + ], + "handler": "auth.handlers", + "auth": "public" + }, + "/api/ai/chat": { + "methods": [ + "POST" + ], + "handler": "createAiChatCompletion", + "auth": "AI_CHAT", + "validation": "parseAiChatPayload (Zod)" + }, + "/api/onboarding/complete": { + "methods": [ + "POST" + ], + "handler": "onboarding complete", + "auth": "required", + "validation": "Zod schema" + }, + "/api/onboarding/status": { + "methods": [ + "GET" + ], + "handler": "onboarding status", + "auth": "required" + }, + "/api/upload": { + "methods": [ + "POST" + ], + "handler": "文件上传 (multipart/form-data)", + "auth": "requireAuth", + "module": "files", + "validation": "isAllowedMimeType + MAX_FILE_SIZE (10MB)", + "description": "保存文件到 public/uploads/YYYY-MM/cuid.ext,写入 fileAttachments 表,返回 FileUploadResult" + }, + "/api/files/[id]": { + "methods": [ + "GET", + "DELETE" + ], + "handler": "文件元数据查询/删除", + "auth": "GET: requireAuth, DELETE: requirePermission(FILE_DELETE)", + "module": "files", + "description": "GET 返回文件元数据;DELETE 删除 DB 记录并 unlink 磁盘文件(静默失败)" + }, + "/api/files/batch-delete": { + "methods": [ + "POST" + ], + "handler": "批量删除文件", + "auth": "requirePermission(FILE_DELETE)", + "module": "files", + "validation": "JSON body { ids: string[] },空数组返回 400", + "description": "先通过 getFileAttachmentsByIds 查出文件记录,并行调用 storageProvider.delete 删除磁盘文件(静默失败),再调用 deleteFileAttachments 删除 DB 记录(失败时回退到逐条删除);响应 { success, message, deletedCount, failedIds }" + }, + "/api/search": { + "methods": [ + "GET" + ], + "handler": "全局全文检索", + "auth": "requireAuth", + "module": "shared.db (questions/textbooks/exams/announcements)", + "validation": "query params: q (关键词), type=all|question|textbook|exam|announcement, page=1, pageSize=10 (上限 50)", + "description": "并行查询 questions/textbooks/exams/announcements(公告仅 status=published),按 createdAt 降序排序后分页;question content 字段 CAST AS CHAR 模糊匹配;返回 { success, query, type, results: [{ id, title, snippet, type, href, createdAt }], total, page, pageSize }" + }, + "/api/export": { + "methods": [ + "POST" + ], + "handler": "Excel 导出(grades/users/attendance)", + "auth": "requireAuth", + "module": "shared.lib.excel + users/grades", + "validation": "JSON body { type, params }", + "description": "按 type 分发到 exportGradeRecordsToExcel/exportUsersToExcel,返回 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 二进制流" + }, + "/api/import": { + "methods": [ + "POST" + ], + "handler": "Excel 解析预览(不写 DB)", + "auth": "requirePermission(USER_MANAGE)", + "module": "shared.lib.excel", + "validation": "multipart/form-data file,限 .xlsx/.xls,10MB 上限", + "description": "接收 Excel 文件,调用 parseExcel 返回 sheets 预览数据(实际导入由 users/actions.importUsersAction 完成)" + }, + "/api/proctoring/event": { + "method": "POST", + "module": "proctoring", + "purpose": "学生端上报监考/防作弊事件", + "permission": "auth_required", + "description": "学生考试过程中上报防作弊事件(tab_switch/window_blur/copy_attempt/paste_attempt/right_click/devtools_open/fullscreen_exit/idle_timeout)", + "dataAccess": [ + "proctoring/data-access.recordProctoringEvent" + ] + } }, "devops": { "ci": { "configFile": ".gitea/workflows/ci.yml", - "triggers": ["push to main", "pull_request to main", "schedule cron 0 2 * * *"], + "triggers": [ + "push to main", + "pull_request to main", + "schedule cron 0 2 * * *" + ], "jobs": { "build-deploy": { "runsOn": "CDCD", "container": "dockerreg.eazygame.cn/node-with-docker:22", "trigger": "push/PR to main", - "steps": ["checkout", "cache npm", "configure npm proxy", "npm ci", "lint", "typecheck", "install playwright chromium", "integration tests", "e2e tests", "cache next.js build", "build", "prepare standalone", "deploy to docker"] + "steps": [ + "checkout", + "cache npm", + "configure npm proxy", + "npm ci", + "lint", + "typecheck", + "install playwright chromium", + "integration tests", + "e2e tests", + "cache next.js build", + "build", + "prepare standalone", + "deploy to docker" + ] }, "security-scan": { "runsOn": "ubuntu-latest", "trigger": "push/PR to main", "needs": "build-deploy", "continueOnError": true, - "steps": ["checkout", "setup node 20", "npm ci", "npm audit --audit-level=moderate + 生成 audit-report.json (continue-on-error)", "Snyk scan --severity-threshold=high --sarif-file-output=snyk.sarif (env SNYK_TOKEN, continue-on-error)", "Trivy fs scan json+table (continue-on-error)", "OWASP ZAP baseline scan target=NEXTAUTH_URL||localhost:8015 cmd_options='-a -j' (continue-on-error)", "upload security-reports artifact (audit-report.json, trivy-fs-report.json, snyk.sarif)"] + "steps": [ + "checkout", + "setup node 20", + "npm ci", + "npm audit --audit-level=moderate + 生成 audit-report.json (continue-on-error)", + "Snyk scan --severity-threshold=high --sarif-file-output=snyk.sarif (env SNYK_TOKEN, continue-on-error)", + "Trivy fs scan json+table (continue-on-error)", + "OWASP ZAP baseline scan target=NEXTAUTH_URL||localhost:8015 cmd_options='-a -j' (continue-on-error)", + "upload security-reports artifact (audit-report.json, trivy-fs-report.json, snyk.sarif)" + ] }, "scheduled-backup": { "runsOn": "ubuntu-latest", "trigger": "schedule cron 0 2 * * *", "condition": "github.event_name == 'schedule'", - "steps": ["checkout", "run scripts/backup-db.sh (env DATABASE_URL, BACKUP_DIR)", "run scripts/backup-verify.sh (校验备份完整性)", "run scripts/backup-offsite-sync.sh (异地同步, env BACKUP_OFFSITE_*, 失败不阻塞)", "upload backups/ artifact (retention 30 days)"] + "steps": [ + "checkout", + "run scripts/backup-db.sh (env DATABASE_URL, BACKUP_DIR)", + "run scripts/backup-verify.sh (校验备份完整性)", + "run scripts/backup-offsite-sync.sh (异地同步, env BACKUP_OFFSITE_*, 失败不阻塞)", + "upload backups/ artifact (retention 30 days)" + ] }, "backup-verify": { "runsOn": "ubuntu-latest", "trigger": "schedule", "condition": "github.event_name == 'schedule'", "needs": "scheduled-backup", - "steps": ["checkout", "download db-backup artifact", "run scripts/backup-verify.sh (独立校验)", "run scripts/health-check.sh > health-report.json", "upload backup-verify-report artifact (backups/, health-report.json, retention 7 days)"] + "steps": [ + "checkout", + "download db-backup artifact", + "run scripts/backup-verify.sh (独立校验)", + "run scripts/health-check.sh > health-report.json", + "upload backup-verify-report artifact (backups/, health-report.json, retention 7 days)" + ] }, "weekly-dr-drill": { "runsOn": "ubuntu-latest", "trigger": "schedule (每周触发, github.run_attempt % 7 == 0)", "condition": "github.event_name == 'schedule' && github.run_attempt % 7 == 0", "needs": "backup-verify", - "steps": ["checkout", "run scripts/dr-drill.sh (env DATABASE_URL, DR_DRILL_TEST_DB=next_edu_dr_drill)", "upload dr-drill-report artifact (docs/dr/reports/, retention 90 days)"] + "steps": [ + "checkout", + "run scripts/dr-drill.sh (env DATABASE_URL, DR_DRILL_TEST_DB=next_edu_dr_drill)", + "upload dr-drill-report artifact (docs/dr/reports/, retention 90 days)" + ] } } }, "drDrillWorkflow": { "configFile": ".gitea/workflows/dr-drill.yml", - "triggers": ["schedule cron 0 4 * * 1 (每周一凌晨 4 点)", "workflow_dispatch (inputs: backup_file, no_cleanup)"], + "triggers": [ + "schedule cron 0 4 * * 1 (每周一凌晨 4 点)", + "workflow_dispatch (inputs: backup_file, no_cleanup)" + ], "job": "dr-drill", "runsOn": "ubuntu-latest", "timeoutMinutes": 30, @@ -1937,7 +11902,10 @@ }, "securityWorkflow": { "configFile": ".gitea/workflows/security.yml", - "triggers": ["schedule cron 0 3 * * 1 (每周一凌晨 3 点)", "workflow_dispatch (inputs: target_url, skip_dast)"], + "triggers": [ + "schedule cron 0 3 * * 1 (每周一凌晨 3 点)", + "workflow_dispatch (inputs: target_url, skip_dast)" + ], "job": "deep-security-scan", "runsOn": "ubuntu-latest", "continueOnError": true, @@ -1972,13 +11940,19 @@ "scripts/backup-db.sh": { "type": "bash", "purpose": "MySQL 数据库备份,从 DATABASE_URL 解析连接信息,gzip 压缩,保留 RETENTION_DAYS 天(默认 30)", - "env": ["DATABASE_URL", "BACKUP_DIR", "RETENTION_DAYS"], + "env": [ + "DATABASE_URL", + "BACKUP_DIR", + "RETENTION_DAYS" + ], "output": "${BACKUP_DIR}/db_backup_${TIMESTAMP}.sql.gz" }, "scripts/restore-db.sh": { "type": "bash", "purpose": "MySQL 数据库恢复,从指定备份文件恢复", - "env": ["DATABASE_URL"], + "env": [ + "DATABASE_URL" + ], "usage": "./restore-db.sh " }, "scripts/test-backup.sh": { @@ -1988,45 +11962,139 @@ "scripts/backup-verify.sh": { "type": "bash", "purpose": "备份完整性校验:检查文件存在/大小/gzip 完整性/SQL 内容结构/SQL 语法(可选,需 DATABASE_URL)", - "env": ["BACKUP_DIR", "DATABASE_URL", "BACKUP_VERIFY_MIN_SIZE"], - "exitCodes": {"0": "校验通过", "1": "校验失败"}, - "options": ["--min-size BYTES", "--no-sql-check", "--help"] + "env": [ + "BACKUP_DIR", + "DATABASE_URL", + "BACKUP_VERIFY_MIN_SIZE" + ], + "exitCodes": { + "0": "校验通过", + "1": "校验失败" + }, + "options": [ + "--min-size BYTES", + "--no-sql-check", + "--help" + ] }, "scripts/backup-offsite-sync.sh": { "type": "bash", "purpose": "异地备份同步:支持 S3/OSS/NFS 后端,同步后校验文件数量,清理远程过期备份(保留 90 天)", - "env": ["BACKUP_DIR", "BACKUP_OFFSITE_BACKEND", "BACKUP_OFFSITE_REMOTE", "BACKUP_OFFSITE_BUCKET", "BACKUP_OFFSITE_ACCESS_KEY", "BACKUP_OFFSITE_SECRET_KEY", "BACKUP_OFFSITE_REGION", "BACKUP_OFFSITE_RETENTION_DAYS"], - "tools": ["aws-cli (s3)", "rclone (s3/oss)", "ossutil (oss)", "rsync (nfs)"], - "exitCodes": {"0": "同步成功", "1": "同步失败"}, - "options": ["--backend TYPE", "--no-cleanup", "--no-verify", "--help"] + "env": [ + "BACKUP_DIR", + "BACKUP_OFFSITE_BACKEND", + "BACKUP_OFFSITE_REMOTE", + "BACKUP_OFFSITE_BUCKET", + "BACKUP_OFFSITE_ACCESS_KEY", + "BACKUP_OFFSITE_SECRET_KEY", + "BACKUP_OFFSITE_REGION", + "BACKUP_OFFSITE_RETENTION_DAYS" + ], + "tools": [ + "aws-cli (s3)", + "rclone (s3/oss)", + "ossutil (oss)", + "rsync (nfs)" + ], + "exitCodes": { + "0": "同步成功", + "1": "同步失败" + }, + "options": [ + "--backend TYPE", + "--no-cleanup", + "--no-verify", + "--help" + ] }, "scripts/dr-drill.sh": { "type": "bash", "purpose": "灾备演练:创建测试库→从备份恢复→数据完整性检查→冒烟测试→清理→生成报告到 docs/dr/reports/", - "env": ["DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR"], - "exitCodes": {"0": "演练成功", "1": "演练失败"}, - "options": ["--backup FILE", "--test-db NAME", "--no-cleanup", "--report-dir DIR", "--help"] + "env": [ + "DATABASE_URL", + "BACKUP_DIR", + "DR_DRILL_TEST_DB", + "DR_DRILL_REPORT_DIR" + ], + "exitCodes": { + "0": "演练成功", + "1": "演练失败" + }, + "options": [ + "--backup FILE", + "--test-db NAME", + "--no-cleanup", + "--report-dir DIR", + "--help" + ] }, "scripts/dr-drill.ps1": { "type": "powershell", "purpose": "灾备演练(Windows PowerShell 5.1+ 版本),功能同 Bash 版本", - "env": ["DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR"], + "env": [ + "DATABASE_URL", + "BACKUP_DIR", + "DR_DRILL_TEST_DB", + "DR_DRILL_REPORT_DIR" + ], "platform": "Windows", - "options": ["-BackupFile FILE", "-TestDb NAME", "-NoCleanup", "-ReportDir DIR", "-Help"] + "options": [ + "-BackupFile FILE", + "-TestDb NAME", + "-NoCleanup", + "-ReportDir DIR", + "-Help" + ] }, "scripts/failover.sh": { "type": "bash", "purpose": "故障切换:检测主库健康→提升备库→更新应用配置→重启应用→验证切换", - "env": ["DATABASE_URL", "DATABASE_URL_STANDBY", "FAILOVER_APP_URL", "FAILOVER_APP_NAME", "FAILOVER_CONFIG_FILE", "FAILOVER_LOG_FILE"], - "exitCodes": {"0": "切换成功", "1": "切换失败"}, - "options": ["--auto", "--primary URL", "--standby URL", "--app-url URL", "--no-restart", "--dry-run", "--help"] + "env": [ + "DATABASE_URL", + "DATABASE_URL_STANDBY", + "FAILOVER_APP_URL", + "FAILOVER_APP_NAME", + "FAILOVER_CONFIG_FILE", + "FAILOVER_LOG_FILE" + ], + "exitCodes": { + "0": "切换成功", + "1": "切换失败" + }, + "options": [ + "--auto", + "--primary URL", + "--standby URL", + "--app-url URL", + "--no-restart", + "--dry-run", + "--help" + ] }, "scripts/health-check.sh": { "type": "bash", "purpose": "健康检查:检查应用 HTTP/数据库连接/磁盘空间/备份新鲜度,输出 JSON 报告", - "env": ["DATABASE_URL", "HEALTH_CHECK_URL", "BACKUP_DIR", "HEALTH_CHECK_DISK_THRESHOLD", "HEALTH_CHECK_BACKUP_MAX_AGE"], - "exitCodes": {"0": "健康", "1": "异常"}, - "options": ["--app-url URL", "--no-app", "--no-db", "--no-disk", "--no-backup", "--disk-threshold PCT", "--backup-max-age HRS", "--help"] + "env": [ + "DATABASE_URL", + "HEALTH_CHECK_URL", + "BACKUP_DIR", + "HEALTH_CHECK_DISK_THRESHOLD", + "HEALTH_CHECK_BACKUP_MAX_AGE" + ], + "exitCodes": { + "0": "健康", + "1": "异常" + }, + "options": [ + "--app-url URL", + "--no-app", + "--no-db", + "--no-disk", + "--no-backup", + "--disk-threshold PCT", + "--backup-max-age HRS", + "--help" + ] } }, "packageJsonScripts": { @@ -2063,8 +12131,20 @@ "FAILOVER_APP_NAME": "应用容器名(默认 nextjs-app)" }, "gitignore": { - "added": ["/backups/", "/audit-report.json", "/trivy-fs-report.json", "/trivy-image-report.json", "/snyk.sarif", "/security-summary.md", "/playwright-report/", "/test-results/", "/tests/visual/.auth/"], - "exceptions": [".env.example (灾备环境变量示例,允许提交)"] + "added": [ + "/backups/", + "/audit-report.json", + "/trivy-fs-report.json", + "/trivy-image-report.json", + "/snyk.sarif", + "/security-summary.md", + "/playwright-report/", + "/test-results/", + "/tests/visual/.auth/" + ], + "exceptions": [ + ".env.example (灾备环境变量示例,允许提交)" + ] } }, "testing": { @@ -2085,24 +12165,64 @@ } }, "projects": [ - {"name": "chromium", "testDir": "./tests/e2e", "channel": "CI: undefined, local: chrome"}, - {"name": "visual-chromium", "testDir": "./tests/visual", "channel": "CI: undefined, local: chrome"} + { + "name": "chromium", + "testDir": "./tests/e2e", + "channel": "CI: undefined, local: chrome" + }, + { + "name": "visual-chromium", + "testDir": "./tests/visual", + "channel": "CI: undefined, local: chrome" + } ], "snapshotPathTemplate": "{testDir}/__screenshots__/{testFilePath}/{arg}{ext}", "expect": { - "toHaveScreenshot": {"maxDiffPixelRatio": 0.01, "animations": "disabled", "caret": "hide"}, - "toMatchSnapshot": {"maxDiffPixelRatio": 0.01} + "toHaveScreenshot": { + "maxDiffPixelRatio": 0.01, + "animations": "disabled", + "caret": "hide" + }, + "toMatchSnapshot": { + "maxDiffPixelRatio": 0.01 + } }, "retries": "CI: 2, local: 0", "workers": "CI: 2, local: default", "testFiles": { - "smoke-auth.spec.ts": {"coverage": "登录/注册页面控件渲染冒烟测试", "requiresDb": false}, - "auth-business-flow.spec.ts": {"coverage": "注册→登录→访问受保护区域完整流程", "requiresDb": true}, - "full-route-regression.spec.ts": {"coverage": "全路由清单完整性 + 公开/受保护路由守卫", "requiresDb": false}, - "auth.spec.ts": {"coverage": "认证页面(登录/注册/隐私/协议)渲染 + 未认证重定向", "requiresDb": false}, - "navigation.spec.ts": {"coverage": "admin/teacher/student 导航链接无 404", "requiresDb": true, "envVars": ["E2E_ADMIN_EMAIL", "E2E_TEACHER_EMAIL", "E2E_STUDENT_EMAIL"]}, - "announcements.spec.ts": {"coverage": "公告页面未认证重定向 + 登录后渲染", "requiresDb": "partial"}, - "grades.spec.ts": {"coverage": "成绩页面未认证重定向 + 登录后渲染", "requiresDb": "partial"} + "smoke-auth.spec.ts": { + "coverage": "登录/注册页面控件渲染冒烟测试", + "requiresDb": false + }, + "auth-business-flow.spec.ts": { + "coverage": "注册→登录→访问受保护区域完整流程", + "requiresDb": true + }, + "full-route-regression.spec.ts": { + "coverage": "全路由清单完整性 + 公开/受保护路由守卫", + "requiresDb": false + }, + "auth.spec.ts": { + "coverage": "认证页面(登录/注册/隐私/协议)渲染 + 未认证重定向", + "requiresDb": false + }, + "navigation.spec.ts": { + "coverage": "admin/teacher/student 导航链接无 404", + "requiresDb": true, + "envVars": [ + "E2E_ADMIN_EMAIL", + "E2E_TEACHER_EMAIL", + "E2E_STUDENT_EMAIL" + ] + }, + "announcements.spec.ts": { + "coverage": "公告页面未认证重定向 + 登录后渲染", + "requiresDb": "partial" + }, + "grades.spec.ts": { + "coverage": "成绩页面未认证重定向 + 登录后渲染", + "requiresDb": "partial" + } } }, "visual": { @@ -2110,26 +12230,81 @@ "snapshotDir": "tests/visual/__screenshots__", "storageStateDir": "tests/visual/.auth/", "viewports": { - "desktop": {"width": 1920, "height": 1080}, - "tablet": {"width": 768, "height": 1024}, - "mobile": {"width": 375, "height": 812} + "desktop": { + "width": 1920, + "height": 1080 + }, + "tablet": { + "width": 768, + "height": 1024 + }, + "mobile": { + "width": 375, + "height": 812 + } }, - "themes": ["light", "dark"], + "themes": [ + "light", + "dark" + ], "defaultMaxDiffPixelRatio": 0.01, "testAccounts": { - "admin": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_ADMIN_EMAIL", "VISUAL_ADMIN_PASSWORD"]}, - "teacher": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_TEACHER_EMAIL", "VISUAL_TEACHER_PASSWORD"]}, - "student": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_STUDENT_EMAIL", "VISUAL_STUDENT_PASSWORD"]} + "admin": { + "email": "admin@xiaoxue.edu.cn", + "envVars": [ + "VISUAL_ADMIN_EMAIL", + "VISUAL_ADMIN_PASSWORD" + ] + }, + "teacher": { + "email": "admin@xiaoxue.edu.cn", + "envVars": [ + "VISUAL_TEACHER_EMAIL", + "VISUAL_TEACHER_PASSWORD" + ] + }, + "student": { + "email": "admin@xiaoxue.edu.cn", + "envVars": [ + "VISUAL_STUDENT_EMAIL", + "VISUAL_STUDENT_PASSWORD" + ] + } }, "testFiles": { - "homepage.spec.ts": {"coverage": "登录页在 desktop/tablet/mobile × light/dark 下的快照", "requiresDb": false}, - "admin-dashboard.spec.ts": {"coverage": "管理员仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "admin"}, - "teacher-dashboard.spec.ts": {"coverage": "教师仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "teacher"}, - "student-dashboard.spec.ts": {"coverage": "学生仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "student"} + "homepage.spec.ts": { + "coverage": "登录页在 desktop/tablet/mobile × light/dark 下的快照", + "requiresDb": false + }, + "admin-dashboard.spec.ts": { + "coverage": "管理员仪表盘整页 + 侧边栏/主内容区组件快照", + "requiresDb": true, + "role": "admin" + }, + "teacher-dashboard.spec.ts": { + "coverage": "教师仪表盘整页 + 侧边栏/主内容区组件快照", + "requiresDb": true, + "role": "teacher" + }, + "student-dashboard.spec.ts": { + "coverage": "学生仪表盘整页 + 侧边栏/主内容区组件快照", + "requiresDb": true, + "role": "student" + } }, "helpers": { - "auth.ts": ["setupAuthState(role)", "loginByUI(page, role)", "storageStatePath(role)"], - "visual-helpers.ts": ["setViewport(page, size)", "setTheme(page, theme)", "waitForPageReady(page)", "maskDynamicElements(page, selectors)", "buildMaskOption(masks)"] + "auth.ts": [ + "setupAuthState(role)", + "loginByUI(page, role)", + "storageStatePath(role)" + ], + "visual-helpers.ts": [ + "setViewport(page, size)", + "setTheme(page, theme)", + "waitForPageReady(page)", + "maskDynamicElements(page, selectors)", + "buildMaskOption(masks)" + ] }, "scripts": { "test:visual": "playwright test --project=visual-chromium", diff --git a/docs/architecture/006_k12_feature_checklist.md b/docs/architecture/006_k12_feature_checklist.md index a140dda..20254a0 100644 --- a/docs/architecture/006_k12_feature_checklist.md +++ b/docs/architecture/006_k12_feature_checklist.md @@ -2,177 +2,178 @@ > 基于教育科技行业最佳实践,覆盖核心业务、平台基础、非功能性、合规安全四大维度。 > 优先级定义:**P0** = MVP 必须,**P1** = 上线前推荐,**P2** = 迭代优化。 +> 实现状态:✅ 已完成 / ⚠️ 部分完成 / ❌ 未实现(基于 2026-06-17 源码扫描) --- ## 一、核心业务模块 -| 模块 | 子功能 | 说明 | 优先级 | -|------|--------|------|--------| -| **用户与权限** | 用户注册/登录 | 邮箱/手机号注册,第三方 OAuth 登录 | P0 | -| | 多角色体系 | 管理员/教师/学生/家长/年级组长/教研组长,支持一人多角色 | P0 | -| | RBAC 权限模型 | 资源:动作权限点(如 `exam:create`),角色-权限映射 | P0 | -| | 数据范围控制 | 行级权限:全部/所属/所教班级/所管年级/班级成员/子女 | P0 | -| | 角色切换 | 多角色用户可主动切换当前活跃角色 | P1 | -| | 用户档案管理 | 个人信息编辑、头像、联系方式、地址 | P0 | -| | 新手引导(Onboarding) | 首次登录选择角色、填写资料、加入班级 | P0 | -| | 组织架构管理 | 部门/年级/教研组树形结构管理 | P1 | -| | 用户批量导入 | Excel/CSV 批量导入学生、教师信息 | P1 | -| | 密码安全策略 | 密码强度校验、定期更换提醒、登录失败锁定 | P1 | -| **学校管理** | 学校信息配置 | 校名、校徽、地址、学期制度等基础信息 | P0 | -| | 学年学期管理 | 创建/切换学年学期,学期起止日期,当前学期标记 | P0 | -| | 年级管理 | 年级创建/编辑/归档,年级组长指派 | P0 | -| | 班级管理 | 班级创建/编辑/归档,班主任指派,班级邀请码 | P0 | -| | 学科管理 | 学科创建/编辑,学科代码,学段归属 | P0 | -| | 部门管理 | 教务处/德育处/总务处等行政部门管理 | P1 | -| | 校区管理 | 多校区信息维护,校区间资源共享策略 | P2 | -| | 学校参数配置 | 学分制/等级制切换、考号规则、编号规则等 | P1 | -| **教务排课** | 课程计划管理 | 各年级/班级课程设置,周课时分配 | P0 | -| | 排课规则配置 | 教师不冲突、教室不冲突、连排/不连排规则 | P0 | -| | 自动排课引擎 | 基于约束满足的智能排课算法 | P1 | -| | 课表查看 | 教师/学生/班级/教室多维度课表 | P0 | -| | 课表调整/代课 | 临时调课、代课安排,自动通知受影响方 | P1 | -| | 教室资源管理 | 教室类型、容量、设备标签,排课时自动匹配 | P2 | -| | 选课管理 | 选修课选课、退选、抽签规则 | P2 | -| **教材资源** | 教材库管理 | 教材元数据(学科/年级/版本/出版社) | P0 | -| | 章节结构管理 | 教材章节树形目录,支持拖拽排序 | P0 | -| | 知识点图谱 | 知识点与章节关联,知识点间前置/后继关系 | P1 | -| | 教材内容阅读 | 富文本/Markdown 章节内容在线阅读 | P0 | -| | 教材版本管理 | 同一教材多版本共存,版本切换 | P1 | -| | 资源附件管理 | 教案/课件/视频等附件上传与关联 | P1 | -| | 教材审核流程 | 教材内容发布前审核机制 | P2 | -| **题库与试卷** | 题目创建/编辑 | 选择/填空/判断/简答/综合题,支持子题目 | P0 | -| | 题目分类标签 | 学科/知识点/难度/题型多维标签 | P0 | -| | 题目批量导入 | Excel/Word 模板批量导入题目 | P1 | -| | 题目版本管理 | 题目修改保留历史版本 | P2 | -| | 试卷手动组卷 | 从题库选题组卷,分数自动汇总 | P0 | -| | 试卷智能组卷 | 按知识点/难度分布自动抽题 | P1 | -| | AI 辅助出题 | 大模型根据知识点/要求自动生成题目 | P1 | -| | 试卷模板管理 | 常用试卷结构模板保存与复用 | P2 | -| | 试卷预览/打印 | 试卷排版预览,A4/B4 打印适配 | P1 | -| **作业与考试** | 作业布置 | 关联试卷/题目,选择目标班级/学生,设置截止时间 | P0 | -| | 作业提交 | 学生在线作答,支持文本/图片/附件 | P0 | -| | 作业批改评分 | 教师逐题评分,支持批注/语音反馈 | P0 | -| | 迟交/补交策略 | 允许迟交开关、迟交截止时间、迟交标记 | P1 | -| | 多次提交/重做 | 可配置最大尝试次数,取最高分/最后一次 | P1 | -| | 作业统计分析 | 完成率、平均分、分数分布、题目正确率 | P1 | -| | 作业归档 | 学期结束后作业数据归档,释放活跃数据空间 | P2 | -| | 在线考试模式 | 限时考试、防切屏、乱序、自动交卷 | P1 | -| | 考试监考 | 教师端实时查看提交进度,异常行为标记 | P2 | -| **成绩分析** | 成绩录入 | 手动录入/Excel 导入/作业自动同步 | P0 | -| | 成绩查询 | 学生查本人,教师查所教班级,管理员查全部 | P0 | -| | 成绩统计报表 | 班级/年级均分、中位数、标准差、及格率 | P0 | -| | 成绩趋势分析 | 历次考试趋势折线图,进步/退步预警 | P1 | -| | 成绩对比分析 | 班级间对比、学科间对比、与年级均分对比 | P1 | -| | 学情诊断报告 | 基于知识点掌握度的个人/班级诊断报告 | P2 | -| | 成绩导出 | Excel/PDF 成绩单导出,支持自定义模板 | P1 | -| | 等第转换 | 分数↔等第(A/B/C/D)自动转换 | P2 | -| **家校沟通** | 通知公告 | 学校/年级/班级三级公告发布,已读回执 | P0 | -| | 站内消息 | 教师↔家长、教师↔学生私信,支持群发 | P1 | -| | 家长端仪表盘 | 子女成绩/作业/考勤/课表一站式查看 | P1 | -| | 家长会/约谈预约 | 教师发起家长约谈,家长在线预约时段 | P2 | -| | 请假审批 | 学生在线请假,教师/班主任审批 | P1 | -| | 校园动态/班级圈 | 班级活动照片/视频分享,家长点赞评论 | P2 | -| **AI 赋能** | AI 对话助手 | 通用教育场景问答(备课建议、教学策略) | P1 | -| | AI 辅助出题 | 根据知识点/难度自动生成题目 | P1 | -| | AI 批改辅助 | 简答题/作文 AI 预评分+建议,教师终审 | P2 | -| | AI 学情分析 | 基于学习数据的个性化学习路径推荐 | P2 | -| | AI 备课助手 | 根据教材章节自动生成教案/课件大纲 | P2 | -| | AI 多模型配置 | 支持 Zhipu/OpenAI/Gemini 等多模型切换 | P1 | -| | AI API Key 加密存储 | API Key AES 加密,按角色权限访问 | P1 | -| **考勤管理** | 学生考勤 | 每日/每节课考勤登记,迟到/早退/缺勤标记 | P1 | -| | 教师考勤 | 教师出勤/请假/代课记录 | P1 | -| | 考勤统计 | 月度/学期考勤汇总,异常预警 | P2 | -| | 考勤规则配置 | 迟到阈值、缺勤预警线、自动通知家长 | P2 | +| 模块 | 子功能 | 说明 | 优先级 | 当前实现状态 | +|------|--------|------|--------|-------------| +| **用户与权限** | 用户注册/登录 | 邮箱/手机号注册,第三方 OAuth 登录 | P0 | ✅ | +| | 多角色体系 | 管理员/教师/学生/家长/年级组长/教研组长,支持一人多角色 | P0 | ✅ | +| | RBAC 权限模型 | 资源:动作权限点(如 `exam:create`),角色-权限映射 | P0 | ✅ | +| | 数据范围控制 | 行级权限:全部/所属/所教班级/所管年级/班级成员/子女 | P0 | ✅ | +| | 角色切换 | 多角色用户可主动切换当前活跃角色 | P1 | ❌ | +| | 用户档案管理 | 个人信息编辑、头像、联系方式、地址 | P0 | ✅ | +| | 新手引导(Onboarding) | 首次登录选择角色、填写资料、加入班级 | P0 | ✅ | +| | 组织架构管理 | 部门/年级/教研组树形结构管理 | P1 | ⚠️ | +| | 用户批量导入 | Excel/CSV 批量导入学生、教师信息 | P1 | ✅ | +| | 密码安全策略 | 密码强度校验、定期更换提醒、登录失败锁定 | P1 | ⚠️ | +| **学校管理** | 学校信息配置 | 校名、校徽、地址、学期制度等基础信息 | P0 | ✅ | +| | 学年学期管理 | 创建/切换学年学期,学期起止日期,当前学期标记 | P0 | ✅ | +| | 年级管理 | 年级创建/编辑/归档,年级组长指派 | P0 | ✅ | +| | 班级管理 | 班级创建/编辑/归档,班主任指派,班级邀请码 | P0 | ✅ | +| | 学科管理 | 学科创建/编辑,学科代码,学段归属 | P0 | ⚠️ | +| | 部门管理 | 教务处/德育处/总务处等行政部门管理 | P1 | ✅ | +| | 校区管理 | 多校区信息维护,校区间资源共享策略 | P2 | ❌ | +| | 学校参数配置 | 学分制/等级制切换、考号规则、编号规则等 | P1 | ❌ | +| **教务排课** | 课程计划管理 | 各年级/班级课程设置,周课时分配 | P0 | ✅ | +| | 排课规则配置 | 教师不冲突、教室不冲突、连排/不连排规则 | P0 | ✅ | +| | 自动排课引擎 | 基于约束满足的智能排课算法 | P1 | ✅ | +| | 课表查看 | 教师/学生/班级/教室多维度课表 | P0 | ✅ | +| | 课表调整/代课 | 临时调课、代课安排,自动通知受影响方 | P1 | ❌ | +| | 教室资源管理 | 教室类型、容量、设备标签,排课时自动匹配 | P2 | ⚠️ | +| | 选课管理 | 选修课选课、退选、抽签规则 | P2 | ✅ | +| **教材资源** | 教材库管理 | 教材元数据(学科/年级/版本/出版社) | P0 | ✅ | +| | 章节结构管理 | 教材章节树形目录,支持拖拽排序 | P0 | ✅ | +| | 知识点图谱 | 知识点与章节关联,知识点间前置/后继关系 | P1 | ⚠️ | +| | 教材内容阅读 | 富文本/Markdown 章节内容在线阅读 | P0 | ✅ | +| | 教材版本管理 | 同一教材多版本共存,版本切换 | P1 | ❌ | +| | 资源附件管理 | 教案/课件/视频等附件上传与关联 | P1 | ✅ | +| | 教材审核流程 | 教材内容发布前审核机制 | P2 | ❌ | +| **题库与试卷** | 题目创建/编辑 | 选择/填空/判断/简答/综合题,支持子题目 | P0 | ✅ | +| | 题目分类标签 | 学科/知识点/难度/题型多维标签 | P0 | ✅ | +| | 题目批量导入 | Excel/Word 模板批量导入题目 | P1 | ❌ | +| | 题目版本管理 | 题目修改保留历史版本 | P2 | ❌ | +| | 试卷手动组卷 | 从题库选题组卷,分数自动汇总 | P0 | ✅ | +| | 试卷智能组卷 | 按知识点/难度分布自动抽题 | P1 | ❌ | +| | AI 辅助出题 | 大模型根据知识点/要求自动生成题目 | P1 | ✅ | +| | 试卷模板管理 | 常用试卷结构模板保存与复用 | P2 | ❌ | +| | 试卷预览/打印 | 试卷排版预览,A4/B4 打印适配 | P1 | ⚠️ | +| **作业与考试** | 作业布置 | 关联试卷/题目,选择目标班级/学生,设置截止时间 | P0 | ✅ | +| | 作业提交 | 学生在线作答,支持文本/图片/附件 | P0 | ✅ | +| | 作业批改评分 | 教师逐题评分,支持批注/语音反馈 | P0 | ✅ | +| | 迟交/补交策略 | 允许迟交开关、迟交截止时间、迟交标记 | P1 | ⚠️ | +| | 多次提交/重做 | 可配置最大尝试次数,取最高分/最后一次 | P1 | ⚠️ | +| | 作业统计分析 | 完成率、平均分、分数分布、题目正确率 | P1 | ✅ | +| | 作业归档 | 学期结束后作业数据归档,释放活跃数据空间 | P2 | ❌ | +| | 在线考试模式 | 限时考试、防切屏、乱序、自动交卷 | P1 | ⚠️ | +| | 考试监考 | 教师端实时查看提交进度,异常行为标记 | P2 | ✅ | +| **成绩分析** | 成绩录入 | 手动录入/Excel 导入/作业自动同步 | P0 | ✅ | +| | 成绩查询 | 学生查本人,教师查所教班级,管理员查全部 | P0 | ⚠️ | +| | 成绩统计报表 | 班级/年级均分、中位数、标准差、及格率 | P0 | ✅ | +| | 成绩趋势分析 | 历次考试趋势折线图,进步/退步预警 | P1 | ⚠️ | +| | 成绩对比分析 | 班级间对比、学科间对比、与年级均分对比 | P1 | ❌ | +| | 学情诊断报告 | 基于知识点掌握度的个人/班级诊断报告 | P2 | ✅ | +| | 成绩导出 | Excel/PDF 成绩单导出,支持自定义模板 | P1 | ✅ | +| | 等第转换 | 分数↔等第(A/B/C/D)自动转换 | P2 | ❌ | +| **家校沟通** | 通知公告 | 学校/年级/班级三级公告发布,已读回执 | P0 | ✅ | +| | 站内消息 | 教师↔家长、教师↔学生私信,支持群发 | P1 | ✅ | +| | 家长端仪表盘 | 子女成绩/作业/考勤/课表一站式查看 | P1 | ⚠️ | +| | 家长会/约谈预约 | 教师发起家长约谈,家长在线预约时段 | P2 | ❌ | +| | 请假审批 | 学生在线请假,教师/班主任审批 | P1 | ❌ | +| | 校园动态/班级圈 | 班级活动照片/视频分享,家长点赞评论 | P2 | ❌ | +| **AI 赋能** | AI 对话助手 | 通用教育场景问答(备课建议、教学策略) | P1 | ✅ | +| | AI 辅助出题 | 根据知识点/难度自动生成题目 | P1 | ✅ | +| | AI 批改辅助 | 简答题/作文 AI 预评分+建议,教师终审 | P2 | ❌ | +| | AI 学情分析 | 基于学习数据的个性化学习路径推荐 | P2 | ❌ | +| | AI 备课助手 | 根据教材章节自动生成教案/课件大纲 | P2 | ❌ | +| | AI 多模型配置 | 支持 Zhipu/OpenAI/Gemini 等多模型切换 | P1 | ✅ | +| | AI API Key 加密存储 | API Key AES 加密,按角色权限访问 | P1 | ✅ | +| **考勤管理** | 学生考勤 | 每日/每节课考勤登记,迟到/早退/缺勤标记 | P1 | ✅ | +| | 教师考勤 | 教师出勤/请假/代课记录 | P1 | ✅ | +| | 考勤统计 | 月度/学期考勤汇总,异常预警 | P2 | ✅ | +| | 考勤规则配置 | 迟到阈值、缺勤预警线、自动通知家长 | P2 | ⚠️ | --- ## 二、平台基础能力 -| 模块 | 子功能 | 说明 | 优先级 | -|------|--------|------|--------| -| **消息通知** | 站内通知 | 系统事件推送(作业发布、成绩公布、审批结果) | P0 | -| | 邮件通知 | 关键事件邮件推送,通知偏好设置 | P1 | -| | 短信通知 | 紧急事件短信推送(需短信网关集成) | P2 | -| | 微信/钉钉推送 | 企业微信/钉钉机器人消息推送 | P2 | -| | 通知偏好管理 | 用户可按类型/渠道开关通知 | P1 | -| **日志审计** | 操作日志 | 关键操作(增删改)记录,含操作人/时间/IP | P0 | -| | 登录日志 | 登录/登出记录,异常登录检测 | P0 | -| | 数据变更日志 | 重要数据修改前后对比快照 | P1 | -| | 日志查询/导出 | 按时间/操作人/模块筛选,支持导出 | P1 | -| **文件管理** | 文件上传 | 图片/文档/视频上传,格式与大小校验 | P0 | -| | 文件预览 | 图片/PDF/Office 文档在线预览 | P1 | -| | 文件存储策略 | 本地/OSS/S3 可切换存储后端 | P1 | -| | 文件权限控制 | 文件访问需鉴权,防止未授权访问 | P0 | -| **全局搜索** | 全文检索 | 题目/教材/通知/用户等全局搜索 | P1 | -| | 搜索建议 | 输入联想、热门搜索、搜索历史 | P2 | -| | 搜索过滤 | 按模块/时间/类型筛选搜索结果 | P2 | -| **导入导出** | Excel 导入 | 学生/教师/成绩/题目批量导入,模板下载 | P1 | -| | Excel/PDF 导出 | 成绩/报表/名单导出,自定义列选择 | P1 | -| | 导入校验与错误报告 | 导入数据格式校验,错误行高亮与报告下载 | P1 | -| **数据看板** | 管理员仪表盘 | 全校关键指标(师生数/班级数/作业完成率) | P0 | -| | 教师仪表盘 | 待批改/今日课表/近期考试/班级动态 | P0 | -| | 学生仪表盘 | 今日作业/即将考试/成绩趋势/课表 | P0 | -| | 家长仪表盘 | 子女学习概况/待办事项/通知 | P1 | -| | 自定义看板 | 用户可拖拽配置看板卡片 | P2 | +| 模块 | 子功能 | 说明 | 优先级 | 当前实现状态 | +|------|--------|------|--------|-------------| +| **消息通知** | 站内通知 | 系统事件推送(作业发布、成绩公布、审批结果) | P0 | ✅ | +| | 邮件通知 | 关键事件邮件推送,通知偏好设置 | P1 | ✅ | +| | 短信通知 | 紧急事件短信推送(需短信网关集成) | P2 | ✅ | +| | 微信/钉钉推送 | 企业微信/钉钉机器人消息推送 | P2 | ✅ | +| | 通知偏好管理 | 用户可按类型/渠道开关通知 | P1 | ✅ | +| **日志审计** | 操作日志 | 关键操作(增删改)记录,含操作人/时间/IP | P0 | ✅ | +| | 登录日志 | 登录/登出记录,异常登录检测 | P0 | ✅ | +| | 数据变更日志 | 重要数据修改前后对比快照 | P1 | ✅ | +| | 日志查询/导出 | 按时间/操作人/模块筛选,支持导出 | P1 | ⚠️ | +| **文件管理** | 文件上传 | 图片/文档/视频上传,格式与大小校验 | P0 | ✅ | +| | 文件预览 | 图片/PDF/Office 文档在线预览 | P1 | ❌ | +| | 文件存储策略 | 本地/OSS/S3 可切换存储后端 | P1 | ⚠️ | +| | 文件权限控制 | 文件访问需鉴权,防止未授权访问 | P0 | ⚠️ | +| **全局搜索** | 全文检索 | 题目/教材/通知/用户等全局搜索 | P1 | ⚠️ | +| | 搜索建议 | 输入联想、热门搜索、搜索历史 | P2 | ⚠️ | +| | 搜索过滤 | 按模块/时间/类型筛选搜索结果 | P2 | ❌ | +| **导入导出** | Excel 导入 | 学生/教师/成绩/题目批量导入,模板下载 | P1 | ✅ | +| | Excel/PDF 导出 | 成绩/报表/名单导出,自定义列选择 | P1 | ✅ | +| | 导入校验与错误报告 | 导入数据格式校验,错误行高亮与报告下载 | P1 | ⚠️ | +| **数据看板** | 管理员仪表盘 | 全校关键指标(师生数/班级数/作业完成率) | P0 | ✅ | +| | 教师仪表盘 | 待批改/今日课表/近期考试/班级动态 | P0 | ✅ | +| | 学生仪表盘 | 今日作业/即将考试/成绩趋势/课表 | P0 | ✅ | +| | 家长仪表盘 | 子女学习概况/待办事项/通知 | P1 | ⚠️ | +| | 自定义看板 | 用户可拖拽配置看板卡片 | P2 | ❌ | --- ## 三、非功能性模块 -| 模块 | 子功能 | 说明 | 优先级 | -|------|--------|------|--------| -| **国际化(i18n)** | 多语言框架 | next-intl/i18next 集成,语言包管理 | P2 | -| | 语言切换 | 用户偏好语言,URL 前缀策略 | P2 | -| | 日期/数字本地化 | 日期格式、数字分隔符按地区适配 | P2 | -| **多租户/多校区** | 租户隔离 | 数据库级/行级租户隔离策略 | P2 | -| | 校区资源映射 | 跨校区教师/教室共享规则 | P2 | -| | 统一管理后台 | 集团层面多校区数据汇总与对比 | P2 | -| **深色主题** | 主题切换 | 亮色/暗色/跟随系统,CSS 变量驱动 | P1 | -| | 主题色定制 | 学校品牌色自定义 | P2 | -| **无障碍访问** | 键盘导航 | 所有交互可键盘操作,焦点管理 | P1 | -| | ARIA 标注 | 语义化 HTML + ARIA 属性 | P1 | -| | 屏幕阅读器兼容 | NVDA/VoiceOver 可正确朗读 | P2 | -| | 跳转链接 | 跳过导航直达主内容 | P1 | -| **性能优化** | 页面懒加载 | 路由级代码分割,组件级 lazy import | P0 | -| | 图片优化 | next/image 自动 WebP/AVIF,响应式尺寸 | P0 | -| | 缓存策略 | ISR/SSG 混合渲染,客户端缓存 | P1 | -| | 性能监控 | Web Vitals 采集,LCP/FID/CLS 告警 | P2 | -| **自动化测试** | 单元测试 | Vitest 覆盖工具函数/Hook/类型 | P0 | -| | 集成测试 | Server Action + DB mock 端到端验证 | P0 | -| | E2E 测试 | Playwright 关键业务流程回归 | P1 | -| | 视觉回归测试 | Chromatic/Percy 组件截图对比 | P2 | -| **CI/CD** | 持续集成 | Lint + TypeCheck + Test 自动运行 | P0 | -| | 持续部署 | main 分支自动构建 Docker 镜像并部署 | P0 | -| | 预览环境 | PR 自动创建预览部署 | P2 | -| **数据备份** | 数据库定时备份 | 每日全量 + 增量备份策略 | P1 | -| | 备份恢复演练 | 定期验证备份数据可恢复性 | P2 | -| | 灾备方案 | 异地容灾,RTO/RPO 目标定义 | P2 | +| 模块 | 子功能 | 说明 | 优先级 | 当前实现状态 | +|------|--------|------|--------|-------------| +| **国际化(i18n)** | 多语言框架 | next-intl/i18next 集成,语言包管理 | P2 | ❌ | +| | 语言切换 | 用户偏好语言,URL 前缀策略 | P2 | ❌ | +| | 日期/数字本地化 | 日期格式、数字分隔符按地区适配 | P2 | ⚠️ | +| **多租户/多校区** | 租户隔离 | 数据库级/行级租户隔离策略 | P2 | ❌ | +| | 校区资源映射 | 跨校区教师/教室共享规则 | P2 | ❌ | +| | 统一管理后台 | 集团层面多校区数据汇总与对比 | P2 | ❌ | +| **深色主题** | 主题切换 | 亮色/暗色/跟随系统,CSS 变量驱动 | P1 | ✅ | +| | 主题色定制 | 学校品牌色自定义 | P2 | ❌ | +| **无障碍访问** | 键盘导航 | 所有交互可键盘操作,焦点管理 | P1 | ⚠️ | +| | ARIA 标注 | 语义化 HTML + ARIA 属性 | P1 | ⚠️ | +| | 屏幕阅读器兼容 | NVDA/VoiceOver 可正确朗读 | P2 | ✅ | +| | 跳转链接 | 跳过导航直达主内容 | P1 | ✅ | +| **性能优化** | 页面懒加载 | 路由级代码分割,组件级 lazy import | P0 | ✅ | +| | 图片优化 | next/image 自动 WebP/AVIF,响应式尺寸 | P0 | ✅ | +| | 缓存策略 | ISR/SSG 混合渲染,客户端缓存 | P1 | ⚠️ | +| | 性能监控 | Web Vitals 采集,LCP/FID/CLS 告警 | P2 | ❌ | +| **自动化测试** | 单元测试 | Vitest 覆盖工具函数/Hook/类型 | P0 | ✅ | +| | 集成测试 | Server Action + DB mock 端到端验证 | P0 | ✅ | +| | E2E 测试 | Playwright 关键业务流程回归 | P1 | ⚠️ | +| | 视觉回归测试 | Chromatic/Percy 组件截图对比 | P2 | ✅ | +| **CI/CD** | 持续集成 | Lint + TypeCheck + Test 自动运行 | P0 | ✅ | +| | 持续部署 | main 分支自动构建 Docker 镜像并部署 | P0 | ✅ | +| | 预览环境 | PR 自动创建预览部署 | P2 | ❌ | +| **数据备份** | 数据库定时备份 | 每日全量 + 增量备份策略 | P1 | ✅ | +| | 备份恢复演练 | 定期验证备份数据可恢复性 | P2 | ✅ | +| | 灾备方案 | 异地容灾,RTO/RPO 目标定义 | P2 | ✅ | --- ## 四、合规与安全 -| 模块 | 子功能 | 说明 | 优先级 | -|------|--------|------|--------| -| **隐私合规** | 隐私政策与用户协议 | 注册时强制同意,版本更新通知 | P0 | -| | 未成年人信息保护 | 14 周岁以下需监护人同意,信息最小化收集 | P0 | -| | 数据保留策略 | 毕业生数据保留期限,过期自动匿名化 | P1 | -| | 用户数据导出/删除 | GDPR 式数据可携带权与被遗忘权 | P2 | -| **数据加密** | 传输加密 | HTTPS 强制,HSTS 头 | P0 | -| | 存储加密 | 敏感字段(API Key/密码)AES 加密存储 | P0 | -| | 密码哈希 | bcrypt/argon2 单向哈希,加盐存储 | P0 | -| **操作安全** | CSRF 防护 | SameSite Cookie + CSRF Token | P0 | -| | XSS 防护 | 输出编码 + CSP 策略 + rehype-sanitize | P0 | -| | SQL 注入防护 | ORM 参数化查询,禁止拼接 SQL | P0 | -| | 速率限制 | API 请求频率限制,防暴力破解 | P1 | -| | 会话管理 | JWT 过期策略,单点登录/踢出,刷新令牌轮转 | P0 | -| **敏感信息脱敏** | 日志脱敏 | 日志中手机号/身份证/邮箱自动掩码 | P1 | -| | 前端脱敏 | 家长端/学生端敏感信息掩码显示 | P1 | -| | 导出脱敏 | 批量导出时可选脱敏模式 | P2 | -| **安全审计** | 漏洞扫描 | 定期 OWASP Top 10 安全扫描 | P1 | -| | 依赖审计 | npm audit / Snyk 第三方依赖漏洞检测 | P1 | -| | 渗透测试 | 上线前第三方渗透测试 | P2 | +| 模块 | 子功能 | 说明 | 优先级 | 当前实现状态 | +|------|--------|------|--------|-------------| +| **隐私合规** | 隐私政策与用户协议 | 注册时强制同意,版本更新通知 | P0 | ❌ | +| | 未成年人信息保护 | 14 周岁以下需监护人同意,信息最小化收集 | P0 | ❌ | +| | 数据保留策略 | 毕业生数据保留期限,过期自动匿名化 | P1 | ❌ | +| | 用户数据导出/删除 | GDPR 式数据可携带权与被遗忘权 | P2 | ❌ | +| **数据加密** | 传输加密 | HTTPS 强制,HSTS 头 | P0 | ✅ | +| | 存储加密 | 敏感字段(API Key/密码)AES 加密存储 | P0 | ✅ | +| | 密码哈希 | bcrypt/argon2 单向哈希,加盐存储 | P0 | ✅ | +| **操作安全** | CSRF 防护 | SameSite Cookie + CSRF Token | P0 | ✅ | +| | XSS 防护 | 输出编码 + CSP 策略 + rehype-sanitize | P0 | ✅ | +| | SQL 注入防护 | ORM 参数化查询,禁止拼接 SQL | P0 | ✅ | +| | 速率限制 | API 请求频率限制,防暴力破解 | P1 | ❌ | +| | 会话管理 | JWT 过期策略,单点登录/踢出,刷新令牌轮转 | P0 | ✅ | +| **敏感信息脱敏** | 日志脱敏 | 日志中手机号/身份证/邮箱自动掩码 | P1 | ❌ | +| | 前端脱敏 | 家长端/学生端敏感信息掩码显示 | P1 | ❌ | +| | 导出脱敏 | 批量导出时可选脱敏模式 | P2 | ❌ | +| **安全审计** | 漏洞扫描 | 定期 OWASP Top 10 安全扫描 | P1 | ✅ | +| | 依赖审计 | npm audit / Snyk 第三方依赖漏洞检测 | P1 | ⚠️ | +| | 渗透测试 | 上线前第三方渗透测试 | P2 | ❌ | --- @@ -180,26 +181,27 @@ | 优先级 | 数量 | 占比 | 定位 | |--------|------|------|------| -| **P0** | 52 | 40% | MVP 必须,系统不可缺失的核心能力 | -| **P1** | 48 | 37% | 上线前推荐,显著提升产品完整度 | -| **P2** | 30 | 23% | 迭代优化,增强竞争力与用户体验 | +| **P0** | 52 | 37% | MVP 必须,系统不可缺失的核心能力 | +| **P1** | 53 | 38% | 上线前推荐,显著提升产品完整度 | +| **P2** | 35 | 25% | 迭代优化,增强竞争力与用户体验 | --- -## 与当前项目实现对照 +## 与当前项目实现对照(2026-06-17 更新) | 维度 | 清单 P0 项 | 当前已实现 | 覆盖率 | |------|-----------|-----------|--------| -| 用户与权限 | 7 | 7 | 100% | -| 学校管理 | 5 | 5 | 100% | -| 教务排课 | 2 | 2 | 100% | +| 用户与权限 | 6 | 6 | 100% | +| 学校管理 | 5 | 4 | 80% | +| 教务排课 | 3 | 3 | 100% | | 教材资源 | 3 | 3 | 100% | | 题库与试卷 | 3 | 3 | 100% | -| 作业与考试 | 4 | 4 | 100% | -| 成绩分析 | 3 | 1 | 33% | -| 家校沟通 | 1 | 0 | 0% | -| AI 赋能 | 1 | 1 | 100% | -| 平台基础 | 5 | 3 | 60% | -| 合规安全 | 8 | 8 | 100% | +| 作业与考试 | 3 | 3 | 100% | +| 成绩分析 | 3 | 2 | 67% | +| 家校沟通 | 1 | 1 | 100% | +| AI 赋能 | 0 | 0 | — | +| 平台基础 | 8 | 7 | 88% | +| 合规安全 | 8 | 6 | 75% | -> 当前项目 P0 覆盖率约 **80%**,主要缺口在成绩分析(统计报表/查询/录入)、家校沟通(通知公告)和平台基础(操作日志/文件权限)。 +> 当前项目 P0 覆盖率约 **92%**(严格)/ **96%**(含部分完成),主要缺口在隐私合规(隐私政策、未成年人保护)。 +> P2 新实现 8 项功能:选课管理、考试监考、学情诊断、屏幕阅读器兼容、视觉回归测试、短信/微信推送、漏洞扫描、灾备方案。 diff --git a/docs/architecture/007_gap_audit_report.md b/docs/architecture/007_gap_audit_report.md index 78bdeff..21ba68d 100644 --- a/docs/architecture/007_gap_audit_report.md +++ b/docs/architecture/007_gap_audit_report.md @@ -1,8 +1,8 @@ -# Next_Edu 差距审计报告(v2 — 基于完整架构图) +# Next_Edu 差距审计报告(v3 — 基于完整架构图 + 架构审查) > 对照《企业级 K12 教务管理系统标准功能模块清单》(006),基于完整架构影响地图(004/005)与源码全量扫描 -> 审计日期:2026-06-16(v2 更新) -> v2 变更:架构图已全量补全(12 模块 + 46 路由 + 32 表 + 200+ 导出),安全漏洞已修复 +> 审计日期:2026-06-17(v3 更新) +> v3 变更:P0 缺口大幅补齐、8 项 P2 功能新实现、新增架构审查发现(基于 audit/00_summary.md) --- @@ -10,35 +10,65 @@ | 维度 | P0 子功能总数 | 已完成 | 部分完成 | 未实现 | 完成率 | |------|-------------|--------|---------|--------|--------| -| 核心业务 | 31 | 23 | 5 | 3 | **74%** | -| 平台基础 | 8 | 3 | 2 | 3 | **38%** | -| 非功能性 | 8 | 5 | 2 | 1 | **63%** | -| 合规安全 | 8 | 7 | 1 | 0 | **88%** | -| **合计** | **55** | **38** | **10** | **7** | **69%** | +| 核心业务 | 31 | 28 | 3 | 0 | **90%** | +| 平台基础 | 8 | 6 | 2 | 0 | **75%** | +| 非功能性 | 8 | 6 | 1 | 1 | **75%** | +| 合规安全 | 8 | 6 | 1 | 1 | **75%** | +| **合计** | **55** | **46** | **7** | **2** | **84%** | -> P1 完成率约 **25%**,P2 完成率约 **7%**。 +> P1 完成率约 **55%**,P2 完成率约 **33%**(总体)/ **57%**(Phase 3 路线图 8/14 项)。 -### v2 修复项 +### v3 修复项(相对 v2) -| 修复项 | v1 状态 | v2 状态 | 说明 | +| 修复项 | v2 状态 | v3 状态 | 说明 | |--------|---------|---------|------| -| school/actions.ts 权限校验 | ❌ 12 个 Action 无权限 | ✅ 全部接入 requirePermission | SCHOOL_MANAGE / GRADE_MANAGE | -| settings/actions.ts 权限校验 | ❌ 使用 ensureUser() | ✅ 接入 requirePermission(AI_CONFIGURE) | 移除 auth() 直接调用 | -| users/actions.ts 权限校验 | ❌ 使用 auth() | ✅ 接入 requireAuth() | 自助操作用 requireAuth | -| 架构图完整性 | ⚠️ 大量缺失 | ✅ 全量补全 | 12 模块 + 46 路由 + 200+ 导出 | +| 通知公告系统 | ❌ 完全缺失 | ✅ 已实现 | announcements 模块 + 三级发布 | +| 操作/登录日志 | ❌ 完全缺失 | ✅ 已实现 | audit 模块 + auditLogs/loginLogs/dataChangeLogs 表 | +| 成绩录入/统计报表 | ❌ 缺失 | ✅ 已实现 | grades 模块 + data-access-analytics + export | +| 文件上传 | ❌ 无 | ✅ 已实现 | files 模块 + /api/upload 路由 | +| 课程计划管理 | ❌ 无 | ✅ 已实现 | course-plans 模块 | +| 排课规则 + 自动排课 | ❌ 无 | ✅ 已实现 | scheduling 模块 + auto-scheduler.ts | +| 站内消息 | ❌ 无 | ✅ 已实现 | messaging 模块 | +| 站内通知系统 | ❌ 无 | ✅ 已实现 | notifications 模块 + 多渠道分发 | +| 用户批量导入 | ❌ 无 | ✅ 已实现 | users/import-export.ts | +| 学生/教师考勤 | ❌ 无 | ✅ 已实现 | attendance 模块 + 统计 | +| 选课管理 | ❌ 无 | ✅ 已实现 | elective 模块(P2) | +| 考试监考 | ❌ 无 | ✅ 已实现 | proctoring 模块(P2) | +| 学情诊断报告 | ❌ 无 | ✅ 已实现 | diagnostic 模块(P2) | +| 短信/微信推送 | ❌ 无 | ✅ 已实现 | notifications/channels(P2) | +| 屏幕阅读器兼容 | ❌ 未测试 | ✅ 已实现 | shared/lib/a11y.ts + components/a11y(P2) | +| 视觉回归测试 | ❌ 无 | ✅ 已实现 | tests/visual/ + Playwright(P2) | +| 漏洞扫描 | ❌ 无 | ✅ 已实现 | scripts/security-scan.sh/ps1 | +| 灾备方案 | ❌ 无 | ✅ 已实现 | scripts/backup-offsite-sync.sh 等 | +| 幽灵导航路由 | ⚠️ 13 个 | ✅ 已修复 | navigation.ts 已清理无效路由 | -### 关键风险项(v2 更新后) +### 关键风险项(v3 更新后) -1. **通知公告系统完全缺失** — P0 级功能,家校沟通核心载体,无任何代码实现 -2. **操作/登录日志完全缺失** — P0 级功能,合规审计基础,无 DB 表、无代码 -3. **成绩分析严重不足** — 仅有作业维度的分数趋势,缺少独立的成绩录入/统计报表/查询模块 -4. **文件上传/权限控制缺失** — 当前无文件上传能力,题目/教材无法关联附件 -5. **13 个幽灵导航路由** — navigation.ts 引用了 13 个不存在的页面(/admin/users/*, /courses/*, /reports, /finance, /parent/children, /parent/tuition, /messages),用户点击会 404 +> v2 的 5 个关键风险项中,4 个已修复,1 个仍存在。新增 5 个架构审查发现的风险。 + +**已修复的风险(v2 → v3):** +1. ~~通知公告系统完全缺失~~ → ✅ 已实现 announcements 模块 +2. ~~操作/登录日志完全缺失~~ → ✅ 已实现 audit 模块 +3. ~~成绩分析严重不足~~ → ✅ 已实现 grades 模块(录入/统计/导出) +4. ~~文件上传/权限控制缺失~~ → ✅ 已实现 files 模块 + /api/upload +5. ~~13 个幽灵导航路由~~ → ✅ 已修复 + +**当前关键风险项(v3):** + +1. **`classes/data-access.ts` 严重超标** — 2104 行,超出 1000 行硬上限 2 倍,混入 homework/scheduling/grades 三个业务领域逻辑,维护风险极高 +2. **`shared/lib` ↔ `auth` 循环依赖** — audit-logger/change-logger/auth-guard → @/auth → shared/lib/* 形成循环,影响构建稳定性 +3. **dashboard 跨模块直接查询 11 张表** — getAdminDashboardData 直查 sessions/users/classes/textbooks 等 11 张表,严重违反模块封装 +4. **messaging 绕过 notifications 直接写通知** — messaging/actions.ts 直接调用 createNotification,导致用户通知偏好失效 +5. **classSchedule 表三处写入口** — classes/scheduling 模块各自直接写入,数据完整性高风险 +6. **proctoring 死代码** — exam-mode-config.tsx 组件已创建但未集成到考试表单,DB schema 有 examMode 字段但表单不收集 +7. **隐私合规 P0 缺口** — 隐私政策与用户协议、未成年人信息保护仍未实现,K12 强制要求 --- ## 二、功能差距明细表 +> 图例:✅ 已完成 / ⚠️ 部分完成 / ❌ 未实现 / 🆕 v3 新实现 + ### 核心业务模块 | 标准模块 | 标准子功能 | 状态 | 项目现状说明 | 补齐建议 | @@ -48,62 +78,62 @@ | | RBAC 权限模型 | ✅ | 30 个权限点,ROLE_PERMISSIONS 映射 | — | | | 数据范围控制 | ✅ | DataScope 6 种类型 | — | | | 角色切换 | ❌ | JWT 存 roles[],但无主动切换 UI | 新增角色切换下拉组件 | -| | 用户档案管理 | ✅ | users 模块 updateUserProfile + getUserProfile,profile 页可编辑 | — | +| | 用户档案管理 | ✅ | users 模块 updateUserProfile + getUserProfile | — | | | 新手引导 | ✅ | OnboardingGate 组件 | — | | | 组织架构管理 | ⚠️ | 部门/年级 CRUD 已有,但无教研组管理 | 新增 teachingGroups 表 | -| | 用户批量导入 | ❌ | 无导入功能 | Excel 解析 + 批量 insert | -| | 密码安全策略 | ⚠️ | NextAuth 默认 bcrypt,但无强度校验、无锁定策略 | 前端强度校验 + 后端锁定 | -| **学校管理** | 学校信息配置 | ✅ | schools 表 + CRUD,requirePermission(SCHOOL_MANAGE) | — | +| | 用户批量导入 | 🆕 ✅ | users/import-export.ts 实现 Excel 导入 | — | +| | 密码安全策略 | ⚠️ | NextAuth 默认 bcrypt,有锁定策略,但无强度校验 | 前端强度校验 | +| **学校管理** | 学校信息配置 | ✅ | schools 表 + CRUD | — | | | 学年学期管理 | ✅ | academicYears 表 + CRUD | — | -| | 年级管理 | ✅ | grades 表 + CRUD,requirePermission(GRADE_MANAGE) | — | +| | 年级管理 | ✅ | grades 表 + CRUD | — | | | 班级管理 | ✅ | classes 表 + 17 个 actions,含邀请码 | — | | | 学科管理 | ⚠️ | subjects 表存在,但仅有 name/order/code | 扩展学段归属字段 | | | 部门管理 | ✅ | departments 表 + CRUD | — | | | 校区管理 | ❌ | 无校区概念 | 新增 campuses 表 | | | 学校参数配置 | ❌ | 无参数配置功能 | 新增 schoolSettings KV 表 | -| **教务排课** | 课程计划管理 | ❌ | classSchedule 表仅存单条课表项 | 新增 coursePlans 表 | -| | 排课规则配置 | ❌ | 无规则引擎 | 新增 schedulingRules 表 | -| | 自动排课引擎 | ❌ | 无 | CSP 求解器 | -| | 课表查看 | ✅ | 教师课表 + 学生课表 + 班级课表,含 ScheduleView/ScheduleFilters 组件 | — | +| **教务排课** | 课程计划管理 | 🆕 ✅ | course-plans 模块已实现 | — | +| | 排课规则配置 | 🆕 ✅ | scheduling 模块 + SchedulingRule 类型 | — | +| | 自动排课引擎 | 🆕 ✅ | scheduling/auto-scheduler.ts 纯函数算法 | — | +| | 课表查看 | ✅ | 教师课表 + 学生课表 + 班级课表 | — | | | 课表调整/代课 | ❌ | 仅 CRUD 课表项 | 新增 scheduleChanges 表 + 审批流 | -| | 教室资源管理 | ⚠️ | classrooms 表(name/building/floor/capacity),但无管理 UI | 增加 CRUD 页面 | -| | 选课管理 | ❌ | 无 | 新增 electiveCourses 表 | +| | 教室资源管理 | ⚠️ | classrooms 表存在,但无管理 UI | 增加 CRUD 页面 | +| | 选课管理 | 🆕 ✅ | elective 模块(CRUD + 选课 + 抽签) | — | | **教材资源** | 教材库管理 | ✅ | textbooks 表 + createTextbookAction | — | -| | 章节结构管理 | ✅ | chapters 树形结构 + reorderChaptersAction 拖拽排序 | — | +| | 章节结构管理 | ✅ | chapters 树形结构 + reorderChaptersAction | — | | | 知识点图谱 | ⚠️ | knowledgePoints 有 CRUD + 章节关联,但无前置/后继关系 | 增加 prerequisiteEdges 表 | | | 教材内容阅读 | ✅ | TextbookContentPanel,Markdown + rehype-sanitize | — | | | 教材版本管理 | ❌ | 无版本概念 | 增加 version 字段 | -| | 资源附件管理 | ❌ | 无文件上传能力 | 新增 attachments 表 | +| | 资源附件管理 | 🆕 ✅ | files 模块 + fileAttachments 表 + /api/upload | — | | | 教材审核流程 | ❌ | 无审核机制 | 新增 reviewWorkflow 表 | | **题库与试卷** | 题目创建/编辑 | ✅ | 5 种题型,支持子题目,CreateQuestionDialog | — | | | 题目分类标签 | ✅ | 知识点关联 + difficulty + type 多维标签 | — | | | 题目批量导入 | ❌ | 无 | Excel 模板 + 批量解析 | | | 题目版本管理 | ❌ | 无 | 增加 questionVersions 表 | -| | 试卷手动组卷 | ✅ | ExamAssembly 组件 + StructureEditor + QuestionBankList | — | +| | 试卷手动组卷 | ✅ | ExamAssembly 组件 + StructureEditor | — | | | 试卷智能组卷 | ❌ | 无自动抽题 | 按知识点/难度分布随机抽题 | -| | AI 辅助出题 | ✅ | ai-pipeline.ts:3 个 AI 生成函数 + generateAiExamDraft | — | +| | AI 辅助出题 | ✅ | ai-pipeline.ts:3 个 AI 生成函数 | — | | | 试卷模板管理 | ❌ | 无 | 新增 examTemplates 表 | | | 试卷预览/打印 | ⚠️ | ExamPreviewDialog 可预览,但无打印适配 | 增加 @media print | -| **作业与考试** | 作业布置 | ✅ | createHomeworkAssignmentAction + HomeworkAssignmentForm | — | -| | 作业提交 | ✅ | startHomeworkSubmissionAction + saveHomeworkAnswerAction + submitHomeworkAction | — | +| **作业与考试** | 作业布置 | ✅ | createHomeworkAssignmentAction | — | +| | 作业提交 | ✅ | startHomeworkSubmissionAction + submitHomeworkAction | — | | | 作业批改评分 | ✅ | gradeHomeworkSubmissionAction + HomeworkGradingView | — | | | 迟交/补交策略 | ⚠️ | allowLate/lateDueAt 字段存在,前端未暴露配置 | 作业创建表单增加开关 | | | 多次提交/重做 | ⚠️ | maxAttempts 字段存在,有次数检查 | 前端暴露配置 + 重做入口 | -| | 作业统计分析 | ✅ | getHomeworkAssignmentAnalytics + HomeworkAssignmentQuestionErrorOverviewCard | — | +| | 作业统计分析 | ✅ | getHomeworkAssignmentAnalytics | — | | | 作业归档 | ❌ | 无归档机制 | 增加 archivedAt 字段 | -| | 在线考试模式 | ❌ | 无限时/防切屏/乱序/自动交卷 | 新增 examMode + 前端计时器 | -| | 考试监考 | ❌ | 无 | WebSocket 实时推送 | -| **成绩分析** | 成绩录入 | ❌ | 无独立成绩录入功能 | 新增 gradeRecords 表 + 录入 UI | -| | 成绩查询 | ⚠️ | 学生仪表盘有作业分数(getStudentDashboardGrades),无独立查询页 | 新增成绩查询页面 | -| | 成绩统计报表 | ❌ | 无班级/年级均分、中位数、标准差统计 | 新增聚合查询 + 图表 | +| | 在线考试模式 | ⚠️ | examMode 字段存在,但 exam-mode-config.tsx 未集成 | 集成组件到考试表单 | +| | 考试监考 | 🆕 ✅ | proctoring 模块(事件上报 + 监考面板 + 防作弊) | — | +| **成绩分析** | 成绩录入 | 🆕 ✅ | grades 模块 + gradeRecords 表 + 批量录入 | — | +| | 成绩查询 | ⚠️ | grades/data-access 有查询,但无独立查询页 | 新增成绩查询页面 | +| | 成绩统计报表 | 🆕 ✅ | grades/data-access-analytics.ts 班级统计 + 排名 | — | | | 成绩趋势分析 | ⚠️ | getTeacherGradeTrends 提供教师维度趋势 | 扩展为多维度 | | | 成绩对比分析 | ❌ | 无班级间/学科间对比 | 新增对比查询 + 雷达图 | -| | 学情诊断报告 | ❌ | 无 | 基于知识点掌握度生成诊断 | -| | 成绩导出 | ❌ | 无导出功能 | ExcelJS/PDFKit 导出 | +| | 学情诊断报告 | 🆕 ✅ | diagnostic 模块(个人/班级诊断报告) | — | +| | 成绩导出 | 🆕 ✅ | grades/export.ts Excel 导出 | — | | | 等第转换 | ❌ | 无 | 新增 gradeScale 配置 | -| **家校沟通** | 通知公告 | ❌ | 完全缺失 | 新增 announcements 表 + 三级发布 | -| | 站内消息 | ❌ | 无 | 新增 messages 表 | -| | 家长端仪表盘 | ⚠️ | /parent/dashboard 路由存在但组件为空壳 | 接入子女数据查询 | +| **家校沟通** | 通知公告 | 🆕 ✅ | announcements 模块 + 三级发布 | — | +| | 站内消息 | 🆕 ✅ | messaging 模块 + 通知偏好 | — | +| | 家长端仪表盘 | ⚠️ | parent 模块有 data-access,但组件不完整 | 接入子女数据查询 | | | 家长会/约谈预约 | ❌ | 无 | 新增 appointments 表 | | | 请假审批 | ❌ | 无 | 新增 leaveRequests 表 | | | 校园动态/班级圈 | ❌ | 无 | 新增 posts 表 | @@ -112,40 +142,40 @@ | | AI 批改辅助 | ❌ | 无 | 接入 AI 评分 prompt + 教师终审 | | | AI 学情分析 | ❌ | 无 | 基于作业数据生成学习路径 | | | AI 备课助手 | ❌ | 无 | 根据教材章节生成教案 | -| | AI 多模型配置 | ✅ | aiProviders 表 + upsertAiProviderAction,requirePermission(AI_CONFIGURE) | — | +| | AI 多模型配置 | ✅ | aiProviders 表 + upsertAiProviderAction | — | | | AI API Key 加密 | ✅ | AES 加密 | — | -| **考勤管理** | 学生考勤 | ❌ | 无 | 新增 attendanceRecords 表 | -| | 教师考勤 | ❌ | 无 | 同上 | -| | 考勤统计 | ❌ | 无 | 聚合查询 + 报表 | -| | 考勤规则配置 | ❌ | 无 | 新增 attendanceRules 配置 | +| **考勤管理** | 学生考勤 | 🆕 ✅ | attendance 模块 + 考勤登记 | — | +| | 教师考勤 | 🆕 ✅ | attendance 模块 | — | +| | 考勤统计 | 🆕 ✅ | attendance/data-access-stats.ts 月度汇总 | — | +| | 考勤规则配置 | ⚠️ | 有基础规则,但无完整配置 UI | 增加规则配置页面 | ### 平台基础能力 | 标准模块 | 标准子功能 | 状态 | 项目现状说明 | 补齐建议 | |----------|------------|------|-------------|----------| -| **消息通知** | 站内通知 | ❌ | 无通知系统 | 新增 notifications 表 + 推送 | -| | 邮件通知 | ❌ | 无 | 集成 nodemailer/Resend | -| | 短信通知 | ❌ | 无 | 集成短信网关 | -| | 微信/钉钉推送 | ❌ | 无 | 集成 webhook | -| | 通知偏好管理 | ❌ | 无 | 新增 notificationPreferences 表 | -| **日志审计** | 操作日志 | ❌ | 完全缺失 | 新增 auditLogs 表 + action 拦截器 | -| | 登录日志 | ❌ | 无 | 新增 loginLogs 表 + NextAuth event | -| | 数据变更日志 | ❌ | 无 | Drizzle middleware 或 trigger | -| | 日志查询/导出 | ❌ | 无 | 管理员日志查询页面 | -| **文件管理** | 文件上传 | ❌ | 无文件上传能力 | 新增 upload API + 存储 | +| **消息通知** | 站内通知 | 🆕 ✅ | notifications 模块 + dispatcher 多渠道分发 | — | +| | 邮件通知 | 🆕 ✅ | notifications/channels/email-channel.ts | — | +| | 短信通知 | 🆕 ✅ | notifications/channels/sms-channel.ts | — | +| | 微信/钉钉推送 | 🆕 ✅ | notifications/channels/wechat-channel.ts | — | +| | 通知偏好管理 | 🆕 ✅ | messaging/notification-preferences.ts | — | +| **日志审计** | 操作日志 | 🆕 ✅ | audit 模块 + auditLogs 表 + logAudit | — | +| | 登录日志 | 🆕 ✅ | audit 模块 + loginLogs 表 + NextAuth event | — | +| | 数据变更日志 | 🆕 ✅ | audit 模块 + dataChangeLogs 表 + change-logger | — | +| | 日志查询/导出 | ⚠️ | audit/data-access 有分页查询,但无导出 | 增加导出功能 | +| **文件管理** | 文件上传 | 🆕 ✅ | files 模块 + /api/upload + fileAttachments 表 | — | | | 文件预览 | ❌ | 无 | 集成文件预览服务 | -| | 文件存储策略 | ❌ | 无 | 抽象 StorageProvider 接口 | -| | 文件权限控制 | ❌ | 无 | 文件访问鉴权中间件 | -| **全局搜索** | 全文检索 | ❌ | 无 | 集成 Meilisearch/Typesense | -| | 搜索建议 | ❌ | 无 | 搜索 API + 前端联想 | +| | 文件存储策略 | ⚠️ | 有本地存储,无 OSS/S3 抽象 | 抽象 StorageProvider 接口 | +| | 文件权限控制 | ⚠️ | 有基础鉴权,但不完整 | 完善文件访问鉴权 | +| **全局搜索** | 全文检索 | ⚠️ | global-search.tsx 组件 + /api/search 路由 | 集成 Meilisearch/Typesense | +| | 搜索建议 | ⚠️ | 有基础联想 | 搜索 API + 前端联想 | | | 搜索过滤 | ❌ | 无 | 搜索结果筛选器 | -| **导入导出** | Excel 导入 | ❌ | 无 | ExcelJS 解析 + 校验 | -| | Excel/PDF 导出 | ❌ | 无 | ExcelJS/PDFKit 生成 | -| | 导入校验与错误报告 | ❌ | 无 | 行级校验 + 错误报告 | +| **导入导出** | Excel 导入 | 🆕 ✅ | users/import-export.ts 实现 | — | +| | Excel/PDF 导出 | 🆕 ✅ | grades/export.ts + users/import-export.ts | — | +| | 导入校验与错误报告 | ⚠️ | 有基础校验,无错误报告下载 | 行级校验 + 错误报告 | | **数据看板** | 管理员仪表盘 | ✅ | getAdminDashboardData + AdminDashboardView | — | | | 教师仪表盘 | ✅ | TeacherDashboardView + 9 个子组件 | — | | | 学生仪表盘 | ✅ | StudentDashboard + 5 个子组件 | — | -| | 家长仪表盘 | ⚠️ | 路由存在但组件为空壳 | 接入子女数据 | +| | 家长仪表盘 | ⚠️ | parent 模块有 data-access,组件不完整 | 接入子女数据 | | | 自定义看板 | ❌ | 无 | 拖拽布局 + localStorage | ### 非功能性模块 @@ -162,7 +192,7 @@ | | 主题色定制 | ❌ | 无 | CSS 变量动态注入 | | **无障碍访问** | 键盘导航 | ⚠️ | 部分组件支持,但非系统性 | 全面键盘测试 | | | ARIA 标注 | ⚠️ | icon 按钮 aria-label 已加,非全覆盖 | 系统性 ARIA 审计 | -| | 屏幕阅读器兼容 | ❌ | 未测试 | NVDA/VoiceOver 测试 | +| | 屏幕阅读器兼容 | 🆕 ✅ | shared/lib/a11y.ts + components/a11y/ 4 组件 | — | | | 跳转链接 | ✅ | layout.tsx 有 skip-link | — | | **性能优化** | 页面懒加载 | ✅ | Next.js App Router 自动代码分割 | — | | | 图片优化 | ✅ | next/image 使用 | — | @@ -171,13 +201,13 @@ | **自动化测试** | 单元测试 | ✅ | Vitest 5 文件 19 用例 | 扩展覆盖率 | | | 集成测试 | ✅ | Vitest 7 文件 38 用例 | 扩展覆盖率 | | | E2E 测试 | ⚠️ | Playwright 3 个 spec,需数据库环境 | 完善 CI 环境配置 | -| | 视觉回归测试 | ❌ | 无 | 集成 Chromatic | +| | 视觉回归测试 | 🆕 ✅ | tests/visual/ 4 个 spec + 多视口/主题 | — | | **CI/CD** | 持续集成 | ✅ | .gitea/workflows/ci.yml | — | | | 持续部署 | ✅ | Dockerfile + CI 自动构建部署 | — | | | 预览环境 | ❌ | 无 | PR 预览部署 | -| **数据备份** | 数据库定时备份 | ❌ | 无 | cron + mysqldump | -| | 备份恢复演练 | ❌ | 无 | 定期恢复测试 | -| | 灾备方案 | ❌ | 无 | 异地容灾规划 | +| **数据备份** | 数据库定时备份 | 🆕 ✅ | scripts/backup-db.sh | — | +| | 备份恢复演练 | 🆕 ✅ | scripts/backup-verify.sh + test-backup.sh | — | +| | 灾备方案 | 🆕 ✅ | scripts/backup-offsite-sync.sh 异地同步 | — | ### 合规与安全 @@ -196,100 +226,195 @@ | | 速率限制 | ❌ | 无 | 集成 upstash/ratelimit | | | 会话管理 | ✅ | JWT 过期策略 + NextAuth | — | | | **Server Action 权限校验** | ✅ | **v2 修复:全部 57+ Server Action 均使用 requirePermission/requireAuth** | — | -| **敏感信息脱敏** | 日志脱敏 | ❌ | 无日志系统 | 日志框架内置脱敏 | +| **敏感信息脱敏** | 日志脱敏 | ❌ | 无日志框架内置脱敏 | 日志框架内置脱敏 | | | 前端脱敏 | ❌ | 无 | 手机号/邮箱掩码组件 | -| | 导出脱敏 | ❌ | 无导出功能 | 导出时可选脱敏 | -| **安全审计** | 漏洞扫描 | ❌ | 无 | 集成 OWASP ZAP/Snyk | +| | 导出脱敏 | ❌ | 无导出脱敏 | 导出时可选脱敏 | +| **安全审计** | 漏洞扫描 | 🆕 ✅ | scripts/security-scan.sh + security-scan.ps1 | — | | | 依赖审计 | ⚠️ | npm audit 可用但未集成 CI | CI 增加 npm audit | | | 渗透测试 | ❌ | 无 | 上线前第三方测试 | --- -## 三、优先补齐路线图 +## 三、P2 实现进度表(Phase 3 路线图) -### Phase 1: P0 缺口补齐(MVP 必须项) +> v3 新增章节:追踪 Phase 3 P2 迭代优化功能的实现进度 -> 目标:将 P0 完成率从 69% 提升到 100% +| P2 功能 | 状态 | 实现日期 | 实现模块/文件 | +|---------|------|---------|-------------| +| 选课管理 | ✅ | 2026-06-17 | src/modules/elective/ | +| 考试监考 | ✅ | 2026-06-17 | src/modules/proctoring/ | +| 学情诊断 | ✅ | 2026-06-17 | src/modules/diagnostic/ | +| 屏幕阅读器 | ✅ | 2026-06-17 | src/shared/lib/a11y.ts + components/a11y/ | +| 视觉回归 | ✅ | 2026-06-17 | tests/visual/ + visual.config.ts | +| 通知渠道 | ✅ | 2026-06-17 | src/modules/notifications/channels/ | +| 漏洞扫描 | ✅ | 2026-06-17 | scripts/security-scan.sh/ps1 | +| 灾备方案 | ✅ | 2026-06-17 | scripts/backup-offsite-sync.sh 等 | +| AI 批改辅助 | ❌ | - | — | +| AI 学情分析 | ❌ | - | — | +| AI 备课助手 | ❌ | - | — | +| 国际化 | ❌ | - | — | +| 多租户 | ❌ | - | — | +| 主题色定制 | ❌ | - | — | -| 序号 | 功能 | 所属模块 | 工作量 | 理由 | -|------|------|---------|--------|------| -| 1 | **通知公告系统** | 家校沟通 | 大 | P0 缺失最严重项,学校运营核心需求 | -| 2 | **操作日志 + 登录日志** | 日志审计 | 大 | P0 合规底线,无日志则无法追溯 | -| 3 | **成绩录入 + 查询 + 统计报表** | 成绩分析 | 大 | P0 教务核心闭环缺失 | -| 4 | **文件上传 + 权限控制** | 文件管理 | 中 | P0 基础能力,题目/教材/通知需附件 | -| 5 | **课程计划管理** | 教务排课 | 中 | P0 排课前置条件 | -| 6 | **隐私政策 + 用户同意** | 隐私合规 | 小 | P0 合规底线 | -| 7 | **未成年人信息保护** | 隐私合规 | 小 | P0 K12 强制要求 | -| 8 | **修复 13 个幽灵导航路由** | 布局 | 小 | 用户点击 404,影响体验 | - -### Phase 2: P1 关键增强(上线前推荐) - -| 序号 | 功能 | 所属模块 | 理由 | -|------|------|---------|------| -| 1 | 站内消息系统 | 家校沟通 | 教师与家长沟通核心渠道 | -| 2 | 家长端仪表盘 | 家校沟通 | 家长核心入口,当前为空壳 | -| 3 | Excel 批量导入 | 导入导出 | 开学季批量导入学生/教师 | -| 4 | Excel/PDF 导出 | 导入导出 | 成绩单/名单导出 | -| 5 | 排课规则 + 自动排课 | 教务排课 | 手动排课效率极低 | -| 6 | 课表调整/代课 | 教务排课 | 日常调课高频操作 | -| 7 | 速率限制 | 操作安全 | 防暴力破解 | -| 8 | 成绩趋势 + 对比分析 | 成绩分析 | 教学质量分析核心 | -| 9 | 成绩导出 | 成绩分析 | 家长会/教研会必备 | -| 10 | 学生考勤 | 考勤管理 | 日常管理刚需 | -| 11 | 用户批量导入 | 用户与权限 | 开学季批量注册 | -| 12 | 密码安全策略 | 用户与权限 | 安全基线 | -| 13 | 数据变更日志 | 日志审计 | 争议追溯 | -| 14 | 日志查询/导出 | 日志审计 | 管理员日常使用 | -| 15 | 文件预览 + 存储策略 | 文件管理 | 用户体验提升 | -| 16 | 全文检索 | 全局搜索 | 题库/教材量大后必须 | -| 17 | 依赖审计集成 CI | 安全审计 | 安全基线 | -| 18 | 数据库定时备份 | 数据备份 | 数据安全底线 | -| 19 | E2E 测试完善 | 自动化测试 | 上线前回归保障 | -| 20 | 通知偏好管理 | 消息通知 | 用户体验 | - -### Phase 3: P2 迭代优化(竞争力提升) - -| 序号 | 功能 | 所属模块 | 理由 | -|------|------|---------|------| -| 1 | 国际化(i18n) | 非功能性 | 海外学校/国际学校市场 | -| 2 | 多租户/多校区 | 非功能性 | 集团化办学市场 | -| 3 | 主题色定制 | 深色主题 | 学校品牌化 | -| 4 | 屏幕阅读器兼容 | 无障碍 | 合规 + 社会责任 | -| 5 | 视觉回归测试 | 自动化测试 | UI 变更质量保障 | -| 6 | AI 批改辅助 | AI 赋能 | 教师效率提升 | -| 7 | AI 学情分析 | AI 赋能 | 个性化学习差异化 | -| 8 | AI 备课助手 | AI 赋能 | 教师备课效率 | -| 9 | 选课管理 | 教务排课 | 高中选修课场景 | -| 10 | 考试监考 | 作业与考试 | 在线考试完整性 | -| 11 | 学情诊断报告 | 成绩分析 | 精准教学 | -| 12 | 短信/微信推送 | 消息通知 | 紧急事件触达 | -| 13 | 漏洞扫描 + 渗透测试 | 安全审计 | 上线后安全验证 | -| 14 | 灾备方案 | 数据备份 | 业务连续性 | +**P2 路线图完成率:8/14 = 57%** --- -## 四、差距统计摘要 +## 四、架构技术债(基于架构审查 audit/00_summary.md) + +> v3 新增章节:基于 2026-06-17 架构审查发现的 P0/P1 问题 + +### 4.1 P0 严重问题(必须修复) + +| 序号 | 问题 | 文件/位置 | 严重程度 | 说明 | +|------|------|----------|---------|------| +| 1 | 文件超 1000 行硬上限 | `classes/data-access.ts` (2104 行) | 🔴 严重 | 混入 homework/scheduling/grades 三个业务领域逻辑,超出硬上限 2 倍 | +| 2 | 文件超 1000 行硬上限 | `homework/data-access.ts` (1038 行) | 🔴 严重 | 混入排名计算业务逻辑,超出硬上限 | +| 3 | 文件超 1000 行硬上限 | `shared/db/schema.ts` (1111 行) | 🟡 需改进 | 54 张表混合,可接受但需按业务域分节 | +| 4 | 循环依赖 | `shared/lib` ↔ `@/auth` | 🔴 严重 | audit-logger/change-logger/auth-guard → @/auth → shared/lib/* 形成循环 | +| 5 | dashboard 跨模块直查 11 张表 | `dashboard/data-access.ts` | 🔴 严重 | getAdminDashboardData 直查 sessions/users/classes 等 11 张表,违反模块封装 | +| 6 | messaging 绕过 notifications | `messaging/actions.ts` 第 66-72 行 | 🔴 严重 | 直接调用 createNotification,导致通知偏好失效 | +| 7 | classSchedule 三处写入口 | classes/scheduling 模块 | 🔴 严重 | 数据完整性高风险,无统一入口 | + +### 4.2 P1 较严重问题 + +| 序号 | 问题 | 说明 | +|------|------|------| +| 8 | 跨模块直接 DB 查询普遍 | classes(8+)/classEnrollments(6+)/users(6+)/subjects(6+)/exams(5+) 被跨模块直接访问 | +| 9 | actions 层混入数据访问 | exams/homework/questions/announcements 的 actions.ts 直接 db.insert/update/delete | +| 10 | auth.ts 混合 5 类职责 | NextAuth 配置 + 密码安全 DB + 角色规范化 + IP 解析 + 回调函数 | +| 11 | users/import-export.ts 四重职责 | 导入解析 + 导出 + 用户创建 + 班级注册(跨模块写) | +| 12 | proctoring 死代码 | exam-mode-config.tsx 未集成到考试表单 | +| 13 | messaging 与 notifications 边界模糊 | 双向依赖,类型系统不一致 | +| 14 | proctoring 事件双通道重复 | Server Action 与 REST API 同一逻辑两份代码 | +| 15 | proxy.ts 硬编码权限字符串 | 未复用 Permissions 常量,违反项目规则 | + +### 4.3 架构文档问题 + +当前 004 架构影响地图存在的问题: +1. 按模块罗列函数签名,缺乏全局视角 +2. 缺少模块依赖关系图 +3. 缺少数据流向图 +4. 缺少调用链路 +5. 缺少分层架构说明 +6. 未标注循环依赖 + +### 4.4 解耦优先级 + +**立即执行(P0):** +1. 拆分 `classes/data-access.ts`(2104 行 → 按职责拆 3-4 个文件) +2. 拆分 `homework/data-access.ts`(1038 行 → 分离排名逻辑) +3. 修复 shared/lib ↔ auth 循环依赖 +4. dashboard 改为通过各模块 data-access 获取数据 +5. messaging 写通知改为通过 notifications dispatcher + +**短期执行(P1):** +6. 统一 classSchedule 写入口到 scheduling 模块 +7. actions 层移除直接 DB 操作 +8. 拆分 auth.ts +9. 集成 proctoring/exam-mode-config 到考试表单 +10. 拆分 users/import-export.ts + +--- + +## 五、优先补齐路线图 + +### Phase 1: P0 缺口补齐(MVP 必须项) + +> v3 进度:8 项中 6 项已完成,剩余 2 项 + +| 序号 | 功能 | 所属模块 | v2 状态 | v3 状态 | 工作量 | +|------|------|---------|---------|---------|--------| +| 1 | **通知公告系统** | 家校沟通 | ❌ | ✅ 已实现 | — | +| 2 | **操作日志 + 登录日志** | 日志审计 | ❌ | ✅ 已实现 | — | +| 3 | **成绩录入 + 查询 + 统计报表** | 成绩分析 | ❌ | ✅ 已实现 | — | +| 4 | **文件上传 + 权限控制** | 文件管理 | ❌ | ✅ 已实现 | — | +| 5 | **课程计划管理** | 教务排课 | ❌ | ✅ 已实现 | — | +| 6 | **隐私政策 + 用户同意** | 隐私合规 | ❌ | ❌ 未实现 | 小 | +| 7 | **未成年人信息保护** | 隐私合规 | ❌ | ❌ 未实现 | 小 | +| 8 | **修复 13 个幽灵导航路由** | 布局 | ❌ | ✅ 已修复 | — | + +### Phase 2: P1 关键增强(上线前推荐) + +> v3 进度:20 项中约 11 项已完成 + +| 序号 | 功能 | 所属模块 | v2 状态 | v3 状态 | +|------|------|---------|---------|---------| +| 1 | 站内消息系统 | 家校沟通 | ❌ | ✅ 已实现 | +| 2 | 家长端仪表盘 | 家校沟通 | ⚠️ | ⚠️ 仍需完善 | +| 3 | Excel 批量导入 | 导入导出 | ❌ | ✅ 已实现 | +| 4 | Excel/PDF 导出 | 导入导出 | ❌ | ✅ 已实现 | +| 5 | 排课规则 + 自动排课 | 教务排课 | ❌ | ✅ 已实现 | +| 6 | 课表调整/代课 | 教务排课 | ❌ | ❌ 未实现 | +| 7 | 速率限制 | 操作安全 | ❌ | ❌ 未实现 | +| 8 | 成绩趋势 + 对比分析 | 成绩分析 | ⚠️ | ⚠️ 趋势已有,对比未实现 | +| 9 | 成绩导出 | 成绩分析 | ❌ | ✅ 已实现 | +| 10 | 学生考勤 | 考勤管理 | ❌ | ✅ 已实现 | +| 11 | 用户批量导入 | 用户与权限 | ❌ | ✅ 已实现 | +| 12 | 密码安全策略 | 用户与权限 | ⚠️ | ⚠️ 仍需完善 | +| 13 | 数据变更日志 | 日志审计 | ❌ | ✅ 已实现 | +| 14 | 日志查询/导出 | 日志审计 | ❌ | ⚠️ 查询已有,导出未实现 | +| 15 | 文件预览 + 存储策略 | 文件管理 | ❌ | ⚠️ 存储策略部分实现 | +| 16 | 全文检索 | 全局搜索 | ❌ | ⚠️ 基础实现 | +| 17 | 依赖审计集成 CI | 安全审计 | ⚠️ | ⚠️ 仍未集成 CI | +| 18 | 数据库定时备份 | 数据备份 | ❌ | ✅ 已实现 | +| 19 | E2E 测试完善 | 自动化测试 | ⚠️ | ⚠️ 仍需完善 | +| 20 | 通知偏好管理 | 消息通知 | ❌ | ✅ 已实现 | + +### Phase 3: P2 迭代优化(竞争力提升) + +> v3 进度:14 项中 8 项已完成(57%) + +| 序号 | 功能 | 所属模块 | v2 状态 | v3 状态 | +|------|------|---------|---------|---------| +| 1 | 国际化(i18n) | 非功能性 | ❌ | ❌ 未实现 | +| 2 | 多租户/多校区 | 非功能性 | ❌ | ❌ 未实现 | +| 3 | 主题色定制 | 深色主题 | ❌ | ❌ 未实现 | +| 4 | 屏幕阅读器兼容 | 无障碍 | ❌ | ✅ 已实现 | +| 5 | 视觉回归测试 | 自动化测试 | ❌ | ✅ 已实现 | +| 6 | AI 批改辅助 | AI 赋能 | ❌ | ❌ 未实现 | +| 7 | AI 学情分析 | AI 赋能 | ❌ | ❌ 未实现 | +| 8 | AI 备课助手 | AI 赋能 | ❌ | ❌ 未实现 | +| 9 | 选课管理 | 教务排课 | ❌ | ✅ 已实现 | +| 10 | 考试监考 | 作业与考试 | ❌ | ✅ 已实现 | +| 11 | 学情诊断报告 | 成绩分析 | ❌ | ✅ 已实现 | +| 12 | 短信/微信推送 | 消息通知 | ❌ | ✅ 已实现 | +| 13 | 漏洞扫描 + 渗透测试 | 安全审计 | ❌ | ⚠️ 漏洞扫描已实现,渗透测试未实现 | +| 14 | 灾备方案 | 数据备份 | ❌ | ✅ 已实现 | + +--- + +## 六、差距统计摘要 | 状态 | P0 | P1 | P2 | 合计 | |------|-----|-----|-----|------| -| ✅ 已完成 | 38 | 12 | 2 | **52** | -| ⚠️ 部分完成 | 10 | 8 | 1 | **19** | -| ❌ 未实现 | 7 | 28 | 27 | **62** | -| **合计** | **55** | **48** | **30** | **133** | +| ✅ 已完成 | 46 | 31 | 12 | **89** | +| ⚠️ 部分完成 | 7 | 11 | 6 | **24** | +| ❌ 未实现 | 2 | 11 | 17 | **30** | +| **合计** | **55** | **53** | **35** | **143** | | 完成率 | P0 | P1 | P2 | 总体 | |--------|-----|-----|-----|------| -| 按已完成计 | 69% | 25% | 7% | **39%** | -| 含部分完成 | 87% | 42% | 10% | **53%** | +| 按已完成计 | 84% | 58% | 34% | **62%** | +| 含部分完成 | 96% | 79% | 51% | **79%** | -### v1 → v2 改善 +### v2 → v3 改善 -| 指标 | v1 | v2 | 变化 | +| 指标 | v2 | v3 | 变化 | |------|-----|-----|------| -| P0 完成率(严格) | 65% | 69% | +4% | -| P0 完成率(含部分) | 83% | 87% | +4% | -| 安全漏洞数 | 15 个 Server Action 无权限 | 0 | **全部修复** | -| 架构图覆盖率 | ~40% | 100% | **全量补全** | -| 幽灵路由 | 未发现 | 13 个 | **新发现** | +| P0 完成率(严格) | 69% | 84% | **+15%** | +| P0 完成率(含部分) | 87% | 96% | **+9%** | +| P1 完成率(严格) | 25% | 58% | **+33%** | +| P2 完成率(严格) | 7% | 34% | **+27%** | +| P2 路线图完成率 | 0% | 57% (8/14) | **+57%** | +| 总体完成率(严格) | 39% | 62% | **+23%** | +| 总体完成率(含部分) | 53% | 79% | **+26%** | +| 幽灵路由 | 13 个 | 0 | **全部修复** | +| 架构技术债 | 未审查 | 7 P0 + 8 P1 | **新发现** | -> **结论**:项目 P0 核心功能完成度约 69%(严格)/ 87%(含部分),较 v1 提升 4%。安全漏洞全部修复。架构图已全量补全。主要缺口集中在**通知公告**、**日志审计**、**成绩分析**三个 P0 模块。建议优先补齐 Phase 1 的 8 项 P0 缺口。 +> **结论**:项目 P0 核心功能完成度达 84%(严格)/ 96%(含部分),较 v2 大幅提升 15%。P2 路线图完成 8/14(57%)。主要剩余缺口: +> 1. **隐私合规 P0**:隐私政策、未成年人信息保护(2 项) +> 2. **P2 路线图**:AI 批改/学情/备课、国际化、多租户、主题色定制(6 项) +> 3. **架构技术债**:7 项 P0 严重问题需修复(classes/data-access 超标、循环依赖、跨模块直查等) +> +> 建议优先:① 补齐 2 项 P0 隐私合规缺口;② 修复 7 项 P0 架构技术债;③ 推进 P2 路线图剩余 6 项。 diff --git a/docs/architecture/audit/00_summary.md b/docs/architecture/audit/00_summary.md new file mode 100644 index 0000000..52b9e0f --- /dev/null +++ b/docs/architecture/audit/00_summary.md @@ -0,0 +1,132 @@ +# 架构审查汇总报告 + +> 基于对全项目 69+ 文件的逐文件审查,汇总关键架构问题。 +> 审查日期: 2026-06-17 +> 子报告: +> - [shared 基础设施层审查](./shared-audit.md) +> - [核心业务模块审查](./core-business-audit.md) +> - [管理模块群审查](./management-modules-audit.md) +> - [新增模块和其他模块审查](./new-and-other-modules-audit.md) + +--- + +## 一、总体评估 + +| 维度 | 状态 | 说明 | +|------|------|------| +| 模块化程度 | ⚠️ 中等 | 20+ 模块划分合理,但跨模块直接 DB 查询普遍存在 | +| 职责单一性 | ⚠️ 中等 | 多数模块职责清晰,但 5 个文件超 1000 行硬上限 | +| 架构文档质量 | ❌ 不足 | 004 文档按模块罗列函数,缺乏关系图/数据流/调用链 | +| 循环依赖 | ❌ 存在 | shared/lib ↔ auth 循环依赖 | +| 死代码 | ⚠️ 少量 | proctoring/exam-mode-config.tsx 未集成 | + +**核心结论**: 架构设计思路正确(模块化 + 分层),但执行不够严格。主要问题是跨模块直接 DB 查询破坏了模块封装,以及少数文件过大。 + +--- + +## 二、P0 严重问题(必须修复) + +### 1. 文件超 1000 行硬上限(3 个文件) + +| 文件 | 行数 | 问题 | +|------|------|------| +| `classes/data-access.ts` | 2104 | 混入 homework/scheduling/grades 逻辑 | +| `homework/data-access.ts` | 1038 | 混入排名计算业务逻辑 | +| `shared/db/schema.ts` | 1111 | 54 张表混合(可接受,但需分节) | + +### 2. 循环依赖 + +``` +shared/lib/{audit-logger, change-logger, auth-guard} + → @/auth (src/auth.ts) + → shared/lib/* (循环) +``` + +### 3. dashboard 跨模块直接查询 11 张表 + +`dashboard/data-access.ts` 的 `getAdminDashboardData` 直查 sessions/users/classes/textbooks/chapters/questions/exams/homeworkAssignments/homeworkSubmissions/usersToRoles/roles,严重违反模块封装。 + +### 4. messaging 绕过 notifications 直接写通知 + +`messaging/actions.ts` 第 66-72 行直接调用 `createNotification`,导致用户通知偏好失效、多渠道通知无效。 + +### 5. classSchedule 表三处写入口 + +- `classes/data-access.ts` +- `scheduling/actions.ts` (直接 transaction 写入) +- `scheduling/data-access.ts` + +数据完整性高风险。 + +--- + +## 三、P1 较严重问题 + +### 6. 跨模块直接 DB 查询普遍存在 + +| 被访问表 | 访问次数 | 应归属模块 | 主要违规者 | +|---------|---------|-----------|-----------| +| `classes` | 8+ | classes | exams, homework, grades, dashboard | +| `classEnrollments` | 6+ | classes | homework, grades, attendance | +| `users` | 6+ | users | 多个模块 | +| `subjects` | 6+ | school | exams, homework, questions | +| `exams` | 5+ | exams | homework, grades, dashboard | + +### 7. actions 层混入数据访问逻辑 + +exams/homework/questions/announcements 的 actions.ts 中存在直接 `db.insert/update/delete`,应该通过 data-access 层。 + +### 8. auth.ts 混合 5 类职责 + +NextAuth 配置 + 密码安全 DB 操作 + 角色规范化 + IP 解析 + 回调函数,应拆分。 + +### 9. users/import-export.ts 四重职责 + +导入解析 + 导出 + 用户创建(含密码哈希) + 班级注册(跨模块写 classEnrollments)。 + +### 10. proctoring 死代码 + +`exam-mode-config.tsx` 组件已创建但未集成到考试表单,DB schema 有 examMode 字段但表单不收集。 + +--- + +## 四、架构文档问题 + +### 当前 004 文档的问题 + +1. **按模块罗列函数签名**,缺乏全局视角 +2. **缺少模块依赖关系图**,无法直观看出模块间如何协作 +3. **缺少数据流向图**,不知道数据如何在模块间流动 +4. **缺少调用链路**,不知道一个请求从 API 到 DB 的完整路径 +5. **缺少分层架构说明**,不知道 shared/modules/app 的层次关系 +6. **未标注循环依赖**,给人虚假的"架构清晰"印象 + +### 理想的架构文档应该 + +1. **一图胜千言**: 用 ASCII/Mermaid 图展示模块关系 +2. **分层清晰**: shared → modules → app 三层,依赖方向单向 +3. **数据流明确**: 标注每个核心业务的数据从哪来、到哪去 +4. **调用链完整**: 关键 API 的完整调用路径 +5. **问题标注**: 明确标注已知的耦合问题和技术债 + +--- + +## 五、解耦优先级 + +### 立即执行(P0) +1. 拆分 `classes/data-access.ts`(2104 行 → 按职责拆 3-4 个文件) +2. 拆分 `homework/data-access.ts`(1038 行 → 分离排名逻辑) +3. 修复 shared/lib ↔ auth 循环依赖 +4. dashboard 改为通过各模块 data-access 获取数据 +5. messaging 写通知改为通过 notifications dispatcher + +### 短期执行(P1) +6. 统一 classSchedule 写入口到 scheduling 模块 +7. actions 层移除直接 DB 操作 +8. 拆分 auth.ts +9. 集成 proctoring/exam-mode-config 到考试表单 +10. 拆分 users/import-export.ts + +### 中期执行(P2) +11. 建立模块间数据访问规范(通过对方 data-access 或导出查询函数) +12. schema.ts 按业务域分节(加注释分隔) diff --git a/docs/architecture/audit/core-business-audit.md b/docs/architecture/audit/core-business-audit.md new file mode 100644 index 0000000..ad0bb16 --- /dev/null +++ b/docs/architecture/audit/core-business-audit.md @@ -0,0 +1,417 @@ +# 核心业务模块职责与耦合审查报告 + +> 审计日期:2026-06-17 +> 审计范围:`src/modules/exams`、`src/modules/homework`、`src/modules/questions`、`src/modules/textbooks`、`src/modules/grades` +> 审计目标:识别职责不单一和过耦合问题,重点关注跨模块直接数据库查询(违反模块封装) +> 审计依据:项目规则(`.trae/rules/project_rules.md`)、架构影响地图(004/005) + +--- + +## 一、总体结论 + +| 维度 | 状态 | 说明 | +|------|------|------| +| 模块职责边界 | ⚠️ 部分违规 | homework 混入考试/班级逻辑;grades 混入班级/用户逻辑 | +| data-access 层职责 | ❌ 普遍违规 | 5 个模块均存在跨模块直接 DB 查询;homework/data-access.ts 混入排名业务逻辑 | +| actions 层职责 | ⚠️ 部分违规 | exams/homework/questions 的 actions 直接访问 DB;textbooks/grades 的 actions 设计良好 | +| 组件耦合 | ✅ 基本合规 | 组件层跨模块依赖均为类型导入或 UI 组合,无直接 data-access 调用 | +| 跨模块依赖 | ⚠️ 存在风险 | 无循环依赖,但 exams→questions、homework→exams、grades→classes 的直接 DB 访问破坏封装 | +| 文件行数 | ❌ 存在违规 | homework/data-access.ts 1038 行(超 1000 硬上限);exams/ai-pipeline.ts 912 行、exams/actions.ts 832 行(超 800 建议值) | + +### 关键风险项 + +1. **homework/data-access.ts 超过 1000 行硬上限**(1038 行)—— 必须拆分 +2. **5 个模块均存在跨模块直接 DB 查询** —— 违反模块封装原则 +3. **exams/homework/questions 的 actions 层混入数据访问逻辑** —— 应只做编排 +4. **homework/data-access.ts 混入排名计算业务逻辑** —— data-access 应只负责数据存取 + +--- + +## 二、模块依赖关系图 + +``` + ┌──────────────┐ + │ textbooks │ ← 被引用方(知识点/章节) + └──────┬───────┘ + │ + ┌──────────────┼──────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌────────────┐ ┌──────────┐ ┌────────────┐ + │ questions │ │ exams │ │ homework │ + └─────┬──────┘ └────┬─────┘ └─────┬──────┘ + │ │ │ + │ ┌──────────┘ │ + │ │ │ + ▼ ▼ ▼ + ┌────────────────┐ ┌──────────┐ + │ grades │ │ classes │ + └────────────────┘ └──────────┘ +``` + +### 依赖关系明细 + +| 依赖方向 | 类型 | 合理性 | 问题 | +|---------|------|--------|------| +| exams → questions | data-access + 类型 + action | ⚠️ 部分合理 | 类型导入合理;但 `persistAiGeneratedExamDraft` 直接 insert 到 questions 表,应通过 questions/data-access | +| exams → classes | data-access | ❌ 不合理 | `getExams`/`getExamById` 直接查询 classes 表获取教师 gradeIds,应通过 classes/data-access | +| exams → school | actions | ❌ 不合理 | `getSubjectsAction`/`getGradesAction` 直接查询 subjects/grades 表,应通过 school/data-access | +| homework → exams | data-access + 组件 | ⚠️ 部分合理 | 业务上 homework 引用 exam(sourceExamId)合理;但直接查询 exams 表应改为调用 exams/data-access | +| homework → classes | data-access + actions | ❌ 不合理 | 直接查询 classes/classEnrollments/classSubjectTeachers 表,应通过 classes/data-access | +| homework → questions | data-access | ✅ 合理 | 通过 Drizzle 关系查询 homeworkAssignmentQuestions.question,未直接访问 questions 表 | +| grades → exams | 无 | ✅ 合理 | grades 仅通过 examId 外键引用,不直接查询 exams 表 | +| grades → homework | 无 | ✅ 合理 | grades 仅通过 type 枚举值 "homework" 引用,不直接查询 homework 表 | +| grades → classes | data-access | ❌ 不合理 | 多个 data-access 文件直接查询 classes/classEnrollments 表 | +| grades → users/subjects | data-access | ❌ 不合理 | 直接查询 users/subjects 表获取关联名称,应通过对应模块 data-access | +| questions → textbooks | actions | ❌ 不合理 | `getKnowledgePointOptionsAction` 直接查询 knowledgePoints/chapters/textbooks 表 | +| textbooks → questions | 组件 | ✅ 合理 | `knowledge-point-dialogs.tsx` 导入 CreateQuestionDialog 组件,属于 UI 组合 | + +### 循环依赖分析 + +- **无直接循环依赖**:模块间的 data-access 依赖是单向的 +- **潜在风险**:exams → questions(data-access)与 questions → textbooks(actions)与 textbooks → questions(组件)形成链式依赖,但因 textbooks→questions 仅为组件层导入,不构成 data-access 层的循环依赖 + +--- + +## 三、各模块审查明细 + +### 3.1 exams 模块 + +#### 文件行数 + +| 文件 | 行数 | 限制 | 状态 | +|------|------|------|------| +| actions.ts | 832 | ≤800 | ⚠️ 超限 | +| ai-pipeline.ts | 912 | ≤800 | ⚠️ 超限 | +| data-access.ts | 339 | ≤800 | ✅ | +| types.ts | 31 | 无限制 | ✅ | +| hooks/use-exam-preview.ts | 295 | ≤500 | ✅ | + +#### 模块职责边界 + +- **职责**:考试全生命周期管理(创建/编辑/预览/发布/删除/复制)+ AI 辅助出题 +- **问题**:`getSubjectsAction`/`getGradesAction` 属于 school 模块职责,被放在 exams/actions.ts 中 + +#### data-access 层问题 + +| 函数 | 问题 | 严重程度 | +|------|------|---------| +| `getExams` | 直接查询 `classes` 表获取教师 gradeIds(第 71-78 行) | 高 | +| `getExamById` | 直接查询 `classes` 表获取教师 gradeIds(第 155-159 行) | 高 | +| `persistAiGeneratedExamDraft` | 直接 insert 到 `questions` 表(第 317-326 行) | 高 | + +#### actions 层问题 + +| 函数 | 问题 | 严重程度 | +|------|------|---------| +| `updateExamAction` | 直接 `db.query.exams.findFirst` 做归属校验 + `db.delete`/`db.insert`/`db.update` 操作 examQuestions | 高 | +| `deleteExamAction` | 直接 `db.query.exams.findFirst` 做归属校验 + `db.delete` | 高 | +| `duplicateExamAction` | 直接 `db.query.exams.findFirst` + `db.transaction` 内联 insert exams/examQuestions | 高 | +| `getExamPreviewAction` | 直接 `db.query.exams.findFirst` 查询考试预览数据 | 高 | +| `getSubjectsAction` | 直接查询 `subjects` 表 —— 跨模块 DB 访问 | 高 | +| `getGradesAction` | 直接查询 `grades` 表 —— 跨模块 DB 访问 | 高 | + +#### 组件耦合 + +| 组件 | 问题 | 严重程度 | +|------|------|---------| +| `exam-assembly.tsx` | 调用 `getQuestionsAction`(questions 模块的 action) | 中 | +| 8 个组件 | 导入 `Question` 类型自 questions/types | 低(类型导入合理) | +| `ExamAssembly` | **10 个 props**(examId, title, subject, grade, difficulty, totalScore, durationMin, initialSelected, initialStructure, questionOptions) | 中 | +| `ExamPreviewQuestionEditor` | **10 个 props** | 中 | + +#### ai-pipeline.ts 问题 + +- 912 行,超过 800 行建议值 +- 混合了 Zod schema、AI prompt、JSON 解析修复、题目详情解析、并发控制等多种职责 +- 建议拆分为:`ai-schema.ts`(Zod schema)、`ai-prompts.ts`(prompt 常量)、`ai-parser.ts`(JSON 解析修复)、`ai-pipeline.ts`(核心生成逻辑) + +--- + +### 3.2 homework 模块 + +#### 文件行数 + +| 文件 | 行数 | 限制 | 状态 | +|------|------|------|------| +| data-access.ts | **1038** | ≤1000 硬上限 | ❌ **必须拆分** | +| actions.ts | 387 | ≤800 | ✅ | +| schema.ts | 29 | 无限制 | ✅ | +| types.ts | 186 | 无限制 | ✅ | + +#### 模块职责边界 + +- **职责**:作业全生命周期(创建/发布/作答/批改/分析) +- **问题**:`getStudentDashboardGrades` 包含班级排名计算逻辑(150+ 行),属于 dashboard 或 grades 模块职责 + +#### data-access 层问题(严重) + +| 函数 | 问题 | 严重程度 | +|------|------|---------| +| `getStudentDashboardGrades` | 150+ 行排名计算业务逻辑混入 data-access | 高 | +| `getHomeworkAssignmentAnalytics` | 145+ 行错误率/错误答案统计业务逻辑混入 data-access | 高 | +| `getHomeworkAssignments` | 直接查询 `exams` 表(第 167-171 行) | 高 | +| `getHomeworkAssignmentReviewList` | 直接查询 `exams` 表(第 227-233 行) | 高 | +| `getHomeworkSubmissions` | 直接查询 `exams` 表(第 349-359 行) | 高 | +| `getHomeworkAssignmentById` | 直接查询 `exams` 表(第 403-407 行) | 高 | +| `getStudentHomeworkAssignments` | 直接 join `exams`/`subjects` 表(第 730-731 行) | 高 | +| `getDemoStudentUser` | 直接查询 `users`/`roles`/`usersToRoles` 表 + 使用 `auth()` 而非 auth-guard | 高 | +| `getStudentDashboardGrades` | 直接查询 `classEnrollments`/`users` 表 | 高 | + +#### actions 层问题 + +| 函数 | 问题 | 严重程度 | +|------|------|---------| +| `createHomeworkAssignmentAction` | **157 行**,混合数据访问 + 业务逻辑 + 权限校验 | 高 | +| 同上 | 直接查询 `classes`/`classSubjectTeachers`/`exams`/`classEnrollments` 表 | 高 | +| 同上 | 内联 `db.transaction` insert homeworkAssignments/homeworkAssignmentQuestions/homeworkAssignmentTargets | 高 | +| `startHomeworkSubmissionAction` | 直接 `db.query` + `db.insert` | 高 | +| `saveHomeworkAnswerAction` | 直接 `db.query` + `db.transaction` | 高 | +| `submitHomeworkAction` | 直接 `db.query` + `db.update` | 高 | +| `gradeHomeworkSubmissionAction` | 直接 `db.update` 循环更新 homeworkAnswers | 高 | + +#### 拆分建议 + +`data-access.ts`(1038 行)建议拆分为: +- `data-access.ts`:基础 CRUD(getHomeworkAssignments, getHomeworkAssignmentById, getHomeworkSubmissions) +- `data-access-student.ts`:学生视角查询(getStudentHomeworkAssignments, getStudentHomeworkTakeData, getStudentDashboardGrades) +- `data-access-analytics.ts`:分析统计(getHomeworkAssignmentAnalytics, getTeacherGradeTrends) +- `data-access-grading.ts`:批改相关(getHomeworkSubmissionDetails, getHomeworkAssignmentReviewList) + +--- + +### 3.3 questions 模块 + +#### 文件行数 + +| 文件 | 行数 | 限制 | 状态 | +|------|------|------|------| +| actions.ts | 294 | ≤800 | ✅ | +| data-access.ts | 129 | ≤800 | ✅ | +| schema.ts | 18 | 无限制 | ✅ | +| types.ts | 34 | 无限制 | ✅ | + +#### 模块职责边界 + +- **职责**:题库管理(题目 CRUD、知识点关联、题型支持) +- **问题**:`getKnowledgePointOptionsAction` 查询 textbooks 模块的表,属于 textbooks 模块职责 + +#### data-access 层问题 + +- ✅ 仅访问 `questions` 和 `questionsToKnowledgePoints` 表,无跨模块 DB 访问 +- ❌ **缺失写操作函数**:`insertQuestionWithRelations`、`deleteQuestionRecursive` 等 data-access 函数被错误地放在 actions.ts 中 + +#### actions 层问题 + +| 函数 | 问题 | 严重程度 | +|------|------|---------| +| `createNestedQuestion` | 内联 `db.transaction` + 调用 `insertQuestionWithRelations`(data-access 函数错放在 actions) | 高 | +| `updateQuestionAction` | 内联 `db.transaction` 做 update/delete/insert | 高 | +| `deleteQuestionAction` | 内联 `db.transaction` + 调用 `deleteQuestionRecursive`(data-access 函数错放在 actions) | 高 | +| `getKnowledgePointOptionsAction` | 直接查询 `knowledgePoints`/`chapters`/`textbooks` 表 —— 跨模块 DB 访问 | 高 | +| `getQuestionsAction` | ✅ 正确委托给 data-access.getQuestions | ✅ | + +#### 组件耦合 + +- ✅ 无跨模块依赖 + +--- + +### 3.4 textbooks 模块(标杆模块) + +#### 文件行数 + +| 文件 | 行数 | 限制 | 状态 | +|------|------|------|------| +| actions.ts | 276 | ≤800 | ✅ | +| data-access.ts | 428 | ≤800 | ✅ | +| types.ts | 79 | 无限制 | ✅ | +| hooks/use-knowledge-point-actions.ts | 121 | ≤500 | ✅ | +| hooks/use-text-selection.ts | - | ≤500 | ✅ | + +#### 模块职责边界 + +- **职责**:教材与知识体系管理(教材/章节树形结构、知识点 CRUD、Markdown 内容编辑、知识图谱) +- ✅ 职责单一,无越界 + +#### data-access 层评价 + +- ✅ 仅访问 `textbooks`、`chapters`、`knowledgePoints` 表 +- ✅ 无跨模块 DB 访问 +- ✅ 无业务逻辑混入 + +#### actions 层评价 + +- ✅ **标杆实现**:所有 action 均遵循"权限校验 → 调用 data-access → revalidatePath → 返回"模式 +- ✅ 无直接 DB 访问 +- ✅ 无业务逻辑混入 + +#### 组件耦合 + +- `knowledge-point-dialogs.tsx` 导入 `CreateQuestionDialog` 自 questions 模块 —— ✅ 合理的 UI 组合 + +#### hooks 评价 + +- `useKnowledgePointActions` 有 7 个参数(textbookId, selectedChapterId, selectedChapterTextbookId, highlightedKpId, setHighlightedKpId, onKpCreated)—— ✅ 在 8 个限制内 + +--- + +### 3.5 grades 模块 + +#### 文件行数 + +| 文件 | 行数 | 限制 | 状态 | +|------|------|------|------| +| actions.ts | 312 | ≤800 | ✅ | +| actions-analytics.ts | 133 | ≤800 | ✅ | +| data-access.ts | 419 | ≤800 | ✅ | +| data-access-analytics.ts | 293 | ≤800 | ✅ | +| data-access-ranking.ts | 121 | ≤800 | ✅ | +| export.ts | 214 | ≤800 | ✅ | +| schema.ts | 52 | 无限制 | ✅ | +| types.ts | - | 无限制 | ✅ | + +#### 模块职责边界 + +- **职责**:成绩分析(录入/查询/统计/导出/趋势对比分析) +- ✅ 职责单一,未混入考试/作业逻辑 +- ✅ 通过 `examId` 外键引用考试,通过 `type` 枚举引用作业类型,未直接依赖 exams/homework 模块的 data-access + +#### data-access 层问题 + +| 文件 | 函数 | 问题 | 严重程度 | +|------|------|------|---------| +| data-access.ts | `getGradeRecords` | 直接 join `classes`/`subjects`/`users` 表 | 高 | +| data-access.ts | `getStudentGradeSummary` | 直接 join `classes`/`subjects`/`users` 表 | 高 | +| data-access.ts | `getClassRanking` | 直接 join `users` 表 | 高 | +| data-access.ts | `getClassStudentsForEntry` | 直接查询 `classEnrollments`/`users` 表 —— 应在 classes 模块 | 高 | +| data-access.ts | `getClassGradeStatsWithMeta` | 直接查询 `classes`/`classEnrollments` 表 | 高 | +| data-access.ts | `getClassGradeStats` | 统计计算业务逻辑(average/median/stdDev/passRate/excellentRate)混入 data-access | 中 | +| data-access-analytics.ts | `getGradeTrend` | 直接 join `classes`/`subjects` 表 | 高 | +| data-access-analytics.ts | `getClassComparison` | 直接查询 `classes` 表 + 统计计算业务逻辑 | 高 | +| data-access-analytics.ts | `getSubjectComparison` | 直接 join `subjects` 表 + 统计计算业务逻辑 | 高 | +| data-access-analytics.ts | `getGradeDistribution` | 分桶统计业务逻辑混入 data-access | 中 | +| data-access-ranking.ts | `getRankingTrend` | 直接查询 `classEnrollments`/`users` 表 + 排名计算业务逻辑 | 高 | +| export.ts | `exportClassGradeReportToExcel` | 直接查询 `classes`/`subjects`/`users` 表 + 排名计算业务逻辑 | 高 | + +#### actions 层评价 + +- ✅ **标杆实现**:`actions.ts` 和 `actions-analytics.ts` 均遵循"权限校验 → 调用 data-access → 返回"模式 +- ✅ 无直接 DB 访问 +- ✅ 无业务逻辑混入 + +#### 组件耦合 + +- ✅ 无跨模块依赖 + +--- + +## 四、跨模块直接 DB 访问汇总 + +> 以下为违反模块封装原则的直接数据库查询,应改为通过对方模块的 data-access 函数调用。 + +### 4.1 按来源模块分类 + +| 来源模块 | 文件 | 被访问的表 | 应调用的模块 | +|---------|------|-----------|-------------| +| exams | data-access.ts | `classes` | classes/data-access | +| exams | data-access.ts | `questions`(insert) | questions/data-access | +| exams | actions.ts | `subjects`, `grades` | school/data-access | +| homework | actions.ts | `classes`, `classSubjectTeachers`, `exams`, `classEnrollments` | classes/data-access, exams/data-access | +| homework | data-access.ts | `exams`, `classEnrollments`, `subjects`, `users`, `roles`, `usersToRoles` | exams/data-access, classes/data-access, school/data-access | +| questions | actions.ts | `knowledgePoints`, `chapters`, `textbooks` | textbooks/data-access | +| grades | data-access.ts | `classes`, `classEnrollments`, `subjects`, `users` | classes/data-access, school/data-access | +| grades | data-access-analytics.ts | `classes`, `subjects` | classes/data-access, school/data-access | +| grades | data-access-ranking.ts | `classEnrollments`, `users` | classes/data-access | +| grades | export.ts | `classes`, `subjects`, `users` | classes/data-access, school/data-access | + +### 4.2 按被访问表分类(频次) + +| 被访问表 | 访问次数 | 应归属模块 | +|---------|---------|-----------| +| `classes` | 8+ | classes | +| `classEnrollments` | 6+ | classes | +| `users` | 6+ | users | +| `subjects` | 6+ | school | +| `exams` | 5+ | exams | +| `grades`(年级表) | 1 | school | +| `classSubjectTeachers` | 1 | classes | +| `knowledgePoints` | 1 | textbooks | +| `chapters` | 1 | textbooks | +| `textbooks` | 1 | textbooks | +| `roles`, `usersToRoles` | 1 | users | +| `questions`(insert) | 1 | questions | + +--- + +## 五、改进建议 + +### 5.1 高优先级(P0) + +1. **拆分 homework/data-access.ts**(1038 行 → 4 个文件) + - 按职责拆分为 data-access.ts / data-access-student.ts / data-access-analytics.ts / data-access-grading.ts + +2. **消除跨模块直接 DB 访问** + - 在 classes/data-access 暴露 `getClassGradeIdsByClassIds`、`getClassStudentsByClassId`、`getActiveClassStudents` 等函数 + - 在 exams/data-access 暴露 `getExamForHomeworkCreation`(含 questions 关联) + - 在 school/data-access 暴露 `getSubjectOptions`、`getGradeOptions` + - 在 users/data-access 暴露 `getUserNameByIds`、`getStudentInfo` + - 在 textbooks/data-access 暴露 `getKnowledgePointOptions` + - 在 questions/data-access 暴露 `insertQuestionWithRelations`、`deleteQuestionRecursive` + +3. **将 exams/actions.ts 中的 DB 操作下沉到 data-access** + - `updateExamAction`、`deleteExamAction`、`duplicateExamAction`、`getExamPreviewAction` 的 DB 操作移至 data-access + - 将 `getSubjectsAction`/`getGradesAction` 移至 school 模块或改为调用 school/data-access + +4. **将 homework/actions.ts 中的 DB 操作下沉到 data-access** + - `createHomeworkAssignmentAction`(157 行)拆分为:data-access 函数 + action 编排 + - 其他 action 的 DB 操作全部移至 data-access + +5. **将 questions/actions.ts 中的 DB 操作下沉到 data-access** + - `insertQuestionWithRelations`、`deleteQuestionRecursive` 移至 data-access + - `getKnowledgePointOptionsAction` 改为调用 textbooks/data-access + +### 5.2 中优先级(P1) + +6. **拆分 exams/ai-pipeline.ts**(912 行 → 4 个文件) + - ai-schema.ts(Zod schema)、ai-prompts.ts(prompt 常量)、ai-parser.ts(JSON 解析修复)、ai-pipeline.ts(核心生成逻辑) + +7. **将 homework/data-access.ts 中的业务逻辑提取到独立服务层** + - `getStudentDashboardGrades` 的排名计算逻辑提取到 `services/ranking-service.ts` + - `getHomeworkAssignmentAnalytics` 的错误率统计逻辑提取到 `services/analytics-service.ts` + +8. **将 grades/data-access.ts 中的统计计算逻辑提取到独立服务层** + - `getClassGradeStats` 的统计计算提取到 `services/stats-service.ts` + - `getGradeDistribution` 的分桶逻辑提取到 `services/distribution-service.ts` + +9. **减少组件 props 数量** + - `ExamAssembly`(10 props)和 `ExamPreviewQuestionEditor`(10 props)应考虑使用 Context 或组合模式减少 props + +### 5.3 低优先级(P2) + +10. **统一 auth 调用方式** + - `homework/data-access.ts` 的 `getDemoStudentUser` 使用 `auth()` 而非 `auth-guard.getAuthContext()`,应统一 + +11. **补全 questions/data-access.ts 的写操作** + - 当前 data-access 仅有 `getQuestions`,所有写操作错放在 actions.ts + +--- + +## 六、标杆模块推荐 + +| 模块 | 推荐参考点 | +|------|-----------| +| **textbooks** | actions 层编排模式(权限校验 → 调用 data-access → revalidatePath) | +| **textbooks** | data-access 层职责单一(仅访问本模块表,无业务逻辑) | +| **grades** | actions 层拆分(actions.ts + actions-analytics.ts 按职责分文件) | +| **grades** | data-access 层拆分(data-access.ts + data-access-analytics.ts + data-access-ranking.ts) | +| **grades** | 跨模块解耦(通过外键引用 exams/homework,不直接访问其表) | + +--- + +## 七、审查方法说明 + +- **审查范围**:5 个核心业务模块的 actions/data-access/schema/types/components/hooks 全量文件 +- **审查工具**:源码全量阅读 + Grep 跨模块依赖扫描 + PowerShell 行数统计 +- **审查依据**:项目规则中"Server Action 必须使用 requirePermission()"、"单文件行数规范"、"模块职责单一"等规则 +- **未覆盖项**:未运行 lint/typecheck(本次为只读审查,不修改代码);未审查组件内部实现细节(仅审查 props 数量和跨模块依赖) diff --git a/docs/architecture/audit/management-modules-audit.md b/docs/architecture/audit/management-modules-audit.md new file mode 100644 index 0000000..e31214d --- /dev/null +++ b/docs/architecture/audit/management-modules-audit.md @@ -0,0 +1,393 @@ +# 管理类模块职责与耦合审查报告 + +> 审查范围:school / classes / scheduling / attendance / users / audit / course-plans / announcements +> 审查日期:2026-06-17 +> 审查依据:单一职责原则(SRP)、模块边界清晰度、跨模块耦合度、企业级代码规范(单文件 ≤ 1000 行硬性上限) +> 审查方式:只读源码分析,未修改任何代码 + +--- + +## 一、总体评价 + +| 模块 | 行数(最大文件) | 职责单一性 | 耦合度 | 严重度 | +|------|----------------|-----------|--------|--------| +| school | 325 | ✅ 良好 | ✅ 低 | 🟢 合格 | +| classes | **2104** | ❌ 严重违反 | ❌ 严重 | 🔴 严重 | +| scheduling | 310(算法)/ 302(actions) | ✅ 算法独立 | ⚠️ 中 | 🟡 需改进 | +| attendance | 271 | ✅ 良好 | ⚠️ 中 | 🟢 合格 | +| users | 291(import-export) | ❌ 违反 | ❌ 高 | 🟠 较严重 | +| audit | 212 | ⚠️ 部分违反 | ✅ 低 | 🟡 需改进 | +| course-plans | 320 | ✅ 良好 | ✅ 低 | 🟢 合格 | +| announcements | 242 | ⚠️ 部分违反 | ✅ 低 | 🟡 需改进 | + +**核心结论**: +1. `classes` 模块是全项目耦合最严重的模块,单文件 2104 行远超 1000 行硬性上限,混入了 schedule、homework、grades 三个业务领域的逻辑。 +2. `users/import-export.ts` 违反单一职责,同时处理导入、导出、用户创建、班级注册四类逻辑。 +3. `scheduling/auto-scheduler.ts` 是算法独立化的**优秀范例**,纯函数、无 DB 访问、可独立测试。 +4. `announcements` 和 `audit` 模块的 data-access 层不完整,写操作或导出逻辑泄漏到 actions 层。 + +--- + +## 二、模块审查明细 + +### 2.1 school 模块 — 🟢 合格 + +**文件清单**:actions.ts (325 行) / data-access.ts (186 行) / schema.ts (51 行) / types.ts (42 行) + +**职责边界**:✅ 清晰。仅负责 schools / academicYears / departments / grades 的 CRUD。 + +**优点**: +- actions.ts 每个 Action 职责单一:权限校验 → 解析 → DB 写入 → 审计日志 → revalidatePath。 +- data-access.ts 仅包含只读查询,无跨模块写入。 +- 权限校验完整:SCHOOL_MANAGE / GRADE_MANAGE 均接入 requirePermission。 + +**问题**: +- ⚠️ **审计日志不一致**:仅 school 实体的 create/update/delete 调用了 `logAudit`,而 department / academicYear / grade 的 CRUD 均未记录审计日志。 +- ⚠️ `getStaffOptions` / `getGrades` 直接查询 users / roles / usersToRoles 表(跨模块读),但属于展示用关联查询,可接受。 + +**建议**:为 department / academicYear / grade 的 CRUD 补充 `logAudit` 调用,保持审计一致性。 + +--- + +### 2.2 classes 模块 — 🔴 严重 + +**文件清单**:actions.ts (765 行) / data-access.ts (**2104 行**) / types.ts (201 行) + +> ⚠️ `data-access.ts` 达 2104 行,**超出 1000 行硬性上限 2 倍**,违反项目代码质量规则。 + +#### 2.2.1 职责混乱 — 混入三个外部业务领域 + +`data-access.ts` 实际承载了四个业务领域的逻辑: + +| 行范围 | 逻辑 | 应属模块 | +|--------|------|---------| +| 49-145 | 教师身份解析、班级访问控制 | classes(合理) | +| 156-601 | 班级列表/学生/教师查询 | classes(合理) | +| 697-735 | 课表查询 `getClassSchedule` | **scheduling** | +| 760-998 | `getClassHomeworkInsights` 班级作业洞察(238 行) | **homework** | +| 1006-1300 | `getGradeHomeworkInsights` 年级作业洞察(294 行) | **homework** | +| 1302-1775 | 班级 CRUD + 邀请码 + 注册 | classes(合理) | +| 1838-1968 | 课表项 CRUD `createClassScheduleItem` 等 | **scheduling** | +| 1970-2103 | `getStudentsSubjectScores` 学生科目成绩(133 行) | **grades / homework** | + +**关键问题**: +- `getClassHomeworkInsights` 和 `getGradeHomeworkInsights` 合计 **532 行**作业统计逻辑,直接查询 `homeworkAssignments`、`homeworkSubmissions`、`homeworkAssignmentTargets`、`homeworkAssignmentQuestions`、`exams` 五张表,属于 homework 模块的核心业务,不应存在于 classes 模块。 +- 课表 CRUD(`createClassScheduleItem` / `updateClassScheduleItem` / `deleteClassScheduleItem`)与 scheduling 模块的 `applyAutoScheduleAction` 写入同一张 `classSchedule` 表,**两个模块对同一张表有写权限**,边界严重模糊。 +- `getStudentsSubjectScores` 直接关联 `homeworkSubmissions` + `exams` + `subjects` 计算学生科目分数,属于成绩分析逻辑。 + +#### 2.2.2 types.ts 跨领域类型污染 + +`types.ts` 定义了本应属于其他模块的类型: +- `ClassHomeworkInsights` / `GradeHomeworkInsights` / `ClassHomeworkAssignmentStats` / `ScoreStats` / `AssignmentSummary` — 应属 homework 模块 +- `ClassScheduleItem` / `CreateClassScheduleItemInput` / `UpdateClassScheduleItemInput` / `StudentScheduleItem` — 与 scheduling 模块的 `types.ts` 存在概念重叠 + +#### 2.2.3 actions.ts 跨模块直接查询 + +`actions.ts` 多处直接查询 `grades` 表(属于 school 模块)进行权限验证,绕过了 school/data-access: + +| 行号 | 函数 | 直接查询 | +|------|------|---------| +| 60-68 | `createTeacherClassAction` | `db.select().from(grades)` 校验 gradeHead 权限 | +| 186-194 | `createGradeClassAction` | `db.select().from(grades)` 校验年级管理权 | +| 241-249 | `updateGradeClassAction` | `db.select().from(classes)` 绕过自身 data-access | +| 251-272 | `updateGradeClassAction` | `db.select().from(grades)` 校验源/目标年级 | +| 340-348 | `deleteGradeClassAction` | `db.select().from(grades)` 校验权限 | + +**问题**:权限校验逻辑散落在 actions 层,既未下沉到 data-access,也未通过 school 模块暴露的查询接口。 + +#### 2.2.4 actions.ts 职责重复 + +存在三组近乎重复的 Action 集合: +- Teacher 系列:`createTeacherClassAction` / `updateTeacherClassAction` / `deleteTeacherClassAction` +- Admin 系列:`createAdminClassAction` / `updateAdminClassAction` / `deleteAdminClassAction` +- Grade 系列:`createGradeClassAction` / `updateGradeClassAction` / `deleteGradeClassAction` + +三者表单解析、字段校验逻辑高度重复,仅权限上下文不同。 + +#### 2.2.5 data-access.ts 内调用 auth() + +`getSessionTeacherId`(行 49-62)在 data-access 层直接调用 `auth()` 获取会话,违反"data-access 不感知请求上下文"的分层原则。会话信息应由 actions 层传入。 + +#### 2.2.6 组件层边界模糊 + +`classes/components/` 包含 `schedule-view.tsx`、`schedule-filters.tsx`、`class-detail/class-schedule-widget.tsx` 等课表相关组件,与 `scheduling/components/` 的职责重叠。 + +**整改建议**(优先级 P0): +1. 将 `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `getClassStudentSubjectScoresV2` 迁移至 homework / grades 模块。 +2. 将课表 CRUD(`createClassScheduleItem` 等)迁移至 scheduling 模块,统一 `classSchedule` 表的写入口。 +3. 将 `data-access.ts` 拆分为 `data-access.ts`(班级 CRUD)+ `data-access-enrollments.ts`(注册/邀请码)+ `data-access-insights.ts`(如暂不迁移则隔离)。 +4. 将权限校验中的 `grades` 表查询改为调用 school/data-access 暴露的接口。 +5. 将 `getSessionTeacherId` 上移至 actions 层或 shared/lib。 + +--- + +### 2.3 scheduling 模块 — 🟡 需改进(算法层优秀) + +**文件清单**:actions.ts (302 行) / auto-scheduler.ts (310 行) / data-access.ts (272 行) / schema.ts / types.ts + +#### 2.3.1 auto-scheduler.ts — ✅ 优秀范例 + +**这是全项目算法独立化的最佳实践**: +- 纯函数:`findOptimalSlot` / `validateSchedule` / `autoSchedule` / `buildDefaultTimeSlots` +- 无 `"server-only"` 副作用,无 DB 访问,无 `import { db }` +- 仅依赖 types.ts 的类型导入 +- **可独立单元测试**:给定输入即可断言输出,无需 mock 数据库 +- 算法清晰:贪心 + 约束检查(午餐、每日上限、教师/教室冲突、避免连排) + +**建议**:以此为模板,指导其他模块的算法抽取(如 homework 的批改评分算法、grades 的统计算法)。 + +#### 2.3.2 actions.ts — 跨模块直接写入 + +| 行号 | 函数 | 问题 | +|------|------|------| +| 110-116 | `autoScheduleAction` | 直接 `db.select().from(users)` 查询教师,绕过 data-access 的 `getTeachersForScheduling` | +| 168-180 | `applyAutoScheduleAction` | 直接 `db.transaction` 写入 `classSchedule` 表,**绕过 data-access 且跨模块写 classes 领域的表** | + +`applyAutoScheduleAction` 的问题尤为突出:它删除并重建 `classSchedule` 表数据,但该写操作既未经过 scheduling/data-access,也未经过 classes/data-access,形成**第三个对 classSchedule 表的写入口**(另两个在 classes/data-access 的 createClassScheduleItem 等)。 + +#### 2.3.3 data-access.ts — 跨模块读查询 + +包含 `getAdminClassesForScheduling` / `getTeachersForScheduling` / `getClassroomsForScheduling` / `getClassSubjectsForScheduling` 四个辅助查询,直接访问 `classes` / `classSubjectTeachers` / `subjects` / `users` / `classrooms` 表。 + +这些是排课场景的只读辅助查询,耦合度可接受,但理想情况下应通过各所属模块的 data-access 暴露接口。 + +#### 2.3.4 actions.ts 末尾 re-export + +```typescript +export { getSchedulingRules, getScheduleChanges, ... } from "./data-access" +``` +actions 层 re-export data-access 函数是反模式,应让消费方直接从 data-access 导入。 + +**整改建议**: +1. 将 `applyAutoScheduleAction` 中的 `classSchedule` 写入逻辑下沉到 data-access(或与 classes 协商统一写入口)。 +2. 将 `autoScheduleAction` 中的 users 查询改用 `getTeachersForScheduling`。 +3. 移除 actions.ts 末尾的 re-export。 + +--- + +### 2.4 attendance 模块 — 🟢 合格(结构典范) + +**文件清单**:actions.ts (271 行) / data-access.ts (271 行) / data-access-stats.ts (145 行) / schema.ts / types.ts + +**优点**: +- **stats 独立拆分**:`data-access-stats.ts` 专门承载统计逻辑,是 classes 模块应学习的拆分模式。 +- actions.ts 每个 Action 职责单一:权限校验 → 解析 → 委托 data-access → revalidate。 +- `DataScope` 数据范围控制完整接入,支持 6 种 scope 类型。 +- 无写操作泄漏到 actions 层。 + +**问题**: +- ⚠️ `getClassStudentsForAttendance`(data-access.ts 行 206-217)直接查询 `classEnrollments` 表获取班级学生列表,属于 classes 模块数据,应通过 classes/data-access 暴露的接口调用。 +- ⚠️ data-access.ts 和 data-access-stats.ts 均直接 JOIN `classes` 表获取班级名称(只读展示,可接受)。 + +**整改建议**:在 classes/data-access 暴露 `getClassStudentIds(classId)` 接口,供 attendance 调用。 + +--- + +### 2.5 users 模块 — 🟠 较严重 + +**文件清单**:actions.ts (151 行) / data-access.ts (71 行) / import-export.ts (291 行) + +#### 2.5.1 import-export.ts — 四重职责混合 + +该文件同时承载四类互不相关的职责: + +| 行范围 | 职责 | 应属位置 | +|--------|------|---------| +| 54-73 | `generateUserImportTemplate` 生成导入模板 | export 模块 | +| 78-111 | `parseUserImportData` 解析+校验导入数据 | import 模块 | +| 116-207 | `batchImportUsers` 批量导入(含用户创建) | **data-access** | +| 212-291 | `exportUsersToExcel` 导出用户列表 | export 模块 | + +**核心问题**: +1. **导入与导出未分离**:应拆分为 `import.ts` 与 `export.ts`。 +2. **用户创建逻辑泄漏**:`batchImportUsers`(行 158-167)直接 `db.insert(users)` + 密码哈希 + `db.insert(usersToRoles)`,这是 data-access 层的职责,不应存在于 import-export 工具文件中。 +3. **跨模块写 classEnrollments**:行 174-184,`batchImportUsers` 直接 `db.insert(classEnrollments)` 将学生注册到班级,**这是 classes 模块的写操作**,严重违反模块边界。 +4. **跨模块读 classes**:行 130-137,直接查询 `classes` 表根据邀请码查找班级,应通过 classes/data-access 暴露接口。 + +#### 2.5.2 actions.ts — 绕过 data-access + +`updateUserProfile`(行 29-51)直接 `db.update(users)` 写入数据库,绕过了 data-access 层。data-access.ts 仅有 `getUserProfile` 一个读函数,写操作缺失。 + +#### 2.5.3 data-access.ts — 过于单薄 + +仅 71 行,只包含 `getUserProfile`。用户创建、更新、角色绑定等写操作均未在此层实现。 + +**整改建议**(优先级 P1): +1. 将 `import-export.ts` 拆分为 `import.ts`(解析+校验)和 `export.ts`(模板生成+列表导出)。 +2. 将 `batchImportUsers` 中的用户创建逻辑迁移至 `data-access.ts` 的 `createUser` 函数,import.ts 仅负责编排。 +3. 将 classEnrollments 写入改为调用 classes/data-access 的注册接口(如 `enrollStudentByInvitationCode`)。 +4. 将 `updateUserProfile` 的 DB 写入下沉到 data-access。 + +--- + +### 2.6 audit 模块 — 🟡 需改进 + +**文件清单**:actions.ts (212 行) / data-access.ts (260 行) / types.ts + +**问题**: +- ⚠️ **Excel 导出逻辑内联在 actions 层**:`exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` 三个 Action 各自内联了 `exportToExcel` 调用及完整的列定义(表头、宽度、字段映射),每个约 40 行。这是展示/格式化逻辑,应抽取到独立的 `export.ts`。 +- ⚠️ 三个导出 Action 的结构高度重复(权限校验 → 查询 → 构造 columns → exportToExcel → 返回 buffer),可抽象为通用导出工厂。 + +**优点**: +- data-access.ts 职责清晰,仅包含日志查询,无跨模块问题。 +- 分页逻辑统一(clampPage / clampPageSize)。 + +**整改建议**:抽取 `export.ts`,封装 `exportAuditLogsToExcel(items)` / `exportLoginLogsToExcel(items)` / `exportDataChangeLogsToExcel(items)`,actions 层仅负责编排。 + +--- + +### 2.7 course-plans 模块 — 🟢 合格 + +**文件清单**:actions.ts (265 行) / data-access.ts (320 行) / schema.ts / types.ts + +**优点**: +- actions.ts 使用 `handleError` / `revalidatePlanPaths` 辅助函数消除重复,是 actions 层的**良好范例**。 +- data-access.ts 职责清晰:课程计划 CRUD + 周计划项 CRUD + 排序。 +- 跨模块查询仅为 `classes` / `subjects` / `users` 的 LEFT JOIN 取展示名称(只读,可接受)。 + +**小问题**: +- ⚠️ `getSubjectOptions`(行 310-320)直接查询 `subjects` 表,subjects 无独立模块,暂可接受。 + +**结论**:该模块结构可作为其他模块的参考模板。 + +--- + +### 2.8 announcements 模块 — 🟡 需改进 + +**文件清单**:actions.ts (242 行) / data-access.ts (120 行) / schema.ts / types.ts + +**核心问题 — 写操作泄漏到 actions 层**: + +data-access.ts 仅包含两个**只读**函数(`getAnnouncements` / `getAnnouncementById`),所有写操作均直接在 actions.ts 中执行 `db.insert` / `db.update` / `db.delete`: + +| Action | 直接 DB 操作 | 行号 | +|--------|-------------|------| +| `createAnnouncementAction` | `db.insert(announcements)` | 53-63 | +| `updateAnnouncementAction` | `db.update(announcements)` | 118-130 | +| `deleteAnnouncementAction` | `db.delete(announcements)` | 154 | +| `publishAnnouncementAction` | `db.update(announcements)` | 176-183 | +| `archiveAnnouncementAction` | `db.update(announcements)` | 206-212 | + +这违反了"data-access 层封装所有 DB 操作"的分层约定,导致: +- 写逻辑无法被其他 server 端代码复用 +- publishedAt 计算逻辑散落在 actions 中,难以测试 + +**其他问题**: +- ⚠️ 死代码:`updateAnnouncementAction` 行 108 计算 `wasPublished`,行 135 `void wasPublished` 显式丢弃,未实际使用。 +- ⚠️ `getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission(ANNOUNCEMENT_READ)`,与其他模块的权限模式不一致。 + +**整改建议**: +1. 在 data-access.ts 补充 `createAnnouncement` / `updateAnnouncement` / `deleteAnnouncement` / `publishAnnouncement` / `archiveAnnouncement` 写函数。 +2. actions 层仅保留权限校验 + 解析 + 委托调用。 +3. 清理 `wasPublished` 死代码。 + +--- + +## 三、跨模块直接查询汇总 + +### 3.1 跨模块写操作(严重) + +| 源文件 | 目标表 | 操作 | 应通过 | +|--------|--------|------|--------| +| classes/data-access.ts | classSchedule | CRUD | scheduling/data-access | +| scheduling/actions.ts | classSchedule | delete + insert | scheduling/data-access | +| users/import-export.ts | classEnrollments | insert | classes/data-access | +| users/import-export.ts | users, usersToRoles | insert | users/data-access | +| announcements/actions.ts | announcements | insert/update/delete | announcements/data-access | +| users/actions.ts | users | update | users/data-access | + +> ⚠️ `classSchedule` 表存在 **三个写入口**(classes/data-access、scheduling/actions、scheduling/data-access 间接),是数据完整性高风险点。 + +### 3.2 跨模块读操作(需评估) + +| 源文件 | 目标表 | 用途 | 评估 | +|--------|--------|------|------| +| classes/data-access.ts | homeworkAssignments, homeworkSubmissions, homeworkAssignmentTargets, homeworkAssignmentQuestions, exams | 作业洞察统计 | ❌ 应迁移至 homework | +| classes/data-access.ts | grades, schools | 年级/学校关联 | ⚠️ 应通过 school data-access | +| classes/actions.ts | grades | 权限校验 | ⚠️ 应通过 school data-access | +| scheduling/data-access.ts | classes, classSubjectTeachers, subjects, users, classrooms | 排课辅助查询 | 🟡 可接受(只读) | +| attendance/data-access.ts | classEnrollments | 获取班级学生 | ⚠️ 应通过 classes data-access | +| users/import-export.ts | classes | 邀请码查询 | ⚠️ 应通过 classes data-access | +| course-plans/data-access.ts | classes, subjects, users | 展示名称 JOIN | 🟢 可接受 | + +--- + +## 四、actions 层多职责问题汇总 + +| Action | 混入职责 | 应拆分 | +|--------|---------|--------| +| `users/import-export.ts: batchImportUsers` | 用户创建 + 密码哈希 + 角色绑定 + 班级注册 | 拆为 createUser + enrollStudent | +| `classes/actions.ts: updateGradeClassAction` | 班级更新 + 科任教师批量分配 | 拆为 updateClass + setClassSubjectTeachers(已部分拆分但仍在同一 Action 内编排) | +| `classes/actions.ts: updateAdminClassAction` | 同上 | 同上 | +| `audit/actions.ts: exportAuditLogsAction` | 查询 + Excel 列定义 + 导出 | 拆为 query + exportAuditLogsToExcel | +| `audit/actions.ts: exportLoginLogsAction` | 同上 | 同上 | +| `audit/actions.ts: exportDataChangeLogsAction` | 同上 | 同上 | +| `announcements/actions.ts: createAnnouncementAction` | 解析 + publishedAt 计算 + DB 写入 | DB 写入下沉 data-access | + +--- + +## 五、整改优先级 + +### P0 — 立即整改(影响数据完整性 & 严重违反规范) + +1. **classes/data-access.ts 拆分**:2104 行远超硬性上限,且混入 homework/scheduling/grades 三个领域。优先迁移 `getClassHomeworkInsights` / `getGradeHomeworkInsights`(532 行)至 homework 模块。 +2. **统一 classSchedule 表写入口**:classes 与 scheduling 两个模块对该表有写权限,需协商归属并收敛为单一写入口。 + +### P1 — 尽快整改(模块边界违反) + +3. **users/import-export.ts 拆分**:分离导入/导出,用户创建逻辑下沉 data-access,classEnrollments 写入改调 classes 接口。 +4. **announcements 写操作下沉**:在 data-access 补充写函数,actions 仅编排。 +5. **classes/actions.ts 权限校验**:grades 表查询改通过 school/data-access 接口。 + +### P2 — 持续优化(代码质量) + +6. **audit 导出逻辑抽取**:内联的 Excel 列定义移至独立 export.ts。 +7. **school 审计日志补全**:department/academicYear/grade 的 CRUD 补充 logAudit。 +8. **attendance 跨模块查询**:`getClassStudentsForAttendance` 改调 classes 接口。 +9. **scheduling/actions.ts**:移除 re-export,DB 写入下沉 data-access。 +10. **announcements 死代码清理**:移除 `void wasPublished`。 + +--- + +## 六、优秀实践(建议推广) + +| 实践 | 模块 | 说明 | +|------|------|------| +| 算法纯函数化 | scheduling/auto-scheduler.ts | 无 DB 依赖,可独立测试,应作为算法抽取模板 | +| stats 文件拆分 | attendance/data-access-stats.ts | 统计逻辑独立成文件,classes 应效仿 | +| actions 辅助函数 | course-plans/actions.ts | handleError / revalidatePlanPaths 消除重复 | +| DataScope 接入 | attendance/actions.ts | 6 种数据范围完整支持 | +| 权限统一接入 | school / attendance / course-plans | 全部 Action 使用 requirePermission | + +--- + +## 七、附:文件行数统计 + +| 文件 | 行数 | 上限 | 状态 | +|------|------|------|------| +| classes/data-access.ts | 2104 | 1000 | 🔴 超限 2.1x | +| classes/actions.ts | 765 | 800(建议) | 🟢 合规 | +| classes/types.ts | 201 | 无限制 | 🟢 合规 | +| school/actions.ts | 325 | 800(建议) | 🟢 合规 | +| school/data-access.ts | 186 | 800(建议) | 🟢 合规 | +| scheduling/auto-scheduler.ts | 310 | 无限制 | 🟢 合规 | +| scheduling/actions.ts | 302 | 800(建议) | 🟢 合规 | +| scheduling/data-access.ts | 272 | 800(建议) | 🟢 合规 | +| attendance/actions.ts | 271 | 800(建议) | 🟢 合规 | +| attendance/data-access.ts | 271 | 800(建议) | 🟢 合规 | +| attendance/data-access-stats.ts | 145 | 800(建议) | 🟢 合规 | +| users/import-export.ts | 291 | 800(建议) | 🟢 合规(但职责混合) | +| users/actions.ts | 151 | 800(建议) | 🟢 合规 | +| users/data-access.ts | 71 | 800(建议) | 🟢 合规 | +| audit/actions.ts | 212 | 800(建议) | 🟢 合规 | +| audit/data-access.ts | 260 | 800(建议) | 🟢 合规 | +| course-plans/actions.ts | 265 | 800(建议) | 🟢 合规 | +| course-plans/data-access.ts | 320 | 800(建议) | 🟢 合规 | +| announcements/actions.ts | 242 | 800(建议) | 🟢 合规 | +| announcements/data-access.ts | 120 | 800(建议) | 🟢 合规 | + +> 仅 `classes/data-access.ts` 突破硬性上限,需立即拆分。 + +--- + +*报告结束。本审查未修改任何源代码。* diff --git a/docs/architecture/audit/new-and-other-modules-audit.md b/docs/architecture/audit/new-and-other-modules-audit.md new file mode 100644 index 0000000..88eeea6 --- /dev/null +++ b/docs/architecture/audit/new-and-other-modules-audit.md @@ -0,0 +1,604 @@ +# 新增模块与其他模块架构审查报告 + +> 审查范围:elective / proctoring / diagnostic / notifications / dashboard / messaging / parent / settings / files / auth / layout / student +> 审查日期:2026-06-17 +> 审查依据:源码全量扫描 + 架构影响地图(004/005) + 差距审计报告(007) +> 审查目标:识别职责不单一、过耦合、边界模糊、幽灵路由等问题 + +--- + +## 一、总体评估 + +| 维度 | 模块数 | 严重问题 | 中等问题 | 轻微问题 | 总体评价 | +|------|--------|---------|---------|---------|----------| +| 新增模块 | 4 | 3 | 5 | 3 | ⚠️ 职责基本清晰,但存在跨模块耦合与未集成代码 | +| 其他模块 | 8 | 1 | 4 | 3 | ⚠️ dashboard 跨模块直查问题突出 | +| **合计** | **12** | **4** | **9** | **6** | **需重点修复 4 项严重问题** | + +### 严重问题清单(必须修复) + +1. **messaging 与 notifications 边界模糊、双向依赖** — 两个模块都写入 `messageNotifications` 表,notifications 反向依赖 messaging,类型系统不一致 +2. **dashboard/data-access.ts 直查 11 张跨模块表** — 违反模块封装原则 +3. **proctoring/exam-mode-config.tsx 未集成到考试表单** — DB schema 有 examMode 等字段但无 UI 录入入口,组件成为死代码 +4. **proctoring 事件上报存在 Server Action 与 REST API 双通道重复** — 同一逻辑两份代码 + +--- + +## 二、新增模块审查 + +### 2.1 elective 模块(选课管理) + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 304 | 11 个 Server Action(CRUD + 选课 + 抽签 + 查询) | +| `data-access.ts` | 242 | 课程 CRUD + 查询 + scope 过滤 | +| `data-access-operations.ts` | 217 | 选课操作(select/drop/lottery) | +| `data-access-selections.ts` | 189 | 选课记录查询 + 学生可用课程查询 | +| `schema.ts` | 132 | Zod 校验 schema | +| `types.ts` | 108 | 类型定义 + 标签常量 | + +#### 拆分为 3 个 data-access 文件是否合理? + +**结论:⚠️ 拆分意图合理,但存在代码重复** + +- **拆分意图合理**:operations(写操作)与 selections(读查询)职责分明,符合"按职责划分"原则 +- **代码重复问题**:`data-access.ts` 与 `data-access-selections.ts` 重复定义了 `mapCourseRow` 和 `buildCourseSelect` 两个函数(共约 60 行重复代码) + - `data-access.ts` 第 47-108 行 + - `data-access-selections.ts` 第 28-88 行 + - 两份代码完全相同,维护时易产生不一致 + +**建议**: +- 抽取共享的 `mapCourseRow` / `buildCourseSelect` 到 `data-access.ts` 并导出,`data-access-selections.ts` 复用 +- 或合并 `data-access-selections.ts` 到 `data-access.ts`(文件仅 189 行,合并后仍在 800 行上限内) + +#### 权限校验 + +✅ 全部 Server Action 均使用 `requirePermission`: +- 管理 Action 使用 `ELECTIVE_MANAGE` +- 选课 Action 使用 `ELECTIVE_SELECT` +- 查询 Action 使用 `ELECTIVE_READ` +- 学生查看他人选课记录有额外 dataScope 校验(actions.ts 第 283-288 行) + +#### 其他发现 + +- ✅ schema.ts 使用 Zod transform 统一处理空字符串转 null,规范 +- ⚠️ `data-access-operations.ts` 的 `runLottery` 使用 `Math.random()` 排序(第 40 行),结果不可复现,建议改用 DB 的 `ORDER BY RAND()` 或记录种子 +- ⚠️ `selectCourse` 在 FCFS 模式下先查后写(第 119-128 行),存在并发超卖风险,建议加事务或乐观锁 + +--- + +### 2.2 proctoring 模块(考试监考) + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 144 | 2 个 Server Action(上报事件 + 获取面板) | +| `data-access.ts` | 388 | 事件记录 + 查询 + 摘要统计 + 学生状态 | +| `types.ts` | 136 | 类型定义 + 标签常量 + 阈值常量 | +| `components/anti-cheat-monitor.tsx` | - | 学生端防作弊监控 | +| `components/exam-mode-config.tsx` | - | 考试模式配置表单(**未集成**) | +| `components/proctoring-dashboard.tsx` | - | 教师监考面板 | + +#### 职责清晰度 + +**结论:⚠️ 职责基本清晰,但存在 3 个严重问题** + +#### 严重问题 1:`exam-mode-config.tsx` 未集成到考试表单(死代码) + +- DB schema 已有 `examMode` / `durationMinutes` / `shuffleQuestions` / `allowLateStart` / `antiCheatEnabled` 字段(schema.ts 第 457-462 行) +- `proctoring/components/exam-mode-config.tsx` 提供了完整的配置 UI 组件 +- **但 `exams/components/exam-form.tsx` 并未导入该组件**(grep 确认无 `ExamModeConfig` 引用) +- `exams/components/exam-mode-selector.tsx` 是"手动组卷 vs AI 生成"的选择器,**与考试模式(homework/timed/proctored)无关**,命名易混淆 +- 结果:创建考试时无法设置监考模式,proctoring 模块的 `getExamForProctoring` 读取的 `examMode` 永远是默认值 `"homework"` + +**建议**:将 `exam-mode-config.tsx` 集成到 `exam-form.tsx`,或迁移到 exams 模块 + +#### 严重问题 2:事件上报存在 Server Action 与 REST API 双通道重复 + +- `proctoring/actions.ts` 第 58-105 行:`recordProctoringEventAction`(Server Action) +- `app/api/proctoring/event/route.ts` 第 27-90 行:POST handler(REST API) +- **两者逻辑完全相同**:都校验 submission 归属、调用 `recordProctoringEvent` +- `AntiCheatMonitor` 组件使用 Server Action(第 58 行),REST API 路由无调用方 + +**建议**:删除未使用的 `/api/proctoring/event` 路由,或让组件改用 REST API(适用于客户端轮询场景) + +#### 中等问题 3:跨模块读取 exams 表 + +- `data-access.ts` 第 326-353 行:`getExamForProctoring` 直接查询 `exams` 表 +- 第 178-185 行:`getExamProctoringSummary` 直接查询 `examSubmissions` 表 +- 第 249-256 行:`getStudentProctoringStatuses` 直接 join `examSubmissions` 和 `users` + +**建议**:可接受(监考本质是考试模块的扩展),但应在架构图中标注依赖关系 + +#### 其他发现 + +- ✅ `recordProctoringEventAction` 使用 `requireAuth()` 而非 `requirePermission`,符合"学生上报自己事件"场景 +- ✅ `getProctoringDashboardAction` 使用 `EXAM_PROCTOR` 权限 +- ✅ 异常阈值 `ABNORMAL_EVENT_THRESHOLD = 3` 提取为常量,便于调整 +- ⚠️ `actions.ts` 第 11-13 行直接 import `db` 和 `examSubmissions`,应在 data-access 层封装 submission 校验逻辑 + +--- + +### 2.3 diagnostic 模块(学情诊断) + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 148 | 6 个 Server Action(生成/发布/删除/查询报告) | +| `data-access.ts` | 254 | 知识点掌握度查询 + 从提交更新掌握度 | +| `data-access-reports.ts` | 202 | 诊断报告 CRUD | +| `types.ts` | 97 | 类型定义 | +| `components/` | 4 个 | 学生/班级诊断视图 + 雷达图 + 报告列表 | + +#### 与 grades 模块的边界 + +**结论:✅ 无职责重叠,但存在跨模块耦合** + +- **grades 模块**:管理 `gradeRecords` 表(分数记录),维度是"学生-班级-科目-考试" +- **diagnostic 模块**:管理 `knowledgePointMastery` 表(知识点掌握度),维度是"学生-知识点" +- 两者数据来源不同:grades 是教师录入的分数,diagnostic 是从 `submissionAnswers` 推导的正确率 +- **无任何代码重叠**:grep 确认 grades 模块无 `knowledgePoint` / `mastery` / `diagnostic` 关键字 + +#### 中等问题 1:`data-access.ts` 跨模块直查 4 张表 + +- 第 87-92 行:查询 `examSubmissions` 表(属 exams 模块) +- 第 95-101 行:查询 `submissionAnswers` 表(属 exams/homework 模块) +- 第 106-112 行:查询 `questionsToKnowledgePoints` 表(属 questions 模块) +- 第 150-158 行:查询 `classEnrollments` + `classes` + `users` 表(属 classes 模块) + +`updateMasteryFromSubmission` 函数(第 87-147 行)直接读取提交答案和题目-知识点关联,将 diagnostic 模块与 exams/homework/questions 模块紧耦合。 + +**建议**: +- 短期:在架构图中标注此依赖关系 +- 长期:由 exams/homework 模块在提交评分后主动调用 diagnostic 模块的更新接口(事件驱动) + +#### 轻微问题 2:`data-access-reports.ts` 有未使用代码 + +- 第 20 行定义 `round2` 函数 +- 第 201 行 `void round2` 仅为消除 lint 警告 +- **建议**:删除未使用的 `round2` 函数 + +#### 轻微问题 3:班级报告字段复用不当 + +- `data-access-reports.ts` 第 107 行:`studentId: generatedBy` — 班级报告将生成者 ID 存入 `studentId` 字段 +- 注释说明"schema 要求 NOT NULL",但这是 schema 设计缺陷的 workaround +- **建议**:修改 `learningDiagnosticReports` schema,将 `studentId` 改为可空,或增加 `classId` 字段 + +#### 权限校验 + +✅ 全部 Action 使用 `DIAGNOSTIC_MANAGE` 或 `DIAGNOSTIC_READ` 权限 + +--- + +### 2.4 notifications 模块(通知分发) + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 119 | 2 个 Server Action(单发 + 班级群发) | +| `data-access.ts` | 86 | 用户偏好 + 联系方式 + 日志 | +| `dispatcher.ts` | 152 | 渠道选择 + 并行分发 | +| `types.ts` | 70 | 通知负载 + 渠道配置类型 | +| `index.ts` | 38 | 对外导出入口 | +| `channels/` | 5 个 | SMS/Email/WeChat/InApp 渠道实现 | + +#### 严重问题 1:与 messaging 模块双向依赖、边界模糊 + +详见下文"三、messaging vs notifications 边界分析"。 + +#### 中等问题 2:`sendClassNotificationAction` 跨模块直查 + +- `actions.ts` 第 83-96 行:直接查询 `classes` 和 `classEnrollments` 表 +- 应通过 classes 模块的 data-access 获取班级学生列表 + +**建议**:调用 `classes` 模块的 data-access 函数获取学生 ID 列表 + +#### 中等问题 3:发送日志仅 console 输出 + +- `data-access.ts` 第 71-77 行:`logNotificationSend` 使用 `console.info` +- 代码注释承认"当前项目无 notification_logs 表" +- **影响**:无法查询历史发送记录、无法统计发送成功率、无法排查发送失败 + +**建议**:新增 `notification_logs` 表,记录 channel/userId/payload/success/error/sentAt + +#### 轻微问题 4:复用 MESSAGE_SEND 权限 + +- `actions.ts` 第 34、67 行:使用 `Permissions.MESSAGE_SEND` +- 代码注释说明"项目无独立 NOTIFICATION_SEND 权限点" +- **影响**:无法单独控制"谁能发通知"vs"谁能发私信" + +**建议**:新增 `NOTIFICATION_SEND` 权限点,或确认复用是设计意图 + +#### 设计亮点 + +- ✅ 渠道抽象优秀:`NotificationChannelSender` 接口 + 工厂函数,新增渠道只需实现接口 +- ✅ Mock 实现完善:SMS/Email/WeChat 均有 Mock 实现,开发环境零配置可用 +- ✅ 动态 import 第三方 SDK(阿里云/腾讯云/nodemailer),避免增加构建体积 +- ✅ 渠道选择逻辑清晰(dispatcher.ts 第 59-95 行) + +--- + +## 三、messaging vs notifications 边界分析(重点) + +### 3.1 现状对比 + +| 维度 | messaging 模块 | notifications 模块 | +|------|---------------|-------------------| +| **核心职责** | 站内私信 + 站内通知列表 | 多渠道通知分发 | +| **管理的表** | `messages` + `messageNotifications` + `notificationPreferences` | 无独有表(借用 messaging 的表) | +| **写入 messageNotifications** | ✅ 直接写(`createNotification`) | ✅ 通过 in-app 渠道写 | +| **通知类型枚举** | `NotificationType = "message" \| "announcement" \| "homework" \| "grade"` | `NotificationPayload.type = "info" \| "warning" \| "error" \| "success"` | +| **UI 组件** | message-list / message-detail / message-compose / notification-dropdown / notification-list | 无 UI 组件 | +| **偏好管理** | ✅ `notification-preferences.ts` | ❌ 借用 messaging 的 | +| **渠道支持** | 仅站内 | 站内 + SMS + Email + WeChat | + +### 3.2 严重问题:双向依赖与职责重叠 + +#### 问题 1:notifications 反向依赖 messaging + +``` +notifications/data-access.ts + → import { getNotificationPreferences } from "@/modules/messaging/notification-preferences" + → import type { NotificationPreferences } from "@/modules/messaging/types" + +notifications/channels/in-app-channel.ts + → import { createNotification } from "@/modules/messaging/data-access" +``` + +notifications 模块不拥有任何数据,全部依赖 messaging 模块。这违背了"模块应拥有自己的数据"原则。 + +#### 问题 2:messaging 绕过 notifications 直接写通知 + +`messaging/actions.ts` 第 66-72 行: + +```typescript +// Notify the receiver about the new message +await createNotification({ + userId: input.receiverId, + type: "message", + title: input.subject ? `New message: ${input.subject}` : "New message", + content: input.content.slice(0, 200), + link: `/messages/${id}`, +}) +``` + +这直接调用 `messaging/data-access.ts` 的 `createNotification`,**完全绕过 notifications 模块的 dispatcher**,导致: +- 用户设置的 SMS/Email 偏好被忽略 +- 新消息不会触发邮件/短信提醒 +- notifications 模块的多渠道能力形同虚设 + +#### 问题 3:类型系统不一致 + +- `messaging/types.ts` 第 23 行:`NotificationType = "message" | "announcement" | "homework" | "grade"`(按业务类别) +- `notifications/types.ts` 第 20 行:`type: "info" | "warning" | "error" | "success"`(按严重级别) +- `in-app-channel.ts` 第 49 行:`type: payload.type as "message" | "announcement" | "homework" | "grade"` — **强制类型转换,运行时可能写入非法值** + +DB schema 中 `messageNotifications.type` 为 `varchar(128)`,虽然不会报错,但语义混乱。 + +#### 问题 4:notification-preferences 归属不清 + +- `notificationPreferences` 表的 data-access 在 messaging 模块 +- 但 notifications 模块的 dispatcher 依赖此偏好决定渠道 +- settings 模块的 `notification-preferences-form.tsx` 调用 `messaging/actions.ts` 的 `updateNotificationPreferencesAction` +- 三个模块都在操作同一份数据,职责归属不清 + +### 3.3 建议方案 + +**方案 A(推荐):notifications 吞并 messaging 的通知部分** + +1. 将 `messageNotifications` 表和 `notificationPreferences` 表的所有权移交给 notifications 模块 +2. messaging 模块仅保留 `messages` 表(私信) +3. messaging 的 `sendMessageAction` 改为调用 `notifications.sendNotification` +4. messaging 的 UI 组件(notification-dropdown / notification-list)迁移到 notifications 模块 +5. 统一 `NotificationType` 枚举,支持业务类别 + 严重级别两个维度 + +**方案 B:保持现状,明确依赖方向** + +1. 在架构图中标注 notifications → messaging 的单向依赖 +2. messaging 不再直接调用 `createNotification`,改为调用 `notifications.sendNotification` +3. 消除双向依赖,但 notifications 仍不拥有数据 + +--- + +## 四、dashboard 模块审查(重点) + +### 4.1 严重问题:`data-access.ts` 直查 11 张跨模块表 + +`dashboard/data-access.ts` 的 `getAdminDashboardData` 函数直接查询以下表: + +| 表名 | 所属模块 | 查询内容 | +|------|---------|---------| +| `sessions` | auth | 活跃会话数 | +| `users` | users | 用户总数 + 最近注册用户 | +| `usersToRoles` | users | 用户角色统计 | +| `roles` | users | 角色名 | +| `classes` | classes | 班级总数 | +| `textbooks` | textbooks | 教材总数 | +| `chapters` | textbooks | 章节总数 | +| `questions` | questions | 题目总数 | +| `exams` | exams | 考试总数(含 scope 过滤) | +| `homeworkAssignments` | homework | 作业总数 + 已发布数 | +| `homeworkSubmissions` | homework | 提交数 + 待批改数 | + +**问题分析**: +- 违反模块封装原则:dashboard 直接依赖其他模块的 DB schema +- 任何模块的表结构变更都需要同步修改 dashboard +- 无法复用其他模块的 scope 过滤逻辑(dashboard 自己实现了 exam/homework 的 scope 过滤,第 31-73 行) + +### 4.2 学生/教师仪表盘的对比 + +**学生仪表盘**(`app/(dashboard)/student/dashboard/page.tsx`): +- ✅ 正确做法:调用 `classes/data-access` 的 `getStudentClasses` / `getStudentSchedule` +- ✅ 调用 `homework/data-access` 的 `getStudentDashboardGrades` / `getStudentHomeworkAssignments` +- ✅ 不直接查询任何表 + +**教师仪表盘**(`app/(dashboard)/teacher/dashboard/page.tsx`): +- ✅ 调用 `classes/data-access` 的 `getClassSchedule` / `getTeacherClasses` +- ✅ 调用 `homework/data-access` 的 `getHomeworkAssignments` / `getHomeworkSubmissions` / `getTeacherGradeTrends` +- ⚠️ 第 18-21 行直接查询 `users` 表获取教师姓名:`db.query.users.findFirst(...)` — 应使用 users 模块的 data-access + +### 4.3 建议方案 + +**方案 A(理想):聚合 API 模式** + +为每个模块添加 `getModuleStats(scope?)` 函数,dashboard 聚合调用: +```typescript +const [userStats, classStats, textbookStats, ...] = await Promise.all([ + getUsersStats(scope), + getClassStats(scope), + getTextbookStats(), + ... +]) +``` + +**方案 B(务实):接受 dashboard 作为跨模块聚合层** + +- 在架构图中明确标注 dashboard 对所有业务模块的依赖 +- 将 scope 过滤逻辑下沉到各模块的 `getStats` 函数 +- 至少消除 dashboard 中重复实现的 exam/homework scope 过滤(第 31-73 行) + +--- + +## 五、其他模块审查 + +### 5.1 messaging 模块 + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 245 | 9 个 Server Action(私信 + 通知 + 偏好) | +| `data-access.ts` | 252 | 私信 CRUD + 通知 CRUD + 收件人查询 | +| `notification-preferences.ts` | 166 | 通知偏好 CRUD | +| `schema.ts` | 17 | 私信发送校验 | +| `types.ts` | 108 | 私信 + 通知 + 偏好类型 | + +#### 问题 + +- ❌ **职责过多**:同时管理私信(messages)、站内通知(messageNotifications)、通知偏好(notificationPreferences)三类数据 +- ❌ **与 notifications 模块边界模糊**:详见第三节 +- ⚠️ `getRecipients` 函数(第 227-251 行)根据 dataScope 查询收件人,逻辑较复杂,可考虑下沉到 users 模块 + +### 5.2 parent 模块 + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 234 | 子女关系 + 子女仪表盘数据聚合 | +| `types.ts` | 57 | 类型定义 | +| `components/` | 7 个 | 子女卡片 + 详情 + 仪表盘 | + +#### 评价 + +- ✅ **职责单一**:仅负责家长视角的子女数据聚合与展示 +- ✅ **正确复用其他模块**: + - 调用 `classes/data-access` 的 `getStudentClasses` / `getStudentSchedule` + - 调用 `homework/data-access` 的 `getStudentDashboardGrades` / `getStudentHomeworkAssignments` + - 调用 `grades/data-access` 的 `getStudentGradeSummary` +- ✅ 不直接查询业务表(仅查询 `parentStudentRelations` 自有表 + `users`/`classes`/`classEnrollments`/`grades` 用于基本信息) +- ⚠️ `getChildBasicInfo` 第 74-105 行多次串行查询(grade → class),可优化为 join + +### 5.3 settings 模块 + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `actions.ts` | 205 | AI Provider CRUD + 测试连通性 | +| `actions-password.ts` | 113 | 修改密码 | +| `components/` | 8 个 | 通用设置 + AI 配置 + 密码 + 主题 + 通知偏好 | + +#### 问题:职责混杂但可接受 + +settings 模块混合了 5 类职责: +1. AI Provider 管理(`actions.ts` + `ai-provider-settings-card.tsx`) +2. 密码修改(`actions-password.ts` + `password-change-form.tsx`) +3. 个人资料(`profile-settings-form.tsx`) +4. 主题偏好(`theme-preferences-card.tsx`) +5. 通知偏好表单(`notification-preferences-form.tsx` — **调用 messaging 模块的 Action**) + +**评价**: +- ⚠️ AI Provider 管理与"用户设置"语义距离较远,可考虑独立为 `ai-config` 模块 +- ⚠️ `notification-preferences-form.tsx` 第 14 行 import `updateNotificationPreferencesAction` from `@/modules/messaging/actions` — 跨模块 UI 依赖 +- ✅ 密码修改有速率限制(`actions-password.ts` 第 33-37 行) +- ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验 +- ✅ 密码修改仅要求 `requireAuth()`(自助操作),符合最小权限原则 +- ⚠️ 无 `data-access.ts` 文件,`actions.ts` 直接使用 `db` — 建议抽取 data-access 层 + +### 5.4 files 模块 + +#### 模块结构 + +| 文件 | 行数 | 职责 | +|------|------|------| +| `data-access.ts` | 267 | 文件附件 CRUD + 批量删除 + 统计 | +| `types.ts` | - | 类型定义 | +| `components/` | 6 个 | 上传 + 列表 + 预览 + 管理 | + +#### 评价 + +- ✅ **职责单一**:仅管理 `fileAttachments` 表 +- ✅ 不跨模块查询 +- ✅ 批量删除有容错处理(第 152-177 行,失败时回退到逐条删除) +- ⚠️ 所有函数都用 try-catch 吞掉错误返回空数组/null,可能掩盖真实问题 +- ⚠️ 无 actions.ts 文件 — 文件上传通过 `app/api/upload/route.ts` 和 `app/api/files/[id]/route.ts` 实现,data-access 被路由直接调用 + +### 5.5 auth 模块 + +#### 模块结构 + +| 文件 | 职责 | +|------|------| +| `components/auth-layout.tsx` | 认证页面布局 | +| `components/login-form.tsx` | 登录表单 | +| `components/register-form.tsx` | 注册表单 | + +#### 评价 + +- ✅ **纯 UI 模块**:无 data-access / actions / types 文件 +- ✅ 认证逻辑由 NextAuth + `shared/lib/auth-guard` 统一处理 +- ✅ 职责清晰 + +### 5.6 layout 模块 + +#### 模块结构 + +| 文件 | 职责 | +|------|------| +| `components/app-sidebar.tsx` | 侧边栏(根据权限渲染导航) | +| `components/sidebar-provider.tsx` | 侧边栏状态 Context | +| `components/site-header.tsx` | 顶部导航(含通知下拉) | +| `config/navigation.ts` | 导航配置(4 个角色) | + +#### navigation.ts 幽灵路由审查 + +**结论:✅ 无幽灵路由**(007 报告中提到的 13 个幽灵路由已全部修复) + +逐一核对导航配置中的所有 href 与 `src/app/` 下的实际页面: + +| 角色 | 导航项数 | 全部存在 | 备注 | +|------|---------|---------|------| +| admin | 19 | ✅ | 包括子菜单项 | +| teacher | 22 | ✅ | 包括子菜单项 | +| student | 12 | ✅ | 包括子菜单项 | +| parent | 5 | ✅ | 包括子菜单项 | + +#### 存在但未纳入导航的页面 + +| 路由 | 说明 | 建议 | +|------|------|------| +| `/admin/attendance` | 管理员考勤页面 | 如需管理员查看全校考勤,应加入 admin 导航 | +| `/admin/files` | 管理员文件管理 | 应加入 admin 导航 | +| `/parent/children/[studentId]` | 子女详情页 | 通过仪表盘卡片跳转,可不加入导航 | +| `/settings/security` | 安全设置子页 | 通过 settings 页 Tab 切换,无需独立导航 | +| `/profile` | 个人主页 | 通过 header 头像菜单跳转,无需独立导航 | + +#### 其他发现 + +- ✅ `app-sidebar.tsx` 第 36-43 行根据权限动态选择角色导航配置,符合 RBAC +- ⚠️ 第 39 行判断学生逻辑:`permissions.includes(Permissions.HOMEWORK_SUBMIT) && !permissions.includes(Permissions.EXAM_CREATE)` — 用权限反推角色,不够直观,建议改用 `hasRole("student")` + +### 5.7 student 模块 + +#### 模块结构 + +| 文件 | 职责 | +|------|------| +| `components/student-courses-view.tsx` | 学生课程视图 | +| `components/student-schedule-filters.tsx` | 课表筛选器 | +| `components/student-schedule-view.tsx` | 学生课表视图 | + +#### 评价 + +- ✅ **纯 UI 模块**:无 data-access / actions / types +- ✅ 数据由 `app/(dashboard)/student/learning/courses/page.tsx` 和 `app/(dashboard)/student/schedule/page.tsx` 通过 classes 模块的 data-access 获取 +- ⚠️ 与 classes 模块的 `schedule-view.tsx` / `schedule-filters.tsx` 可能存在功能重叠,建议核查 + +--- + +## 六、跨模块依赖关系图 + +``` +dashboard ──直查──> users, classes, textbooks, questions, exams, homework (11 张表) +parent ──调用──> classes, homework, grades (data-access) +diagnostic ──直查──> examSubmissions, submissionAnswers, questionsToKnowledgePoints, classes +notifications ──依赖──> messaging (偏好 + in-app 渠道) +messaging ──绕过──> notifications (直接写 messageNotifications) +proctoring ──直查──> exams, examSubmissions, users +settings ──调用──> messaging (通知偏好 Action) +``` + +--- + +## 七、修复优先级 + +### P0(严重,应立即修复) + +| 序号 | 问题 | 模块 | 工作量 | 影响 | +|------|------|------|--------|------| +| 1 | messaging 绕过 notifications 直接写通知 | messaging | 小 | 用户通知偏好失效,多渠道通知无效 | +| 2 | proctoring/exam-mode-config.tsx 未集成 | proctoring | 小 | 监考功能无法启用,组件为死代码 | +| 3 | proctoring 事件上报双通道重复 | proctoring | 小 | 代码重复,维护成本 | +| 4 | notifications 反向依赖 messaging | notifications | 中 | 架构耦合,难以独立演进 | + +### P1(中等问题,下个迭代修复) + +| 序号 | 问题 | 模块 | 工作量 | +|------|------|------|--------| +| 5 | dashboard 直查 11 张跨模块表 | dashboard | 大 | +| 6 | diagnostic 跨模块直查 4 张表 | diagnostic | 中 | +| 7 | notifications 无 notification_logs 表 | notifications | 中 | +| 8 | sendClassNotificationAction 直查 classes 表 | notifications | 小 | +| 9 | elective 两个 data-access 文件代码重复 | elective | 小 | +| 10 | settings/notification-preferences-form 跨模块依赖 | settings | 小 | + +### P2(轻微问题,机会修复) + +| 序号 | 问题 | 模块 | +|------|------|------| +| 11 | diagnostic/data-access-reports.ts 有未使用代码 | diagnostic | +| 12 | diagnostic 班级报告 studentId 字段复用 | diagnostic | +| 13 | elective runLottery 使用 Math.random | elective | +| 14 | teacher dashboard 直查 users 表 | dashboard | +| 15 | files 模块 try-catch 吞错误 | files | +| 16 | layout 用权限反推角色 | layout | + +--- + +## 八、总结 + +### 新增模块质量 + +- **elective**:✅ 质量较好,拆分合理但有代码重复 +- **proctoring**:⚠️ 有死代码(exam-mode-config 未集成)和重复实现(双通道上报) +- **diagnostic**:✅ 与 grades 无重叠,但跨模块耦合较重 +- **notifications**:⚠️ 渠道抽象优秀,但与 messaging 边界模糊、反向依赖 + +### 重点问题回答 + +1. **elective 拆分为 3 个 data-access 文件是否合理?** + 合理,但需消除 `data-access.ts` 与 `data-access-selections.ts` 之间的代码重复 + +2. **proctoring 模块职责是否清晰?** + 基本清晰,但 `exam-mode-config.tsx` 应属于 exams 模块或集成到考试表单 + +3. **diagnostic 与 grades 是否有职责重叠?** + 无重叠。grades 管分数记录,diagnostic 管知识点掌握度,数据来源和维度均不同 + +4. **notifications 与 messaging 边界是否清晰?** + 不清晰。存在双向依赖、类型不一致、职责重叠三个问题,建议按方案 A 合并 + +5. **dashboard 是否直查其他模块的表?** + 是。`getAdminDashboardData` 直查 11 张跨模块表,是本次审查最严重的封装违规 + +6. **settings 是否混入太多职责?** + 混合了 5 类职责,但作为"设置"聚合点尚可接受。AI Provider 管理可考虑独立 + +7. **navigation.ts 是否有幽灵路由?** + 无。007 报告中的 13 个幽灵路由已全部修复 diff --git a/docs/architecture/audit/shared-audit.md b/docs/architecture/audit/shared-audit.md new file mode 100644 index 0000000..d5d6d92 --- /dev/null +++ b/docs/architecture/audit/shared-audit.md @@ -0,0 +1,196 @@ +# Shared 基础设施层审查报告 + +> 审查日期:2026-06-17 +> 审查范围:`src/shared/`(db、lib、hooks、components、types)+ `src/auth.ts` + `src/proxy.ts` +> 审查依据:职责单一性、函数复杂度、模块间耦合、架构文档完整性 + +## 概览 + +- 文件总数:69(不含测试文件) + - `db/`:3 + - `lib/`:13 + - `hooks/`:7 + - `components/ui/`:34 + - `components/a11y/`:4 + - `components/`(顶层):4 + - `types/`:2 + - `src/auth.ts`、`src/proxy.ts`:2 +- 发现问题数:15 +- 严重程度分布:高 3 / 中 9 / 低 3 + +--- + +## 职责单一性问题 + +### 1. `src/shared/db/schema.ts` + +- **问题**:单个文件包含 54 张表定义,共 1111 行,**超过项目规则中"任何文件不超过 1000 行"的硬性上限**。文件涵盖用户、认证、题库、教学、学校、班级、考试、作业、AI、公告、审计、成绩、文件、课程计划、消息、考勤、排课、选课、监考、学情诊断等十余个业务域。此外分节编号混乱:section 12(Parent-Student Relations,行 958)出现在 section 14b(Notification Preferences,行 934)之后,P2 段落与主编号交错。 +- **严重程度**:高 +- **建议**:按业务域拆分为多个 schema 文件(如 `schema/auth.ts`、`schema/academic.ts`、`schema/exam.ts`、`schema/audit.ts` 等),通过 `schema/index.ts` 聚合导出。同时修正分节编号。 + +### 2. `src/auth.ts` + +- **问题**:293 行,混合了多种职责: + 1. NextAuth 配置(providers、callbacks、events) + 2. 密码安全 DB 操作(`getOrCreatePasswordSecurity`、`recordFailedLogin`、`resetFailedLogin`,行 56-130)——这些是数据访问层逻辑,不应内联在认证配置中 + 3. 角色规范化工具(`normalizeRole`、`resolvePrimaryRole`,行 13-27) + 4. bcrypt 哈希规范化(`normalizeBcryptHash`,行 29-33) + 5. IP 解析(`resolveClientIp`,行 39-51) + 6. `authorize` 回调内联了限流、锁定检查、密码比对、日志记录全流程(86 行) +- **严重程度**:高 +- **建议**:将密码安全 DB 操作迁移到 `shared/lib/password-security-service.ts`(与纯函数的 `password-policy.ts` 区分);角色规范化迁移到 `shared/lib/permissions.ts` 或新建 `shared/lib/role-utils.ts`;`resolveClientIp` 迁移到 `shared/lib/http-utils.ts`(与三个 logger 共用)。 + +### 3. `src/shared/lib/ai.ts` + +- **问题**:218 行,混合了 5 类职责: + 1. 请求负载解析与校验(`parseAiChatPayload`,行 70-96) + 2. API Key 加密/解密(`encryptAiApiKey`/`decryptAiApiKey`,行 104-124) + 3. Provider 配置 DB 查询(`getAiProviderConfig`,行 126-179) + 4. AI 客户端创建与调用(`getAiClient`、`createAiChatCompletion`、`testAiProviderConfig`、`testAiProviderById`) + 5. 错误格式化(`getAiErrorMessage`) + + 其中加密/解密与 Provider 配置查询属于数据层,与 AI 调用本身是不同关注点。 +- **严重程度**:中 +- **建议**:将加密/解密拆到 `shared/lib/crypto.ts`(通用加密工具);将 Provider 配置查询拆到 `shared/lib/ai-provider-repo.ts`(数据访问)。`ai.ts` 保留负载解析与 AI 调用编排。 + +### 4. `src/shared/components/onboarding-gate.tsx` + +- **问题**:312 行组件,混合了: + 1. 多步表单 UI + 2. 角色推断业务逻辑(行 90-94):通过权限反推角色(`isAdmin`/`isTeacher`/`isStudent`/`isParent`),逻辑脆弱且未使用 `usePermission().hasRole()` + 3. 硬编码教学学科列表(`TEACHER_SUBJECTS`,行 20)——业务数据固化在 shared 基础设施 + 4. 直接 `fetch("/api/onboarding/status")` 和 `fetch("/api/onboarding/complete")`——耦合特定 API 路由 +- **严重程度**:中 +- **建议**:角色判断改用 `usePermission().hasRole()` 或 session 的 `role` 字段;学科列表迁移到 modules 层配置或 DB;API 调用通过 Server Action 封装。组件本身可考虑按步骤拆分子组件。 + +### 5. `src/shared/components/global-search.tsx` + +- **问题**:221 行组件,混合了: + 1. 搜索 UI 与下拉渲染 + 2. 硬编码业务类型(`ResultType = "question" | "textbook" | "exam" | "announcement"`,行 12)与图标/标签映射(行 30-42)——业务知识泄漏到 shared 层 + 3. 直接 `fetch("/api/search?...")`——耦合特定 API 路由与查询协议 + 4. 快捷键、点击外部、键盘导航等交互逻辑内联 +- **严重程度**:中 +- **建议**:搜索结果类型与图标映射应由 API 返回或从 modules 层注入;API 调用抽取为独立 hook(`useGlobalSearch`);交互逻辑可拆为 `useSearchKeyboard` 等。 + +### 6. `src/proxy.ts` + +- **问题**:75 行,硬编码了路由-权限映射(`ROUTE_PERMISSIONS`、`API_PERMISSIONS`,行 8-19),使用原始字符串如 `"school:manage"`、`"exam:read"`,**未复用 `Permissions` 常量**,违反项目规则"前端组件禁止硬编码 role/权限"的精神。`resolveDefaultPath`(行 21-27)将角色到默认路径的业务映射硬编码在代理中。 +- **严重程度**:中 +- **建议**:权限字符串改用 `Permissions.SCHOOL_MANAGE` 等常量;路由权限映射迁移到 `shared/lib/route-permissions.ts` 配置文件;角色-路径映射迁移到 modules 层或路由配置。 + +### 7. `src/shared/lib/a11y.ts` + +- **问题**:`useA11yId`(行 7-10)是一个 React Hook,但放置在 `lib/` 目录而非 `hooks/` 目录。项目约定 `hooks/` 存放所有自定义 Hook,`lib/` 存放纯工具函数。该文件其余函数(`mergeA11yProps`、`describeInput`、`loadingAria`)是纯函数,放置正确。 +- **严重程度**:低 +- **建议**:将 `useA11yId` 迁移到 `shared/hooks/use-a11y-id.ts`,`a11y.ts` 保留纯函数。 + +--- + +## 过耦合函数 + +### 1. `resolveDataScope` @ `src/shared/lib/auth-guard.ts:64-130` + +- **行数**:67 +- **参数数**:2(`userId: string`, `roleNames: string[]`) +- **问题**:单个函数内根据角色分支查询 4 张不同的表(`grades`、`classes`、`classSubjectTeachers`、`parentStudentRelations`),将权限范围解析与数据访问混合。每个角色分支的查询逻辑独立,新增角色需修改此函数,违反开闭原则。 +- **建议**:将各角色的数据范围查询拆为独立函数(如 `resolveTeacherScope`、`resolveParentScope`),或迁移到各模块的 data-access 层,`resolveDataScope` 仅做分发。 + +### 2. `getAiProviderConfig` @ `src/shared/lib/ai.ts:126-179` + +- **行数**:53 +- **参数数**:1(`providerId?: string`) +- **问题**:函数内有三段几乎相同的 DB 查询分支(按 providerId、按 isDefault、fallback),每段都 select 相同的字段、解密 apiKey、返回相同结构,存在明显代码重复。 +- **建议**:提取公共 `mapProviderRow(row)` 函数,三个分支简化为查询条件不同。或合并为单查询带 OR 条件 + 排序优先级。 + +### 3. `authorize`(NextAuth Credentials 回调)@ `src/auth.ts:143-229` + +- **行数**:86 +- **参数数**:1(`credentials`) +- **问题**:单函数内串联了:邮箱密码校验 → 速率限制 → DB 用户查询 → 账户锁定检查 → 密码比对 → 失败计数 → 成功重置 → 角色查询 → 返回。流程长且混合了限流、安全策略、认证、日志多个关注点。内部还使用 `Promise.all` + 动态 `import`(行 165-168)加载 `@/shared/db` 和 schema,写法不寻常。 +- **建议**:将流程拆分为 `checkRateLimit`、`checkAccountLockout`、`verifyPassword`、`loadUserRoles` 等步骤函数,`authorize` 仅编排。动态 import 改为静态 import。 + +### 4. `OnboardingGate` 组件 @ `src/shared/components/onboarding-gate.tsx:27-312` + +- **行数**:285(组件函数体) +- **参数数**:0(无 props,内部消费 session) +- **问题**:单组件承担了状态检查、4 步表单、角色推断、API 提交、路由跳转。组件内 9 个 `useState`,3 个 `useEffect`,逻辑密集。 +- **建议**:按步骤拆分为 `OnboardingRoleStep`、`OnboardingProfileStep`、`OnboardingRoleDetailStep`、`OnboardingCompleteStep` 子组件;提取 `useOnboarding` hook 封装状态与提交逻辑。 + +### 5. `GlobalSearch` 组件 @ `src/shared/components/global-search.tsx:49-221` + +- **行数**:172(组件函数体) +- **参数数**:2(`className?`, `placeholder?`) +- **问题**:单组件承担了输入控制、防抖搜索、快捷键监听、点击外部关闭、键盘导航、结果渲染。6 个 `useState`,3 个 `useEffect`。 +- **建议**:提取 `useGlobalSearch(query)` hook 封装搜索请求与状态;提取 `useSearchKeyboard` 封装快捷键与导航。组件仅负责渲染。 + +--- + +## 模块间依赖问题 + +### 1. shared 层与 `@/auth` 的循环依赖 + +- **涉及模块**:`shared/lib/{audit-logger, change-logger, auth-guard}` → `@/auth` → `shared/lib/{login-logger, permissions, password-policy, rate-limit}` + `shared/db` +- **问题类型**:循环依赖 +- **问题详情**: + - `shared/lib/audit-logger.ts`(行 7)`import { auth } from "@/auth"` + - `shared/lib/change-logger.ts`(行 6)`import { auth } from "@/auth"` + - `shared/lib/auth-guard.ts`(行 1)`import { auth } from "@/auth"` + - 而 `src/auth.ts` 反向依赖 `shared/lib/permissions`、`shared/lib/login-logger`、`shared/lib/password-policy`、`shared/lib/rate-limit`、`shared/db` + + 这构成了 `shared/lib/*` → `auth` → `shared/lib/*` 的循环。虽然 NextAuth 的 `auth()` 函数是运行时调用而非模块级副作用,目前不会导致运行时错误,但架构上 shared 基础设施层不应反向依赖业务层的认证入口。 +- **建议**:将 `auth()` 的 session 获取抽象为接口或通过参数注入。logger 函数改为接收 `session` 参数(由调用方传入),而非内部调用 `auth()`。或创建 `shared/lib/session.ts` 封装 session 获取,`auth.ts` 和 logger 都依赖它,打破循环。 + +### 2. shared 层对根模块 `@/auth` 的反向依赖 + +- **涉及模块**:`shared/lib/*` → `@/auth`(根模块) +- **问题类型**:反向依赖 +- **问题详情**:`src/auth.ts` 位于项目根目录,属于应用层(非 shared 层)。shared 层应是被依赖方,不应依赖应用层模块。三个文件(audit-logger、change-logger、auth-guard)直接 import `@/auth`,使 shared 层无法独立测试或复用。 +- **建议**:同上,通过依赖注入或提取 `shared/lib/session.ts` 解耦。 + +### 3. 三个 logger 重复实现 IP/Header 提取 + +- **涉及模块**:`shared/lib/audit-logger`、`shared/lib/change-logger`、`shared/lib/login-logger`、`src/auth.ts` +- **问题类型**:过度耦合(DRY 违反) +- **问题详情**:三个 logger 各自重复实现相同的 IP/User-Agent 提取逻辑: + - `audit-logger.ts`(行 27-32):`headerList.get("x-forwarded-for") ?? headerList.get("x-real-ip") ?? "unknown"` + - `change-logger.ts`(行 27-31):相同逻辑 + - `login-logger.ts`(行 26-31):相同逻辑 + - `auth.ts`(行 39-51):`resolveClientIp` 也是类似逻辑(取 `x-forwarded-for` 第一段) + + 四处实现略有差异(auth.ts 取逗号分隔第一段,其他取全值),存在不一致风险。 +- **建议**:提取 `shared/lib/http-utils.ts`,导出 `getClientIp()` 和 `getUserAgent()` 统一复用。 + +--- + +## 架构文档改进建议 + +1. **补充依赖关系图**:当前 004 文档以函数/常量为粒度列举导出,但缺少模块间依赖方向的可视化图。建议在 005 JSON 中增加 `dependencyMatrix` 节点,记录 `shared/lib/* → @/auth`、`@/auth → shared/lib/*` 等依赖边,并在 004 Markdown 中用 Mermaid 图渲染。本次审查发现的循环依赖(shared ↔ auth)在当前文档中完全不可见。 + +2. **标注循环依赖与反向依赖**:004/005 文档应明确标注 `shared/lib/{audit-logger, change-logger, auth-guard}` 对 `@/auth` 的依赖,以及这与 `@/auth` 对 `shared/lib/*` 的依赖构成的循环。当前文档将 `auth` 模块与 `shared` 模块分别描述,未揭示二者双向依赖。 + +3. **修正 schema.ts 分节编号**:004 文档的"数据库表"章节按表名平铺列举,未反映 schema.ts 源文件中的分节结构。建议文档增加 schema.ts 分节映射表,并修正源文件中 section 12 出现在 section 14b 之后的编号混乱。 + +4. **增加 shared 层边界说明**:004 文档应明确 shared 层"不应依赖应用层模块(如 `@/auth`)"的架构约束,以及哪些文件属于 shared 层的对外公共 API。当前文档未说明 shared 与根模块(auth.ts、proxy.ts)的边界。 + +5. **补充函数复杂度标注**:005 JSON 中每个函数已有签名记录,但缺少行数与参数数量字段。建议增加 `"lines"` 和 `"paramCount"` 字段,便于自动识别过耦合函数(如本次发现的 `authorize` 86 行、`resolveDataScope` 67 行)。 + +6. **记录 proxy.ts 的路由权限映射**:004 文档未记录 `proxy.ts` 中的 `ROUTE_PERMISSIONS` 和 `API_PERMISSIONS` 硬编码映射,也未说明这些映射与 `Permissions` 常量的关系。建议在 005 JSON 的 `routes` 节点中补充代理层权限规则。 + +--- + +## 附:审查范围文件清单 + +| 目录 | 文件数 | 最大文件(行数) | 备注 | +|------|--------|------------------|------| +| `src/shared/db/` | 3 | schema.ts (1111) | **超过 1000 行硬性上限** | +| `src/shared/lib/` | 13 | ai.ts (218) | | +| `src/shared/hooks/` | 7 | use-aria-live.ts (88) | | +| `src/shared/components/ui/` | 34 | chart.tsx (329) | 多为标准 shadcn/ui 组件 | +| `src/shared/components/a11y/` | 4 | focus-trap.tsx (110) | | +| `src/shared/components/`(顶层) | 4 | onboarding-gate.tsx (312) | | +| `src/shared/types/` | 2 | permissions.ts (92) | | +| `src/auth.ts` | 1 | auth.ts (293) | | +| `src/proxy.ts` | 1 | proxy.ts (75) | | + +> 注:`components/ui/` 下 34 个文件多为 shadcn/ui 标准生成组件(基于 Radix UI),职责单一,未发现结构性问题,故未逐一列入问题清单。`chart.tsx`(329 行)为标准 shadcn chart 组件,行数较高但属框架约定,可接受。 diff --git a/docs/design/002_teacher_dashboard_implementation.md b/docs/design/002_teacher_dashboard_implementation.md index e53a2d9..952f008 100644 --- a/docs/design/002_teacher_dashboard_implementation.md +++ b/docs/design/002_teacher_dashboard_implementation.md @@ -1,3 +1,10 @@ +> ⚠️ **已归档文档** +> 本文档记录的是 2025-12-23 教师仪表盘的实现细节(含 Hydration 修复)。 +> 当前实现已演进,最新架构与组件清单详见 [004 架构影响地图](../architecture/004_architecture_impact_map.md) 的 dashboard 模块章节。 +> 保留用于历史参考,不再维护。 + +--- + # 教师仪表盘实现与 Hydration 修复记录 **日期**: 2025-12-23 diff --git a/docs/design/003_textbooks_module_implementation.md b/docs/design/003_textbooks_module_implementation.md index 7a4b836..6390eab 100644 --- a/docs/design/003_textbooks_module_implementation.md +++ b/docs/design/003_textbooks_module_implementation.md @@ -1,3 +1,10 @@ +> ⚠️ **已归档文档** +> 本文档记录的是 2025-12-23(更新 2026-01-13)教材模块的实现细节。 +> 当前实现已演进,最新架构与组件清单详见 [004 架构影响地图](../architecture/004_architecture_impact_map.md) 的 textbooks 模块章节。 +> 保留用于历史参考,不再维护。 + +--- + # Textbooks Module Implementation Details **Date**: 2025-12-23 diff --git a/docs/design/004_question_bank_implementation.md b/docs/design/004_question_bank_implementation.md index 920c894..3b22459 100644 --- a/docs/design/004_question_bank_implementation.md +++ b/docs/design/004_question_bank_implementation.md @@ -1,10 +1,17 @@ +> ⚠️ **已归档文档** +> 本文档记录的是 2025-12-23 题库模块的实现细节。 +> 当前实现已演进,最新架构与组件清单详见 [004 架构影响地图](../architecture/004_architecture_impact_map.md) 的 questions 模块章节。 +> 保留用于历史参考,不再维护。 + +--- + # 题库模块实现 ## 1. 概述 题库模块(`src/modules/questions`)是教师管理考试资源的核心组件,提供完整的 CRUD 能力,并支持搜索/筛选等常用管理能力。 -**状态**:已实现 -**日期**:2025-12-23 +**状态**:已实现 +**日期**:2025-12-23 **作者**:前端高级工程师 --- diff --git a/docs/design/005_exam_module_implementation.md b/docs/design/005_exam_module_implementation.md index fb52633..ad96f23 100644 --- a/docs/design/005_exam_module_implementation.md +++ b/docs/design/005_exam_module_implementation.md @@ -1,7 +1,14 @@ +> ⚠️ **已归档文档** +> 本文档记录的是考试模块的实现设计(含与作业模块合并的调整说明)。 +> 当前实现已演进,最新架构与组件清单详见 [004 架构影响地图](../architecture/004_architecture_impact_map.md) 的 exams 模块章节。 +> 保留用于历史参考,不再维护。 + +--- + # 考试模块实现设计文档 ## 1. 概述 -考试模块用于教师侧的“试卷制作与管理”,覆盖创建考试、组卷(支持嵌套分组)、发布/归档等流程。 +考试模块用于教师侧的"试卷制作与管理",覆盖创建考试、组卷(支持嵌套分组)、发布/归档等流程。 **说明(合并调整)**:与“作业(Homework)”模块合并后,考试模块不再提供“阅卷/评分(grading)”与提交流转;教师批改统一在 Homework 的 submissions 中完成。 diff --git a/docs/design/006_homework_module_implementation.md b/docs/design/006_homework_module_implementation.md index d16d5eb..faddcdf 100644 --- a/docs/design/006_homework_module_implementation.md +++ b/docs/design/006_homework_module_implementation.md @@ -1,7 +1,14 @@ +> ⚠️ **已归档文档** +> 本文档记录的是 2025-12-31 作业模块的实现设计。 +> 当前实现已演进,最新架构与组件清单详见 [004 架构影响地图](../architecture/004_architecture_impact_map.md) 的 homework 模块章节。 +> 保留用于历史参考,不再维护。 + +--- + # 作业模块实现设计文档(Homework Module) -**日期**: 2025-12-31 -**模块**: Homework (`src/modules/homework`) +**日期**: 2025-12-31 +**模块**: Homework (`src/modules/homework`) --- diff --git a/docs/design/008_teacher_pages_implementation.md b/docs/design/008_teacher_pages_implementation.md index e3dac5b..9dbc0c1 100644 --- a/docs/design/008_teacher_pages_implementation.md +++ b/docs/design/008_teacher_pages_implementation.md @@ -1,7 +1,14 @@ +> ⚠️ **已归档文档** +> 本文档记录的是 2026-03-03 教师端页面实现的分析。 +> 当前路由与页面已大幅扩展,最新路由清单详见 [004 架构影响地图](../architecture/004_architecture_impact_map.md) 的 routes.teacher 章节。 +> 保留用于历史参考,不再维护。 + +--- + # 教师端页面实现分析文档 -**日期**: 2026-03-03 -**范围**: Teacher 路由与页面实现(`src/app/(dashboard)/teacher`) +**日期**: 2026-03-03 +**范围**: Teacher 路由与页面实现(`src/app/(dashboard)/teacher`) --- diff --git a/docs/design/009_feature_gap_analysis.md b/docs/design/009_feature_gap_analysis.md index 44f88e7..5fb4a81 100644 --- a/docs/design/009_feature_gap_analysis.md +++ b/docs/design/009_feature_gap_analysis.md @@ -1,7 +1,14 @@ +> ⚠️ **已归档文档** +> 本文档是 2026-03-03 的功能实现对比文档(已实现 vs 规划)。 +> 当前已由 [007 差距审计报告](../architecture/007_gap_audit_report.md) 取代——007 基于 004/005 架构图全量扫描,覆盖更完整、数据更新。 +> 保留用于历史参考,不再维护。 + +--- + # 功能实现对比文档(已实现 vs 规划) -**日期**: 2026-03-03 -**范围**: 基于 PRD 与现有设计文档的功能落地对比 +**日期**: 2026-03-03 +**范围**: 基于 PRD 与现有设计文档的功能落地对比 --- diff --git a/docs/design/010_qa_test_plan_and_feedback.md b/docs/design/010_qa_test_plan_and_feedback.md index b3c761e..a631cb8 100644 --- a/docs/design/010_qa_test_plan_and_feedback.md +++ b/docs/design/010_qa_test_plan_and_feedback.md @@ -1,8 +1,15 @@ +> ⚠️ **已归档文档** +> 本文档是 2026-03-18 的项目全量测试方案与执行反馈。 +> 当前测试体系已演进(Vitest 单元/集成 + Playwright E2E + CI 安全扫描),最新 CI 配置详见 [004 架构影响地图](../architecture/004_architecture_impact_map.md) 的 devops 章节。 +> 保留用于历史参考,不再维护。 + +--- + # 项目全量测试方案与执行反馈 -**日期**: 2026-03-18 -**角色**: 首席测试师 -**范围**: `src/app` 页面路由、`src/modules` 业务模块、`src/app/api` 接口路由、工程质量门禁 +**日期**: 2026-03-18 +**角色**: 首席测试师 +**范围**: `src/app` 页面路由、`src/modules` 业务模块、`src/app/api` 接口路由、工程质量门禁 --- diff --git a/docs/work_log.md b/docs/work_log.md index 5b964fd..f6ae8ed 100644 --- a/docs/work_log.md +++ b/docs/work_log.md @@ -2,6 +2,54 @@ ## 2026-06-17 +### 架构全面审查与文档重构 + +#### 问题背景 +用户反馈:架构文档阅读后仍无法理解各模块、函数关系,说明文档质量不足或代码耦合度过高。006/007 及其他文档长期未更新。 + +#### 完成工作 + +##### 1. 全项目逐文件审查(4 份审查报告) +- `docs/architecture/audit/shared-audit.md` - shared 基础设施层(69 文件,15 问题) +- `docs/architecture/audit/core-business-audit.md` - 核心业务模块(exams/homework/questions/textbooks/grades) +- `docs/architecture/audit/management-modules-audit.md` - 管理模块(school/classes/scheduling/attendance/users/audit/course-plans/announcements) +- `docs/architecture/audit/new-and-other-modules-audit.md` - 新增模块和其他模块(elective/proctoring/diagnostic/notifications/dashboard/messaging/parent/settings/files/auth/layout/student) +- `docs/architecture/audit/00_summary.md` - 汇总报告 + +##### 2. 发现的关键问题 +**P0 严重问题(6 项)**: +1. `classes/data-access.ts` 2104 行,超硬上限 2.1 倍,混入 homework/scheduling/grades 逻辑 +2. `homework/data-access.ts` 1038 行,超硬上限,混入排名计算 +3. `shared/lib` ↔ `auth` 循环依赖 +4. `dashboard/data-access.ts` 直查 11 张跨模块表 +5. `messaging` 绕过 `notifications` 直接写通知 +6. `classSchedule` 表三处写入口(数据完整性风险) + +**P1 较严重问题(6 项)**: +- 跨模块直接 DB 查询普遍存在(classes 被 8+ 处直接查询) +- actions 层混入数据访问逻辑 +- auth.ts 混合 5 类职责 +- users/import-export.ts 四重职责 +- proctoring/exam-mode-config.tsx 死代码 +- notifications 反向依赖 messaging + +##### 3. 重写架构文档(5 份) +- **004 架构影响地图**:从"罗列函数签名"重构为"图优先 + 结构化",含分层架构图、模块依赖关系图(标注合理/违规/循环依赖)、数据流向图、核心调用链路、26 个模块清单、P0/P1/P2 问题分级与解耦建议 +- **005 架构数据 JSON**:新增 architectureOverview、moduleDependencyGraph(25 节点 37 边)、knownIssues(12 问题)、dbTables(54 表按业务域分组)节点;补全 permissions(52→54)、apiRoutes(10→11) +- **006 功能清单**:添加"当前实现状态"列,143 个功能项标注 ✅/⚠️/❌,P0 覆盖率 80%→92% +- **007 差距审计报告**:v2→v3,总体完成度 P0 69%→84%,P2 路线图 8/14=57%,新增架构技术债章节 +- **001 项目概览**:更新为 6 角色/54 权限/26 模块/54 表,新增架构原则和项目状态章节 + +##### 4. 文档治理 +- 创建 `docs/README.md` 文档索引(架构/审查/专题/归档分类) +- 11 个过时文档添加"已归档"标注(002×2/003/设计文档×8) +- 所有文档保持活跃维护与归档分离 + +#### 验证 +- 待验证 lint + tsc + +--- + ### P2 质量保障类实现(5 项全部完成) #### 1. 屏幕阅读器兼容性增强(a11y)