12 KiB
12 KiB
grades 成绩模块
成绩全生命周期管理:录入(单条/批量/按试卷)/导入导出/分析统计/草稿锁定/申诉处理/成绩单生成 代码规模: 60 文件 · 360 符号 · 120,932 行(arch.db) 模块路径:
src/modules/grades/
架构图
graph TB
subgraph Client["客户端组件"]
direction TB
BatchEntry["batch-grade-entry<br/>(376 行) 批量录入"]
ExcelDlg["excel-import-dialog<br/>(519 行) Excel 导入"]
RecordList["grade-record-list<br/>(232 行) 成绩列表"]
RecordForm["grade-record-form<br/>(196 行) 录入表单"]
ReportCard["report-card-view<br/>(324 行) 成绩单"]
Charts["分析图表<br/>distribution/comparison/<br/>trend/mastery/growth"]
DraftBanner["draft-lock-banner<br/>草稿锁定横幅"]
end
subgraph Hooks["Hooks"]
UseUndo["use-batch-grade-entry-undo<br/>(101 行) 批量撤销"]
UseLock["use-draft-lock<br/>(185 行) 锁定状态"]
end
subgraph Server["服务端 Actions(6 文件)"]
Actions["actions.ts<br/>(745 行) 核心 CRUD"]
ActAnalytics["actions-analytics<br/>(353 行) 分析"]
ActImport["actions-import<br/>(295 行) 导入"]
ActAppeal["actions-appeal<br/>(215 行) 申诉"]
ActLock["actions-lock<br/>(209 行) 锁定"]
ActDraft["actions-draft<br/>(123 行) 草稿"]
end
subgraph DA["Data Access"]
DA1["data-access.ts<br/>(635 行) 核心 CRUD"]
DADraft["data-access-drafts<br/>(410 行) 草稿"]
DAAppeal["data-access-appeals<br/>(273 行) 申诉"]
DARank["data-access-ranking<br/>(199 行) 排名"]
DAExam["data-access-exam-entry<br/>(190 行) 按试卷录入"]
DAAnalytics["analytics 4 维度<br/>class(390)/overview(136)<br/>student(262)/trend(135)"]
end
subgraph Lib["纯函数库"]
Stats["stats-service.ts<br/>(696 行) 统计纯函数"]
StatsTest["stats-service.test.ts<br/>(665 行) 单测"]
Scope["lib/scope-check.ts<br/>assertClassInScope"]
ScopeFilter["lib/scope-filter.ts"]
Report["lib/report-card.ts<br/>(385 行) 成绩单生成"]
Export["export.ts + import-export.ts<br/>导出/导入"]
Notify["lib/notify.ts<br/>(188 行) 通知"]
Guards["lib/type-guards.ts"]
GradeUtils["lib/grade-utils.ts"]
end
subgraph DB["数据库表"]
GR[(grade_records)]
GRD[(grade_record_drafts)]
GA[(grade_appeals)]
GL[(grade_locks)]
end
subgraph Cross["跨模块联动"]
Diagnostic["diagnostic<br/>updateMasteryFromExamScore"]
Exams["exams<br/>getExamForGradeEntry"]
ErrorBook["error-book<br/>collectFromExamSubmission"]
end
BatchEntry --> UseUndo
BatchEntry --> Actions
ExcelDlg --> ActImport
RecordList --> Actions
RecordForm --> Actions
ReportCard --> Report
Charts --> ActAnalytics
DraftBanner --> UseLock
UseLock --> ActLock
Actions --> DA1
ActAnalytics --> DAAnalytics
ActAnalytics --> Stats
ActImport --> Export
ActAppeal --> DAAppeal
ActLock --> GL
ActDraft --> DADraft
DAExam --> DAExam2["data-access-exam-entry"]
DA1 --> GR
DADraft --> GRD
DAAppeal --> GA
DA1 --> Scope
DA1 --> Notify
Actions --> Diagnostic
DAExam --> Exams
Actions --> ErrorBook
核心流程图
流程 1: 按试卷批量录入成绩(跨模块联动)
sequenceDiagram
participant U as 教师
participant BE as batch-grade-entry
participant Act as actions.ts
participant DAExam as data-access-exam-entry
participant Ex as exams data-access
participant Diag as diagnostic
participant EB as error-book
participant DB as MySQL
U->>BE: 选择试卷 + 班级
BE->>Act: batchCreateByExamAction
Act->>Act: requirePermission(GRADE_RECORD_MANAGE)
Act->>Act: assertClassInScope(班级范围校验)
Act->>Ex: getExamForGradeEntry(题目分值)
Act->>DAExam: batchCreateGradeRecordsByExam
DAExam->>DB: INSERT grade_records(批量)
DAExam-->>Act: 成功
Act->>Diag: updateMasteryFromExamScore<br/>更新知识点掌握度
Act->>EB: collectFromExamSubmission<br/>采集错题
Act->>Act: notifyGradeEntered + notifyScoreAnomaly
Act-->>BE: ActionState
BE->>U: 支持撤销(undoBatchCreate)
流程 2: 成绩分析(纯函数统计)
flowchart LR
A[选择班级/学生/学期] --> B[actions-analytics]
B --> C{分析维度}
C -->|班级统计| D[data-access-analytics-class<br/>390 行]
C -->|全校概览| E[data-access-analytics-overview<br/>136 行]
C -->|学生汇总| F[data-access-analytics-student<br/>262 行]
C -->|趋势分析| G[data-access-analytics-trend<br/>135 行]
D --> H[stats-service.ts<br/>696 行纯函数]
E --> H
F --> H
G --> H
H --> I[computeGradeStats<br/>average/median/stdDev<br/>passRate/excellentRate]
I --> J[charts 渲染<br/>distribution/comparison/trend]
流程 3: 草稿保存与成绩锁定
sequenceDiagram
participant U as 教师
participant BE as batch-grade-entry
participant Lock as use-draft-lock
participant ActD as actions-draft
participant ActL as actions-lock
participant DAD as data-access-drafts
participant DB as MySQL
U->>BE: 录入中(未提交)
BE->>ActD: saveDraftAction
ActD->>DAD: upsertDraft
DAD->>DB: UPSERT grade_record_drafts
Note over U,DB: 草稿不写入 grade_records<br/>支持断点续录
U->>BE: 提交成绩
BE->>ActD: commitDraftAction
ActD->>DAD: 读取草稿 → 批量插入 grade_records
ActD->>DAD: 删除草稿
ActD-->>BE: 成功
U->>Lock: 锁定成绩(防篡改)
Lock->>ActL: lockGradesAction
ActL->>ActL: requirePermission(GRADE_LOCK)
ActL->>DB: INSERT grade_locks
ActL-->>Lock: 锁定后成绩只读
技术栈
| 类别 | 技术 | 用途 |
|---|---|---|
| 数据库 | Drizzle ORM | grade_records / drafts / appeals / locks 表 CRUD |
| 缓存 | cacheFn | data-access 函数请求级去重(86 处) |
| Server Action | requirePermission | GRADE_RECORD_MANAGE / GRADE_LOCK 等权限点 |
| URL 状态 | nuqs | 分析页筛选条件 URL 同步 |
| 表单验证 | Zod | schema.ts(238 行) 输入校验 |
| 数据可视化 | recharts | 分布/对比/趋势/掌握度/成长档案图表 |
| Excel 导入导出 | 自定义 export/import-export | 成绩导出 + Excel 模板导入 |
| 统计计算 | stats-service 纯函数 | average/median/stdDev/passRate/excellentRate |
| 权限范围 | scope-check | assertClassInScope 防越权访问他班成绩 |
文件结构
src/modules/grades/
├─ actions.ts # 核心 CRUD Action (745 行): 成绩录入/更新/删除/查询
├─ actions-analytics.ts # 分析 Action (353 行)
├─ actions-import.ts # 导入 Action (295 行)
├─ actions-appeal.ts # 申诉 Action (215 行)
├─ actions-lock.ts # 锁定 Action (209 行)
├─ actions-draft.ts # 草稿 Action (123 行)
├─ data-access.ts # 核心 CRUD (635 行): grade_records
├─ data-access-drafts.ts # 草稿数据访问 (410 行)
├─ data-access-appeals.ts # 申诉数据访问 (273 行)
├─ data-access-ranking.ts # 排名查询 (199 行)
├─ data-access-exam-entry.ts # 按试卷录入 (190 行)
├─ data-access-analytics.ts # 分析入口 (58 行)
├─ data-access-analytics-{class,overview,student,trend}.ts # 四维度 (390/136/262/135)
├─ stats-service.ts # 统计纯函数 (696 行)
├─ stats-service.test.ts # 统计单测 (665 行)
├─ export.ts # 导出 (291 行)
├─ import-export.ts # 导入 (278 行)
├─ schema.ts # Zod 验证 (238 行)
├─ types.ts # 类型定义 (387 行)
├─ lib/
│ ├─ report-card.ts # 成绩单生成 (385 行)
│ ├─ notify.ts # 通知 (188 行)
│ ├─ scope-check.ts # 班级范围校验 (37 行)
│ ├─ scope-filter.ts # 范围过滤 (52 行)
│ ├─ scope-check.test.ts # 范围校验单测 (74 行)
│ ├─ grade-utils.ts # 工具函数 (21 行)
│ └─ type-guards.ts # 类型守卫 (30 行)
├─ hooks/
│ ├─ use-draft-lock.ts # 草稿/锁定状态 (185 行)
│ └─ use-batch-grade-entry-undo.ts # 批量撤销 (101 行)
└─ components/ # 28 个组件
├─ excel-import-dialog.tsx # Excel 导入 (519 行)
├─ batch-grade-entry.tsx # 批量录入 (376 行)
├─ report-card-view.tsx # 成绩单 (324 行)
├─ grade-record-detail.tsx # 详情 (239 行)
├─ grade-record-list.tsx # 列表 (232 行)
├─ grade-record-row.tsx # 行 (200 行)
├─ grade-record-form.tsx # 表单 (196 行)
├─ knowledge-point-mastery-chart.tsx # 知识点掌握度 (237 行)
├─ class-comparison-chart.tsx # 班级对比 (260 行)
├─ grade-distribution-chart.tsx # 分布 (210 行)
└─ growth-archive-chart.tsx / grade-trend-card.tsx / ranking-trend-card.tsx / ...
依赖关系
依赖(本模块 → 他模块)
| 模块 | 依赖类型 | 用途 |
|---|---|---|
| exams | import | getExamForGradeEntry 按试卷录入 |
| diagnostic | data-access-call + import | updateMasteryFromExamScore 联动掌握度 |
| error-book | data-access-call + import | collectFromExamSubmission 采集错题 |
| classes | data-access-call + import | 班级信息 + 范围校验 |
| school | data-access-call + import | 学科/年级/学年过滤 |
| users | data-access-call + import | 学生/教师身份 |
| notifications | data-access-call + import | 成绩录入/异常通知 |
| parent | data-access-call + import | 成绩单推送家长 |
| attendance | data-access-call | 考勤关联成绩分析 |
| shared | import | DB / cache / auth-guard / 工具函数 |
被依赖(他模块 → 本模块)
| 模块 | 依赖类型 | 用途 |
|---|---|---|
| attendance | data-access-call + import | 考勤模块引用成绩分析 |
| diagnostic | import | 诊断引用成绩数据 |
| parent | data-access-call + import | 家长端查看成绩 |
| school | import | 学校维度成绩统计 |
关键约束
- actions 按职责拆 6 文件: actions.ts(745 行)近上限,按 CRUD/分析/导入/申诉/锁定/草稿拆分,避免超 1000 行
- data-access 按职责 + 维度拆分: 主 CRUD + 草稿/申诉/排名/试卷录入 + analytics 四维度(class/overview/student/trend)
- scope-check 统一范围校验:
assertClassInScope确保班级在用户权限范围内,防止越权访问他班成绩 - type-guards 替代 as 断言: 遵循禁止 as 断言规范,类型收窄通过类型守卫
- 录入联动诊断与错题: 成绩录入后调用
updateMasteryFromExamScore更新掌握度 +collectFromExamSubmission采集错题 - stats-service 纯函数 + 单测: 696 行统计逻辑独立 + 665 行单测,保证计算正确且可独立测试
架构决策
| 决策 | 原因 |
|---|---|
| actions 按职责拆 6 文件 | 成绩场景多(CRUD/分析/导入/申诉/锁定/草稿),拆分避免单文件超 1000 行硬上限 |
| stats-service 独立纯函数 | 统计逻辑与 DB I/O 解耦,696 行纯函数 + 665 行单测,保证计算正确可复用 |
| analytics 按维度拆 4 文件 | 班级/概览/学生/趋势四维度独立,按场景加载,避免单文件臃肿 |
| scope-check 集中范围校验 | 防越权访问他班成绩,权限校验收口单一入口,assertClassInScope 返回错误码 |
| type-guards 替代 as 断言 | 遵循编码规范禁止 as,类型收窄通过类型守卫实现 |
| 录入联动 diagnostic/error-book | 成绩是掌握度计算与错题采集的源头,录入时同步触发,保证数据一致 |