diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index e7520bd..75912f8 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -1316,6 +1316,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P2 已修复:~~`updateMasteryFromSubmission` 循环内串行 await upsert~~ 改为 `Promise.all` 并行执行所有 upsert - ✅ P2 已修复:~~`getClassMasterySummary` 串行查询(className → studentIds → userMap → masteryRows)~~ 改为两组 `Promise.all` 并行(className+studentIds,userMap+masteryRows) - ✅ P2 已修复:~~`getDiagnosticReports` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注 +- ✅ P0-2 已修复:~~`data-access-reports.ts` 直查 `users` 表获取姓名~~ 改为通过 `users/data-access.getUserNamesByIds` 跨模块接口 +- ✅ P2-2 已修复:~~`class-diagnostic-view.tsx`/`student-diagnostic-view.tsx`/`mastery-radar-chart.tsx` 中存在 Tailwind 任意值~~ 改用标准 Tailwind 类(w-44/max-w-32/text-xs/h-96/max-w-lg) - ⚠️ P2:班级报告将生成者 ID 存入 `studentId` 字段(schema 设计缺陷 workaround) - ✅ 与 grades 模块无职责重叠(grades 管分数,diagnostic 管知识点掌握度) diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index 99f3daa..6fc5307 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -7941,6 +7941,102 @@ "usedBy": [ "updateGradeRecordAction" ] + }, + { + "name": "DeleteGradeRecordSchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "deleteGradeRecordAction" + ] + }, + { + "name": "GetGradeRecordByIdSchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getGradeRecordByIdAction" + ] + }, + { + "name": "GradeQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getGradeRecordsAction" + ] + }, + { + "name": "ClassGradeStatsQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getClassGradeStatsAction" + ] + }, + { + "name": "StudentGradeSummaryQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getStudentGradeSummaryAction" + ] + }, + { + "name": "ClassRankingQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getClassRankingAction" + ] + }, + { + "name": "ExportGradesSchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "exportGradesAction" + ] + }, + { + "name": "GradeTrendQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getGradeTrendAction" + ] + }, + { + "name": "ClassComparisonQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getClassComparisonAction" + ] + }, + { + "name": "SubjectComparisonQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getSubjectComparisonAction" + ] + }, + { + "name": "GradeDistributionQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getGradeDistributionAction" + ] + }, + { + "name": "RankingTrendQuerySchema", + "type": "ZodSchema", + "file": "schema.ts", + "usedBy": [ + "getRankingTrendAction" + ] } ], "types": [ @@ -8154,6 +8250,48 @@ "migratedTo": "shared/lib/utils.formatDateForFile" } ], + "lib": [ + { + "name": "toNumber", + "signature": "(v: unknown) => number", + "file": "lib/grade-utils.ts", + "purpose": "安全将 unknown 值转换为有限数字,非有限值返回 0(P1-2 新增:从 data-access/data-access-analytics/data-access-ranking 抽取)", + "usedBy": [ + "data-access.serializeRecord", + "data-access.getClassGradeStats", + "data-access-analytics.getGradeTrend", + "data-access-analytics.getClassComparison", + "data-access-analytics.getSubjectComparison", + "data-access-analytics.getGradeDistribution", + "data-access-ranking.getRankingTrend" + ] + }, + { + "name": "normalize", + "signature": "(score: number, fullScore: number) => number", + "file": "lib/grade-utils.ts", + "purpose": "将原始分数归一化到 0-100 分制(P1-2 新增:从 data-access-analytics/data-access-ranking 抽取)", + "usedBy": [ + "data-access-analytics.getGradeTrend", + "data-access-analytics.getSubjectComparison", + "data-access-analytics.getGradeDistribution", + "data-access-ranking.getRankingTrend" + ] + }, + { + "name": "buildScopeClassFilter", + "signature": "(scope: DataScope) => SQL | null", + "file": "lib/grade-utils.ts", + "purpose": "根据 DataScope 构建 gradeRecords 表的行级权限过滤条件(P1-2 新增:从 data-access/data-access-analytics 抽取)", + "usedBy": [ + "data-access.getGradeRecords", + "data-access-analytics.getGradeTrend", + "data-access-analytics.getClassComparison", + "data-access-analytics.getSubjectComparison", + "data-access-analytics.getGradeDistribution" + ] + } + ], "components": [ { "name": "GradeRecordForm", diff --git a/docs/architecture/audit/grades-diagnostic-audit-report.md b/docs/architecture/audit/grades-diagnostic-audit-report.md new file mode 100644 index 0000000..04d54dd --- /dev/null +++ b/docs/architecture/audit/grades-diagnostic-audit-report.md @@ -0,0 +1,672 @@ +# 成绩和学情诊断模块审计报告 + +> 审查日期:2026-06-22 +> 审查范围:`src/modules/grades/**`(成绩模块)、`src/modules/diagnostic/**`(学情诊断模块)、`src/app/(dashboard)/{admin,teacher,student,parent}/grades/**`、`src/app/(dashboard)/{teacher,student}/diagnostic/**`、`src/app/(dashboard)/management/grade/**`、相关 i18n 翻译文件 +> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.6(grades)、§2.22(diagnostic)、`docs/architecture/005_architecture_data.json` L7362(grades)、L10927(diagnostic) + +--- + +## 一、现有实现概要 + +### 1.1 文件分布 + +#### grades 模块(成绩分析) + +| 层 | 路径 | 文件数 | 行数 | 说明 | +|----|------|--------|------|------| +| Actions | `src/modules/grades/actions.ts` | 1 | 312 | 10 个 Server Action(CRUD + 查询 + 导出) | +| Actions | `src/modules/grades/actions-analytics.ts` | 1 | 133 | 5 个分析 Server Action(趋势/对比/分布/排名) | +| Data-access | `src/modules/grades/data-access.ts` | 1 | 433 | 成绩 CRUD + 统计(含统计业务逻辑) | +| Data-access | `src/modules/grades/data-access-analytics.ts` | 1 | 337 | 趋势/对比/分布分析(含统计业务逻辑) | +| Data-access | `src/modules/grades/data-access-ranking.ts` | 1 | 119 | 排名查询(含 normalize 逻辑) | +| Export | `src/modules/grades/export.ts` | 1 | 200 | Excel 导出(明细 + 班级汇总) | +| Schema | `src/modules/grades/schema.ts` | 1 | 52 | 4 个 Zod schema | +| Types | `src/modules/grades/types.ts` | 1 | 186 | 14 个类型定义 | +| Components | `src/modules/grades/components/*` | 16 | 41~442 | 16 个组件(含 batch-grade-entry 442 行) | + +#### diagnostic 模块(学情诊断) + +| 层 | 路径 | 文件数 | 行数 | 说明 | +|----|------|--------|------|------| +| Actions | `src/modules/diagnostic/actions.ts` | 1 | 172 | 6 个 Server Action(生成/发布/删除/查询) | +| Data-access | `src/modules/diagnostic/data-access.ts` | 1 | 257 | 知识点掌握度查询 + 更新 | +| Data-access | `src/modules/diagnostic/data-access-reports.ts` | 1 | 203 | 诊断报告 CRUD(**直查 users 表**) | +| Schema | `src/modules/diagnostic/schema.ts` | 1 | 48 | 6 个 Zod schema | +| Types | `src/modules/diagnostic/types.ts` | 1 | 97 | 11 个类型定义 | +| Components | `src/modules/diagnostic/components/*` | 4 | 69~267 | 4 个组件(含 class-diagnostic-view 267 行) | + +#### 路由层 + +| 角色 | 路由 | 文件数 | 说明 | +|------|------|--------|------| +| admin | `/admin/school/grades/`、`/admin/school/grades/insights/` | 4 | 含 loading.tsx + error.tsx | +| teacher | `/teacher/grades/`、`/teacher/grades/analytics/`、`/teacher/grades/entry/`、`/teacher/grades/stats/` | 4 | **无 loading.tsx / error.tsx** | +| teacher | `/teacher/diagnostic/`、`/teacher/diagnostic/class/[classId]/`、`/teacher/diagnostic/student/[studentId]/` | 3 | **无 loading.tsx / error.tsx** | +| student | `/student/grades/`、`/student/diagnostic/` | 4 | 含 loading.tsx,**无 error.tsx** | +| parent | `/parent/grades/` | 2 | 含 loading.tsx,**无 error.tsx** | +| management | `/management/grade/`、`/management/grade/classes/`、`/management/grade/insights/` | 5 | **`/management/grade/page.tsx` 缺失**(孤儿 loading/error) | + +### 1.2 主要数据流 + +``` +[成绩录入] teacher/grades/entry + └─▶ grades/actions.batchCreateGradeRecordsAction + ├─▶ requirePermission(GRADE_RECORD_MANAGE) + └─▶ data-access.batchCreateGradeRecords → db.insert(gradeRecords) + +[成绩查询] teacher/grades / student/grades / parent/grades + └─▶ grades/actions.getGradeRecordsAction + ├─▶ requirePermission(GRADE_RECORD_READ) + ├─▶ data-access.getGradeRecords(含 scope 行级过滤) + └─▶ 跨模块:classes/school/users data-access + +[成绩分析] teacher/grades/analytics + └─▶ grades/actions-analytics.getGradeTrendAction / getClassComparisonAction / ... + ├─▶ requirePermission(GRADE_RECORD_READ) + └─▶ data-access-analytics(含统计计算逻辑) + +[学情诊断-学生] teacher/diagnostic/student/[id] / student/diagnostic + └─▶ diagnostic/data-access.getStudentMasterySummary + └─▶ 跨模块:users data-access(getUserNamesByIds) + +[学情诊断-班级] teacher/diagnostic/class/[id] + └─▶ diagnostic/data-access.getClassMasterySummary + └─▶ 跨模块:classes/exams/questions/users data-access + +[诊断报告生成] teacher/diagnostic + └─▶ diagnostic/actions.generateStudentReportAction / generateClassReportAction + ├─▶ requirePermission(DIAGNOSTIC_MANAGE) + └─▶ data-access-reports.createDiagnosticReport + └─▶ ⚠️ 直查 users 表(违反三层架构) +``` + +### 1.3 架构图记录情况 + +`004_architecture_impact_map.md` §2.6(grades)和 §2.22(diagnostic)已记录两个模块的导出函数、依赖关系、已知问题和文件清单。架构图信息基本完整,但存在以下遗漏: + +- **grades 模块行数过时**:架构图 L681 标注 `data-access.ts` 419 行(实际 433 行)、L682 `data-access-analytics.ts` 293 行(实际 337 行) +- **diagnostic 模块 deps 过时**:`005_architecture_data.json` L10922/L10937-10941/L10954-10958/L10972-10975 仍记录 diagnostic 直查对方表,实际代码已通过 data-access 接口访问(P1-1 已修复但文档未同步) +- **diagnostic `data-access-reports.ts` 直查 users 表未记录**:架构图未标注此违规 +- **grades 模块 actions-analytics.ts 的 5 个 Action 未完整列入 exports 清单** +- **`/management/grade/page.tsx` 缺失**未在路由清单中标注 +- **teacher 端 grades/diagnostic 路由普遍缺少 loading.tsx/error.tsx** 未标注 + +--- + +## 二、现存问题与原因分析 + +### 2.1 安全性:权限校验缺失或不一致(P0) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [teacher/grades/entry/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/entry/page.tsx) | **无任何权限校验**(既无 `requirePermission` 也无 `getAuthContext`) | "所有 Server Action 必须调用 `requirePermission()` 进行权限校验" | +| [teacher/grades/stats/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/stats/page.tsx) | **无任何权限校验** | 同上 | +| [teacher/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(GRADE_RECORD_READ)` | 同上 | +| [teacher/grades/analytics/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/analytics/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(GRADE_RECORD_READ)` | 同上 | +| [teacher/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(DIAGNOSTIC_READ)` | 同上 | +| [teacher/diagnostic/class/[classId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx) | 仅 `getAuthContext()`(有 dataScope 校验),无 `requirePermission(DIAGNOSTIC_READ)` | 同上 | +| [teacher/diagnostic/student/[studentId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) | 仅 `getAuthContext()`(有 dataScope 校验),无 `requirePermission(DIAGNOSTIC_READ)` | 同上 | +| [student/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/grades/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(GRADE_RECORD_READ)` | 同上 | +| [student/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/diagnostic/page.tsx) | 仅 `getAuthContext()`,无 `requirePermission(DIAGNOSTIC_READ)` | 同上 | +| [parent/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/grades/page.tsx) | 仅 `getAuthContext()`(有 dataScope 校验),无 `requirePermission(GRADE_RECORD_READ)` | 同上 | + +**后果**:成绩录入页面(`/teacher/grades/entry`)和成绩统计页面(`/teacher/grades/stats`)完全无权限校验,依赖路由中间件做粗粒度角色路由。若中间件配置错误或绕过,任意已登录用户可访问成绩录入页面并调用 `batchCreateGradeRecordsAction`(虽然 Action 层有 `requirePermission`,但页面层缺少二次校验不符合"Server Action 二次校验"要求)。 + +### 2.2 架构分层:跨模块直接查询 users 表(P0) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L8 | `import { learningDiagnosticReports, users } from "@/shared/db/schema"` | "modules/ 之间通过对方 data-access 通信,不直接查询对方 DB 表" | +| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L137-140 | `getDiagnosticReports` 直接 `leftJoin(users, ...)` 查询学生姓名 | 同上 | +| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L149-153 | 直接 `db.select({ id: users.id, name: users.name }).from(users)` 查询生成者姓名 | 同上 | +| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L168-170 | `getDiagnosticReportById` 直接 `leftJoin(users, ...)` | 同上 | +| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L177-182 | 直接 `db.select({ name: users.name }).from(users)` | 同上 | + +**后果**:`diagnostic` 模块绕过 `users` 模块的 data-access 层直接查询 `users` 表,破坏模块封装性。`users` 表 schema 变更将直接影响 diagnostic 模块。同模块的 `data-access.ts` 已正确通过 `getUserNamesByIds` 访问,但 `data-access-reports.ts` 却绕过,存在不一致。 + +### 2.3 架构分层:统计业务逻辑混入 data-access(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L217-270 | `getClassGradeStats` 包含 average/median/max/min/variance/stdDev/passRate/excellentRate 计算(53 行统计逻辑) | "严格三层架构,依赖方向单向" — 统计计算属业务逻辑层 | +| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L272-337 | `getStudentGradeSummary` 包含 averageScore 计算 | 同上 | +| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L339-373 | `getClassRanking` 包含 rank 计算 | 同上 | +| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L59-119 | `getGradeTrend` 包含 normalized/avg 计算 | 同上 | +| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L128-218 | `getClassComparison` 包含 normalized/median/avg/passCount/excellentCount 计算(90 行) | 同上 | +| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L226-289 | `getSubjectComparison` 包含 median/avg/passRate/excellentRate 计算 | 同上 | +| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L299-336 | `getGradeDistribution` 包含 bucket 分类逻辑 | 同上 | +| [grades/data-access-ranking.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-ranking.ts) L31-118 | `getRankingTrend` 包含 normalize/rank 计算逻辑 | 同上 | + +**后果**:data-access 层职责混乱,既负责数据读取又负责业务计算,难以单独测试统计逻辑。架构图 L671 已标记此 P2 问题。应抽取到独立的 `stats-service.ts`(参考 homework 模块的 `stats-service.ts` 范例)。 + +### 2.4 重复代码:工具函数多处重复(P1) + +| 重复函数 | 出现位置 | 违反规则 | +|----------|----------|----------| +| `buildScopeClassFilter` | [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L57-75、[grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L34-48 | "工具函数:建议 ≤ 40 行" + DRY 原则 | +| `toNumber` | [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L34-37、[grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L24-27、[grades/data-access-ranking.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-ranking.ts) L16-19 | 同上 | +| `normalize` | [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts)、[grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L29-32、[grades/data-access-ranking.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-ranking.ts) L21-24 | 同上 | + +**后果**:3 个文件重复实现相同工具函数,修改时需同步多处,易遗漏导致行为不一致。应抽取到 `grades/lib/stats-utils.ts` 或 `shared/lib/grade-utils.ts`。 + +### 2.5 国际化:完全缺失(P0) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| `src/modules/grades/components/*`(16 个文件) | 全部使用硬编码英文字符串,0 处 `useTranslations` 调用 | "所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键" | +| `src/modules/diagnostic/components/*`(4 个文件) | 全部使用硬编码英文字符串,0 处 `useTranslations` 调用 | 同上 | +| [grades/export.ts](file:///e:/Desktop/CICD/src/modules/grades/export.ts) L12-17, L54-61, L68-80, L287, L295 | Excel 导出表头、指标名、文件名硬编码中文 | 同上 | +| `src/shared/i18n/messages/{zh-CN,en}/` | **不存在 `grades.json` 和 `diagnostic.json` 翻译文件** | 同上 | +| [i18n/request.ts](file:///e:/Desktop/CICD/src/i18n/request.ts) L22-28 | 仅加载 5 个命名空间(common/auth/onboarding/classes/errors),未加载 grades/diagnostic | 同上 | +| `src/modules/grade-management/components/*`(7 个文件,12 处) | 调用 `useTranslations("grade")` 但 `grade.json` 翻译文件不存在,**运行时会报 `MISSING_MESSAGE` 错误** | 同上 | + +**后果**: +1. grades 和 diagnostic 模块完全无法国际化,所有用户可见文本固定为英文(部分中文混合),无法支持多语言。 +2. grade-management 模块(年级管理,与成绩模块不同)调用未加载的 `grade` 命名空间,访问 `/management/grade/`、`/admin/school/grades/insights` 等页面会因找不到翻译键而**运行时报错**。 + +### 2.6 前端规范:Error Boundary 和 Suspense 缺失(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| `src/modules/grades/components/*`(16 个文件) | 全部无 Error Boundary | "每个独立的数据区块必须用 React Error Boundary 包裹" | +| `src/modules/diagnostic/components/*`(4 个文件) | 全部无 Error Boundary | 同上 | +| `src/modules/grades/components/*`(16 个文件) | 全部无 Suspense + 骨架屏 | "异步数据使用 React Suspense + 骨架屏" | +| `src/modules/diagnostic/components/*`(4 个文件) | 全部无 Suspense + 骨架屏 | 同上 | +| `src/app/(dashboard)/teacher/grades/` | **无 loading.tsx / error.tsx** | 路由级错误边界和加载态缺失 | +| `src/app/(dashboard)/teacher/diagnostic/` | **无 loading.tsx / error.tsx** | 同上 | + +**后果**:单个组件抛错会导致整个页面崩溃;异步加载无骨架屏过渡,用户体验差(白屏等待)。 + +### 2.7 前端规范:a11y 无障碍缺失(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| `src/modules/grades/components/*`(15/16 个文件) | 无 ARIA 属性(仅 batch-grade-entry.tsx 有 `aria-hidden` 和 `aria-invalid`) | "可访问性(a11y):语义化标签、ARIA 属性、键盘导航" | +| `src/modules/diagnostic/components/*`(4 个文件) | 无 ARIA 属性 | 同上 | +| [grades/components/grade-record-list.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-list.tsx) L93-100 | 删除按钮无 `aria-label` | 同上 | +| [diagnostic/components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L128-139 | 热力图色块仅靠 `title` 属性,无 `role="img"` 和 `aria-label` | 同上 | +| [diagnostic/components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) L38-66 | 雷达图无 `aria-label` / `role="img"` 描述 | 同上 | +| [diagnostic/components/report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) L192-200, L202-210 | 发布/删除按钮仅 `title`,无 `aria-label` | 同上 | + +**后果**:屏幕阅读器用户无法识别图表内容、按钮用途,不符合 WCAG 2.1 AA 标准。 + +### 2.8 TypeScript 规范:`as` 断言违规(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [grades/components/batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L221 | `remark: undefined as string \| undefined` | "禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)" | +| [grades/components/batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L312 | `setType(v as typeof type)` | 同上 | +| [grades/components/grade-record-form.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-form.tsx) L142 | `setType(v as typeof type)` | 同上 | +| [grades/components/grade-distribution-chart.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-distribution-chart.tsx) L66-67 | `payload as { payload?: {...} }`(从 unknown 转换但未使用类型守卫) | 同上 | + +**后果**:`as` 断言绕过 TypeScript 类型检查,可能隐藏运行时类型错误。应使用类型守卫或 Zod 运行时校验。 + +### 2.9 Tailwind 规范:任意值违规(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [diagnostic/components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L255 | `className="w-[180px]"` | "禁止使用任意值(`w-[137px]`),除非有充分理由并注释" | +| [diagnostic/components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) L45 | `className="mx-auto h-[360px] w-full max-w-[520px]"` | 同上 | + +**后果**:绕过设计令牌系统,无法统一调整尺寸主题。 + +### 2.10 数据模型缺陷:班级报告 studentId 字段语义错误(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L111-114 | 班级报告 `studentId: generatedBy` 将生成者 ID 写入 studentId 字段 | "数据模型设计应语义清晰" | +| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L111 | 同上 | 同上 | + +**后果**:`report-list.tsx` L178 显示 `r.studentName` 时,班级报告会显示生成者(教师)姓名而非学生姓名,存在数据语义错误。架构图 L1245 已标记此 P2 问题。 + +### 2.11 Server Action 规范:Zod 校验缺失(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L154-170 | `deleteGradeRecordAction` 无 Zod 校验(仅 id 字符串) | "输入使用 Zod 验证,验证失败返回结构化错误" | +| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L171-190 | `getGradeRecordsAction` 无 Zod 校验(使用 `GradeQueryParams` 类型) | 同上 | +| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L191-208 | `getClassGradeStatsAction` 无 Zod 校验 | 同上 | +| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L209-232 | `getStudentGradeSummaryAction` 无 Zod 校验 | 同上 | +| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L233-250 | `getClassRankingAction` 无 Zod 校验 | 同上 | +| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L251-269 | `getGradeRecordByIdAction` 无 Zod 校验 | 同上 | +| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L270-312 | `exportGradesAction` 无 Zod 校验(params 为内联对象类型) | 同上 | +| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L26-45 | `getGradeTrendAction` 无 Zod 校验 | 同上 | +| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L46-64 | `getClassComparisonAction` 无 Zod 校验 | 同上 | +| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L65-83 | `getSubjectComparisonAction` 无 Zod 校验 | 同上 | +| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L84-103 | `getGradeDistributionAction` 无 Zod 校验 | 同上 | +| [grades/actions-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/actions-analytics.ts) L104-133 | `getRankingTrendAction` 无 Zod 校验 | 同上 | + +**后果**:12 个 Action 缺失 Zod 校验,客户端可传入任意类型参数,可能导致运行时错误或 SQL 注入风险。diagnostic 模块的 6 个 Action 全部使用 Zod 校验,是标杆范例。 + +### 2.12 业务逻辑漏洞:grade_managed scope 返回空数据(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L62-64 | `grade_managed` scope 返回 `sql\`1=0\``(始终无数据) | "权限过滤应正确反映角色数据范围" | +| [grades/data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access-analytics.ts) L39 | 同上 | 同上 | + +**后果**:年级管理员(grade_managed scope)无法查看任何成绩数据,可能是业务逻辑漏洞。年级管理员应能查看所管年级的所有班级成绩。 + +### 2.13 路由缺陷:page.tsx 缺失(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| `src/app/(dashboard)/management/grade/page.tsx` | **文件缺失**,但有 loading.tsx/error.tsx 孤儿文件 | "路由页面应完整" | + +**后果**:访问 `/management/grade` 会 404,但 loading.tsx 和 error.tsx 仍存在,造成混乱。 + +### 2.14 角色覆盖不一致:admin/parent 无 diagnostic UI(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| `005_architecture_data.json` L174-175, L214-215 | admin 和 parent 都有 `DIAGNOSTIC_MANAGE`/`DIAGNOSTIC_READ` 权限 | "权限点应有对应 UI" | +| `src/app/(dashboard)/admin/` | **无 diagnostic 页面** | 同上 | +| `src/app/(dashboard)/parent/` | **无 diagnostic 页面** | 同上 | + +**后果**:admin 和 parent 拥有 diagnostic 权限但无对应 UI,权限与 UI 覆盖不一致。家长无法查看子女的学情诊断报告。 + +### 2.15 SearchParams 工具未统一(P3) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [student/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/grades/page.tsx) | 自定义 `SearchParams` 类型和 `getParam` 函数 | "最大化复用" | +| [management/grade/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/management/grade/insights/page.tsx) | 自定义 `SearchParams` 类型和 `getParam` 函数 | 同上 | + +**后果**:与 teacher 端 grades 页面已复用 `@/shared/lib/search-params` 的做法不一致,存在重复代码。 + +--- + +## 三、行业差距对比 + +### 3.1 成绩模块(grades)行业对标 + +| 功能维度 | 行业优秀实践(K12 成绩管理系统) | 当前实现 | 差距影响 | +|----------|-------------------------------|----------|----------| +| **成绩录入** | 支持Excel批量导入、扫码录入、语音录入;录入时实时校验分数范围;自动计算总分、平均分 | 仅支持单条录入 + 批量录入(表单式);有分数范围校验;无 Excel 导入 | 教师录入效率低,大班级成绩录入耗时 | +| **成绩分析** | 多维度分析(班级/年级/个人/科目);支持自定义分析维度;提供归因分析(哪些题目失分多) | 5 种分析(趋势/班级对比/科目对比/分布/排名);无归因分析;无自定义维度 | 教师无法定位失分原因,难以针对性教学 | +| **可视化** | 交互式图表(hover 显示详情、点击下钻);支持图表下载为图片;支持自定义图表配置 | 静态图表(TrendLineChart/SimpleBarChart);无 hover 详情;无下载功能 | 数据呈现不够直观,教师难以深入分析 | +| **报告导出** | 支持 PDF/Excel/CSV 多格式;支持自定义报告模板;支持批量导出(按班级/年级) | 仅 Excel 导出(明细 + 班级汇总);无 PDF;无自定义模板 | 无法满足学校正式报告需求(如家长会报告需 PDF) | +| **预警机制** | 成绩异常预警(突然下降/持续低迷);及格率预警;班级对比异常预警 | 无预警机制 | 教师无法及时发现学生成绩异常 | +| **多角色视图** | 学生看自己 + 班级平均;家长看子女 + 班级排名;教师看所教班级;管理员看全校 | 4 角色都有基本视图;但 parent 无 diagnostic;admin 无 diagnostic | 家长无法全面了解子女学情 | +| **空状态/加载态** | 完善的空状态插画 + 引导操作;骨架屏过渡 | 仅部分页面有 loading.tsx;组件无 Suspense | 用户体验差,白屏等待 | +| **数据联动** | 成绩 → 学情诊断 → 推荐练习;成绩 → 作业 → 知识点掌握度 | grades 与 diagnostic 无数据联动;无推荐练习 | 无法形成"诊断-练习-反馈"闭环 | + +### 3.2 学情诊断模块(diagnostic)行业对标 + +| 功能维度 | 行业优秀实践(K12 学情诊断系统) | 当前实现 | 差距影响 | +|----------|-------------------------------|----------|----------| +| **知识点掌握度** | 基于IRT(项目反应理论)计算;支持知识点权重;支持时间衰减(近期表现权重更高) | 基于正确率简单计算;无权重;无时间衰减 | 掌握度计算不够精准 | +| **诊断报告** | 自动生成 PDF 报告;支持自定义模板;含学习建议、练习推荐、进步轨迹 | 生成 draft 报告(JSON 存储);无 PDF;建议为静态文本 | 报告不够专业,无法直接发给家长 | +| **可视化** | 雷达图 + 热力图 + 知识图谱;支持知识点下钻;支持时间对比 | 雷达图 + 热力图;无知识图谱;无下钻 | 知识结构呈现不够清晰 | +| **个性化推荐** | 基于弱项推荐练习题/微课;支持难度自适应;支持学习路径规划 | 仅列出弱项知识点 + "Practice" 链接(跳转到作业列表) | 无法精准推荐练习内容 | +| **班级诊断** | 班级整体掌握度 + 重点关注学生列表 + 教学建议;支持按知识点筛选学生 | 班级掌握度摘要 + 需关注学生列表;无教学建议 | 教师难以根据诊断调整教学 | +| **历史趋势** | 掌握度随时间变化曲线;支持对比多个时间段 | 无历史趋势(仅当前快照) | 无法评估学习进步情况 | +| **多角色覆盖** | 学生/家长/教师/管理员都能查看;家长看子女诊断报告 | 仅 teacher + student 有 UI;parent/admin 无 UI | 家长无法了解子女学情 | + +### 3.3 关键差距总结 + +1. **数据孤岛**:grades 和 diagnostic 模块无数据联动,无法形成"成绩 → 诊断 → 练习 → 反馈"闭环。行业优秀产品(如猿题库、作业帮)已实现完整学习闭环。 +2. **家长端缺失**:parent 无 diagnostic UI,家长无法查看子女学情诊断报告。K12 场景下家长是重要决策者,缺失影响家校沟通。 +3. **报告专业度不足**:diagnostic 报告为 JSON 存储,无 PDF 导出,无法直接用于家长会。行业产品普遍支持专业 PDF 报告。 +4. **预警机制空白**:成绩异常、掌握度低迷无预警,教师无法主动干预。 +5. **可视化深度不足**:无知识图谱、无下钻分析、无时间对比,数据呈现停留在表层。 + +--- + +## 四、改进优先级建议 + +### P0(紧急 — 安全与合规) + +| # | 问题 | 改进方向 | +|---|------|----------| +| P0-1 | 权限校验缺失(10 个页面) | 所有页面调用 `requirePermission()`:teacher/grades 用 `GRADE_RECORD_READ`/`GRADE_RECORD_MANAGE`,teacher/diagnostic 用 `DIAGNOSTIC_READ`/`DIAGNOSTIC_MANAGE`,student/parent 用对应 READ 权限 | +| P0-2 | diagnostic/data-access-reports.ts 直查 users 表 | 改为调用 `@/modules/users/data-access` 的 `getUserNamesByIds`,删除 `users` 表 import | +| P0-3 | i18n 完全缺失 + grade-management 运行时报错 | 创建 `grades.json` 和 `diagnostic.json` 翻译文件(zh-CN + en);修复 `grade-management` 模块的 `grade` 命名空间(创建 `grade.json` 或改用 `gradeManagement`);在 `i18n/request.ts` 注册新命名空间 | +| P0-4 | `/management/grade/page.tsx` 缺失 | 补齐 page.tsx 或删除孤儿 loading.tsx/error.tsx | + +### P1(较严重 — 架构与质量) + +| # | 问题 | 改进方向 | +|---|------|----------| +| P1-1 | 统计业务逻辑混入 data-access | 抽取 `grades/stats-service.ts`,将 `getClassGradeStats`/`getClassComparison`/`getSubjectComparison`/`getGradeDistribution`/`getRankingTrend` 的统计计算迁移至纯函数(参考 homework/stats-service.ts 范例) | +| P1-2 | 重复工具函数 | 抽取 `grades/lib/scope-filter.ts`(`buildScopeClassFilter`)和 `grades/lib/stats-utils.ts`(`toNumber`/`normalize`) | +| P1-3 | 12 个 Action 缺失 Zod 校验 | 为 `deleteGradeRecordAction`/`getGradeRecordsAction`/`getClassGradeStatsAction`/`getStudentGradeSummaryAction`/`getClassRankingAction`/`getGradeRecordByIdAction`/`exportGradesAction` + 5 个 analytics Action 创建对应 Zod schema | +| P1-4 | `as` 断言违规(4 处) | 使用类型守卫或 Zod 运行时校验替代 | +| P1-5 | Error Boundary 和 Suspense 缺失 | 创建 `grades/components/widget-boundary.tsx`(Error Boundary + Suspense + Skeleton 组合);每个数据区块独立包裹;teacher/grades 和 teacher/diagnostic 路由补齐 loading.tsx/error.tsx | +| P1-6 | 架构图同步 | 更新 `004` 和 `005` 文档:grades 行数、diagnostic deps、新增 stats-service.ts、新增 lib/、补齐 actions-analytics exports | + +### P2(优化 — 体验与扩展) + +| # | 问题 | 改进方向 | +|---|------|----------| +| P2-1 | a11y 无障碍缺失 | 补充 ARIA 属性:图表 `role="img"` + `aria-label`;按钮 `aria-label`;表格 `caption`;列表 `role="list"` | +| P2-2 | Tailwind 任意值 | 移除 `w-[180px]`/`h-[360px]`/`max-w-[520px]`,改用设计令牌或注释说明 | +| P2-3 | 班级报告 studentId 字段语义错误 | 修改 `learningDiagnosticReports` schema,将 `studentId` 改为可空,或增加 `classId`/`generatedBy` 字段 | +| P2-4 | grade_managed scope 返回空数据 | 修复 `buildScopeClassFilter`,grade_managed scope 应返回所管年级的班级过滤条件 | +| P2-5 | admin/parent 无 diagnostic UI | 新增 `/parent/diagnostic/` 页面(家长查看子女诊断报告);admin 可复用 teacher 视图 | +| P2-6 | SearchParams 工具未统一 | student/grades 和 management/grade/insights 改用 `@/shared/lib/search-params` | + +### P3(长期 — 行业对标) + +| # | 问题 | 改进方向 | +|---|------|----------| +| P3-1 | grades 与 diagnostic 无数据联动 | 设计联动接口:成绩录入后触发掌握度更新;诊断报告含成绩趋势 | +| P3-2 | 无预警机制 | 新增 `grades/alerts-service.ts`:成绩下降预警、及格率预警、掌握度低迷预警 | +| P3-3 | 诊断报告无 PDF 导出 | 集成 PDF 生成库(如 @react-pdf/renderer),支持专业报告模板 | +| P3-4 | 无知识图谱可视化 | 引入知识图谱组件(如 react-flow),展示知识点关系与掌握度 | +| P3-5 | 无个性化练习推荐 | 基于弱项推荐练习题,对接 questions 模块 | +| P3-6 | Widget 配置系统 | 设计 `GradesWidgetConfig`/`DiagnosticWidgetConfig` 类型,按角色配置渲染哪些 Widget | + +--- + +## 五、架构图同步说明 + +本次审计发现架构图存在以下遗漏或不一致,需在实现后同步更新: + +### 5.1 `004_architecture_impact_map.md` 需补充 + +1. **§2.6 grades 模块**: + - 更新文件清单行数:`data-access.ts` 419→433、`data-access-analytics.ts` 293→337 + - 补充 `actions-analytics.ts` 的 5 个 Action 到 exports 清单(当前仅列 11 个,实际 15 个) + - 新增 `stats-service.ts`(P1-1 抽取后) + - 新增 `lib/scope-filter.ts`、`lib/stats-utils.ts`(P1-2 抽取后) + - 新增 `components/widget-boundary.tsx`(P1-5 新增) + +2. **§2.22 diagnostic 模块**: + - 更新已知问题:标注 `data-access-reports.ts` 直查 users 表(P0-2 修复前) + - 更新文件清单行数(如有变化) + +3. **路由清单**: + - 标注 `/management/grade/page.tsx` 缺失(P0-4 修复前) + - 标注 teacher/grades 和 teacher/diagnostic 路由缺少 loading.tsx/error.tsx + - 新增 `/parent/diagnostic/` 路由(P2-5 实现后) + +### 5.2 `005_architecture_data.json` 需修改 + +1. `modules.grades` 节点(L7362): + - 更新 `dataAccess` 中各函数的 `deps`:移除直查 `classes`/`classEnrollments`/`subjects`/`users`,改为 `classes/data-access.*`/`school/data-access.*`/`users/data-access.*` + - 新增 `stats-service.ts` 的 exports + - 新增 `lib/scope-filter.ts`、`lib/stats-utils.ts` 的 exports + - 补充 `actions-analytics.ts` 的 5 个 Action 到 `actions` 数组 + +2. `modules.diagnostic` 节点(L10927): + - 更新 `dataAccess` 中各函数的 `deps`:移除直查 `users`/`classes`/`classEnrollments`/`examSubmissions`/`submissionAnswers`/`questionsToKnowledgePoints`,改为对应模块 data-access + - 标注 `data-access-reports.ts` 的 `getDiagnosticReports`/`getDiagnosticReportById` 依赖 `users/data-access.getUserNamesByIds`(P0-2 修复后) + +3. `permissions` 节点: + - 确认 `GRADE_RECORD_READ`/`GRADE_RECORD_MANAGE`/`DIAGNOSTIC_READ`/`DIAGNOSTIC_MANAGE` 权限点已定义(已存在 ✓) + +4. `routes` 节点: + - 补充 teacher/grades/entry、teacher/grades/stats、teacher/diagnostic/class/[classId]、teacher/diagnostic/student/[studentId] 路由 + - 标注 `/management/grade/page.tsx` 缺失 + - 新增 `/parent/diagnostic/` 路由(P2-5 实现后) + +5. `dependencyMatrix`: + - 更新 grades → classes/school/users 的依赖关系(通过 data-access,已正确) + - 更新 diagnostic → classes/exams/questions/users 的依赖关系(通过 data-access,P0-2 修复后完全正确) + +### 5.3 翻译文件结构示例 + +``` +src/shared/i18n/messages/ +├─ zh-CN/ +│ ├─ grades.json # 新增(成绩模块) +│ ├─ diagnostic.json # 新增(学情诊断模块) +│ └─ grade.json # 新增(grade-management 模块,修复运行时报错) +└─ en/ + ├─ grades.json # 新增 + ├─ diagnostic.json # 新增 + └─ grade.json # 新增 +``` + +`grades.json` 结构示例(zh-CN): + +```json +{ + "title": { + "list": "成绩查询", + "entry": "成绩录入", + "analytics": "成绩分析", + "stats": "成绩统计" + }, + "filters": { + "class": "班级", + "subject": "科目", + "type": "类型", + "semester": "学期", + "allClasses": "全部班级", + "allSubjects": "全部科目", + "allTypes": "全部类型", + "allSemesters": "全部学期", + "searchPlaceholder": "按标题搜索..." + }, + "type": { + "exam": "考试", + "quiz": "测验", + "homework": "作业", + "other": "其他" + }, + "semester": { + "s1": "第一学期", + "s2": "第二学期" + }, + "list": { + "empty": "暂无成绩记录", + "columns": { + "student": "学生", + "class": "班级", + "subject": "科目", + "title": "标题", + "score": "分数", + "type": "类型", + "semester": "学期", + "recordedBy": "录入人", + "date": "日期" + } + }, + "form": { + "title": "录入成绩", + "save": "保存", + "saving": "保存中...", + "cancel": "取消", + "selectClass": "选择班级", + "selectSubject": "选择科目", + "selectStudent": "选择学生", + "titlePlaceholder": "如期中考试", + "score": "分数", + "fullScore": "满分", + "remark": "备注(可选)", + "remarkPlaceholder": "关于此成绩的备注..." + }, + "delete": { + "title": "删除成绩记录", + "confirmation": "确定要删除此成绩记录吗?此操作不可撤销。", + "confirm": "删除", + "cancel": "取消", + "deleting": "删除中..." + }, + "export": { + "detail": "导出成绩明细", + "classReport": "导出班级成绩总表", + "success": "导出成功", + "failed": "导出失败" + }, + "stats": { + "title": "统计", + "average": "平均分", + "median": "中位数", + "max": "最高分", + "min": "最低分", + "stdDev": "标准差", + "variance": "方差", + "passRate": "及格率", + "excellentRate": "优秀率", + "count": "人数" + }, + "analytics": { + "trend": "成绩趋势", + "classComparison": "班级对比", + "subjectComparison": "科目对比", + "distribution": "分数分布", + "ranking": "排名", + "rankingTrend": "排名趋势" + }, + "batch": { + "title": "批量录入", + "saving": "保存中...", + "restored": "已恢复未保存的成绩草稿", + "invalidScores": "存在无效分数", + "fullScoreRequired": "满分必填", + "saved": "已录入" + }, + "empty": { + "noRecords": "暂无成绩记录", + "noData": "暂无数据" + }, + "error": { + "loadFailed": "加载失败", + "saveFailed": "保存失败", + "deleteFailed": "删除失败", + "retry": "重试" + } +} +``` + +`diagnostic.json` 结构示例(zh-CN): + +```json +{ + "title": { + "student": "学生学情诊断", + "class": "班级学情诊断", + "reportList": "诊断报告" + }, + "type": { + "individual": "个人", + "class": "班级", + "grade": "年级" + }, + "status": { + "draft": "草稿", + "published": "已发布", + "archived": "已归档" + }, + "filters": { + "reportType": "报告类型", + "status": "状态", + "allTypes": "全部类型", + "allStatuses": "全部状态" + }, + "summary": { + "overallMastery": "总体掌握度", + "strengths": "强项", + "weaknesses": "弱项", + "students": "学生数", + "avgMastery": "平均掌握度", + "needAttention": "需重点关注" + }, + "chart": { + "radarTitle": "知识点掌握度", + "radarDescription": "掌握度雷达图", + "heatmapTitle": "知识点掌握度热力图", + "rankingTitle": "知识点排名" + }, + "report": { + "generate": "生成诊断报告", + "generateStudent": "生成学生诊断报告", + "generateClass": "生成班级诊断报告", + "publish": "发布", + "delete": "删除", + "publishTitle": "发布报告", + "deleteTitle": "删除报告", + "recommendations": "学习建议", + "history": "报告历史" + }, + "strengths": { + "title": "强项(≥80%)", + "practice": "练习" + }, + "weaknesses": { + "title": "弱项(<60%)", + "practice": "练习" + }, + "empty": { + "noData": "暂无诊断数据", + "noClassData": "无法加载班级掌握度摘要", + "noMastery": "暂无知识点掌握度记录", + "noReports": "暂无诊断报告" + }, + "error": { + "generateFailed": "生成报告失败", + "publishFailed": "发布失败", + "deleteFailed": "删除失败", + "loadFailed": "加载失败", + "retry": "重试" + } +} +``` + +--- + +## 六、合规项确认 + +以下条目**已通过审计**: + +- ✅ **grades 模块跨模块依赖全部通过 data-access**:所有跨模块访问(classes/school/users)均通过对方 data-access 函数 +- ✅ **diagnostic 模块 data-access.ts 跨模块依赖通过 data-access**(仅 data-access-reports.ts 违规) +- ✅ **所有 Server Action 调用 `requirePermission()`**:grades 15 个 + diagnostic 6 个 = 21 个 Action 全部合规 +- ✅ **所有 Server Action 返回 `ActionState`** +- ✅ **所有 Server Action 使用 `revalidatePath` 精确刷新** +- ✅ **无 `role === "xxx"` 硬编码**:全模块无 +- ✅ **diagnostic 组件使用 `usePermission().hasPermission()`**:class-diagnostic-view.tsx 和 report-list.tsx 已使用 +- ✅ **无 `dangerouslySetInnerHTML`** +- ✅ **无 `any` 类型** +- ✅ **文件行数全部合规**:最大为 grades/components/batch-grade-entry.tsx 442 行 < 500 行组件建议上限 +- ✅ **`"use client"` / `"use server"` / `"server-only"` 正确放置** +- ✅ **`import type` 使用规范** +- ✅ **diagnostic schema.ts 枚举与 types.ts 联合类型一致** +- ✅ **接口命名规范**(无 I 前缀,PascalCase) + +--- + +## 七、重构方案设计要点(供后续实现参考) + +### 7.1 完全解耦 + +- 定义 `GradesDataService` 接口抽象数据依赖,使用 React Context 注入 +- 模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access +- 不同角色差异通过接口不同实现隔离(如 `TeacherGradesService`/`StudentGradesService`/`ParentGradesService`) + +### 7.2 组合优先 + +- 所有 UI 通过组件组合(children、slots、render props)实现灵活性 +- 逻辑复用抽取为自定义 hooks(如 `useGradeRecords`/`useGradeTrend`/`useMasterySummary`) +- 严禁继承或深层嵌套 HOC + +### 7.3 最大化复用 + +- 识别四角色共用 UI 块:`GradeTrendChart`/`GradeStatsCard`/`MasteryRadarChart`/`WidgetBoundary` +- 抽象泛型组件:`>`/``/``/`` +- 各角色模块仅组合复用单元,可配置化显示内容 + +### 7.4 配置驱动 + +- 设计 `GradesWidgetConfig` 类型,按角色配置渲染哪些 Widget +- 示例:teacher 看 [录入, 查询, 分析, 统计],student 看 [我的成绩, 趋势],parent 看 [子女成绩, 趋势] + +### 7.5 错误与边界处理 + +- 每个独立数据区块用 ``(Error Boundary + Suspense + Skeleton 组合)包裹 +- 明确处理空数据、无权限、网络异常等边界状态 +- 支持流式渲染(React Server Components 获取初始数据) + +### 7.6 可测试性 + +- 数据获取、计算、格式化等纯逻辑放入 `stats-service.ts` 或 hooks +- 导出清晰接口类型以便 mock +- 统计函数为纯函数,易于单测 + +### 7.7 监控埋点 + +- 预留关键操作埋点接口:成绩录入、报告生成、报告发布、导出操作 +- 通过 `shared/lib/analytics` 统一上报 diff --git a/src/app/(dashboard)/management/grade/page.tsx b/src/app/(dashboard)/management/grade/page.tsx new file mode 100644 index 0000000..953325f --- /dev/null +++ b/src/app/(dashboard)/management/grade/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation" + +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" + +export const dynamic = "force-dynamic" + +export default async function GradeManagementPage(): Promise { + await requirePermission(Permissions.GRADE_MANAGE) + redirect("/management/grade/classes") +} diff --git a/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx b/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx index fb1569f..cffaaf3 100644 --- a/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx +++ b/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx @@ -1,7 +1,8 @@ import type { JSX } from "react" import { notFound } from "next/navigation" import { Stethoscope } from "lucide-react" -import { getAuthContext } from "@/shared/lib/auth-guard" +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" import { getClassMasterySummary } from "@/modules/diagnostic/data-access" import { ClassDiagnosticView } from "@/modules/diagnostic/components/class-diagnostic-view" @@ -13,7 +14,7 @@ export default async function ClassDiagnosticPage({ params: Promise<{ classId: string }> }): Promise { const { classId } = await params - const ctx = await getAuthContext() + const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ) // DataScope 校验:教师只能查看所教班级,学生/家长不可访问 if (ctx.dataScope.type === "class_taught" && !ctx.dataScope.classIds.includes(classId)) { diff --git a/src/app/(dashboard)/teacher/diagnostic/page.tsx b/src/app/(dashboard)/teacher/diagnostic/page.tsx index 2298d0b..d372471 100644 --- a/src/app/(dashboard)/teacher/diagnostic/page.tsx +++ b/src/app/(dashboard)/teacher/diagnostic/page.tsx @@ -1,5 +1,6 @@ import type { JSX } from "react" -import { getAuthContext } from "@/shared/lib/auth-guard" +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" import { getParam, type SearchParams } from "@/shared/lib/search-params" import { getDiagnosticReports } from "@/modules/diagnostic/data-access-reports" import { ReportList } from "@/modules/diagnostic/components/report-list" @@ -33,7 +34,7 @@ export default async function TeacherDiagnosticPage({ searchParams: Promise }): Promise { const sp = await searchParams - const ctx = await getAuthContext() + const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ) const reportType = getParam(sp, "reportType") const status = getParam(sp, "status") diff --git a/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx b/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx index b5228b8..caff7f7 100644 --- a/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx +++ b/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx @@ -1,7 +1,8 @@ import type { JSX } from "react" import { notFound } from "next/navigation" import { Stethoscope } from "lucide-react" -import { getAuthContext } from "@/shared/lib/auth-guard" +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" import { getStudentMasterySummary, getKnowledgePointStats, @@ -18,7 +19,7 @@ export default async function StudentDiagnosticPage({ params: Promise<{ studentId: string }> }): Promise { const { studentId } = await params - const ctx = await getAuthContext() + const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ) // DataScope 二次校验:学生只能看自己,家长只能看子女 if (ctx.dataScope.type === "class_members" && ctx.userId !== studentId) { diff --git a/src/app/(dashboard)/teacher/grades/analytics/page.tsx b/src/app/(dashboard)/teacher/grades/analytics/page.tsx index 5592d07..662e200 100644 --- a/src/app/(dashboard)/teacher/grades/analytics/page.tsx +++ b/src/app/(dashboard)/teacher/grades/analytics/page.tsx @@ -4,7 +4,8 @@ import { BarChart3, ArrowLeft } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { EmptyState } from "@/shared/components/ui/empty-state" -import { getAuthContext } from "@/shared/lib/auth-guard" +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" import { getParam, type SearchParams } from "@/shared/lib/search-params" import { getTeacherClasses } from "@/modules/classes/data-access" import { getGrades } from "@/modules/school/data-access" @@ -30,7 +31,7 @@ export default async function GradeAnalyticsPage({ searchParams: Promise }): Promise { const sp = await searchParams - const ctx = await getAuthContext() + const ctx = await requirePermission(Permissions.GRADE_RECORD_READ) const classId = getParam(sp, "classId") const subjectId = getParam(sp, "subjectId") diff --git a/src/app/(dashboard)/teacher/grades/page.tsx b/src/app/(dashboard)/teacher/grades/page.tsx index 07863ae..4d4c1ff 100644 --- a/src/app/(dashboard)/teacher/grades/page.tsx +++ b/src/app/(dashboard)/teacher/grades/page.tsx @@ -3,7 +3,9 @@ import Link from "next/link" import { PlusCircle, BarChart3, ClipboardList } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { EmptyState } from "@/shared/components/ui/empty-state" -import { getAuthContext } from "@/shared/lib/auth-guard" +import { ListPagination, computePagination, paginate } from "@/shared/components/ui/list-pagination" +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" import { getParam, type SearchParams } from "@/shared/lib/search-params" import { getTeacherClasses } from "@/modules/classes/data-access" import { getGradeRecords } from "@/modules/grades/data-access" @@ -26,13 +28,15 @@ function parseSemester(v?: string): GradeRecordSemester | undefined { return v && VALID_SEMESTERS.has(v) ? (v as GradeRecordSemester) : undefined } +const PAGE_SIZE = 20 + export default async function TeacherGradesPage({ searchParams, }: { searchParams: Promise }): Promise { const sp = await searchParams - const ctx = await getAuthContext() + const ctx = await requirePermission(Permissions.GRADE_RECORD_READ) const classId = getParam(sp, "classId") const subjectId = getParam(sp, "subjectId") @@ -55,24 +59,32 @@ export default async function TeacherGradesPage({ const classOptions = classes.map((c) => ({ id: c.id, name: c.name })) const subjectOptions = allSubjects.map((s) => ({ id: s.id, name: s.name })) + // 分页计算 + const { page } = computePagination(sp, PAGE_SIZE) + const total = records.length + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) + const currentPage = Math.min(page, totalPages) + const pagedRecords = paginate(records, currentPage, PAGE_SIZE) + const hasFilters = Boolean(classId || subjectId || type || semester) + return (
-

Grades

-

Manage student grade records.

+

成绩记录

+

管理学生成绩记录。

@@ -91,18 +103,31 @@ export default async function TeacherGradesPage({ - {records.length === 0 && !classId && !subjectId ? ( + {records.length === 0 && !hasFilters ? ( ) : ( - +
+ + {total > 0 ? ( + + ) : null} +
)}
) diff --git a/src/modules/diagnostic/components/class-diagnostic-view.tsx b/src/modules/diagnostic/components/class-diagnostic-view.tsx index 94e59af..6945c8a 100644 --- a/src/modules/diagnostic/components/class-diagnostic-view.tsx +++ b/src/modules/diagnostic/components/class-diagnostic-view.tsx @@ -131,7 +131,7 @@ export function ClassDiagnosticView({ summary }: ClassDiagnosticViewProps) { className={`flex flex-col items-center justify-center rounded-md px-3 py-2 text-white ${masteryColor(kp.averageMastery)}`} title={`${kp.knowledgePointName}: ${kp.averageMastery.toFixed(1)}% (mastered ${kp.masteredCount}/${kp.totalStudents})`} > - + {kp.knowledgePointName} {kp.averageMastery.toFixed(0)}% @@ -252,7 +252,7 @@ export function ClassDiagnosticView({ summary }: ClassDiagnosticViewProps) { type="month" value={period} onChange={(e) => setPeriod(e.target.value)} - className="w-[180px]" + className="w-44" />