diff --git a/docs/architecture/001_project_overview.md b/docs/architecture/001_project_overview.md index 6711377..5154938 100644 --- a/docs/architecture/001_project_overview.md +++ b/docs/architecture/001_project_overview.md @@ -368,7 +368,7 @@ AI 模式: 选择 AI Provider → 粘贴试卷源文本 → AI 解析生成 → | API 路由 | 9 | | Server Actions | 80+ | | 用户角色 | 6 (admin/teacher/student/parent/grade_head/teaching_head) | -| 权限点 | 54 | +| 权限点 | 67 | --- @@ -380,3 +380,4 @@ AI 模式: 选择 AI Provider → 粘贴试卷源文本 → AI 解析生成 → | [005 架构数据](./005_architecture_data.json) | AI 友好的结构化架构数据 | | [006 功能清单](./006_k12_feature_checklist.md) | 企业级 K12 标准功能模块清单 | | [007 差距审计报告](./007_gap_audit_report.md) | 功能差距审计与补齐路线图 | +| [008 模块角色映射](./008_module_role_mapping.md) | 模块-角色-功能映射总览 | diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index fbe9fb0..6c1cf57 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -19,6 +19,7 @@ - [附录 A:模块间依赖矩阵](#附录-a模块间依赖矩阵) - [附录 B:关键参数影响链](#附录-b关键参数影响链) - [附录 C:核心函数签名索引](#附录-c核心函数签名索引) +- [相关文档](#相关文档) --- @@ -46,6 +47,7 @@ │ 教学管理: classes · school · scheduling · attendance · course-plans│ │ 用户与沟通:users · messaging · notifications · parent · audit │ │ 扩展功能: elective · proctoring · diagnostic · dashboard │ +│ 权限管理: rbac │ │ 其他: announcements · files · settings · auth · layout · student│ │ │ │ 每个模块标准结构:actions.ts (编排) → data-access.ts (DB) → schema.ts│ @@ -81,6 +83,14 @@ --- +## 1.1.1 M7 移动端 PWA 支持(2026-07-01 新增) + +- **PWA 清单**:新增 `src/app/manifest.ts`(Next.js App Router 元数据路由),生成 `/manifest.webmanifest`,声明 `name`/`short_name=NextEdu`/`start_url=/`/`display=standalone`/`theme_color=#1976d2`/`icons=/icon.svg`。 +- **根布局**:`src/app/layout.tsx` 新增 `viewport` 导出(`width=device-width`、`initialScale=1`、`maximumScale=5`、`userScalable=true`、`themeColor` 按明暗主题切换)+ `metadata` 新增 `manifest`/`appleWebApp` 配置,支持添加到主屏幕与独立显示。 +- **触控友好**:`lesson-preparation` 模块 `node-editor.tsx` 与 `lesson-plan-readonly-view.tsx` 的 `` 组件新增 `panOnDrag`/`zoomOnPinch`/`zoomOnDoubleClick={false}`/`selectionOnDrag={false}` 属性,防止移动端双击误缩放、优化触控拖拽体验。 + +--- + ## 1.2 模块依赖关系图 下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。 @@ -393,12 +403,16 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" ├─▶ /admin/school/grades/insights → requirePermission(SCHOOL_MANAGE) ├─▶ /admin/users/import → requirePermission(USER_MANAGE) ├─▶ /admin/users → requirePermission(USER_MANAGE) + ├─▶ /admin/roles → requirePermission(ROLE_READ) + ├─▶ /admin/roles/[id] → requirePermission(ROLE_READ) + ├─▶ /admin/permissions → requirePermission(PERMISSION_READ) ├─▶ /admin/scheduling/auto → requirePermission(SCHEDULE_AUTO) ├─▶ /admin/scheduling/changes → requirePermission(SCHEDULE_ADJUST) ├─▶ /admin/scheduling/rules → requirePermission(SCHEDULE_ADJUST) ├─▶ /admin/announcements → requirePermission(ANNOUNCEMENT_MANAGE) ├─▶ /admin/announcements/[id] → requirePermission(ANNOUNCEMENT_MANAGE) - └─▶ /admin/audit-logs → requirePermission(AUDIT_LOG_READ) + ├─▶ /admin/audit-logs → requirePermission(AUDIT_LOG_READ) + └─▶ /admin/invitation-codes → requirePermission(USER_MANAGE) ✅ audit-P2-3 新增 ✅ P0 安全修复:admin/layout.tsx 提供登录态统一守卫, 各页面 requirePermission() 提供细粒度权限校验 @@ -408,6 +422,12 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" ├─▶ /teacher/classes/my/[id] → requirePermission(CLASS_READ) ✅ 已修复 ├─▶ /teacher/classes/schedule → requirePermission(CLASS_READ) ✅ 已修复 └─▶ /teacher/classes/students → requirePermission(CLASS_READ) ✅ 已修复 + + student/learning + student/schedule 路由权限校验(2026-06-26 审计修复): + ├─▶ /student/learning → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) + ├─▶ /student/learning/courses → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) + ├─▶ /student/learning/courses/[classId]→ requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) + └─▶ /student/schedule → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) ``` --- @@ -432,6 +452,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - `downloadBase64File(base64, filename, mimeType?)` / `downloadBlob(blob, filename)` — 客户端文件下载(P1-c 重构新增:从 grades/export-button、users/user-import-dialog、audit/audit-log-export-button 三处重复实现抽取,位于 `lib/download.ts`) - `handleActionError(e)` / `safeActionCall(action, options?)` / `safeJsonParse(json, msg)` / `safeParseDate(v, field)` / `safeParseNumber(v, field)` / `escapeLikePattern(input)` — Server Action 错误处理与客户端调用包装(2026-06-23 审计修复新增:位于 `lib/action-utils.ts`,统一所有 Server Action catch 块错误处理,避免内部错误消息暴露给客户端) - `BusinessError` / `NotFoundError` / `ValidationError` — 错误类(2026-06-23 审计修复新增:位于 `lib/action-utils.ts`,已知业务错误基类,message 可安全返回客户端) +- `extractQuestionPreview(content, fallback?, maxLength?)` — 题目内容纯文本预览提取(✅ 2026-06-24 新增:位于 `lib/question-content.ts`,从 `unknown` 类型题目内容递归提取纯文本,支持字符串/数组/对象格式,可选截断;被 error-book、homework 等模块共享使用,避免各模块重复实现题目内容解析逻辑) +- `isRecord(v)` — 通用类型守卫(P1-6 代码去重新增:位于 `lib/type-guards.ts`,判断未知值是否为非空对象 `v is Record`;消除 homework/stats-service、lesson-preparation/node-summary、lesson-preparation/ai-suggest 等 12+ 个文件中重复定义的 `isRecord` 局部实现,从 `unknown` 安全收窄类型,替代 `as Record` 断言) +- `trackEvent(name, payload)` / `trackExamEvent(name, payload)` / `trackAuthEvent(name, payload)` — 监控埋点接口(位于 `lib/track-event.ts`,server-only,当前输出到 console.info,预留接入外部监控服务;EventName 联合类型覆盖 announcement/message/notification/attendance/elective/exam/homework/ai/audit/files/auth 模块事件;✅ P3 新增事件名 `homework.excellent_viewed`、`homework.remind_unsubmitted`;✅ audit-P1-9 新增 auth 模块 8 个事件 `auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.2fa_disabled`/`auth.account_locked`/`auth.rate_limited`,并新增 `trackAuthEvent` 便捷函数自动设置 targetType="user") +- `ROLE_NAMES` — 角色名常量对象(✅ 2026-06-24 新增:位于 `types/permissions.ts`,包含 ADMIN/TEACHER/STUDENT/PARENT/GRADE_HEAD/TEACHING_HEAD 键,替代 DB 查询中硬编码的角色名字符串;被 error-book/data-access-analytics.getAllStudentIds 使用 `ROLE_NAMES.STUDENT`) **共享组件导出**(P0-b / P1-a / P1-b / P1-c / P2-a / P2-b / P3-a / P3-b / P3-c / P3-d / 第二轮 P0-1/P0-2/P0-3/P1-1/P1-2/P1-3/P1-4 重构新增,按类别组织): @@ -446,6 +470,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | **UI 组件** | `Pagination` | `components/ui/pagination.tsx` | 通用分页 UI(Showing X-Y of Z + Page X of Y + 上一页/下一页按钮) | 3 个(P0-2: audit-log-table, login-log-table, data-change-log-table) | | **UI 组件** | `EmptyTableRow` | `components/ui/empty-table-row.tsx` | 表格空状态行(TableRow + TableCell 居中显示空状态文案) | 3 个(P0-3: audit-log-table, login-log-table, data-change-log-table) | | **UI 组件** | `ResizablePanel` | `components/ui/resizable-panel.tsx` | 自实现可拖拽分栏容器(左右两栏 + 中间分隔条 pointer 拖拽调整宽度,无新依赖;用于试卷富文本编辑器左编辑右预览、阅卷式批改左题目右扫描图等场景) | 2 个(exams/components/exam-rich-form, homework/components/homework-scan-grading-view) | +| **UI 组件** | `ErrorState` | `components/error-state.tsx` | 通用错误边界展示组件("use client",统一 error.tsx 渲染逻辑:AlertTriangle 图标 + i18n title/description + retry 按钮;props:`{ error, reset, namespace }`;翻译键查找规则:在传入 namespace 下查找 `error.title`/`error.description`/`error.retry`) | 10+ 个(2026-06-26 审计修复新增:classes/student 等模块的 error.tsx) | | **UI 组件** | `StatusBadge` | `components/ui/status-badge.tsx` | 通用状态徽章(Badge + 状态→variant/label/className 映射表,修复 in_progress 颜色不一致 bug) | 9+ 个(P1-1: audit 3 文件, grades 2 文件, student/learning/assignments, parent/child-homework-summary, student-upcoming-assignments-card, question-columns) | | **表单字段** | `TextField` | `components/form-fields/text-field.tsx` | 通用文本字段(FormField + Input 包装,支持 text/number/password/datetime-local 类型 + value 转换器) | 3 个文件 16 处(P1-2: profile-settings-form 6, exam-basic-info-form 4, ai-provider-settings-card 4) | | **表单字段** | `SelectField` | `components/form-fields/select-field.tsx` | 通用选择字段(FormField + Select 包装,支持 toSelectValue/fromSelectValue 处理 number↔string) | 4 个文件 8 处(P1-2: exam-basic-info-form 3, ai-provider-settings-card 1, create-question-dialog 2, profile-settings-form 1) | @@ -456,6 +481,13 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | **图表组件** | `ComparisonRadarChart` | `components/charts/comparison-radar-chart.tsx` | 对比雷达图(RadarChart 统一配置,支持双 Radar 对比) | 8 个(P3-c: subject-comparison-chart, mastery-radar-chart 等) | | **课表组件** | `ScheduleList` / `ScheduleListItem` | `components/schedule/schedule-list.tsx` | 课表列表+列表项(课程+时间+地点+班级徽章,separator/card 两种变体) | 3 个(P3-a: student-today-schedule-card, child-schedule-card, student-schedule-view) | | **题库组件** | `QuestionBankFilters` | `components/question/question-bank-filters.tsx` | 题库筛选栏(搜索+题型+难度,default/compact 两种布局) | 2 个(P3-d: exam-assembly, question-bank-picker) | +| **题库组件** | `QuestionCascadeFilter` | `modules/questions/components/question-cascade-filter.tsx` | ✅ 审计新增:教材→章节→知识点三级级联筛选(nuqs URL 状态同步,清除上级自动清除下级) | 1 个(question-filters) | +| **题库组件** | `KnowledgePointSelector` | `modules/questions/components/knowledge-point-selector.tsx` | ✅ 审计新增:知识点选择器(支持搜索过滤,从 create-question-dialog 拆分) | 1 个(create-question-dialog) | +| **题库组件** | `OptionsEditor` | `modules/questions/components/options-editor.tsx` | ✅ 审计新增:选项编辑器(单选/多选模式,增删改选项,从 create-question-dialog 拆分) | 1 个(create-question-dialog) | +| **题库组件** | `QuestionBankResultsClient` | `modules/questions/components/question-bank-results-client.tsx` | ✅ 审计新增:RSC 与客户端 Hook 桥接(解决 useQuestionColumns Hook 在客户端组件中使用) | 1 个(teacher/questions/page) | +| **题库组件** | `QuestionContentRenderer` | `modules/questions/components/question-content-renderer.tsx` | ✅ 审计新增:题目结构化渲染(题干/选项/答案/解析,单选圆点/多选方框,正确答案高亮) | 1 个(question-actions 查看详情) | +| **题库组件** | `BatchOperations` | `modules/questions/components/batch-operations.tsx` | ✅ 审计新增:批量操作工具栏(批量删除,权限感知,事务原子性) | 1 个(question-data-table) | +| **题库组件** | `ImportExportButtons` | `modules/questions/components/import-export-buttons.tsx` | ✅ 审计新增:导入导出组件(JSON 格式,导出下载/上传导入,导入前预览确认) | 2 个(teacher/admin questions page) | | **设置组件** | `SettingsView` | `modules/settings/components/settings-view.tsx` | 统一设置页布局(5 标签页:General/Notifications/Appearance/Security/AI,角色差异通过 props 注入,Tab URL 持久化,登出二次确认) | 4 个(P2-a: admin/teacher/student/parent 设置页) | **共享 Hooks 导出**(第二轮 P1-4 重构新增): @@ -508,32 +540,48 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `lib/change-logger.ts` | - | 数据变更日志(通过 session.ts 获取 session,http-utils 获取 IP) | | `lib/login-logger.ts` | - | 登录日志(通过 http-utils 获取 IP/UA) | | `lib/password-policy.ts` | - | 密码策略纯函数 | -| `lib/rate-limit.ts` | - | 内存滑动窗口限流 | -| `lib/role-utils.ts` | 31 | 角色规范化纯函数(normalizeRole / resolvePrimaryRole) | +| `lib/rate-limit/` | - | 速率限制器抽象(audit-P1-6 新增:抽象 `RateLimiter` 接口;默认内存实现 `MemoryRateLimiter`;可选 Redis 实现 `RedisRateLimiter` 基于 @upstash/ratelimit;通过 `RATE_LIMIT_DRIVER=memory\|redis` 环境变量切换;`rateLimit()` / `resetRateLimit()` 改为 Promise 返回以支持异步后端;`rateLimitKey` / `RATE_LIMIT_RULES` / `rateLimitHeaders` 保持同步纯函数) | +| `lib/role-utils.ts` | 51 | 角色规范化纯函数(normalizeRole / resolvePrimaryRole;✅ P2-8 修改:保留 grade_head/teaching_head 独立性,不再映射为 teacher;优先级 admin>grade_head>teaching_head>teacher>student>parent) | | `lib/bcrypt-utils.ts` | 18 | bcrypt 哈希前缀规范化纯函数 | +| `lib/breached-password.ts` | 125 | 已泄露密码检测(✅ audit-P2-4 新增:HIBP k-anonymity API 范围查询;仅发送 SHA-1 前 5 字符到 `api.pwnedpasswords.com`,本地比对完整哈希后缀,不泄露用户密码;3 秒超时 + AbortController;fail-open 策略:API 不可用时返回 `checkSkipped=true` 不阻断注册/改密;server-only) | | `lib/http-utils.ts` | 44 | 请求头解析(resolveClientIp / getUserAgent,server-only) | | `lib/password-security-service.ts` | 84 | 密码安全 DB 操作(账户锁定/失败登录追踪,server-only) | +| `lib/route-resolver.ts` | 21 | 角色默认路径解析纯函数 `resolveDefaultPath(roles: string[]): string`(✅ audit-P0-6 新增:合并 `proxy.ts` 与 `onboarding/data-access.resolveDefaultPathByRoles` 重复实现;纯函数无 `server-only`,可在 edge/proxy 运行时使用;优先级 admin>grade_head/teaching_head>teacher>student>parent>fallback `/dashboard`) | +| `lib/route-permissions.ts` | 82 | 路由权限配置常量(✅ audit-P1-10 + audit-P1-12 新增:从 proxy.ts 抽取,配置驱动设计;导出 4 个 `Record` 常量:`SPECIFIC_ROUTE_PERMISSIONS`(15 个精确路由权限,audit-P1-10 扩展自 3 个至 15 个覆盖 `/admin/*` 子路径)/ `ROUTE_PREFIX_PERMISSIONS`(5 个角色路由前缀权限)/ `DASHBOARD_ROUTE_PERMISSIONS`(4 个仪表盘细粒度权限防跨角色访问)/ `API_ROUTE_PERMISSIONS`(1 个 API 路由权限);纯常量无 `server-only`,新增路由权限只需修改此文件无需改 proxy.ts 逻辑) | +| `lib/permission-bitmap.ts` | 222 | 权限位图编解码(✅ audit-P1-7 新增:将 67 个权限点字符串数组约 1.1KB 压缩为约 13 字符 base36 字符串,体积减少 99%;`PERMISSION_BITMAP_ORDER` 顺序固定不可变,新增权限只能追加末尾;纯函数无 `server-only`,可在 edge runtime 使用;导出 `encodePermissionsBitmap(permissions)` / `decodePermissionsBitmap(bitmap)` / `hasPermissionInBitmap(bitmap, permission)`;BigInt 精度安全:手工按位累加 base36 → BigInt,避免 `parseInt(bitmap, 36)` 的 Number 53 bit 精度丢失) | | `lib/excel.ts` | - | Excel 导入导出 | | `lib/file-storage.ts` | - | 文件存储抽象 | | `hooks/use-permission.ts` | - | 客户端权限 Hook | -| `components/ui/*` | 34 文件 | shadcn/ui 标准组件 | +| `components/ui/*` | 35 文件 | shadcn/ui 标准组件(✅ P2-3 新增 alert.tsx) | | `components/ui/stat-card.tsx` | 95 | StatCard 统计卡片(P1-a 新增) | | `components/ui/stat-item.tsx` | 38 | StatItem 紧凑统计项(P1-a 新增) | | `components/ui/chip-nav.tsx` | 78 | ChipNav 芯片导航(P1-b 新增) | | `components/ui/page-header.tsx` | 44 | PageHeader 页面头部(P2-b 新增,含 icon 属性) | | `components/ui/filter-bar.tsx` | 124 | FilterBar + FilterSearchInput + FilterResetButton(P3-b 新增) | | `components/ui/resizable-panel.tsx` | - | ResizablePanel 可拖拽分栏容器(自实现,无新依赖;用于富文本编辑器与阅卷式批改的左右分栏布局) | +| `components/error-state.tsx` | 35 | ErrorState 通用错误边界展示组件("use client";2026-06-26 审计修复新增:统一 30+ error.tsx 渲染逻辑;props `{ error, reset, namespace }`,基于 EmptyState + AlertTriangle + next-intl useTranslations) | | `components/charts/chart-card-shell.tsx` | 90 | ChartCardShell 图表卡片外壳(P3-c 新增) | | `components/charts/trend-line-chart.tsx` | 153 | TrendLineChart 趋势折线图(P3-c 新增) | | `components/charts/simple-bar-chart.tsx` | 162 | SimpleBarChart 柱状图(P3-c 新增) | | `components/charts/comparison-radar-chart.tsx` | 143 | ComparisonRadarChart 对比雷达图(P3-c 新增) | | `components/schedule/schedule-list.tsx` | 112 | ScheduleList + ScheduleListItem 课表列表(P3-a 新增) | | `components/question/question-bank-filters.tsx` | 137 | QuestionBankFilters 题库筛选栏(P3-d 新增) | +| `modules/questions/components/question-cascade-filter.tsx` | 130+ | QuestionCascadeFilter 三级级联筛选(✅ 审计新增:教材→章节→知识点,nuqs URL 状态) | +| `modules/questions/components/knowledge-point-selector.tsx` | 80+ | KnowledgePointSelector 知识点选择器(✅ 审计新增:从 create-question-dialog 拆分) | +| `modules/questions/components/options-editor.tsx` | 120+ | OptionsEditor 选项编辑器(✅ 审计新增:从 create-question-dialog 拆分) | +| `modules/questions/components/question-bank-results-client.tsx` | 30+ | QuestionBankResultsClient RSC 桥接(✅ 审计新增) | +| `modules/questions/utils/parse-content.ts` | 60+ | parseQuestionContent 类型守卫(✅ 审计新增:替代 `as` 断言) | +| `modules/questions/utils/track-event.ts` | 30+ | trackQuestionEvent 埋点接口(✅ 审计新增:no-op 实现) | +| `modules/questions/components/question-content-renderer.tsx` | 110+ | QuestionContentRenderer 题目结构化渲染(✅ 审计新增) | +| `modules/questions/components/batch-operations.tsx` | 110+ | BatchOperations 批量操作工具栏(✅ 审计新增) | +| `modules/questions/components/import-export-buttons.tsx` | 180+ | ImportExportButtons 导入导出组件(✅ 审计新增) | | `lib/download.ts` | 47 | downloadBase64File + downloadBlob 客户端下载工具(P1-c 新增) | +| `lib/type-guards.ts` | - | 通用类型守卫集合(P1-6 新增 `isRecord(v): v is Record`,消除 12+ 个文件重复定义;含 grades/lesson-preparation 等模块的类型守卫 `isGradeDraftData`/`isExerciseBlockData`/`isPermission` 等) | +| `lib/track-event.ts` | - | 监控埋点接口(server-only,no-op console.info 实现;EventName 联合类型覆盖 11 个模块事件;P3 新增 `homework.excellent_viewed`、`homework.remind_unsubmitted`;audit-P1-9 新增 auth 模块 8 个事件 + `trackAuthEvent` 便捷函数) | | `lib/utils.ts` | - | 通用工具(P1-a/P1-c 新增 getInitials + formatDateForFile) | -| `components/onboarding-gate.tsx` | 312 | ~~引导流程(业务泄漏)~~ 已废弃,逻辑迁移至 `modules/onboarding/`(P2-4 已修复) | +| ~~`components/onboarding-gate.tsx`~~ | ~~312~~ | ✅ audit-P0-4 已删除(引导流程业务逻辑已迁移至 `modules/onboarding/`,原文件无任何 import 引用,删除以避免文件膨胀) | | `components/global-search.tsx` | 221 | 全局搜索(业务泄漏) | -| `types/permissions.ts` | 157 | 61 个权限点常量 + Role/DataScope/AuthContext 类型 | +| `types/permissions.ts` | 157 | 67 个权限点常量 + Role/DataScope/AuthContext 类型(✅ rbac 新增:`Role` 类型从固定联合类型改为 `string` 支持动态角色;新增 `BUILTIN_ROLES` 常量、`BuiltinRole` 类型、`isBuiltinRole()` 类型守卫;`isRole()` 标记为 deprecated) | --- @@ -542,20 +590,23 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **职责**:考试全生命周期管理(创建/编辑/预览/发布/删除/复制)+ AI 辅助出题。 **导出函数**: -- Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度3,按 gradeId 查询年级下所有考试 + 提交统计,EXAM_READ 权限)/ `autoMarkExamAction`(✅ 2026-06-24 新增:AI 自动标记试卷文本,将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档,EXAM_AI_GENERATE 权限,返回 `{ doc, title }` 供富文本编辑器载入)/ `createExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器保存试卷草稿,将 Tiptap JSONContent 通过 `editor/editor-to-structure.editorDocToStructure` 转换为 questions + structure 后复用 `persistAiGeneratedExamDraft` 持久化,EXAM_CREATE 权限)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access) -- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)/ `getExamsForGradeEntry`(✅ 2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷,供成绩录入页试卷选择器使用,返回 id/title/subjectName/gradeName/questionCount/totalScore)/ `getExamForGradeEntry`(✅ 2026-06-24 新增:获取单个试卷详情含题目列表,innerJoin questions 获取 type,含 scope 校验,返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}],供 grades 模块按试卷录入成绩使用) -- AI Pipeline:`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` -- Utils:`normalizeStructure`(v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取) +- Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度3,按 gradeId 查询年级下所有考试 + 提交统计,EXAM_READ 权限)/ `autoMarkExamAction`(✅ 2026-06-24 新增:AI 自动标记试卷文本,将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档,EXAM_AI_GENERATE 权限,返回 `{ doc, title }` 供富文本编辑器载入。✅ 2026-06-24 拆分:迁移至 `ai-pipeline/auto-mark.ts`,actions.ts re-export 保持向后兼容)/ `createExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器保存试卷草稿,将 Tiptap JSONContent 通过 `editor/editor-to-structure.editorDocToStructure` 转换为 questions + structure 后复用 `persistAiGeneratedExamDraft` 持久化,EXAM_CREATE 权限;✅ 2026-06-24 升级:递归 `convertStructureNode` 支持 section/group/question 三级层级,section 数据不再丢失。✅ 2026-06-24 拆分:迁移至 `actions-rich-editor.ts`,actions.ts re-export 保持向后兼容)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access) / `updateExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器更新已有试卷,校验所有权后递归转换 EditorDoc→structure,为无 questionId 的题目创建 questions 记录,调用 `data-access.updateExamWithRichEditorData` 持久化,EXAM_UPDATE 权限,revalidatePath 列表页与组卷页。✅ P0-4 修复(2026-06-24):原直接 db.transaction 写 exams/examQuestions 表,现已下沉到 data-access.updateExamWithRichEditorData。✅ 2026-06-24 拆分:迁移至 `actions-rich-editor.ts`,actions.ts re-export 保持向后兼容) +- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)/ `getExamsForGradeEntry`(✅ 2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷,供成绩录入页试卷选择器使用,返回 id/title/subjectName/gradeName/questionCount/totalScore)/ `getExamForGradeEntry`(✅ 2026-06-24 新增:获取单个试卷详情含题目列表,innerJoin questions 获取 type,含 scope 校验,返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}],供 grades 模块按试卷录入成绩使用)/ `updateExamWithRichEditorData`(✅ 2026-06-24 新增,P0-4 修复:从富文本编辑器更新已有试卷的 DB 事务,单事务内更新 exams.title/structure + 删除旧 examQuestions + 插入新 examQuestions,原 updateExamFromRichEditorAction 直接 db.transaction 的逻辑下沉至此) +- AI Pipeline:`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` / `loadAiDraftQuestionsAndStructure`(✅ 2026-06-24 新增:从 actions.ts 下沉,加载 AI 草稿题目与结构,优先从 rawAiQuestions JSON 解析,否则从源文本生成) +- Utils:`normalizeStructure`(v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取;✅ 2026-06-24 升级:支持 section/group/question 三级层级递归校验,section 节点含 title/level/children,group 节点含 title/instruction/children) +- Preview Utils(`components/exam-preview-utils.ts`,✅ 2026-06-24 补充记录):`buildPreviewNodes` / `parseEditableContent` / `flattenPreviewQuestions` / `findPreviewQuestionNode` / `updatePreviewQuestionNodeInList` / `buildPreviewSignature` / `buildPreviewPayload` / `buildPreviewRequestData`(以上 8 个为预览纯函数,供 Hook 与组件复用)+ ✅ 2026-06-24 新增 5 个(从 use-exam-preview-tasks Hook 提取以控制行数):`buildTaskFormValues`(提取表单字段子集供任务回填)/ `isPreviewBackgroundTask`(类型守卫,校验 localStorage 恢复数据)/ `persistPreviewTasksToStorage`(持久化任务到 localStorage)/ `parseStoredPreviewTasks`(解析 localStorage 任务,queued/running 标记为 failed 防僵尸)/ `executeBackgroundPreviewTask`(执行单个后台预览任务的纯函数) +- Hooks(✅ 2026-06-24 拆分记录):`useExamPreview`(主组合器,68 行,组合三个子 Hook + handlePreview 编排,返回 API 与拆分前一致)/ `usePreviewState`(状态子 Hook,67 行,10 个 useState + 3 个更新函数)/ `usePreviewBackgroundTasks`(后台任务子 Hook,80 行,PQueue 队列 + localStorage 持久化 + 入队/打开任务)/ `usePreviewRewrite`(AI 重写子 Hook,79 行,handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction) - Stats-service(V3-8 新增):`getExamAnalytics`(cache 包装,聚合考试所有作业的已批改提交,计算平均分/及格率/分数段分布/逐题错误率与难度等级,对标智学网考试分析)+ `ExamAnalyticsSummary` 类型 - Types(✅ 2026-06-24 新增成绩录入相关类型):`ExamQuestionItem`(试卷中单个题目的精简结构 { id, order, score, type })/ `ExamForGradeEntry`(成绩录入用的试卷详情,含题目列表)/ `ExamOptionForEntry`(成绩录入页试卷选择器选项 { id, title, subjectName, gradeName, questionCount, totalScore }) - Editor 子模块(✅ 2026-06-24 新增 `editor/` 目录,基于 Tiptap 的试卷富文本编辑器): - 组件:`ExamRichEditor`(富文本编辑器主体,forwardRef 暴露 `ExamRichEditorHandle` 以便父组件获取 JSONContent)/ `SelectionToolbar`(浮动选择工具栏,对选中文本应用 DottedMark 等标记) - - 自定义 Tiptap 扩展(`editor/extensions/`):`DottedMark`(加点字标记,拼音注音题)/ `BlankNode`(填空空位节点,原子节点)/ `ImageNode`(图片节点,含 fileId/url/alt 属性)/ `QuestionBlock`(题目块节点,含 type/score 等 attrs)/ `GroupBlock`(大题分组节点,含 title) - - 类型(`editor/exam-rich-editor-types.ts`):`RichQuestionType`(`single_choice | multiple_choice | judgment | text | composite`)/ `RichQuestionContent`(题干文本+选项+填空+图片+子题+正确答案)/ `EditorQuestion` / `EditorStructureNode`(group|question 递归)/ `EditorDoc`({ title, questions, structure })/ `EditorJSONContent`(JSONContent 别名) - - 转换工具:`editorDocToStructure`(`editor/editor-to-structure.ts`,Tiptap JSONContent → EditorDoc,供 `createExamFromRichEditorAction` 持久化使用)/ `structureToEditorDoc`(`editor/structure-to-editor.ts`,EditorDoc → Tiptap JSONContent,供编辑器回填已保存试卷使用) + - 自定义 Tiptap 扩展(`editor/extensions/`):`DottedMark`(加点字标记,拼音注音题)/ `BlankNode`(填空空位节点,原子节点)/ `ImageNode`(图片节点,含 fileId/url/alt 属性)/ `QuestionBlock`(题目块节点,含 type/score 等 attrs)/ `GroupBlock`(大题分组节点,含 title)/ `SectionBlock`(✅ 2026-06-24 新增:分卷节点,含 title/level attrs,isolating: true,对应第Ⅰ卷/第Ⅱ卷层级) + - 类型(`editor/exam-rich-editor-types.ts`):`RichQuestionType`(`single_choice | multiple_choice | judgment | text | composite`)/ `RichQuestionContent`(题干文本+选项+填空+图片+子题+正确答案)/ `EditorQuestion` / `EditorStructureNode`(✅ 2026-06-24 升级:section|group|question 三级递归,section 含 title/level/children,group 含 title/instruction/children)/ `EditorDoc`({ title, questions, structure })/ `EditorJSONContent`(JSONContent 别名) + - 转换工具:`editorDocToStructure`(`editor/editor-to-structure.ts`,Tiptap JSONContent → EditorDoc,供 `createExamFromRichEditorAction`/`updateExamFromRichEditorAction` 持久化使用)/ `structureToEditorDoc`(`editor/structure-to-editor.ts`,EditorDoc → Tiptap JSONContent,✅ 2026-06-24 升级支持 section 节点回填,供编辑器回填已保存试卷使用)/ `examNodesToEditorDoc`(✅ 2026-06-24 新增:`editor/exam-nodes-to-editor-doc.ts`,BuildExam 的 `ExamNode[]` + `Question[]` → EditorDoc 桥梁函数,递归转换 section/group/question,供 edit-rich 页面加载已有试卷到富文本编辑器) - Barrel 导出:`editor/index.ts` 聚合导出上述所有组件、扩展、类型与转换函数 - Components(V3-8 新增):`ExamAnalyticsDashboard`(考试分析仪表盘:汇总卡片+分数段分布+逐题分析表) -- Components(✅ 2026-06-24 新增):`ExamRichForm`(富文本编辑器试卷创建表单,使用 `ResizablePanel` 左编辑右预览布局,集成 `autoMarkExamAction` 一键 AI 标记 + `createExamFromRichEditorAction` 保存草稿,调用 `getSubjectsAction`/`getGradesAction` 填充科目年级选项) +- Components(✅ 2026-06-24 新增):`ExamRichForm`(富文本编辑器试卷创建/编辑表单,使用 `ResizablePanel` 左编辑右预览布局,集成 `autoMarkExamAction` 一键 AI 标记;✅ 2026-06-24 升级支持 `mode="create"|"edit"` 双模式:create 模式调用 `createExamFromRichEditorAction` 保存草稿,edit 模式调用 `updateExamFromRichEditorAction` 更新已有试卷,编辑模式隐藏科目/年级/难度等字段并显示"Back to Build"按钮) +- Components(✅ P3 新增记录):`ExamPreview`(试卷结构预览组件,从 exam-rich-form.tsx 提取,渲染 section/group/question 三级嵌套;P3 修改:题目图片 `` → `next/image` ``,新增 `next/image` 导入;`useTranslations('examHomework')` 国际化) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`questions`(✅ P0-1 已修复:通过 data-access.createQuestionWithRelations)、`classes`(✅ P0-2 已修复:通过 data-access.getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`homework`(V3-8 新增:stats-service 通过 `homework/data-access.getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId` 获取作业与提交数据,合理跨模块调用) @@ -566,6 +617,9 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P0-2 已修复:~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds` - ✅ P1-1 已修复:~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions` - ✅ P1-2 已修复:~~`actions.ts` 832 行(超 800 建议),多处直接 DB 操作~~ DB 操作已下沉到 data-access,actions.ts 现 691 行 +- ✅ P0-4 已修复(2026-06-24):~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData +- ✅ actions.ts 拆分(2026-06-24):~~`actions.ts` 1525 行(超 1000 硬上限)~~ 拆分为 4 个文件:actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action),actions.ts re-export 保持向后兼容 +- ✅ P0-8 已修复:~~`data-access.ts` 1036 行(超 1000 硬上限)~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口),文末 re-export 保持向后兼容 - ⚠️ P1:`ai-pipeline.ts` 857 行(超 800 建议),混合 4 类职责 - ✅ P2 已修复:`ai-pipeline.ts` 中 3 处非空断言清理(`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`) - ✅ V3-5/V3-8/V3-12:`exam-actions.tsx` 增强:V3-5 角色化菜单(按角色显示不同操作项)、V3-8 新增 analytics 菜单项(BarChart3 图标,跳转 `/teacher/exams/[id]/analytics`)、V3-12 移动端触摸目标尺寸优化 @@ -573,11 +627,15 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `actions.ts` | 691+ | 12 个 Server Action(P1-2 已修复,无直接 DB 操作;2026-06-24 新增 `autoMarkExamAction`/`createExamFromRichEditorAction`) | +| `actions.ts` | 633 | 10 个核心 Server Action(✅ 2026-06-24 拆分:从 1525 行降至 633 行;re-export autoMarkExamAction/createExamFromRichEditorAction/updateExamFromRichEditorAction 保持向后兼容;P0-4 已修复:无直接 DB 操作) | +| `actions-helpers.ts` | 96 | ✅ 2026-06-24 新增:跨文件共享辅助函数(getStringValue/getBoolValue/parseExamModeConfig/failState/successState/invalidFormState/prepareExamCreateContext) | +| `actions-rich-editor.ts` | 250 | ✅ 2026-06-24 新增:富文本编辑器 Server Action(createExamFromRichEditorAction/updateExamFromRichEditorAction)+ 共享辅助 convertStructureNode + RichExamCreateSchema/RichExamUpdateSchema。P0-4 修复:updateExamFromRichEditorAction 调用 data-access.updateExamWithRichEditorData 不再直访 DB | +| `ai-pipeline/auto-mark.ts` | 356 | ✅ 2026-06-24 新增:autoMarkExamAction Server Action + 纯转换辅助(buildTiptapDocFromAiResponse/splitByDottedTexts/extractTitleFromAiResponse/AutoMarkSchema/AutoMarkResult)。从 actions.ts 迁出 | | `ai-pipeline.ts` | 857 | AI 出题管线(超限) | -| `data-access.ts` | 560+ | 考试 CRUD(含 P1-2 新增 7 个写/查询函数,P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信;v4-P2-7 新增 getExamsByGradeId;2026-06-24 新增 getExamsForGradeEntry/getExamForGradeEntry 供 grades 模块按试卷录入成绩) | +| `data-access.ts` | 542 | 考试 CRUD(✅ 2026-06-24 拆分:跨模块接口迁移至 data-access-cross-module.ts,原 1036 行降至 542 行;含 P1-2 写/查询函数,P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信;✅ P0-4 修复:新增 updateExamWithRichEditorData 将富文本编辑器更新事务下沉;导出 parseExamMeta/getString/getNumber/toExamStatus 供 cross-module 复用;文末 re-export 跨模块接口保持向后兼容) | +| `data-access-cross-module.ts` | 462 | ✅ 2026-06-24 新增:跨模块查询接口(供 homework/grades/proctoring/diagnostic/dashboard/lesson-preparation 等模块调用,避免直查 exams/examSubmissions 表)。包含 getExamsDashboardStats/getExamIdsByGradeIds/getExamWithQuestionsForHomework/getExamSubjectIdMap/getExamTitleById/getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionWithAnswers/getExamSubmissionsForExam/addExamQuestions/getExamsByGradeId/getExamsForGradeEntry/getExamForGradeEntry,标记 `server-only`,从 data-access.ts 导入 parseExamMeta/getString/getNumber/toExamStatus 复用 | | `stats-service.ts` | - | V3-8 新增:考试分析数据聚合(`getExamAnalytics` + `ExamAnalyticsSummary` 类型) | -| `types.ts` | 50+ | 类型定义(2026-06-24 新增:ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用;新增 `AutoMarkResult`/`AutoMarkSchema`/`RichExamCreateSchema` 供富文本编辑器使用) | +| `types.ts` | 50+ | 类型定义(2026-06-24 新增:ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用。注:`AutoMarkResult`/`AutoMarkSchema` 已迁移至 `ai-pipeline/auto-mark.ts`,`RichExamCreateSchema`/`RichExamUpdateSchema` 已迁移至 `actions-rich-editor.ts`) | | `editor/index.ts` | 23 | 2026-06-24 新增:富文本编辑器 barrel 导出(聚合组件/扩展/类型/转换函数) | | `editor/exam-rich-editor.tsx` | - | 2026-06-24 新增:ExamRichEditor 富文本编辑器主体(forwardRef + ExamRichEditorHandle) | | `editor/selection-toolbar.tsx` | - | 2026-06-24 新增:SelectionToolbar 浮动选择工具栏 | @@ -590,12 +648,39 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `editor/extensions/image-node.ts` | - | 2026-06-24 新增:ImageNode 图片节点扩展 | | `editor/extensions/group-block.tsx` | - | 2026-06-24 新增:GroupBlock 大题分组节点扩展 | | `editor/extensions/question-block.tsx` | - | 2026-06-24 新增:QuestionBlock 题目块节点扩展 | -| `hooks/use-exam-preview.ts` | 295 | 预览 Hook | +| `hooks/use-exam-preview.ts` | 68 | 预览 Hook 主组合器(✅ 2026-06-24 拆分:从 318 行降至 68 行,组合 state/tasks/rewrite 三个子 Hook,保留 handlePreview 编排逻辑,返回 API 与拆分前完全一致,调用方 exam-form.tsx 无需修改) | +| `hooks/use-exam-preview-state.ts` | 67 | ✅ 2026-06-24 新增:预览状态子 Hook(10 个 useState + updatePreviewQuestionNode/updateSelectedQuestionFromAi/applyPreviewResult 三个更新函数) | +| `hooks/use-exam-preview-tasks.ts` | 80 | ✅ 2026-06-24 新增:预览后台任务子 Hook(PQueue 并发队列 + localStorage 持久化恢复 + handleBackgroundPreview/handleOpenPreviewTask) | +| `hooks/use-exam-preview-rewrite.ts` | 79 | ✅ 2026-06-24 新增:预览 AI 重写子 Hook(handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction) | +| `components/exam-preview-utils.ts` | 293 | 预览纯函数工具集(buildPreviewNodes/parseEditableContent/flattenPreviewQuestions/findPreviewQuestionNode/updatePreviewQuestionNodeInList/buildPreviewSignature/buildPreviewPayload/buildPreviewRequestData + ✅ 2026-06-24 新增 5 个:buildTaskFormValues/isPreviewBackgroundTask/persistPreviewTasksToStorage/parseStoredPreviewTasks/executeBackgroundPreviewTask,从 use-exam-preview-tasks Hook 提取以控制行数) | +| `components/exam-preview.tsx` | - | 试卷结构预览组件(use client):从 exam-rich-form.tsx 提取,渲染试卷富文本编辑器的结构预览,支持分卷(section)/大题分组(group)/题目(question)三级嵌套递归渲染。✅ P3 修改:题目块中图片将原生 `` 替换为 `next/image` 的 `` 组件(自动优化、懒加载、防布局偏移),新增导入 `next/image`;使用 `useTranslations('examHomework')` 国际化 section/group 标题与统计文案 | | `utils/normalize-structure.ts` | 57 | v3 新增:exam.structure 运行时校验与归一化(从 build/page.tsx 提取) | | `components/exam-analytics-dashboard.tsx` | - | V3-8 新增:考试分析仪表盘组件 | | `components/exam-actions.tsx` | - | V3-5/V3-8/V3-12 增强:角色化菜单+analytics 链接+移动端触摸优化 | -| `components/exam-rich-form.tsx` | - | 2026-06-24 新增:富文本编辑器试卷创建表单(ResizablePanel 左编辑右预览 + AI 自动标记 + 保存草稿) | +| `components/exam-rich-form.tsx` | - | 2026-06-24 新增:富文本编辑器试卷创建/编辑表单(ResizablePanel 左编辑右预览 + AI 自动标记 + 保存草稿)。✅ 2026-06-24 拆分后:import 路径直连 `ai-pipeline/auto-mark.ts`(autoMarkExamAction)和 `actions-rich-editor.ts`(createExamFromRichEditorAction/updateExamFromRichEditorAction),支持 create/edit 双模式 | +| `components/exam-boundaries.tsx` | 189 | ✅ 2026-06-25 新增(P2-3):模块统一边界组件。导出 `ExamErrorBoundary`(薄包装 SectionErrorBoundary,固定 examHomework 命名空间)、`ExamEmptyState`(i18n 键驱动的空状态)、`ExamSkeleton`(list/card/detail/analytics/editor 五种变体)。复用 shared 层 EmptyState/Skeleton/SectionErrorBoundary,避免每页重复实现。新增 i18n 键 `exam.error.boundaryTitle/boundaryDescription/retry` | | `components/*` | 19+ 文件 | 考试表单/组卷/预览/分析组件 | +| `services/exam-service-port.ts` | 95 | ✅ 2026-06-25 新增(P2-4):模块对内的服务端口契约。导出 `ExamServicePort` 接口(listExams/getExam/createExam/updateExam/deleteExam/duplicateExam/autoMarkExam/getExamPreview/getAnalytics/getSubjects/getGrades 共 12 个方法)+ `ExamDetail`/`ExamCreateInput`/`ExamUpdateInput` 类型。区别于 shared 层的 ExamHomeworkServicePort(跨模块调用方契约,单例注册),本接口面向模块内部组件,通过 React Context 注入 | +| `services/exam-service-context.tsx` | 72 | ✅ 2026-06-25 新增(P2-4):ExamServicePort 的 React Context 注入点。导出 `ExamServiceProvider`(Provider 组件)、`useExamService`(消费者 Hook,未注入时抛错)、`useTryExamService`(测试用,未注入时返回 null)。当前为骨架,具体实现将在 P2-6+ 落地 | +| `services/index.ts` | 23 | ✅ 2026-06-25 新增:services 子模块 barrel 导出 | +| `config/exam-widgets.ts` | 192 | ✅ 2026-06-25 新增(P2-5):配置驱动的 Widget 布局。导出 `ExamWidgetConfig`/`ExamWidgetLayoutConfig` 类型、四个角色的默认配置(TEACHER_EXAM_WIDGET_CONFIG/ADMIN_EXAM_WIDGET_CONFIG/STUDENT_EXAM_WIDGET_CONFIG/PARENT_EXAM_WIDGET_CONFIG)、`getExamWidgetConfig(role)` 与 `getWidgetsBySlot(config, slot)` 工具函数。与 dashboard/config/widget-configs.ts 模式对齐 | + +**已知问题**: +- ✅ P0-1 已修复:~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`,通过 ID 映射保持 structure 引用一致 +- ✅ P0-2 已修复:~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds` +- ✅ P1-1 已修复:~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions` +- ✅ P1-2 已修复:~~`actions.ts` 832 行(超 800 建议),多处直接 DB 操作~~ DB 操作已下沉到 data-access,actions.ts 现 691 行 +- ✅ P0-4 已修复(2026-06-24):~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData +- ✅ actions.ts 拆分(2026-06-24):~~`actions.ts` 1525 行(超 1000 硬上限)~~ 拆分为 4 个文件:actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action),actions.ts re-export 保持向后兼容 +- ✅ P0-8 已修复:~~`data-access.ts` 1036 行(超 1000 硬上限)~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口),文末 re-export 保持向后兼容 +- ⚠️ P1:`ai-pipeline.ts` 857 行(超 800 建议),混合 4 类职责 +- ✅ P2 已修复:`ai-pipeline.ts` 中 3 处非空断言清理(`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`) +- ✅ V3-5/V3-8/V3-12:`exam-actions.tsx` 增强:V3-5 角色化菜单(按角色显示不同操作项)、V3-8 新增 analytics 菜单项(BarChart3 图标,跳转 `/teacher/exams/[id]/analytics`)、V3-12 移动端触摸目标尺寸优化 +- ✅ 2026-06-25 P2 落地(exams 审计报告实施): + - ✅ P2-1:在 `editor/exam-rich-editor-types.ts` 新增 `isRichQuestionType`/`isStandaloneQuestionType`/`toRichQuestionType`/`toStandaloneQuestionType` 类型守卫;`editor/editor-to-structure.ts` 与 `editor/exam-nodes-to-editor-doc.ts` 改用 `toRichQuestionType`;`actions-rich-editor.ts` 两处 `as "single_choice"|"multiple_choice"|"text"|"judgment"` 断言改为 `toStandaloneQuestionType` 安全收窄(消除 4 处 `as` 违规) + - ✅ P2-3:新增 `components/exam-boundaries.tsx`(ExamErrorBoundary/ExamEmptyState/ExamSkeleton 三组合单元,5 种骨架变体) + - ✅ P2-4:新增 `services/exam-service-port.ts` + `services/exam-service-context.tsx`(ExamServicePort 接口 + Context 注入骨架,与 shared 层 ExamHomeworkServicePort 区分定位) + - ✅ P2-5:新增 `config/exam-widgets.ts`(四角色 Widget 配置 + getExamWidgetConfig/getWidgetsBySlot) --- @@ -604,17 +689,19 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **职责**:作业全生命周期(创建/发布/作答/批改/分析)。 **导出函数**: -- Actions:`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` / `batchAutoGradeSubmissionsAction`(V3-7 新增:批量自动批改,HOMEWORK_GRADE 权限+非管理员仅可批改自己创建的作业)/ `getScansAction`(✅ 2026-06-24 新增:获取某次提交的所有答题扫描图,扫描图存储在 fileAttachments 表中 targetType="homework"/targetId=submissionId;支持两类访问者——教师 HOMEWORK_GRADE 仅可访问自己创建的作业的提交,学生 HOMEWORK_SUBMIT 仅可访问自己的提交)/ `deleteScanAction`(✅ 2026-06-24 新增:删除答题扫描图,仅允许提交者本人删除且仅在提交状态为 started 时允许,调用 `files/data-access.deleteFileAttachment` 删除 DB 记录)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access/data-access-write) -- Data-access:`getHomeworkAssignments` / `getHomeworkAssignmentById` / `getHomeworkSubmissions` / `getStudentHomeworkAssignments` / `getStudentHomeworkTakeData` / `getHomeworkAssignmentReviewList` / `getHomeworkSubmissionDetails` / `getDemoStudentUser`(已迁移至 users 模块 `getCurrentStudentUser`,此处为 re-export 向后兼容)/ `isRecord` / `toQuestionContent` / `getAssignmentMaxScoreById`(后三者供 stats-service 使用)/ `getHomeworkAssignmentsByExamId`(V3-8 新增:按考试 ID 查作业+目标/提交/批改计数)/ `getGradedSubmissionsByExamId`(V3-8 新增:按考试 ID 查已批改提交,按学生去重)/ `getStudentSubmissionResult`(V3-9 新增:查学生指定作业最新提交,用于结果页)/ `getStudentExamResults`(V3-11 新增:查学生考试结果列表,供家长端展示) +- Actions:`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` / `batchAutoGradeSubmissionsAction`(V3-7 新增:批量自动批改,HOMEWORK_GRADE 权限+非管理员仅可批改自己创建的作业)/ `getScansAction`(✅ 2026-06-24 新增:获取某次提交的所有答题扫描图,扫描图存储在 fileAttachments 表中 targetType="homework"/targetId=submissionId;支持两类访问者——教师 HOMEWORK_GRADE 仅可访问自己创建的作业的提交,学生 HOMEWORK_SUBMIT 仅可访问自己的提交)/ `deleteScanAction`(✅ 2026-06-24 新增:删除答题扫描图,仅允许提交者本人删除且仅在提交状态为 started 时允许,调用 `files/data-access.deleteFileAttachment` 删除 DB 记录)/ `getExcellentSubmissionsAction`(✅ P3-1 新增:查询优秀作业列表,HOMEWORK_GRADE 权限,按 ctx.dataScope 过滤,调用前埋点 `homework.excellent_viewed`)/ `remindUnsubmittedAction`(✅ P3-2 新增:催交提醒,HOMEWORK_GRADE 权限,查询未提交学生后通过动态 import 调用 `notifications/data-access.createNotification` 创建 type=homework/priority=high 站内通知,Promise.allSettled 容错统计,埋点 `homework.remind_unsubmitted`)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access/data-access-write) +- Data-access:`getHomeworkAssignments` / `getHomeworkAssignmentById` / `getHomeworkSubmissions` / `getStudentHomeworkAssignments` / `getStudentHomeworkTakeData` / `getHomeworkAssignmentReviewList` / `getHomeworkSubmissionDetails` / `getDemoStudentUser`(已迁移至 users 模块 `getCurrentStudentUser`,此处为 re-export 向后兼容)/ `toQuestionContent` / `getAssignmentMaxScoreById`(后两者供 stats-service 使用;`isRecord` 已于 2026-06-24 迁移至 `@/shared/lib/type-guards` 统一复用)/ `getHomeworkAssignmentsByExamId`(V3-8 新增:按考试 ID 查作业+目标/提交/批改计数)/ `getGradedSubmissionsByExamId`(V3-8 新增:按考试 ID 查已批改提交,按学生去重)/ `getStudentSubmissionResult`(V3-9 新增:查学生指定作业最新提交,用于结果页)/ `getStudentExamResults`(V3-11 新增:查学生考试结果列表,供家长端展示)/ `getExcellentSubmissions`(✅ P3-1 新增:查优秀作业列表,按得分率过滤 minPercentage 默认 80,同一学生取最高分,按得分率降序,支持 scope 过滤)/ `getUnsubmittedStudents`(✅ P3-2 新增:查未提交作业的学生列表,对比 targets 与已提交学生集合返回差集,供催交提醒使用) - Data-access-classes:`getAssignmentIdsForStudents` / `getHomeworkAssignmentsWithSubject` / `getHomeworkAssignmentsByIds` / `getAssignmentTargetCounts` / `getHomeworkSubmissionsForStudents` / `getPublishedHomeworkAssignmentsWithSubject` / `getHomeworkSubmissionsForAssignments`(P0-7 新增,供 classes 模块跨模块调用;✅ P1-1 已修复:内部不再直查 exams/subjects 表,改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`) - Data-access-write:11 个写操作函数(P1-2 新增 10 个从 actions 下沉 + V3-7 新增 `batchAutoGradeSubmissions`) - Stats-service:`getTeacherGradeTrends` / `getHomeworkAssignmentAnalytics` / `getStudentDashboardGrades`(从 data-access.ts re-export 以保持向后兼容) - Components(V3-7/V3-9 新增):`HomeworkBatchGradingView`(批量批改视图:勾选+一键批改+toast 反馈)/ `HomeworkSubmissionResult`(提交后即时反馈:分数汇总+对错分布+错题预览) - Components(✅ 2026-06-24 新增扫描图相关):`ScanUploader`(学生扫描图上传组件,调用 /api/upload 上传图片,targetType="homework",支持增删/排序,导出 `ScanImage` 类型)/ `ScanImageViewer`(扫描图查看器,支持翻页/缩放/旋转/全屏,用于阅卷式批改时查看学生答题图片)/ `HomeworkScanGradingView`(教师阅卷式批改视图,使用 `ResizablePanel` 左侧题目+批改表单、右侧扫描图查看器,集成 `getScansAction` 拉取扫描图、`gradeHomeworkSubmissionAction` 批改,支持上一份/下一份提交快速切换) +- Components(✅ P3-1 新增):`ExcellentSubmissions`(优秀作业展示 async 服务端组件,按得分率降序,前三名金/银/铜徽章,迟交 Clock 标注,仅展示姓名不展示学号保护隐私,直接调用 `getExcellentSubmissions` 避免多余 action 往返)/ `ExcellentSubmissionsWithBoundary`(带 `SectionErrorBoundary` + `Suspense` 包裹版本,提供 Skeleton 骨架屏与局部错误降级,避免本区块故障影响整个作业详情页) - Types(✅ 2026-06-24 新增):`ScanAttachment`(扫描图附件结构 { fileId, url, filename, originalName, page },由 `getScansAction` 返回)/ `ScanImage`(扫描图 UI 结构,由 `ScanUploader` 导出供 `ScanImageViewer` 复用) +- Types(✅ P3-1 新增):`ExcellentSubmissionItem`(优秀作业展示项 { submissionId, assignmentId, assignmentTitle, studentName, totalScore, maxScore, percentage, submittedAt, isLate },percentage 为得分率 0-100,isLate 标注迟交但质量优秀样例)/ `ExcellentSubmissionQuery`(优秀作业查询参数 { assignmentId, minPercentage?, limit? },minPercentage 默认 80,limit 默认 10) **依赖关系**: -- 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamForProctoringCrossModule)、`classes`(✅ P1-1 已修复:通过 classes data-access.getStudentIdsByClassId 等 7 个函数)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getSubjectNameMapByIds)、`users`(✅ P1-1 已修复:通过 users data-access.getUserWithRole/getUserNamesByIds)、`files`(✅ 2026-06-24 新增:`deleteScanAction` 调用 `files/data-access.deleteFileAttachment` 删除扫描图 DB 记录,合理跨模块调用) +- 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamForProctoringCrossModule)、`classes`(✅ P1-1 已修复:通过 classes data-access.getStudentIdsByClassId 等 7 个函数)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getSubjectNameMapByIds)、`users`(✅ P1-1 已修复:通过 users data-access.getUserWithRole/getUserNamesByIds)、`files`(✅ 2026-06-24 新增:`deleteScanAction` 调用 `files/data-access.deleteFileAttachment` 删除扫描图 DB 记录,合理跨模块调用)、`notifications`(✅ P3-2 新增:`remindUnsubmittedAction` 通过动态 import 调用 `notifications/data-access.createNotification` 为未提交学生创建催交通知,合理跨模块调用) - 被依赖:`dashboard`(通过 data-access,合理)、`parent`(通过 data-access,合理;V3-11 新增 `getStudentExamResults` 供 parent 调用)、`classes`(✅ P0-7 已修复:classes 通过 `homework/data-access-classes` 获取作业数据,不再反向直查 homework/exams 表)、`exams`(V3-8 新增:`exams/stats-service.getExamAnalytics` 调用 `getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId`,合理跨模块调用) **已知问题**: @@ -630,51 +717,108 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ V3-9:新增 `getStudentSubmissionResult` + `HomeworkSubmissionResult` + 路由 `/student/learning/assignments/[assignmentId]/result`,`homework-take-view.tsx` 提交后跳转结果页 - ✅ V3-12:`homework-take-view.tsx` 移动端触摸目标尺寸优化 - ✅ 2026-06-23 审计修复:data-access 层 4 个查询函数新增 `scope?: DataScope` 参数(`getHomeworkAssignments`/`getHomeworkAssignmentById`/`getHomeworkSubmissions`/`getHomeworkAssignmentReviewList`),教师仅查看自己创建的作业,学生/家长仅查看相关作业;Server Action catch 块改用 `handleActionError` 统一错误处理 +- ✅ 2026-06-24 i18n 完成:11 个文件(6 个客户端组件 + 3 个服务端页面 + 2 个已有 `t` 的页面)全部接入 `next-intl`,硬编码中文/英文文案替换为 `examHomework.homework.*` 翻译键(zh-CN + en 双语);客户端组件使用 `useTranslations("examHomework")`,服务端页面使用 `getTranslations("examHomework")` +- ✅ 2026-06-25 审计修复 P0:13 个缺失的 `loading.tsx`/`error.tsx` 路由文件全部补全(teacher/homework 根 + assignments + submissions + submissions/[submissionId] + scan-grading;student/learning/assignments + result);`homework-take-view.tsx` 拆分为 5 个组件(`HomeworkTakeView` 主容器 396 行 + `HomeworkTakeSidebar` 侧边栏 + `HomeworkTakeConfirmDialog` 提交确认对话框);`schema.ts` 错误消息改为 i18n 键 `homework.form.error.*`,Server Action 通过 `translateZodErrors` 翻译后返回 `fieldErrors`;`ScanAttachment` 类型从 `actions.ts` 迁移至 `types.ts` +- ✅ 2026-06-25 审计修复 P1:`homework-grading-view.tsx` 删除本地 wrapper 函数(`isAutoGradable`/`applyAutoGrades`/`getCorrectnessState`/`extractQuestionText`),直接调用 `lib/question-content-utils`;`data-access.ts` 末尾 re-export(`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId`/`getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData`)已删除,调用方改为直接 import 子模块;`teacher/homework/assignments/[id]/page.tsx` Analytics 区块接入 `SectionErrorBoundary + Suspense + Skeleton`;`getScansAction` 中的 DB 查询下沉到新建的 `data-access-scans.ts::getScansBySubmissionId` +- ✅ 2026-06-25 审计修复 P2:`startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `deleteScanAction` 补全 `trackExamEvent` 埋点(`homework.started` / `homework.answer_saved` / `homework.scan_deleted`);`extractQuestionText` 从 `homework-grading-view.tsx` 迁移到 `lib/question-content-utils.ts` **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `data-access.ts` | 598+ | 作业 CRUD + 学生视角 + 批改(含 re-export stats 函数;V3-8/V3-9/V3-11 新增 4 个查询函数) | -| `data-access-write.ts` | 285+ | 作业写操作(P1-2 新增 10 个写函数从 actions 下沉;V3-7 新增 `batchAutoGradeSubmissions`) | -| `data-access-classes.ts` | 240+ | 跨模块查询封装(P0-7 新增;✅ P1-1 已修复:内部通过 exams/school data-access 获取考试科目信息,不再直查 exams/subjects 表) | -| `stats-service.ts` | 425 | 统计分析(教师趋势/作业分析/学生仪表盘成绩) | -| `actions.ts` | 239+ | 8 个 Server Action(P1-2 已修复,无直接 DB 操作;V3-7 新增 `batchAutoGradeSubmissionsAction`;2026-06-24 新增 `getScansAction`/`deleteScanAction`;新增 `ScanAttachment` 类型导出) | -| `types.ts` | 186 | 类型定义 | -| `schema.ts` | 29 | Zod 校验 | -| `components/homework-batch-grading-view.tsx` | - | V3-7 新增:批量批改视图(use client) | -| `components/homework-submission-result.tsx` | - | V3-9 新增:提交后即时反馈页 | -| `components/homework-take-view.tsx` | - | V3-9/V3-12 增强:提交后跳转结果页+移动端触摸优化;✅ P0-竞品:集成限时/监考模式倒计时(useExamCountdown)+ 到时自动提交 | -| `components/scan-uploader.tsx` | - | 2026-06-24 新增:学生扫描图上传组件(调用 /api/upload,targetType="homework",导出 `ScanImage` 类型) | -| `components/scan-image-viewer.tsx` | - | 2026-06-24 新增:扫描图查看器(翻页/缩放/旋转/全屏) | -| `components/homework-scan-grading-view.tsx` | - | 2026-06-24 新增:教师阅卷式批改视图(ResizablePanel 左题目右扫描图 + 上一份/下一份切换) | -| `hooks/use-exam-countdown.ts` | 122 | P0-竞品新增:考试倒计时 Hook(每秒更新、紧急状态高亮、到时回调自动提交) | +| `data-access.ts` | 716 | 作业 CRUD + 批改(✅ 2026-06-25:删除末尾 re-export,调用方改为直接 import 子模块;✅ P3-1 新增 `getExcellentSubmissions`:按得分率过滤优秀作业,同一学生取最高分;✅ P3-2 新增 `getUnsubmittedStudents`:查询未提交作业学生供催交提醒使用) | +| `data-access-write.ts` | 455 | 作业写操作(P1-2 新增 10 个写函数从 actions 下沉;V3-7 新增 `batchAutoGradeSubmissions`) | +| `data-access-classes.ts` | 285 | 跨模块查询封装(P0-7 新增;✅ P1-1 已修复:内部通过 exams/school data-access 获取考试科目信息,不再直查 exams/subjects 表) | +| `data-access-student.ts` | 293 | 学生端只读查询(✅ 2026-06-25:从 data-access.ts 拆出 `getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData`) | +| `data-access-exam-cross.ts` | 119 | 考试跨模块查询(`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId`) | +| `data-access-scans.ts` | 44 | ✅ 2026-06-25 审计新增:扫描图查询(`getScansBySubmissionId`,从 actions.ts 下沉的 DB 查询) | +| `data-access-error-collection.ts` | 121 | 错题采集查询 | +| `data-access-utils.ts` | 8 | 内部工具函数 | +| `stats-service.ts` | 414 | 统计分析(教师趋势/作业分析/学生仪表盘成绩) | +| `actions.ts` | 688 | 10 个 Server Action(P1-2 已修复,无直接 DB 操作;✅ 2026-06-25:`ScanAttachment` 类型迁移至 types.ts;schema.ts 错误消息改为 i18n 键,新增 `translateZodErrors` 翻译辅助函数;补全 `trackExamEvent` 埋点;✅ P3-1 新增 `getExcellentSubmissionsAction`:查询优秀作业列表,HOMEWORK_GRADE 权限 + scope 过滤 + 埋点 `homework.excellent_viewed`;✅ P3-2 新增 `remindUnsubmittedAction`:催交提醒,调用 `notifications/data-access.createNotification` 创建站内通知,埋点 `homework.remind_unsubmitted`) | +| `types.ts` | 282 | 类型定义(✅ 2026-06-25:新增 `ScanAttachment` 接口,从 actions.ts 迁入;✅ P3-1 新增 `ExcellentSubmissionItem`/`ExcellentSubmissionQuery` 类型) | +| `schema.ts` | 63 | Zod 校验(✅ 2026-06-25:错误消息改为 i18n 键 `homework.form.error.*`) | +| `lib/question-content-utils.ts` | 321 | 题目内容工具函数(✅ 2026-06-25:`extractQuestionText` 从 grading-view 迁入) | +| `lib/question-content-utils.test.ts` | 415 | 工具函数单测 | +| `hooks/use-exam-countdown.ts` | 106 | P0-竞品新增:考试倒计时 Hook(每秒更新、紧急状态高亮、到时回调自动提交) | +| `hooks/use-debounced-auto-save.ts` | 182 | 答案防抖自动保存 Hook(localStorage 离线缓存) | +| `components/homework-take-view.tsx` | 396 | ✅ 2026-06-25 重构:从 520 行拆分为 3 个组件(主容器 + Sidebar + ConfirmDialog);V3-9/V3-12:提交后跳转结果页+移动端触摸优化;P0-竞品:集成限时/监考模式倒计时(useExamCountdown)+ 到时自动提交 | +| `components/homework-take-sidebar.tsx` | 169 | ✅ 2026-06-25 新增:从 homework-take-view 拆出(自动保存状态/作业信息/截止时间/尝试次数/描述/进度网格/提交按钮) | +| `components/homework-take-confirm-dialog.tsx` | 62 | ✅ 2026-06-25 新增:从 homework-take-view 拆出(提交确认对话框,含未作答数量警告) | +| `components/homework-grading-view.tsx` | 472 | ✅ 2026-06-25:删除 4 个本地 wrapper 函数,直接调用 lib/question-content-utils | +| `components/homework-batch-grading-view.tsx` | 165 | V3-7 新增:批量批改视图(use client) | +| `components/homework-submission-result.tsx` | 224 | V3-9 新增:提交后即时反馈页 | +| `components/homework-scan-grading-view.tsx` | 262 | 2026-06-24 新增:教师阅卷式批改视图(ResizablePanel 左题目右扫描图 + 上一份/下一份切换) | +| `components/scan-uploader.tsx` | 254 | 2026-06-24 新增:学生扫描图上传组件(调用 /api/upload,targetType="homework",导出 `ScanImage` 类型) | +| `components/scan-image-viewer.tsx` | 194 | 2026-06-24 新增:扫描图查看器(翻页/缩放/旋转/全屏) | +| `components/question-renderer.tsx` | 339 | 题目渲染器(支持单选/多选/判断/文本题型) | +| `components/excellent-submissions.tsx` | 206 | P3-1:优秀作业展示(服务端组件 + SectionErrorBoundary + Suspense + 骨架屏;✅ P3-5 修复:`getTranslations` 从 `next-intl/server` 导入) | +| `components/student-homework-review-view.tsx` | 206 | 学生作业回顾视图 | +| `components/homework-assignment-form.tsx` | 219 | 作业创建/编辑表单 | +| `components/assignment-filters.tsx` | 46 | 作业列表筛选器(use client):基于 nuqs 的 URL 状态管理(✅ 2026-06-24 i18n:接入 `useTranslations`) | +| `components/homework-assignment-exam-content-card.tsx` | 29 | 作业考试内容卡片(✅ 2026-06-24 i18n:接入 `useTranslations`) | +| `components/homework-assignment-exam-preview-pane.tsx` | 39 | 作业考试预览面板(✅ 2026-06-24 i18n:接入 `useTranslations`) | +| `components/homework-assignment-exam-error-explorer.tsx` | 39 | 作业错误分析主组件 | +| `components/homework-assignment-exam-error-explorer-lazy.tsx` | 76 | 作业错误分析懒加载组件(✅ 2026-06-24 i18n:Fallback 接入 `useTranslations`) | +| `components/homework-assignment-question-error-detail-panel.tsx` | 132 | 题目错误详情面板(✅ 2026-06-24 i18n:接入 `useTranslations`,`formatAnswer` 移入组件内闭包以访问 `t`) | +| `components/homework-assignment-question-error-overview-card.tsx` | 89 | 题目错误概览卡片(✅ 2026-06-24 i18n:接入 `useTranslations`) | --- ## 2.4 questions(题库模块) -**职责**:题库管理(题目 CRUD、知识点关联、题型支持)。 +**职责**:题库管理(题目 CRUD、知识点关联、题型支持、教材→章节→知识点级联筛选)。 **导出函数**: -- Actions:`getQuestionsAction` / `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction`(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access) -- Data-access:`getQuestions` / `createQuestionWithRelations` / `updateQuestionById` / `deleteQuestionByIdRecursive` / `getKnowledgePointOptions`(后 4 个为 P1-2 新增/迁移) +- Actions(11 个,全部调用 `requirePermission()`):`getQuestionsAction` / `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction` / `getTextbookOptionsAction`(✅ 审计新增:级联第一级)/ `getChapterOptionsAction(textbookId)`(✅ 审计新增:级联第二级)/ `getKnowledgePointOptionsByChapterAction(chapterId)`(✅ 审计新增:级联第三级)/ `deleteQuestionsBatchAction`(✅ 审计新增:批量删除)/ `exportQuestionsAction`(✅ 审计新增:导出)/ `importQuestionsAction`(✅ 审计新增:导入) +- Data-access:`getQuestions`(✅ 审计升级:支持 `textbookId`/`chapterId` 级联筛选参数,通过 textbooks data-access 获取知识点 ID 集合)/ `createQuestionWithRelations` / `updateQuestionById` / `deleteQuestionByIdRecursive` / `getKnowledgePointOptions` / `getTextbookOptions`(✅ 审计新增)/ `getChapterOptions(textbookId)`(✅ 审计新增)/ `getKnowledgePointOptionsByChapter(chapterId)`(✅ 审计新增)/ `deleteQuestionsBatch`(✅ 审计新增:批量删除,事务原子性)/ `exportQuestions`(✅ 审计新增:导出,权限范围过滤)/ `importQuestions`(✅ 审计新增:批量导入,事务原子性) +- Types:`Question` / `QuestionWithRelations` / `GetQuestionsParams`(✅ 审计升级:新增 `textbookId`/`chapterId` 字段)/ `CreateQuestionInput` / `KnowledgePointOption` / `TextbookOption`(✅ 审计新增)/ `ChapterOption`(✅ 审计新增)/ `QuestionContent`(✅ 审计新增:类型守卫接口)/ `QuestionOption`(✅ 审计新增)/ `QUESTION_TYPE_I18N_KEY`(✅ 审计新增:题型 i18n 键映射)/ `DIFFICULTY_I18N_KEY`(✅ 审计新增:难度 i18n 键映射) +- Utils:`parseQuestionContent(raw)`(✅ 审计新增:类型守卫,替代 `as` 断言)/ `getQuestionPreview` / `getQuestionOptions` / `getQuestionText` / `trackQuestionEvent`(✅ 审计新增:埋点接口 no-op)/ `trackQuestionCreated` / `trackQuestionUpdated` / `trackQuestionDeleted` / `trackQuestionSearched` +- Components:`QuestionFilters`(✅ 审计重写:组合 QuestionCascadeFilter)/ `QuestionCascadeFilter`(✅ 审计新增:三级级联筛选,nuqs 管理 URL 状态)/ `QuestionDataTable`(✅ 审计重写:i18n + 集成 BatchOperations)/ `useQuestionColumns`(✅ 审计改为 Hook:因 useTranslations 是 Hook)/ `CreateQuestionButton`(✅ 审计重写:权限感知)/ `QuestionActions`(✅ 审计重写:权限感知 + 类型守卫 + QuestionContentRenderer 结构化预览)/ `CreateQuestionDialog`(✅ 审计重写:拆分为 281 行)/ `KnowledgePointSelector`(✅ 审计新增:从 dialog 拆分)/ `OptionsEditor`(✅ 审计新增:从 dialog 拆分)/ `QuestionBankResultsClient`(✅ 审计新增:RSC 与客户端 Hook 桥接)/ `QuestionContentRenderer`(✅ 审计新增:题目结构化渲染)/ `BatchOperations`(✅ 审计新增:批量删除工具栏)/ `ImportExportButtons`(✅ 审计新增:JSON 导入导出) **依赖关系**: -- 依赖:`shared/*`、`@/auth`、`textbooks`(✅ P1-1 已修复:通过 textbooks data-access.getKnowledgePointOptions) -- 被依赖:`exams`(通过类型导入,合理)、`textbooks`(UI 组合,合理) +- 依赖:`shared/*`、`@/auth`、`textbooks`(✅ 审计升级:通过 textbooks data-access 的 `getTextbooks` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` 实现级联筛选) +- 被依赖:`exams`(通过类型导入,合理)、`textbooks`(UI 组合,合理)、`error-book`(通过 data-access,合理)、`adaptive-practice`(通过 data-access,合理) **已知问题**: -- ✅ P1-2 已修复:~~写操作函数错放在 `actions.ts`(`insertQuestionWithRelations` / `deleteQuestionRecursive`)~~ 已下沉到 data-access(`createQuestionWithRelations` / `updateQuestionById` / `deleteQuestionByIdRecursive` / `getKnowledgePointOptions`) -- ✅ P1-1 已修复:~~`getKnowledgePointOptionsAction` 直查 textbooks 模块表~~ 改为调用 `textbooks/data-access.getKnowledgePointOptions` -- ✅ P2 已解决:~~`data-access.ts` 仅 129 行,写操作缺失~~ P1-2 后 data-access.ts 扩充至 260 行 +- ✅ P1-2 已修复:~~写操作函数错放在 `actions.ts`~~ 已下沉到 data-access +- ✅ P1-1 已修复:~~`getKnowledgePointOptionsAction` 直查 textbooks 模块表~~ 改为调用 textbooks data-access +- ✅ P2 已解决:~~`data-access.ts` 仅 129 行~~ 扩充至 260+ 行 +- ✅ 审计 P0 已修复:~~按名字搜索等于无用~~ 新增教材→章节→知识点三级级联筛选(QuestionCascadeFilter + URL 状态同步) +- ✅ 审计 P0 已修复:~~前端权限硬编码~~ 全部改用 `usePermission().hasPermission()` +- ✅ 审计 P0 已修复:~~`as` 断言违反 TypeScript 严格模式~~ 提取 `parseQuestionContent` 类型守卫函数 +- ✅ 审计 P0 已修复:~~i18n 完全缺失~~ 新增 `questions.json` 翻译文件(zh-CN + en),所有展示文本使用 i18n 键 +- ✅ 审计 P0 已修复:~~缺 Error Boundary~~ 新增 `app/(dashboard)/teacher/questions/error.tsx` +- ✅ 审计 P1 已修复:~~create-question-dialog.tsx 453 行超规范~~ 拆分为 KnowledgePointSelector + OptionsEditor + 主对话框(281 行) +- ✅ 审计 P1 已修复:~~列定义为纯函数无法使用 useTranslations~~ 改为 `useQuestionColumns` Hook,通过 `QuestionBankResultsClient` 客户端桥接 +- ✅ 审计 P1 已修复:~~缺监控埋点~~ 预留 `trackQuestionEvent` no-op 接口 +- ✅ 审计 P2 已修复:~~Server Action 缺权限校验~~ 全部 8 个 Action 均调用 `requirePermission()` +- ✅ 审计 P2 已实施:~~仅支持教师角色~~ 新增 admin/questions 页面,复用全部组件,权限感知自动适配 +- ✅ 审计 P2 已实施:~~无题目结构化预览~~ 新增 QuestionContentRenderer 组件(题干/选项/答案/解析,单选圆点/多选方框,正确答案高亮) +- ✅ 审计 P2 已实施:~~无批量操作~~ 新增 deleteQuestionsBatch data-access + deleteQuestionsBatchAction + BatchOperations 组件(事务原子性,权限感知) +- ✅ 审计 P2 已实施:~~无导入导出~~ 新增 exportQuestions/importQuestions data-access + exportQuestionsAction/importQuestionsAction + ImportExportButtons 组件(JSON 格式,权限范围过滤,导入预览确认) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `actions.ts` | 149 | 5 个 Server Action(P1-2 已修复,无直接 DB 操作) | -| `data-access.ts` | 260 | 题目 CRUD + 知识点选项(含 P1-2 新增 4 个写/查询函数) | +| `actions.ts` | 380+ | 11 个 Server Action(全部 requirePermission,含 3 个级联筛选 + 批量删除 + 导入导出) | +| `data-access.ts` | 600+ | 题目 CRUD + 知识点选项 + 级联筛选 + 批量删除 + 导入导出 | | `schema.ts` | 18 | Zod 校验 | -| `types.ts` | 34 | 类型定义 | +| `types.ts` | 60+ | 类型定义(含 TextbookOption/ChapterOption/QuestionContent/i18n 键映射) | +| `utils/parse-content.ts` | 60+ | 类型守卫(parseQuestionContent + 便捷方法) | +| `utils/track-event.ts` | 30+ | 埋点接口(no-op 实现) | +| `components/question-cascade-filter.tsx` | 130+ | 三级级联筛选(nuqs URL 状态) | +| `components/question-filters.tsx` | 40+ | 筛选栏(组合 QuestionCascadeFilter) | +| `components/question-data-table.tsx` | 155+ | 数据表格(i18n + 集成 BatchOperations) | +| `components/question-columns.tsx` | 100+ | 列定义 Hook(useQuestionColumns) | +| `components/create-question-button.tsx` | 30+ | 创建按钮(权限感知) | +| `components/question-actions.tsx` | 200+ | 行操作(权限感知 + 类型守卫 + QuestionContentRenderer) | +| `components/create-question-dialog.tsx` | 281 | 创建对话框(组合 KnowledgePointSelector + OptionsEditor) | +| `components/knowledge-point-selector.tsx` | 80+ | 知识点选择器(从 dialog 拆分) | +| `components/options-editor.tsx` | 120+ | 选项编辑器(从 dialog 拆分) | +| `components/question-bank-results-client.tsx` | 30+ | RSC 与客户端 Hook 桥接 | +| `components/question-content-renderer.tsx` | 110+ | 题目结构化渲染(✅ 审计新增) | +| `components/batch-operations.tsx` | 110+ | 批量操作工具栏(✅ 审计新增) | +| `components/import-export-buttons.tsx` | 180+ | 导入导出组件(✅ 审计新增) | --- @@ -689,7 +833,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - Constants(✅ 新增):`SUBJECTS` / `GRADES` / `SUBJECT_COLORS` / `getSubjectColor` / `getSubjectLabelKey` / `getGradeLabelKey` - ✅ v1 测试修复:`SUBJECTS` 新增 `Chinese` 学科、`GRADES` 新增 `Grade 1`/`Grade 2`,与 seed 数据对齐 - ✅ v1 测试修复:`SUBJECT_COLORS` 新增 `Chinese` 主题色(rose) -- Utils(✅ 新增,纯函数 + 单测):`sortChapters` / `buildChapterTree` / `buildChapterIndex` / `findChapterParent` / `filterKnowledgePointsByChapter` / `normalizeOptional` / `highlightKnowledgePoints` / `hasCycleAfterAddingEdge`(Task 4 新增:循环依赖检测,DFS 算法) +- Utils(✅ 新增,纯函数 + 单测):`sortChapters` / `buildChapterTree` / `buildChapterIndex` / `findChapterParent` / `filterKnowledgePointsByChapter` / `normalizeOptional` / `highlightKnowledgePoints` / `hasCycleAfterAddingEdge`(Task 4 新增:循环依赖检测,DFS 算法)/ `findChapterById`(V3 续审计新增:递归查找章节树中指定 ID 的章节,替代各模块本地重复实现) - Graph-layout(✅ 新增,纯函数 + 单测):`computeGraphLayout` - Components(✅ Task 13 新增):`GraphNodeDetailPanel` — 知识图谱节点详情侧边面板,展示知识点名称/描述/掌握度/关联题目/前置后置知识点,支持跳转与前置关系增删 - Analytics(✅ 新增):`TextbookAnalytics` / `TextbookAnalyticsProvider` / `useTextbookAnalytics` @@ -716,35 +860,67 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ v1 测试修复:新增 `teacher-textbook-reader.tsx` 客户端包装组件,解决 Server Component 向 Client Component 传递函数 prop(`renderQuestionCreator`)违反 Next.js 序列化约束的问题 - ✅ v1 测试修复:`scripts/seed.ts` 教材 subject/grade 改为英文 value(与 `constants.ts` 对齐),消除 i18n `MISSING_MESSAGE` 错误 - ✅ P1 纯逻辑已导出并补单测:新增 `utils.ts` / `graph-layout.ts` 及对应 `.test.ts` -- ⚠️ i18n 覆盖率约 95%(`chapter-sidebar-list` 已接入,`actions.ts` 已接入,`section-error-boundary` 默认值已改英文) -- ⚠️ 类型断言残留 3 处 `as string` +- ✅ v3 审计修复(2026-06-24):学生端两个页面添加 `requirePermission(Permissions.TEXTBOOK_READ)` 显式权限校验 +- ✅ v3 审计修复(2026-06-24):`getKnowledgeGraphDataAction` 添加学生年级 scope 过滤,防止跨年级越权查看图谱数据 +- ✅ v3 审计修复(2026-06-24):`getKnowledgeGraphDataAction` 的 class-mastery 模式限制为教师/管理员使用 +- ✅ v3 审计修复(2026-06-24):`deleteChapter` 级联删除 `knowledgePointPrerequisites` 孤儿记录,保证数据完整性 +- ✅ v3 审计修复(2026-06-24):`knowledge-graph.tsx` 前置依赖操作添加 try/catch/finally,防止 loading 状态卡死 +- ✅ v3 审计修复(2026-06-24):`textbook-reader.tsx` `onCreateKnowledgePoint` 添加 try/finally +- ✅ v3 审计修复(2026-06-24):`section-error-boundary.tsx` 添加 `componentDidCatch` 错误日志记录 + `role="alert"` a11y +- ✅ v3 审计修复(2026-06-24):4 个 error.tsx 抽取为共享 `RouteError` 组件,消除重复代码 +- ✅ v3 审计修复(2026-06-24):所有 loading.tsx 添加 `role="status"` / `aria-busy` a11y 标记 +- ✅ v3 审计修复(2026-06-24):`chapter-sidebar-list.tsx` 章节项添加 `role="button"` / `tabIndex` / `onKeyDown` 键盘支持 +- ✅ v3 审计修复(2026-06-24):`graph-node-detail-panel.tsx` 图标按钮添加 `aria-label` +- ✅ v3 审计修复(2026-06-24):`data-access.ts` `as [string, string]` 断言改为返回类型标注 +- ✅ v3 审计修复(2026-06-24):`knowledge-point-dialogs.tsx` `as` 断言改为 void 包装 +- ✅ v3 审计修复(2026-06-24):`graph-kp-node.tsx` 双重 `as unknown as` 断言改为类型守卫函数 `extractNodeData` +- ✅ v3 审计修复(2026-06-24):`graph-layout.ts` `GraphLayoutNodeData` 新增 `graphData` 字段类型定义 +- ✅ v3 审计修复(2026-06-24):删除死代码 `verifyKnowledgePointBelongsToChapter`、`ReorderChaptersSchema`、deprecated `canEdit` prop +- ✅ v3 审计修复(2026-06-24):`textbook-settings-dialog.tsx` 新增 `redirectAfterDelete` prop 消除硬编码 URL +- ✅ v3 审计修复(2026-06-24):`teacher/textbooks/[id]/loading.tsx` 骨架屏布局改为匹配阅读器布局 +- ✅ v3 P2 修复(2026-06-24):`data-access.ts` 3 处硬编码英文异常文案改为 `NotFoundError`(教材/章节),消息可安全返回客户端 +- ✅ v3 P2 修复(2026-06-24):`graph-layout.ts` O(n²) `some()` 查找改为 `Set` 查找,性能降为 O(n) +- ✅ v3 P2 修复(2026-06-24):抽取 `textbook-form-fields.tsx` 共享组件,消除 `textbook-form-dialog` 与 `textbook-settings-dialog` 表单字段重复 +- ✅ v3 P2 修复(2026-06-24):`textbook-content-panel.tsx` 17 个 props 按 `chapter`/`editing`/`selection`/`highlight` 四组重构,提升可读性 +- ✅ v3 P2 修复(2026-06-24):`textbook-reader.tsx` 移除命令式 `classList` 操作,视觉高亮改为 `isHighlighted` 状态驱动,仅保留 `scrollIntoView` +- ✅ v3 P2 修复(2026-06-24):`use-kp-crud.ts`(122 行)拆分为 `use-kp-create`/`use-kp-delete`/`use-kp-update` 三个子 Hook + 门面,均在 80 行限制内 +- ⚠️ i18n 覆盖率约 99%(v3 新增 `notFound`/`noClassMasteryPermission`/`close`/`prerequisiteAddFailed`/`prerequisiteRemoveFailed` 等键,data-access 异常文案已错误码化) - ⚠️ P2 图谱方向键导航未实现 +- ⚠️ P2 data-access 函数未导出非缓存版本,单测难以 mock **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `actions.ts` | 515 | 14 个 Server Action(写操作,含 Zod 校验 + 资源归属校验 + 知识图谱查询/前置依赖管理 + class-mastery 视图) | -| `data-access.ts` | 662 | 教材/章节/知识点 CRUD + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUD | -| `data-access-graph.ts` | 207 | 知识图谱只读查询(✅ Task 5 新增:知识点关联聚合、学生/班级掌握度、前置后置知识点,标记 `server-only`;v2 修复:前置依赖双向 IN 过滤防跨教材污染) | -| `types.ts` | 106 | 类型定义(含知识图谱类型:GraphViewMode/MasteryInfo/KpWithRelations/GraphNodeData/GraphEdgeData/KnowledgeGraphData/MasteryLevel) | -| `schema.ts` | 81 | Zod 校验(含 CreatePrerequisiteSchema/DeletePrerequisiteSchema;v2 修复:refine 消息改为英文) | -| `constants.ts` | 96 | 学科/年级常量与颜色映射(✅ 新增;v1 测试修复:新增 Chinese/Grade 1/Grade 2) | -| `utils.ts` | 225 | 章节树构建/排序/查找等纯函数 + 循环检测(✅ 新增,含单测) | -| `graph-layout.ts` | 121 | 知识图谱布局计算纯函数(✅ Task 8 重写为 dagre 集成,输出 React Flow 格式,含单测) | -| `analytics.tsx` | 43 | 教材分析 Context/Provider/Hook(✅ 新增) | -| `hooks/use-knowledge-point-actions.ts` | 49 | 知识点操作门面 Hook(v2 拆分:组合 useKpDialogState + useKpCrud,对外 API 不变) | -| `hooks/use-kp-dialog-state.ts` | 38 | 知识点对话框状态管理 Hook(v2 新增:从 use-knowledge-point-actions 拆分) | -| `hooks/use-kp-crud.ts` | 122 | 知识点 CRUD 操作 Hook(v2 新增:从 use-knowledge-point-actions 拆分) | -| `hooks/use-text-selection.ts` | 57 | 文本选区捕获 Hook | -| `hooks/use-graph-data.ts` | 76 | 知识图谱数据加载 Hook(✅ Task 11 新增:派生值模式避免 effect 中 setState;v2 修复:区分 isLoading/isRefreshing 避免 UI 闪烁) | -| `components/knowledge-graph.tsx` | 376 | React Flow 知识图谱主组件(✅ Task 10/15 重写:全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色;v2 新增:添加/删除前置依赖 Dialog) | -| `components/graph-kp-node.tsx` | 92 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条;v2 修复:节点宽度常量化 NODE_WIDTH) | -| `components/graph-prerequisite-edge.tsx` | 48 | React Flow 自定义边(✅ Task 9 新增:虚线+箭头表示前置依赖;v2 修复:GraphEdgeData 类型安全转换) | -| `components/graph-toolbar.tsx` | 93 | 图谱工具栏(✅ Task 9 新增:视图模式切换 + 搜索 + 重置视图;v2 新增:isRefreshing 轻量指示器) | -| `components/graph-node-detail-panel.tsx` | 181 | 节点详情侧边面板(✅ Task 13 新增:描述/掌握度/关联题目/前置后置列表;v2 修复:移除未使用 textbookId prop) | -| `components/chapter-sidebar-list.tsx` | 342 | 章节侧边栏列表(v2 修复:canEdit 默认 false + orderUpdateFailed i18n key) | -| `components/section-error-boundary.tsx` | 71 | 章节内容错误边界(v2 修复:默认值改为空字符串 + 条件渲染) | -| `components/*` | 16 文件 | 教材编辑/知识图谱组件(新增 `section-error-boundary.tsx`、5 个 graph-* 组件;`teacher-textbook-reader.tsx` 已移至 app 层) | +| `actions.ts` | 502 | 14 个 Server Action(写操作,含 Zod 校验 + 资源归属校验 + 知识图谱查询/前置依赖管理 + class-mastery 视图) | +| `data-access.ts` | 593 | 教材/章节/知识点 CRUD + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUD(v3 P2:硬编码异常改为 NotFoundError) | +| `data-access-graph.ts` | 188 | 知识图谱只读查询(✅ Task 5 新增:知识点关联聚合、学生/班级掌握度、前置后置知识点,标记 `server-only`;v2 修复:前置依赖双向 IN 过滤防跨教材污染) | +| `types.ts` | 94 | 类型定义(含知识图谱类型:GraphViewMode/MasteryInfo/KpWithRelations/GraphNodeData/GraphEdgeData/KnowledgeGraphData/MasteryLevel) | +| `schema.ts` | 56 | Zod 校验(含 CreatePrerequisiteSchema/DeletePrerequisiteSchema;v2 修复:refine 消息改为英文;v3 修复:移除死代码 ReorderChaptersSchema) | +| `constants.ts` | 91 | 学科/年级常量与颜色映射(✅ 新增;v1 测试修复:新增 Chinese/Grade 1/Grade 2) | +| `utils.ts` | 226 | 章节树构建/排序/查找等纯函数 + 循环检测(✅ 新增,含单测) | +| `graph-layout.ts` | 109 | 知识图谱布局计算纯函数(✅ Task 8 重写为 dagre 集成,输出 React Flow 格式,含单测;v3 P2:O(n²) 改为 Set 查找) | +| `analytics.tsx` | 37 | 教材分析 Context/Provider/Hook(✅ 新增) | +| `hooks/use-knowledge-point-actions.ts` | 45 | 知识点操作门面 Hook(v2 拆分:组合 useKpDialogState + useKpCrud,对外 API 不变) | +| `hooks/use-kp-dialog-state.ts` | 33 | 知识点对话框状态管理 Hook(v2 新增:从 use-knowledge-point-actions 拆分) | +| `hooks/use-kp-crud.ts` | 52 | 知识点 CRUD 门面 Hook(v3 P2 拆分:组合 useKpCreate/useKpDelete/useKpUpdate,对外 API 不变) | +| `hooks/use-kp-create.ts` | 43 | 知识点创建 Hook(v3 P2 新增:从 use-kp-crud 拆分) | +| `hooks/use-kp-delete.ts` | 49 | 知识点删除 Hook(v3 P2 新增:从 use-kp-crud 拆分) | +| `hooks/use-kp-update.ts` | 36 | 知识点更新 Hook(v3 P2 新增:从 use-kp-crud 拆分) | +| `hooks/use-text-selection.ts` | 49 | 文本选区捕获 Hook | +| `hooks/use-graph-data.ts` | 65 | 知识图谱数据加载 Hook(✅ Task 11 新增:派生值模式避免 effect 中 setState;v2 修复:区分 isLoading/isRefreshing 避免 UI 闪烁) | +| `components/knowledge-graph.tsx` | 356 | React Flow 知识图谱主组件(✅ Task 10/15 重写:全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色;v2 新增:添加/删除前置依赖 Dialog) | +| `components/graph-kp-node.tsx` | 92 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条;v2 修复:节点宽度常量化 NODE_WIDTH;v3 修复:类型守卫替代 as 断言) | +| `components/graph-prerequisite-edge.tsx` | 43 | React Flow 自定义边(✅ Task 9 新增:虚线+箭头表示前置依赖;v2 修复:GraphEdgeData 类型安全转换) | +| `components/graph-toolbar.tsx` | 84 | 图谱工具栏(✅ Task 9 新增:视图模式切换 + 搜索 + 重置视图;v2 新增:isRefreshing 轻量指示器) | +| `components/graph-node-detail-panel.tsx` | 171 | 节点详情侧边面板(✅ Task 13 新增:描述/掌握度/关联题目/前置后置列表;v2 修复:移除未使用 textbookId prop;v3 修复:aria-label) | +| `components/chapter-sidebar-list.tsx` | 325 | 章节侧边栏列表(v2 修复:canEdit 默认 false + orderUpdateFailed i18n key;v3 修复:键盘导航) | +| `components/section-error-boundary.tsx` | 53 | 章节内容错误边界(v2 修复:默认值改为空字符串 + 条件渲染;v3 修复:componentDidCatch + a11y) | +| `components/textbook-form-fields.tsx` | 103 | 教材表单字段共享组件(v3 P2 新增:消除 form-dialog 与 settings-dialog 表单重复) | +| `components/textbook-content-panel.tsx` | 199 | 章节内容面板(v3 P2:17 props 按 chapter/editing/selection/highlight 四组重构) | +| `components/textbook-form-dialog.tsx` | 68 | 教材创建对话框(v3 P2:复用 TextbookFormFields) | +| `components/textbook-settings-dialog.tsx` | 136 | 教材设置对话框(v3 P2:复用 TextbookFormFields) | +| `components/textbook-reader.tsx` | 412 | 教材阅读器主组件(v3 P2:移除命令式 classList,高亮改为状态驱动) | +| `components/*` | 17 文件 | 教材编辑/知识图谱组件(新增 `textbook-form-fields.tsx`、5 个 graph-* 组件、3 个 use-kp-* 子 Hook;`teacher-textbook-reader.tsx` 已移至 app 层) | --- @@ -753,8 +929,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **职责**:成绩分析(录入/查询/统计/导出/趋势对比分析)。 **导出函数**: -- Actions:`getGradeRecordsAction` / `createGradeRecordAction`(v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`)/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`(v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度1,按 gradeId 查询年级整体 + 按班级拆分的成绩分布,GRADE_RECORD_READ 权限)/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`(v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `batchCreateGradeRecordsByExamAction`(✅ 2026-06-24 新增:按试卷批量录入每题得分,流程 requirePermission → getExamForGradeEntry scope 校验 → assertClassInScope → safeJsonParse → BatchGradeEntryByExamSchema 校验 → batchCreateGradeRecordsByExam 单事务写入 → updateMasteryFromExamScore → notifyGradeEntered → revalidatePath,返回 gradeRecordId 列表供撤销)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions,分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限)。注:`assertClassInScope` 原位于 actions.ts(✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用),✅ v4-P2-6 修复:因 "use server" 文件要求所有 export 为 async,而 `assertClassInScope` 是同步函数,已迁移至独立文件 `lib/scope-check.ts`,actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入 -- Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats 纯函数,返回整体分布 + 按班级拆分)/ `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`)/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUD,upsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总)/ `batchCreateGradeRecordsByExam`(✅ 2026-06-24 新增:按试卷批量录入成绩,单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩,返回 gradeRecordId 列表供撤销) +- Actions:`getGradeRecordsAction` / `createGradeRecordAction`(v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`)/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`(v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度1,按 gradeId 查询年级整体 + 按班级拆分的成绩分布,GRADE_RECORD_READ 权限)/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`(v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `batchCreateGradeRecordsByExamAction`(✅ 2026-06-24 新增,✅ 2026-06-25 增强错题采集:按试卷批量录入每题得分,流程 requirePermission → getExamForGradeEntry scope 校验 → assertClassInScope → safeJsonParse → BatchGradeEntryByExamSchema 校验 → batchCreateGradeRecordsByExam 单事务写入(返回 {gradeRecordId, studentId, submissionId}[])→ Promise.allSettled 并行执行 updateMasteryFromExamScore + collectFromExamSubmission(考试错题采集,失败不阻断)→ notifyGradeEntered → revalidatePath,返回 gradeRecordId 列表供撤销)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions,分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限)。注:`assertClassInScope` 原位于 actions.ts(✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用),✅ v4-P2-6 修复:因 "use server" 文件要求所有 export 为 async,而 `assertClassInScope` 是同步函数,已迁移至独立文件 `lib/scope-check.ts`,actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入 +- Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats 纯函数,返回整体分布 + 按班级拆分)/ `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`)/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUD,upsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总)/ `batchCreateGradeRecordsByExam`(✅ 2026-06-24 新增,✅ 2026-06-25 更新返回类型:按试卷批量录入成绩,单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩,返回 `BatchGradeRecordResult[]`({ gradeRecordId, studentId, submissionId })供调用方触发错题采集等后置钩子) - Types(✅ v3-P2 新增,✅ v4-P2-7 新增年级分布类型,✅ 2026-06-24 新增按试卷录入类型):`SchoolWideGradeSummaryItem` / `SchoolWideGradeSummary` / `GradeDraftData`(草稿数据接口)/ `GradeDistributionByGradeResult`(✅ v4-P2-7 新增:年级维度成绩分布结果,含 overall 整体分布 + stats 统计 + byClass 按班级拆分数组)/ `GradeDistributionByGradeClassItem`(✅ v4-P2-7 新增:按班级拆分的分布项:classId/className/distribution/stats)/ `GradeRecordAnswer`(✅ 2026-06-24 新增:成绩记录-题目得分明细,对应 grade_record_answers 表)/ `BatchGradeEntryByExamQuestion`(✅ 2026-06-24 新增:按试卷录入时单个题目得分 { questionId, score })/ `BatchGradeEntryByExamItem`(✅ 2026-06-24 新增:按试卷录入时单个学生所有题目得分 { studentId, answers }) - Lib(✅ P1-2 新增,✅ P3 更新签名,✅ P3-26 拆分,✅ v4-P2-6 新增 scope-check):`toNumber` / `normalize`(位于 `lib/grade-utils.ts`);`buildScopeClassFilter(scope, currentUserId?)`(P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`,P3 修复:`class_members` scope 内置 studentId 过滤,需传入 currentUserId 参数);`assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 actions.ts 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因:actions.ts 是 "use server" 文件要求所有 export 为 async,而 assertClassInScope 是同步函数) - Stats-service(✅ P1-1 新增):`computeGradeStats` / `computeAverageScore` / `buildGradeTrendPoints` / `computeTrendAverage` / `computeClassComparisonStats` / `computeSubjectComparisonStats` / `computeGradeDistribution` / `buildRankingTrendPoints`(从 3 个 data-access 文件抽取的纯函数,使数据层专注 DB I/O,统计逻辑可独立测试) @@ -762,7 +938,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - Components(✅ P1-5 新增):`WidgetBoundary`(Error Boundary + Suspense + Skeleton 组合,含 a11y 属性)/ `SchoolWideSummaryCard`(✅ v3-P2 新增:管理员全校成绩汇总卡片,4 个统计卡片 + 各年级对比表格)/ `ScoreCell`(✅ v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%/黄60-84%/绿≥85%,使用语义化 Tailwind 类名避免动态拼接,fullScore<=0 时不着色) **依赖关系**: -- 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds)、`exams`(✅ 2026-06-24 新增:通过 exams data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用) +- 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds)、`exams`(✅ 2026-06-24 新增:通过 exams data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用)、`error-book`(✅ 2026-06-25 新增:通过 error-book data-access.collectFromExamSubmission 在成绩录入后自动采集考试错题)、`diagnostic`(通过 diagnostic data-access.updateMasteryFromExamScore 更新掌握度) - 被依赖:`parent`(通过 data-access,合理)、`dashboard` **已知问题**: @@ -813,23 +989,59 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ v4-P3-2 改进(2026-06-23):`batch-grade-entry.tsx` 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态避免重复显示 - ✅ v4-P2-6 修复(2026-06-23):~~`assertClassInScope` 是同步函数但位于 "use server" 文件 actions.ts 中~~ Next.js 要求 "use server" 文件中所有 export 必须为 async,同步 export 会导致构建错误。修复:将 `assertClassInScope` 迁移至独立文件 `lib/scope-check.ts`(含 `import "server-only"`),actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入 - ✅ 2026-06-24 重新设计:批量录入从"只录总分"改为"按试卷录入每题得分"。新增 `grade_record_answers` 表存储每题得分明细(迁移 0010_grade_record_answers.sql)。`batchCreateGradeRecordsByExam` data-access 单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩(学生只看到对错,不知道学生答案)。`BatchGradeEntryByExam` 组件完全重写为 Excel 式表格(行=学生,列=题目,末列=总分自动计算),支持多行多列粘贴、Enter 跳下一行、Tab 跳下一格、分数校验、撤销机制(sessionStorage 5 分钟有效)。新增对 `exams` 模块的依赖(getExamsForGradeEntry/getExamForGradeEntry)。i18n 新增 batchByExam 章节(zh-CN/en ~35 键) +- ✅ 审计修复(2026-06-22):~~`school-wide-summary-card.tsx` 整个组件无 i18n(13 处硬编码中文)~~ 改为 async 组件使用 `getTranslations("grades")`,新增 `schoolWide` 翻译键节(zh-CN/en 同步 14 键) +- ✅ 审计修复(2026-06-22):~~5 个 teacher/grades 页面文件大量硬编码中文/英文~~ `page.tsx`/`analytics/page.tsx`/`entry/page.tsx`/`stats/page.tsx`/`error.tsx` 全部使用 `getTranslations("grades")` 或 `useTranslations("grades")`,新增 `page` 翻译键节(zh-CN/en 同步 ~30 键) +- ✅ 审计修复(2026-06-22):~~`actions.ts` 15+ 处硬编码中文错误消息/通知内容~~ `notifyGradeEntered` 使用 `getTranslations("grades")` 获取通知标题和内容;`batchCreateGradeRecordsByExamAction` 错误消息改用 `t("action.*")` 翻译键;`exportGradesAction` 文件名和错误消息改用翻译键,新增 `action` 翻译键节(zh-CN/en 同步 ~20 键) +- ✅ 审计修复(2026-06-22):~~`data-access.ts` 2 处硬编码中文 throw Error~~ 改为英文错误消息(data-access 层为防御性检查,action 层已先行校验) +- ✅ 审计修复(2026-06-22):~~`data-access.ts:782` `as` 断言无类型守卫~~ 新增 `isGradeDraftData` 类型守卫函数(lib/type-guards.ts),从 unknown 安全验证 DB JSON 结构 +- ✅ 审计修复(2026-06-22):~~`data-access-analytics.ts:360-362` 非空断言 `!`~~ 改用类型谓词 `filter((r): r is typeof r & { stats: NonNullable } => r.stats !== null)` 消除非空断言 +- ✅ 审计修复(2026-06-22):~~`export.ts` 内存过滤而非 DB 层过滤~~ `exportStudentGradeRecordsToExcel` 传入 `studentId` 到 `getGradeRecords` DB 层过滤 +- ✅ 审计修复(2026-06-22):~~`teacher/grades/page.tsx` 内联类型守卫 + `as` 断言~~ 改用 `lib/type-guards.ts` 的 `isGradeType`/`isSemester` 函数 +- ✅ 审计修复(2026-06-22):~~11 个组件函数缺少返回值类型标注~~ 全部添加 `: JSX.Element` 或 `: Promise` 返回值类型 +- ✅ 审计修复(2026-06-24):~~17 处 Tailwind 任意值(`w-[140px]`/`h-[280px]`/`min-w-[80px]` 等)~~ 图表组件移除匹配默认值的 props 或改用标准类(`h-60`/`w-52`/`w-60`),过滤器改用 `w-36`/`w-32`,表格改用 `min-w-32`/`min-w-20` +- ✅ 审计修复(2026-06-24):~~`batch-grade-entry.tsx` 2 处 `window.confirm()`~~ 改用声明式 AlertDialog + `pendingSwitch` 状态管理,新增 3 个 i18n 键(confirmTitle/confirmAction/cancelAction) +- ✅ 审计修复(2026-06-24):~~`class-grade-report.tsx` 缺少返回值类型标注~~ 补充 `: Promise` 返回值类型 +- ✅ 审计修复(2026-06-25 P0):~~3 个 error.tsx 硬编码中文(parent/grades、admin/school/grades、admin/school/grades/insights)~~ 改为 `useTranslations("grades")` + 新增 `error.*` 翻译键(zh-CN/en 同步);~~7 个 loading/error.tsx 函数缺返回值类型~~ 补全 `: JSX.Element` 返回值类型标注 +- ✅ 审计修复(2026-06-25 P1-1):~~`actions.ts` 超 800 行(856 行)~~ 拆分草稿相关 3 个 Action 到 `actions-draft.ts`(106 行);通知逻辑 `notifyGradeEntered` 抽取至 `lib/notify.ts`(60 行);actions.ts 降至 646 行 +- ✅ 审计修复(2026-06-25 P1-2):~~`data-access.ts` 超 800 行(816 行)~~ 拆分草稿 CRUD 到 `data-access-drafts.ts`(107 行);拆分按试卷录入到 `data-access-exam-entry.ts`(167 行);data-access.ts 降至 536 行 +- ✅ 审计修复(2026-06-25 P1-3):~~`actions.ts` 多处英文错误消息未 i18n~~ 新增 `action.error.*` 翻译键,所有错误消息改用 `t("action.error.*")`;scope 校验错误改用 `t("action.scopeError.{code}")` +- ✅ 审计修复(2026-06-25 P1-4):~~`lib/scope-check.ts` 返回硬编码英文消息~~ 改为返回 `ScopeCheckErrorCode` 枚举(`"class_taught" | "class_members" | "denied"`),由 Action 层通过 i18n 键 `grades.action.scopeError.{code}` 翻译 +- ✅ 审计修复(2026-06-25 P1-5):~~`actions-analytics.ts` scope 校验消息未 i18n~~ 所有 6 个 Action 使用 `getTranslations("grades")` 获取翻译;新增 `ownRankingTrendOnly`/`childrenRankingTrendOnly` 翻译键 +- ✅ 审计修复(2026-06-25 P1-6/P1-7):~~`batch-grade-entry.tsx` 超 500 行(604 行)+ as 断言~~ 拆分为 `BatchGradeEntryTable` + `BatchGradeEntryStats` + `BatchGradeEntryDialog` 子组件 + `useBatchGradeEntryUndo` Hook;主组件降至 422 行;撤销数据使用 `isUndoData` 类型守卫(in 运算符)替代 `as { ids: string[]; timestamp: number }` 断言 +- ✅ 审计修复(2026-06-25 P1-8):~~`parent/grades/page.tsx` 第 63 行 as 断言~~ 改用 `for` 循环 + 类型守卫 `if (summary === null) continue` 替代 `.filter().map()` + `as NonNullable<...>` 断言 +- ✅ 审计修复(2026-06-25 P1-9):~~`teacher/grades/page.tsx` 列表与统计未 WidgetBoundary 包裹~~ 新增 `` 包裹 ``,避免单数据区块失败导致整页崩溃 +- ✅ 审计修复(2026-06-25 P2-2):~~`grade-filters.tsx`/`grade-query-filters.tsx`/`export-button.tsx` 函数缺返回值类型~~ 补全 `: JSX.Element` / `: Promise` 返回值类型标注 +- ✅ 审计修复(2026-06-25 P2-3):~~`updateGradeRecord` 使用 `Record` 累积更新字段~~ 改用 `Partial` 类型安全更新 +- ✅ 审计修复(2026-06-25 P2-4):~~`getStudentGradeSummary` 内部调用 `getClassRanking` 形成 data-access 层内部耦合~~ 新增 `getStudentRankInClass` 独立函数(两步法:查学生平均分 → 查所有学生平均分 → 计算严格高于的不同平均分数),避免获取整个班级排名(含姓名查询)只为查单个学生排名 +- ✅ 审计修复(2026-06-25 P2-9):~~`lib/type-guards.ts` 第 8/12 行 as 断言~~ `isGradeType`/`isSemester` 改用 `GRADE_TYPES.some((t) => t === v)` 替代 `(GRADE_TYPES as readonly string[]).includes(v)`;`isGradeDraftData` 改用 `in` 运算符替代 `as Record` 断言 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `actions.ts` | 770+ | 20 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v3-P2 新增:saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v4-P1-6:createGradeRecordAction/batchCreateGradeRecordsAction 新增通知;v4-P1-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除;v4-P2-6:assertClassInScope 迁移至 lib/scope-check.ts;2026-06-24 新增:batchCreateGradeRecordsByExamAction 按试卷录入每题得分) | -| `actions-analytics.ts` | 170 | 5 个分析 Action(含 Zod 校验,P3 修复:handleActionError + assertClassInScope 校验;v4-P2-6:assertClassInScope 改从 ./lib/scope-check 导入) | -| `data-access.ts` | 600+ | 成绩 CRUD + 统计 + 草稿 + 按试卷录入(含 v2-P2-9 修复:recorderName 批量查询;P3 修复:PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名;v3-P2 新增:saveGradeDraft/getGradeDraft/deleteGradeDraft + GradeDraftData 接口;v3-P3-2 新增:bulkDeleteGradeRecords 使用 inArray 批量删除;2026-06-24 新增:batchCreateGradeRecordsByExam 单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions/submission_answers) | -| `data-access-analytics.ts` | 200+ | 趋势/对比分析(P3 修复:getClassComparison 应用 buildScopeClassFilter;v3-P2 新增:getExamOptionsForGrades/getSchoolWideGradeSummary;getGradeTrend/getClassComparison/getSubjectComparison/getGradeDistribution 新增 semester/examId 可选参数) | -| `data-access-ranking.ts` | 83 | 排名查询(P3 修复:getRankingTrend 接受 scope 参数 + class_taught 校验) | +| `actions.ts` | 646 | 17 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v4-P1-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除;v4-P2-6:assertClassInScope 迁移至 lib/scope-check.ts;2026-06-24 新增:batchCreateGradeRecordsByExamAction 按试卷录入每题得分;**P1-1 重构(2026-06-25)**:草稿相关 3 个 Action(saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction)迁移至 actions-draft.ts;通知逻辑 notifyGradeEntered 抽取至 lib/notify.ts;错误消息全部 i18n(t(\"action.*\"));scope 校验错误改用 ScopeCheckErrorCode + t(\"action.scopeError.{code}\") 翻译) | +| `actions-draft.ts` | 106 | **P1-1 新增(2026-06-25)**:成绩录入草稿 3 个 Server Action(saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction),从 actions.ts 拆分以使 actions.ts 降至 800 行以内。权限分别为 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE,调用 data-access-drafts,错误消息使用 i18n 翻译键 | +| `actions-lock.ts` | 197 | **P3-6 新增(2026-06-26)**:协同录入锁 4 个 Server Action(getDraftLockStatusAction/acquireDraftLockAction/renewDraftLockAction/releaseDraftLockAction)。权限 GRADE_RECORD_READ(查询)/GRADE_RECORD_MANAGE(获取/续期/释放),调用 data-access-drafts 锁函数 + assertClassInScope 校验 + getUserNamesByIds 解析锁持有者姓名(避免跨模块 DB join)。releaseDraftLockAction 失败不阻断主流程 | +| `actions-import.ts` | 297 | **P3-10 新增(2026-06-26)**:Excel 批量导入成绩 3 个 Server Action(downloadGradeImportTemplateAction/importGradesFromExcelAction/previewGradeImportAction)。权限 GRADE_RECORD_MANAGE + assertClassInScope,调用 import-export.ts 解析 Excel + 匹配学生姓名 + 批量入库。支持模板下载(含班级学生姓名)、预校验、正式导入 | +| `actions-appeal.ts` | 197 | **P3-7 新增(2026-06-26)**:成绩申诉 5 个 Server Action(createGradeAppealAction/withdrawGradeAppealAction/getStudentAppealsAction/reviewGradeAppealAction/getPendingAppealsForReviewAction)。学生权限 GRADE_RECORD_READ(提交/撤回/查看自己申诉),教师权限 GRADE_RECORD_MANAGE(复核)。审核通过且提供 adjustedScore 时同步更新 grade_records.score | +| `data-access-appeals.ts` | 261 | **P3-7 新增(2026-06-26)**:成绩申诉数据访问层。7 个函数(hasPendingAppeal/createGradeAppeal/getStudentAppeals/getPendingAppealsForReview/getAppealById/reviewGradeAppeal/withdrawGradeAppeal)+ GradeAppeal/GradeAppealWithRecord 接口。申诉状态机 pending→approved/rejected/withdrawn,originalScore 快照留痕,reviewGradeAppeal 同步更新成绩记录 | +| `import-export.ts` | 278 | **P3-10 新增(2026-06-26)**:Excel 导入成绩逻辑。generateGradeImportTemplate 生成含班级学生姓名的模板,parseGradeExcelBuffer 解析 Excel(支持中英双语表头),parseGradeImportData 匹配学生姓名到 ID(处理重名),buildBatchInputFromImport 转换为批量录入输入。依赖 @/shared/lib/excel | +| `actions-analytics.ts` | 219 | 6 个分析 Action(含 Zod 校验,P3 修复:handleActionError + assertClassInScope 校验;v4-P2-6:assertClassInScope 改从 ./lib/scope-check 导入;**P1-5 重构(2026-06-25)**:所有 Action 使用 getTranslations(\"grades\") 获取翻译,错误消息改用 t(\"action.invalidQuery\")/t(\"action.scopeError.{code}\");学生/家长排名趋势访问校验使用 t(\"action.ownRankingTrendOnly\")/t(\"action.childrenRankingTrendOnly\")) | +| `data-access.ts` | 536 | 成绩 CRUD + 统计(含 v2-P2-9 修复:recorderName 批量查询;P3 修复:PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名;v3-P3-2 新增:bulkDeleteGradeRecords 使用 inArray 批量删除;**P1-2 重构(2026-06-25)**:草稿 CRUD(saveGradeDraft/getGradeDraft/deleteGradeDraft)迁移至 data-access-drafts.ts;按试卷录入(batchCreateGradeRecordsByExam)迁移至 data-access-exam-entry.ts;**P2-3**:updateGradeRecord 类型强化为 Partial;**P2-4**:新增 getStudentRankInClass 解耦 getStudentGradeSummary 与 getClassRanking) | +| `data-access-drafts.ts` | 405 | **P1-2 新增(2026-06-25)**:成绩录入草稿 CRUD(saveGradeDraft/getGradeDraft/deleteGradeDraft)+ GradeDraftData 接口,从 data-access.ts 拆分。使用 cache() memoize getGradeDraft,isGradeDraftData 类型守卫安全解析 DB JSON 列,24 小时过期。**P3-6 扩展(2026-06-26)**:新增协同录入锁机制 5 个函数(getDraftLockStatus/acquireDraftLock/renewDraftLock/releaseDraftLock/cleanupExpiredDraftLocks)+ DraftLockStatus/AcquireLockResult 接口,LOCK_TTL_MS=5 分钟,悲观锁语义+乐观 TTL+令牌续期,锁以 grade_drafts 行为载体(lockedBy/lockedAt/lockToken 列) | +| `data-access-exam-entry.ts` | 167 | **P1-2 新增(2026-06-25)**:按试卷批量录入成绩(batchCreateGradeRecordsByExam)+ BatchGradeRecordResult 接口,从 data-access.ts 拆分。单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),返回 BatchGradeRecordResult[]({gradeRecordId, studentId, submissionId})供调用方触发错题采集等后置钩子 | +| `data-access-analytics.ts` | 417 | 趋势/对比分析(P3 修复:getClassComparison 应用 buildScopeClassFilter;v3-P2 新增:getExamOptionsForGrades/getSchoolWideGradeSummary;getGradeTrend/getClassComparison/getSubjectComparison/getGradeDistribution 新增 semester/examId 可选参数) | +| `data-access-ranking.ts` | 166 | 排名查询(P3 修复:getRankingTrend 接受 scope 参数 + class_taught 校验) | | `stats-service.ts` | 285 | 统计计算纯函数(P1-1 新增:8 个纯函数 + 2 个常量 + 2 个接口;P3-10:createDefaultBuckets 改为内部函数;P3-24:buildGradeTrendPoints 使用 isGradeTrendType 类型守卫替代 as 断言) | | `export.ts` | 290+ | Excel 导出(v2-P1-5 修复:传递 currentUserId 到 data-access;P3 修复:适配 PaginatedGradeRecords 结构 + 传递 scope;P3-6:复用 stats-service.computeAverageScore 替代局部 avg;P3-7:硬编码中文改用 next-intl getTranslations;v4-P1-12 新增:exportStudentGradeRecordsToExcel 家长视角单学生导出) | -| `schema.ts` | 130+ | Zod 校验(含 12 个查询 schema;P3 修复:score .max(1000) + records .max(500) + 补全查询字段;v3-P2 新增:grade_drafts 表定义第 1444-1469 行;2026-06-24 新增:BatchGradeEntryByExamSchema 按试卷录入校验 + BatchGradeEntryByExamQuestionSchema/ItemSchema 子 schema,导出 BatchGradeEntryByExamInput 类型) | +| `schema.ts` | 237 | Zod 校验(含 12 个查询 schema;P3 修复:score .max(1000) + records .max(500) + 补全查询字段;v3-P2 新增:grade_drafts 表定义;2026-06-24 新增:BatchGradeEntryByExamSchema;**P3-10 新增(2026-06-26)**:ExcelImportRowSchema/ExcelImportGradesSchema + 导出 ExcelImportRowInput/ExcelImportGradesInput 类型;**P3-7 新增(2026-06-26)**:GradeAppealStatusEnum/CreateGradeAppealSchema/ReviewGradeAppealSchema + 导出 CreateGradeAppealInput/ReviewGradeAppealInput 类型) | | `lib/grade-utils.ts` | 20 | 公共工具函数(toNumber/normalize;P3-26:buildScopeClassFilter 迁移至 scope-filter.ts) | | `lib/scope-filter.ts` | 56 | DB 行级权限过滤(buildScopeClassFilter;P3-26 从 grade-utils.ts 迁移;v2-P2-2 修复:改用 classes data-access 子查询;P3 修复:新增 currentUserId 参数) | -| `lib/scope-check.ts` | 34 | v4-P2-6 新增:班级 scope 校验工具(assertClassInScope 同步函数,从 actions.ts 迁移至此独立文件以避开 "use server" 文件要求 export 必须为 async 的限制;含 `import "server-only"`) | +| `lib/scope-check.ts` | 34 | v4-P2-6 新增:班级 scope 校验工具(assertClassInScope 同步函数,从 actions.ts 迁移至此独立文件以避开 "use server" 文件要求 export 必须为 async 的限制;含 `import "server-only"`;**P1-4 重构(2026-06-25)**:返回类型从 string \| null 改为 ScopeCheckErrorCode \| null(\"class_taught\" \| \"class_members\" \| \"denied\"),消除硬编码英文字符串,调用方通过 i18n 键 grades.action.scopeError.{code} 翻译) | +| `lib/notify.ts` | 60 | **P1-1 新增(2026-06-25)**:成绩录入通知工具(notifyGradeEntered),从 actions.ts 抽取。使用 next-intl getTranslations 获取通知标题和内容(i18n);**P2-7 改进**:使用 Promise.allSettled 并行化通知写入,避免批量录入 30+ 学生时串行等待;依赖 notifications/data-access.createNotification + parent/data-access.getParentIdsByStudentIds + school/data-access.getSubjectNameById | +| `lib/report-card.ts` | 380 | **P3-1 新增(2026-06-26)**:成绩报告卡数据访问层。getReportCardData(cached)聚合学生成绩+班级信息+班主任+学年+科目排名+总体统计,computeSubjectRank 计算单科排名。三层 scope 校验:owned(学生自己)/children(家长子女)/class_taught(教师班级学生)。静态导入 data-access(getStudentRankInClass/getClassNamesByIds)+ stats-service(computeAverageScore) | +| `lib/type-guards.ts` | 25 | 类型守卫纯函数(isGradeType/isSemester/isGradeDraftData;**P2-9 修复(2026-06-25)**:isGradeType/isSemester 使用 GRADE_TYPES.some((t) => t === v) 替代 (GRADE_TYPES as readonly string[]).includes(v),消除 as 断言;isGradeDraftData 使用 in 运算符替代 as Record 断言) | | `types.ts` | 200+ | 类型定义(v3-P2 新增:SchoolWideGradeSummaryItem/SchoolWideGradeSummary;2026-06-24 新增:GradeRecordAnswer/BatchGradeEntryByExamQuestion/BatchGradeEntryByExamItem) | -| `components/widget-boundary.tsx` | 136 | Widget 边界组件(P1-5 新增,v2-P1-1 已在 3 个页面应用) | +| `components/widget-boundary.tsx` | 136 | Widget 边界组件(P1-5 新增,v2-P1-1 已在 3 个页面应用;**P1-9(2026-06-25)**:teacher/grades/page.tsx 列表与统计卡片新增 WidgetBoundary 包裹,避免单数据区块失败导致整页崩溃) | | `components/school-wide-summary-card.tsx` | - | v3-P2 新增:管理员全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格) | | `components/score-cell.tsx` | 41 | v4-P1-7 新增:成绩单元格组件,根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名 | | `components/grade-trend-card.tsx` | 69 | 趋势卡片(v2-P2-9 修复:a11y;v2-P1-4:i18n;P3 修复:NaN 日期检查 + fullScore > 0 守卫) | @@ -839,15 +1051,26 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `components/class-comparison-chart.tsx` | 194 | 班级对比图(v2-P1-4:i18n;v3-P3-5 新增:显著性分析区域,基于极差和样本量的经验规则判断班级间差异,含可折叠详细分析) | | `components/grade-trend-chart.tsx` | 59 | 趋势图(v2-P1-4:i18n) | | `components/grade-record-form.tsx` | 177 | 录入表单(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall 包装提交) | -| `components/batch-grade-entry.tsx` | 600+ | 2026-06-24 完全重写:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?(原 v2-P2-7 Label htmlFor;v2-P1-4 i18n;P3 safeActionCall + localStorage 安全检查;v3-P2 服务端草稿;v3-P3-1 下载 CSV 模板;v4-P3-2 新手引导均已替换) | -| `components/grade-filters.tsx` | 76 | 过滤器(v2-P1-4:i18n) | +| `components/batch-grade-entry.tsx` | 422 | 2026-06-24 完全重写:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。**P1-6/P1-7 重构(2026-06-25)**:拆分为 3 个子组件 + 1 个 Hook(BatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo),主组件从 604 行降至 422 行;撤销数据使用 isUndoData 类型守卫替代 as { ids: string[]; timestamp: number } 断言;window.confirm() 已替换为声明式 AlertDialog。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?(原 v2-P2-7 Label htmlFor;v2-P1-4 i18n;P3 safeActionCall + localStorage 安全检查;v3-P2 服务端草稿;v3-P3-1 下载 CSV 模板;v4-P3-2 新手引导均已替换) | +| `components/batch-grade-entry-table.tsx` | 115 | **P1-6 新增(2026-06-25)**:批量录入 Excel 式表格子组件(行=学生,列=题目,末列=总分自动计算)。支持多行多列 Excel 粘贴、Enter 跳下一行、Tab 跳下一格、分数校验(超过题目满分标红)。接受 inputRefs 供父组件管理输入焦点 | +| `components/batch-grade-entry-stats.tsx` | 68 | **P1-6 新增(2026-06-25)**:批量录入统计栏子组件,显示已录入/总数/均分/最高/最低/无效分数徽章,含学生搜索框过滤。导出 BatchGradeEntryStatsData 接口 | +| `components/batch-grade-entry-dialog.tsx` | 58 | **P1-6 新增(2026-06-25)**:批量录入切换确认对话框子组件(替代 window.confirm())。导出 PendingSwitch 联合类型 { kind: "exam"; examId; message } \| { kind: "class"; classId; message } \| null | +| `hooks/use-batch-grade-entry-undo.ts` | 86 | **P1-6/P1-7 新增(2026-06-25)**:批量录入撤销 Hook。封装撤销令牌的序列化、sessionStorage 读写、5 分钟 TTL 过期检查、撤销 Action 调用。包含 isUndoData 类型守卫函数(替代 as 断言),使用 in 运算符做安全属性访问 | +| `components/grade-filters.tsx` | 76 | 过滤器(v2-P1-4:i18n;**P2-2(2026-06-25)**:补全 : JSX.Element 返回值类型) | | `components/student-grade-summary.tsx` | 107 | 学生成绩摘要(v2-P1-4:i18n) | -| `components/export-button.tsx` | 79 | 导出按钮(v2-P1-4:i18n;P3 修复:safeActionCall 包装导出操作) | +| `components/export-button.tsx` | 79 | 导出按钮(v2-P1-4:i18n;P3 修复:safeActionCall 包装导出操作;**P2-2(2026-06-25)**:handleExport 补全 : Promise 返回值类型) | | `components/analytics-filters.tsx` | 86+ | 分析过滤器(v2-P1-4:i18n;v3-P2 新增:exams/currentExamId/currentSemester props,添加学期和考试筛选 ChipNav) | | `components/stats-class-selector.tsx` | 40 | 统计班级选择器(v2-P1-4:i18n) | | `components/grade-stats-card.tsx` | 74 | 统计卡片(v2-P1-4:i18n) | | `components/class-grade-report.tsx` | 90 | 班级成绩报告(v2-P1-4:i18n) | -| `components/grade-query-filters.tsx` | 96 | 查询过滤器(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n) | +| `components/grade-query-filters.tsx` | 96 | 查询过滤器(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;**P2-2(2026-06-25)**:补全 : JSX.Element 返回值类型) | +| `components/report-card-view.tsx` | 323 | **P3-1 新增(2026-06-26)**:A4 打印格式成绩报告卡服务端组件。含表头(学校名+标题+学期)、学生信息网格、科目成绩表格(rowSpan 合并科目名/排名列)、汇总统计(均分/班排名/及格率/优秀率)、教师评语+签字区。使用 globals.css `@media print` 规则控制打印布局 | +| `components/report-card-print-button.tsx` | 106 | **P3-1 新增(2026-06-26)**:跳转至报告卡页面的客户端按钮,支持学年/学期筛选 | +| `components/report-card-print-action.tsx` | 48 | **P3-1 新增(2026-06-26)**:调用 window.print() 的客户端按钮组件,含 loading 状态和错误 toast | +| `components/excel-import-dialog.tsx` | 518 | **P3-10 新增(2026-06-26)**:Excel 批量导入成绩对话框(3 步流程:下载模板→填写参数→上传文件)。展示导入结果(成功/失败计数、未匹配学生、无效行表格前 20 条)。使用 safeActionCall 包装 Action 调用 | +| `components/draft-lock-banner.tsx` | 75 | **P3-6 新增(2026-06-26)**:协同录入锁状态提示条。三种状态:acquiring(loading)/conflict(锁被他人持有,警告+重试按钮)/acquired(不渲染)。使用 Alert 组件 | +| `components/knowledge-point-mastery-chart.tsx` | 232 | **P3-3 新增(2026-06-26)**:知识点掌握度柱状图组件,集成 diagnostic 模块的 getClassMasterySummary 数据。使用 SimpleBarChart + masteryBarColor 函数着色 + isMasteryTooltipPayload 类型守卫 | +| `hooks/use-draft-lock.ts` | 184 | **P3-6 新增(2026-06-26)**:协同录入锁管理 Hook。职责:进入页面自动获取锁、3 分钟心跳续期(TTL 5 分钟)、组件卸载/提交成功释放锁、锁冲突状态暴露。使用 ref 保存最新 token 避免闭包捕获旧值,防重复 acquire/release | --- @@ -857,8 +1080,11 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **导出函数**: - Actions:`createTeacherClassAction` / `updateTeacherClassAction` / `deleteTeacherClassAction` / `createAdminClassAction` / `updateAdminClassAction` / `deleteAdminClassAction` / `createGradeClassAction` / `updateGradeClassAction` / `deleteGradeClassAction` -- Data-access:`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `verifyTeacherOwnsClass` / `getTeacherIdsByClassIds`(获取多个班级的所有教师 ID:班主任 + 任课教师,跨模块接口,供 messaging 模块调用)(✅ P0-5 已修复:classSchedule 写函数 createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem 已迁移至 scheduling/data-access-class-schedule.ts,classes 模块仅保留 classSchedule 读函数;✅ P2 已修复:`getAccessibleClassIdsForTeacher` 使用 `Promise.all` 并行化 ownedIds 与 assignedIds 查询) +- Data-access:`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `verifyTeacherOwnsClass` / `getTeacherIdsByClassIds`(获取多个班级的所有教师 ID:班主任 + 任课教师,跨模块接口,供 messaging 模块调用)/ `getClassIdByScheduleId(scheduleId: string): Promise`(✅ 2026-06-26 审计修复新增:根据课表项 ID 获取所属班级 ID,供 actions-schedule 的 create/update/delete 三个 Action 做越权校验,防止教师通过 scheduleId 修改他人班级课表)(✅ P0-5 已修复:classSchedule 写函数 createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem 已迁移至 scheduling/data-access-class-schedule.ts,classes 模块仅保留 classSchedule 读函数;✅ P2 已修复:`getAccessibleClassIdsForTeacher` 使用 `Promise.all` 并行化 ownedIds 与 assignedIds 查询) - Schema:`CreateTeacherClassSchema` / `UpdateTeacherClassSchema` / `DeleteTeacherClassSchema` / `CreateAdminClassSchema` / `UpdateAdminClassSchema` / `DeleteAdminClassSchema` / `CreateGradeClassSchema` / `UpdateGradeClassSchema` / `DeleteGradeClassSchema` / `CreateClassScheduleItemSchema` / `UpdateClassScheduleItemSchema` / `DeleteClassScheduleItemSchema` / `EnrollStudentByEmailSchema` +- Components:`AdminClassesClient` / `GradeClassesClient`(P1-D 重构:降至 ≤160 行,组合使用共享组件)/ `ClassListTable`(admin/grade 共享班级列表表格,P1-D 抽取)/ `ClassFormDialog`(admin/grade 共享创建/编辑表单弹窗,`schools` 可选控制学校选择显隐,P1-D 抽取)/ `ClassDeleteDialog`(admin/grade 共享删除确认弹窗,基于 ConfirmDeleteDialog,P1-D 抽取)/ `ClassListToolbar`(admin/grade 共享工具栏:新建按钮+计数徽章,P1-D 抽取)/ `class-form-utils`(`formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型,P1-D 抽取)/ `ClassErrorBoundary`(P1-C 新增:class component Error Boundary + i18n + router.refresh 重试)/ `ClassListSkeleton` / `ClassCardSkeleton` / `ClassGridSkeleton` / `StudentListSkeleton` / `ScheduleGridSkeleton`(P1-C 新增:5 个骨架屏组件)/ `MyClassesGrid`(P0-A 修复:已接入 i18n)/ `ScheduleView`(✅ P2 修复:已拆分出 `ScheduleCreateDialog`/`ScheduleEditDialog`/`ScheduleDeleteDialog` 3 个对话框子组件,主组件从 464 行降至 175 行;不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态,下沉到子对话框;WEEKDAYS 常量改用从 `./schedule-utils` 导入的 `SCHEDULE_WEEKDAYS`)/ `ScheduleCreateDialog`(P2 修复新增:课表新增对话框,从 schedule-view 拆分)/ `ScheduleEditDialog`(P2 修复新增:课表编辑对话框,从 schedule-view 拆分)/ `ScheduleDeleteDialog`(P2 修复新增:课表删除确认对话框,从 schedule-view 拆分)/ `ScheduleFilters` / `StudentsTable` / `StudentsFilters`(P0-A 修复:已接入 i18n)/ `ClassHeader` / `ClassOverviewStats` / `ClassQuickActions` / `ClassScheduleWidget` / `ClassAssignmentsWidget` / `ClassStudentsWidget` / `ClassTrendsWidget` / `EditClassDialog`(P0-A 修复:class-detail 下 7 个子组件 + 编辑对话框已接入 i18n) +- 工具函数(components/schedule-utils.ts,2026-06-26 审计修复新增):`getSubjectColor(subject: string): string`(根据科目名返回对应颜色类名,中英文双语关键词匹配;修复原实现仅匹配英文关键词导致中文 `DEFAULT_CLASS_SUBJECTS` 全部落到 default 分支的颜色分组失效 bug)/ `getPositionStyle(startTime: string, endTime: string): { top: string; height: string }`(根据课程开始/结束时间计算课表块在 8:00-18:00 时间轴上的定位百分比)/ `timeToMinutes(time: string): number`(将 "HH:MM" 时间字符串转换为当天从 0 点起的分钟数)/ `SCHEDULE_WEEKDAYS`(✅ P2 修复新增:课表周常量数组,供 schedule-view 及拆分出的 3 个对话框子组件复用,替换原 schedule-view.tsx 内联的 WEEKDAYS 常量) +- Hooks:`useClassData`(admin/grade 共享对话框+表单状态管理+重置逻辑,P1-E 抽取)/ `useClassFilters`(按学校筛选年级,admin 模式使用,P1-E 抽取) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`school`(✅ P1-1 已修复:通过 school data-access.isGradeHead/isGradeManager/findGradeIdByHeadAndName)、`homework`(✅ P0-7 已修复:通过 `homework/data-access-classes` 暴露的函数获取作业数据,不再直查 homework/exams 表) @@ -874,6 +1100,22 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P0-3 修复(2026-06-22):~~`actions.ts` 974 行接近 1000 行硬上限~~ 拆分为 6 个文件(actions-teacher/actions-admin/actions-grade/actions-invitations/actions-schedule/actions-shared),原 `actions.ts` 改为 50 行 barrel re-export - ✅ P1-1 修复(2026-06-22):~~`ctx.roles.includes("admin"/"teacher"/"student")` 角色硬编码~~ 改为 `hasAdminScope(ctx)`/`hasTeacherScope(ctx)`/`hasStudentScope(ctx)` 基于 `dataScope.type` 判断 - ✅ P1-4 修复(2026-06-22):`types.ts` 中 ClassHomeworkInsights 等跨领域类型保留在 classes 模块(因为是 classes 对 homework 数据的视图),添加注释说明归属决策 +- ✅ P1-D 修复(2026-06-24):~~`admin-classes-view.tsx` 533 行与 `grade-classes-view.tsx` 439 行 ~80% 代码重复~~ 拆分为组合模式:抽取 `ClassListTable` / `ClassFormDialog` / `ClassDeleteDialog` / `ClassListToolbar` 4 个共享组件 + `class-form-utils`(`formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型);admin-classes-view 降至 159 行,grade-classes-view 降至 153 行;admin 删除弹窗统一改用 `ConfirmDeleteDialog`(与 grade 一致) +- ✅ P1-E 修复(2026-06-24):抽取 `useClassData` hook(对话框开关+表单字段+提交中标记+重置逻辑,admin/grade 共享)+ `useClassFilters` hook(按学校筛选年级,admin 模式使用) +- ✅ P0-A 修复(2026-06-24):~~classes 模块 13/14 个组件未接入 i18n~~ 全部 13 个组件(admin-classes-view/grade-classes-view/my-classes-grid/schedule-view/schedule-filters/students-filters/students-table/class-detail 下 7 个子组件/edit-class-dialog)已接入 `useTranslations("classes")`;my-classes-grid 中 5 处硬编码英文("Failed to regenerate"/"Failed to copy"/"No Room"/"vs last week")已替换为翻译键 +- ✅ P0-B 修复(2026-06-24):~~classes.json 仅 55 行~~ 扩充至 ~370 行,覆盖 invitation/class/list/form/delete/filters/myClasses/students/schedule/detail(header/overview/trends/assignments/widgets/edit/empty)/grade/bulk/errors 全部场景;en 镜像同步 +- ✅ P1-C 修复(2026-06-24):新增 `class-error-boundary.tsx`(class component Error Boundary + i18n + router.refresh 重试)和 `class-skeleton.tsx`(ClassListSkeleton/ClassCardSkeleton/ClassGridSkeleton/StudentListSkeleton/ScheduleGridSkeleton);6 个页面均已包裹 Error Boundary:admin/school/classes + teacher/classes/my + teacher/classes/my/[id] + teacher/classes/schedule + teacher/classes/students(ClassErrorBoundary)、management/grade/dashboard(SchoolErrorBoundary);schedule/students 页面 EmptyState 硬编码英文替换为 i18n +- ✅ 2026-06-26 审计修复(i18n 全覆盖 + 越权校验 + 角色硬编码消除 + 错误处理统一 + 路由错误边界完善): + - **i18n 翻译键扩充**:classes.json 新增 `actions` 命名空间(42 个键,覆盖全部 Server Action 错误/成功消息)、`error` 命名空间(3 个键:title/description/retry,供 ErrorState 组件使用)、`filters` 命名空间新增 10 个键、`detail` 命名空间新增 12 个键 + - **越权校验加固**:`actions-schedule.ts` 新增 `assertClassOwnership` 辅助函数(调用 `getClassIdByScheduleId` + `verifyTeacherOwnsClass`),create/update/delete 三个 Action 增加班级归属校验,防止教师通过 scheduleId 修改他人班级课表;`actions-teacher.ts` 的 `updateTeacherClassAction` / `deleteTeacherClassAction` 增加越权校验 + - **角色硬编码消除**:`data-access.ts` / `data-access-teacher.ts` / `data-access-admin.ts` 中所有 `roles.name, "teacher"` / `roles.name, "student"` 改为 `ROLE_NAMES.TEACHER` / `ROLE_NAMES.STUDENT`(共 7 处),统一引用 `@/shared/types/permissions` 常量 + - **错误处理统一**:`actions-admin.ts` / `actions-grade.ts` / `actions-invitations.ts` / `actions-schedule.ts` / `actions-teacher.ts` 所有 catch 块改为使用 `handleActionError`(来自 `@/shared/lib/action-utils`),所有 message 改为使用 `await getTranslations("classes")` + t(),移除 `PermissionDeniedError` 直接 import;`actions-invitations.ts` 的 `assertClassOwnership` 签名变更:新增 `t` 参数;`actions-shared.ts` 的 `toWeekday` / `parseSubjectTeachers` 改为 async,使用 `ValidationError` 代替 `throw new Error` + - **DEFAULT_CLASS_SUBJECTS 回退逻辑**:`data-access.ts` 的 `getClassSubjects()` 新增回退逻辑——subjects 表为空时返回 `DEFAULT_CLASS_SUBJECTS`;`data-access-teacher.ts` / `data-access-admin.ts` 中 `inArray(subjects.name, DEFAULT_CLASS_SUBJECTS)` 改为 `inArray(subjects.name, await getClassSubjects())`,确保筛选条件与回退数据源一致 + - **DB 错误处理修正**:`data-access-teacher.ts` 的 `getTeacherClasses()` DB 错误时 throw 而非返回空数组(避免静默吞错) + - **路由错误边界完善**(10 个 error.tsx + 1 个 loading.tsx 新增):teacher/classes/my/error.tsx、teacher/classes/my/[id]/error.tsx + loading.tsx、teacher/classes/students/error.tsx、teacher/classes/schedule/error.tsx、student/learning/error.tsx + loading.tsx、student/learning/courses/error.tsx、student/learning/courses/[classId]/error.tsx、student/schedule/error.tsx;统一使用 shared 层 `ErrorState` 组件 + - **新增工具函数文件**:`components/schedule-utils.ts` 抽取 `getSubjectColor` / `getPositionStyle` / `timeToMinutes` 3 个纯函数(修复中文科目颜色分组失效 bug + 提升可测试性) + - **新增权限点使用**:`student/learning/page.tsx` / `student/learning/courses/page.tsx` / `student/learning/courses/[classId]/page.tsx` / `student/schedule/page.tsx` 接入 `requirePermission(CLASS_READ)` + - **generateMetadata 接入**(✅ P2 修复新增):8 个 page.tsx 新增 `generateMetadata` 导出(通过 next-intl `getTranslations` 生成页面标题):`teacher/classes/my/page.tsx`、`teacher/classes/my/[id]/page.tsx`、`teacher/classes/schedule/page.tsx`、`teacher/classes/students/page.tsx`、`student/learning/page.tsx`、`student/learning/courses/page.tsx`、`student/learning/courses/[classId]/page.tsx`、`student/schedule/page.tsx` **文件清单**: | 文件 | 行数 | 职责 | @@ -892,6 +1134,48 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `actions-shared.ts` | 60 | 共享工具(hasAdminScope/hasTeacherScope/hasStudentScope/parseSubjectTeachers/toWeekday) | | `schema.ts` | 152 | Zod 校验(13 个 schema:教师/管理员/年级班级 CRUD + 课表 CRUD + 邮箱注册) | | `types.ts` | 201 | 类型定义(含跨领域类型说明注释,P1-4 修复) | +| components/admin-classes-view.tsx | 159 | 管理员班级客户端(P1-D 重构:组合共享组件) | +| components/grade-classes-view.tsx | 153 | 年级班级客户端(P1-D 重构:组合共享组件) | +| components/class-list-table.tsx | 115 | 班级列表表格(admin/grade 共享,P1-D 抽取) | +| components/class-form-dialog.tsx | 200 | 班级创建/编辑表单弹窗(admin/grade 共享,P1-D 抽取) | +| components/class-delete-dialog.tsx | 35 | 班级删除确认弹窗(admin/grade 共享,P1-D 抽取) | +| components/class-list-toolbar.tsx | 30 | 班级列表工具栏(admin/grade 共享,P1-D 抽取) | +| components/class-form-utils.ts | 35 | formatSubjectTeachers + 表单类型(P1-D 抽取) | +| components/schedule-utils.ts | 89 | 课表纯函数工具集(2026-06-26 审计修复新增:getSubjectColor/getPositionStyle/timeToMinutes;✅ P2 修复新增 SCHEDULE_WEEKDAYS 周常量;修复中文科目颜色分组失效 bug + 提升可测试性) | +| components/class-error-boundary.tsx | 45 | 班级模块 Error Boundary(P1-C 新增) | +| components/class-skeleton.tsx | 80 | 班级模块骨架屏组件(5 个:List/Card/Grid/StudentList/ScheduleGrid,P1-C 新增) | +| components/my-classes-grid.tsx | 418 | 教师班级卡片网格(P0-A 修复:已接入 i18n) | +| components/schedule-view.tsx | 175 | 课表视图(P0-A 修复:已接入 i18n;✅ P2 修复:拆分出 ScheduleCreateDialog/ScheduleEditDialog/ScheduleDeleteDialog 3 个对话框子组件,主组件从 464 行降至 175 行;不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态,下沉到子对话框;WEEKDAYS 常量改用从 ./schedule-utils 导入的 SCHEDULE_WEEKDAYS) | +| components/schedule-create-dialog.tsx | - | 课表新增对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleCreate 状态) | +| components/schedule-edit-dialog.tsx | - | 课表编辑对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleUpdate 状态) | +| components/schedule-delete-dialog.tsx | - | 课表删除确认对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleDelete 状态) | +| components/schedule-filters.tsx | 60 | 课表筛选器(P0-A 修复:已接入 i18n) | +| components/students-filters.tsx | 60 | 学生筛选器(P0-A 修复:已接入 i18n) | +| components/students-table.tsx | 120 | 学生表格(P0-A 修复:已接入 i18n) | +| components/class-detail/class-header.tsx | 100 | 班级详情头部(P0-A 修复:已接入 i18n) | +| components/class-detail/class-overview-stats.tsx | 80 | 班级概览统计(P0-A 修复:已接入 i18n) | +| components/class-detail/class-quick-actions.tsx | 60 | 快捷操作(P0-A 修复:已接入 i18n) | +| components/class-detail/class-schedule-widget.tsx | 120 | 课表 Widget(P0-A 修复:已接入 i18n) | +| components/class-detail/class-assignments-widget.tsx | 100 | 作业 Widget(P0-A 修复:已接入 i18n) | +| components/class-detail/class-students-widget.tsx | 120 | 学生 Widget(P0-A 修复:已接入 i18n) | +| components/class-detail/class-trends-widget.tsx | 150 | 趋势 Widget(P0-A 修复:已接入 i18n) | +| components/class-detail/edit-class-dialog.tsx | 145 | 编辑班级对话框(P0-A 修复:已接入 i18n) | +| hooks/use-class-data.ts | 95 | 对话框+表单状态管理 hook(P1-E 抽取) | +| hooks/use-class-filters.ts | 30 | 按学校筛选年级 hook(P1-E 抽取) | + +**路由文件清单**(`app/(dashboard)/`,2026-06-26 审计修复新增错误边界 + 加载骨架): +| 文件 | 职责 | +|------|------| +| `teacher/classes/my/error.tsx` | 教师我的班级错误边界(使用 shared `ErrorState` + namespace="classes") | +| `teacher/classes/my/[id]/error.tsx` | 班级详情错误边界(使用 shared `ErrorState`) | +| `teacher/classes/my/[id]/loading.tsx` | 班级详情加载骨架(ClassGridSkeleton) | +| `teacher/classes/students/error.tsx` | 学生管理错误边界(使用 shared `ErrorState`) | +| `teacher/classes/schedule/error.tsx` | 课表错误边界(使用 shared `ErrorState`) | +| `student/learning/error.tsx` | 学生学习中心错误边界(使用 shared `ErrorState`) | +| `student/learning/loading.tsx` | 学生学习中心加载骨架 | +| `student/learning/courses/error.tsx` | 学生课程列表错误边界(使用 shared `ErrorState`) | +| `student/learning/courses/[classId]/error.tsx` | 学生课程详情错误边界(使用 shared `ErrorState`) | +| `student/schedule/error.tsx` | 学生课表错误边界(使用 shared `ErrorState`) | --- @@ -927,6 +1211,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P2-5 修复(2026-06-23):为 department/academicYear/grade 的 9 个 CRUD Action 补充 `after(() => logAudit(...))` 审计日志(action: department.create/update/delete、academicYear.create/update/delete、grade.create/update/delete),与 school 实体审计日志策略一致 - ✅ v4-P2-6 改进(2026-06-23):年级管理体验增强——新增 `getGradeOverviewStats()` data-access 函数(返回 `GradeOverviewStats[]`:每个年级的 classCount/studentCount/teacherCount,动态导入 classes/data-access 避免循环依赖);`GradesClient` 新增 `gradeStats` prop,渲染年级概览卡片视图(班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口);admin/school/grades/page.tsx 新增 `getGradeOverviewStats()` 查询并传入 GradesClient - ✅ v4-P2-6 改进(2026-06-23):新增 `GradeInsightsFilters` 组件(使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新);admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写,使用 `GradeInsightsFilters` 替代原生 form get,添加 i18n,表格添加 `overflow-x-auto` 水平滚动 +- ✅ P1-A 修复(2026-06-24):~~`grades-view.tsx` 860 行超出组件行数限制~~ 拆分为组合模式:`GradeFormDialog`(创建/编辑表单对话框,含客户端校验+去重+isDirty 检测)+ `GradeDeleteDialog`(删除确认)+ `GradeListToolbar`(搜索/筛选/排序工具栏)+ `GradeOverviewCards`(年级概览卡片视图)+ `useGradeData` hook(对话框状态管理);主容器 `grades-view.tsx` 降至 290 行,参照 `schools-view.tsx` 组合模式 **文件清单**: | 文件 | 行数 | 职责 | @@ -942,9 +1227,14 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | components/school-error-boundary.tsx | 72 | 共享 Error Boundary(P1-3 修复) | | components/school-skeleton.tsx | 69 | 共享骨架屏(P1-3 修复) | | components/org-tree-nav.tsx | 134 | 学校→年级→班级三级树形导航(P2-2 修复:搜索过滤 + 选中高亮 + 展开折叠 + 节点类型图标) | -| components/grades-view.tsx | 920+ | 年级管理客户端(v4-P2-6 更新:新增 gradeStats prop + 年级概览卡片视图,展示班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口) | +| components/grades-view.tsx | 290 | 年级管理客户端(P1-A 修复:拆分为组合模式,仅负责列表渲染+筛选+组合子组件) | +| components/grade-form-dialog.tsx | 280 | 年级创建/编辑表单对话框(P1-A 新增:含客户端校验+去重+isDirty 检测,useActionMutation 管理 mutation) | +| components/grade-delete-dialog.tsx | 65 | 年级删除确认对话框(P1-A 新增) | +| components/grade-list-toolbar.tsx | 115 | 年级列表工具栏(P1-A 新增:搜索+学校筛选+主任筛选+排序+新建按钮) | +| components/grade-overview-cards.tsx | 165 | 年级概览卡片视图(P1-A 新增:班级/学生/教师数+年级主任/教学主任+快捷操作) | | components/grade-insights-filters.tsx | 48 | v4-P2-6 新增:年级洞察筛选器(ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新) | | hooks/use-school-data.ts | 40 | 学校数据管理 hook(P2-1 修复) | +| hooks/use-grade-data.ts | 42 | 年级数据管理 hook(P1-A 新增:对话框状态管理,参照 use-school-data 模式) | --- @@ -992,57 +1282,94 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" ## 2.10 attendance(考勤模块)— 结构典范 -**职责**:考勤记录管理 + 统计分析 + 规则配置。 +**职责**:考勤记录管理 + 统计分析 + 规则配置。✅ L-6 节次考勤:支持按节次(早读/上午/下午/晚自习/全日)记录与过滤。✅ L-9 关联分析: 考勤与成绩跨模块关联分析(Pearson 相关系数 + 风险分级)。 **导出函数**: -- Actions(10 个):`recordAttendanceAction` / `batchRecordAttendanceAction` / `updateAttendanceAction` / `deleteAttendanceAction` / `getAttendanceAction` / `getStudentAttendanceAction` / `getClassAttendanceStatsAction` / `getClassAttendanceForDateAction` / `saveAttendanceRulesAction` / `getAttendanceRulesAction`(✅ P2-2:5 个写 Action 错误消息改用 `getTranslations("attendance")` 国际化) -- Data-access:`getAttendanceRecords` / `createAttendanceRecord` / `updateAttendanceRecord` / `deleteAttendanceRecord` / `getClassStudentsForAttendance` / `getAttendanceStats`(✅ P2-1 已修复:改用 SQL `COUNT()` + `SUM(CASE WHEN ...)` 聚合查询,不再依赖 `getAttendanceRecords` 分页结果)/ `upsertAttendanceRules` / `getAttendanceRules` -- Data-access-stats:`getStudentAttendanceSummary`(✅ P2-6 已修复:stats 改用 SQL 聚合查询,recentRecords 增加 `recentLimit` 参数默认 20,不再加载全部记录)/ `getClassAttendanceStats` / `computeStats` -- Import-export:`exportAttendanceRecordsToExcel`(✅ P2-11 新增:Sheet1 考勤明细 + Sheet2 统计汇总,列头 i18n 化) -- Components:`AttendanceSheet`(批量点名表单)/ `AttendanceRecordList`(记录列表 + 删除)/ `AttendanceFilters`(URL 同步筛选器)/ `AttendanceStatsCard`(单卡片统计)/ `AttendanceStatsCards`(管理员 6 卡片总览)/ `AttendanceStatsClassSelector`(班级筛选 ChipNav)/ `AttendanceRulesForm`(规则配置表单)/ `StudentAttendanceView`(学生/家长只读视图)/ `AttendancePageLayout`(✅ P2-3 新增:页面布局骨架,admin/teacher 考勤页复用) +- Actions(5 个写 Action):`recordAttendanceAction`(✅ L-6:formData 读取 period)/ `batchRecordAttendanceAction`(✅ L-2:保存成功后调用 `notifyParentsOfAbsence` 异步通知家长,失败不阻断主流程;✅ L-6:每条记录携带 period)/ `updateAttendanceAction`(✅ L-6:formData 读取 period)/ `deleteAttendanceAction` / `saveAttendanceRulesAction`(✅ 审计修复:均调用 `requirePermission(ATTENDANCE_MANAGE)` + 资源归属校验 + `trackEvent` 埋点) +- Data-access:`getAttendanceRecords`(✅ 审计修复:移除跨模块 JOIN,委托 `getUserNamesByIds`/`getClassNamesByIds`;✅ L-6:支持按 period 过滤)/ `getClassAttendanceForDate`(✅ L-6:新增可选 period 参数)/ `buildPeriodFilter`(✅ L-6 新增:纯函数,full_day 匹配 NULL OR 'full_day' 向后兼容)/ `createAttendanceRecord`(✅ L-6:写入 period 默认 full_day)/ `batchCreateAttendanceRecords`(✅ L-6:批量写入 period)/ `updateAttendanceRecord`(✅ L-6:支持更新 period)/ `deleteAttendanceRecord` / `getClassStudentsForAttendance`(✅ 审计修复:委托 `classes/data-access.getClassActiveStudentsWithInfo`)/ `getAttendanceStats`(✅ 审计修复:SQL 聚合 + 返回统一 `AttendanceStats` 类型)/ `upsertAttendanceRules` / `getAttendanceRules` / `getAttendanceRecordClassId` / `syncExcusedFromLeaveRequest`(✅ L-5 新增:请假审批通过后遍历日期区间插入 excused 记录,幂等不覆盖已有记录;✅ L-6:插入时 period: full_day;被 `leave-requests/actions.reviewLeaveRequestAction` 调用) +- Data-access-stats:`getStudentAttendanceSummary`(✅ 审计修复:SQL 聚合 + 委托 data-access;✅ L-6:recentRecords 映射 period)/ `getClassAttendanceStats`(✅ 审计修复:SQL 聚合 + 委托 data-access;✅ L-6:studentRecords 映射 period)/ `computeStats`(✅ 纯函数导出,可单测) +- Notifications(✅ L-2 新增):`notifyParentsOfAbsence`(批量考勤录入后多渠道通知家长:in_app/sms/email/wechat,一次性批量查询 parent-student 映射避免 N+1) +- Import-export:`exportAttendanceRecordsToExcel`(✅ 审计修复:类型守卫 + i18n 列头) +- Services(✅ 审计新增 P1-12/P2-6):`AttendanceDataService` 接口 / `AttendanceReadService` / `AttendanceWriteService` / `AttendanceRepository` / `AttendanceProvider` / `useAttendanceService` / `createAttendanceDataService` / `createAttendanceReadService` +- Components:`AttendanceSheet`(✅ 审计修复:AlertDialog + 常量统一 + 容器内 keydown + useMemo;✅ L-6:新增节次选择器,3 列布局,提交时携带 period)/ `AttendanceRecordList`(✅ 审计修复:`usePermission` 控制删除按钮 + `resolveActionError`;✅ L-6:新增节次列,null 视为 full_day 展示)/ `AttendanceFilters` / `AttendanceStatsCard` / `AttendanceStatsCards`(✅ 审计修复:统一 `AttendanceStats` 类型)/ `AttendanceStatsClassSelector` / `AttendanceRulesForm`(✅ 审计修复:`resolveActionError`)/ `StudentAttendanceView` / `AttendancePageLayout` +- Data-access-correlation(✅ L-9 新增): `getAttendanceGradeCorrelation`(跨模块委托 classes/users/grades data-access 聚合数据) / `canAccessCorrelationAnalysis`(权限校验) +- Correlation-compute(✅ L-9 新增纯函数, 与 IO 解耦便于单测): `computePearsonCorrelation` / `classifyRiskLevel` / `interpretCorrelation` / `computeCorrelationSummary` +- Components L-9(✅ L-9 新增): `AttendanceGradeCorrelationCard`(散点图 + 风险表格, recharts ScatterChart 风险等级着色) **依赖关系**: -- 依赖:`shared/*`、`@/auth`、`classes`(⚠️ P1-1 未修复:`getClassStudentsForAttendance` 仍直查 `classEnrollments` 表)、`next-intl`(✅ P2-2:Server Action 错误消息 i18n) -- 被依赖:`parent`(⚠️ 跨模块 UI 类型依赖:3 个 parent 组件直接 import `@/modules/attendance/types`)、`app/api/export`(✅ P2-11:通过 `exportAttendanceRecordsToExcel`) +- 依赖:`shared/*`(含 `shared/constants/attendance-status` ✅ 审计新增:状态类型与视觉常量下沉至 shared)、`@/auth`、`classes`(✅ 审计修复:通过 `classes/data-access` 获取班级学生/名称)、`users`(✅ 审计修复:通过 `users/data-access.getUserNamesByIds` 获取姓名)、`parent`(✅ L-2 新增:通过 `parent/data-access.getParentStudentMapByStudentIds` 获取家长-学生映射)、`notifications`(✅ L-2 新增:通过 `sendBatchNotifications` 多渠道分发)、`grades`(✅ L-9 新增:通过 `grades/data-access.getGradeRecords` 获取成绩数据用于关联分析)、`next-intl` +- 被依赖:`parent`(✅ 审计修复:通过 `createAttendanceReadService` 接口消费,不再直接 import attendance 组件;常量改从 `shared/constants/attendance-status` 导入)、`app/api/export`(通过 `exportAttendanceRecordsToExcel`)、`leave-requests`(✅ L-5 新增:审批通过后通过 `syncExcusedFromLeaveRequest` 同步 excused 考勤记录) -**已知问题**(详见 `docs/architecture/audit/attendance-elective-audit-report.md`): -- ✅ P2-1 已修复:`getAttendanceStats` 改用 SQL 聚合查询,消除分页截断导致统计失真 -- ✅ P2-2 已修复:5 个写 Action 错误消息改用 `getTranslations("attendance")` 国际化 -- ✅ P2-3 已修复:新增 `AttendancePageLayout` 组件,admin/teacher 考勤页复用布局 -- ✅ P2-5 已修复:`parent-attendance-calendar.tsx` 增加键盘导航(ARIA grid 模式,方向键/Home/End) -- ✅ P2-6 已修复:`getStudentAttendanceSummary` stats 改用 SQL 聚合 + recentRecords 分页限制 -- ✅ P2-11 已修复:新增 `export.ts`,考勤记录/统计可导出 Excel -- ❌ P0:`getClassStudentsForAttendance` 仍直查 `classEnrollments` 表 -- ❌ P0:6 个读 Action 无调用方(页面绕过 Action 直接调用 data-access),违反三层架构 -- ❌ P0:update/delete Action 缺资源归属校验(教师 A 可修改/删除教师 B 的记录) -- ❌ P0:i18n 完全缺失(`ATTENDANCE_STATUS_LABELS` 硬编码英文,组件中硬编码中文) -- ❌ P0:错误边界完全缺失(5 个角色目录均无 `error.tsx`) -- ⚠️ P1:`computeStats` 未导出,无法单测 -- ⚠️ P1:`attendance-sheet.tsx` 使用 `window.confirm`(与项目 AlertDialog 模式不一致) -- ⚠️ P1:`attendance-sheet.tsx` 存在 `{} as Record` 类型断言 -- ⚠️ P1:`STATUS_OPTIONS`/`SHORTCUTS`/`STYLES` 常量在 types.ts 与 attendance-sheet.tsx 重复定义 +**L-6 节次考勤数据模型**: +- DB:`attendanceRecords.period` enum('morning_reading','morning','afternoon','evening','full_day') nullable(向后兼容 NULL 视为 full_day) +- 索引:`classDatePeriodIdx`(class_id + date + period 复合索引) +- 迁移:`drizzle/0021_attendance_period.sql` +- 类型:`AttendancePeriod`(types.ts),`AttendanceRecord`/`AttendanceListItem`/`AttendanceQueryParams` 新增 period 字段 +- Schema:`AttendancePeriodEnum`(Zod),`RecordAttendanceSchema` 默认 full_day,`UpdateAttendanceSchema` 可选 period +- i18n:`attendance.period.{label,morning_reading,morning,afternoon,evening,full_day,allPeriods}` 中英文翻译 +- 过滤策略:`buildPeriodFilter` 对 full_day 使用 `or(isNull(period), eq(period, "full_day"))` 匹配旧数据 +- 对外统一:mapListItem 中 `r.period ?? "full_day"`,前端展示 null 统一为 full_day + +**L-9 考勤与成绩关联分析**: +- 类型: `AttendanceGradeRiskLevel`(high/medium/low) / `AttendanceGradeCorrelationItem` / `AttendanceGradeCorrelationSummary`(types.ts) +- 纯函数(correlation-compute.ts): `computePearsonCorrelation`(Pearson r) / `classifyRiskLevel`(high=低出勤+低分, medium=中低出勤+中低分, low=其余) / `interpretCorrelation` / `computeCorrelationSummary` +- 数据访问(data-access-correlation.ts): `getAttendanceGradeCorrelation` 跨模块委托 classes/users/grades data-access; `canAccessCorrelationAnalysis` scope 校验 +- UI(attendance-grade-correlation-card.tsx): recharts ScatterChart 散点图, 风险等级着色(red/amber/emerald) +- 集成: admin/attendance/page.tsx 指定班级时显示 +- i18n: attendance.correlation.* 中英文翻译 + +**已知问题**(详见 `docs/architecture/audit/attendance-audit-report.md`): +- ✅ P0-1 已修复:teacher/sheet、teacher/stats 页面增加 `requirePermission(ATTENDANCE_READ)` +- ✅ P0-2 已修复:teacher 分页使用后端返回的 `totalPages` +- ✅ P0-3 已修复:`safeParseDate` fieldName 改为中性 key,前端通过 `resolveActionError` 按 errorCode 本地化 +- ✅ P0-4 已修复:error.tsx 重试按钮改用 `t("actions.retry")` +- ✅ P0-5 已修复:data-access 移除跨模块 JOIN,改为委托 data-access +- ✅ P1-1 已修复:teacher/student 主页面增加 `requirePermission(ATTENDANCE_READ)` +- ✅ P1-2 已修复:parent 通过 `AttendanceReadService` 接口消费 +- ✅ P1-3 已修复:parent-attendance-calendar 改从 `shared/constants/attendance-status` 导入 +- ✅ P1-6 已修复:export.ts 类型守卫替代 `as` 断言 +- ✅ P1-7 已修复:calendar 使用 `useLocale()` 替代硬编码 locale +- ✅ P1-8 已修复:teacher 子路由新增 error.tsx +- ✅ P1-9 已修复:常量统一从 `constants.ts`(re-export shared)导入 +- ✅ P1-10 已修复:4 个 error.tsx 抽取为 shared `RouteErrorBoundary` +- ✅ P1-11 已修复:`getClassAttendanceStats` 改用 SQL 聚合 +- ✅ P1-12 已修复:新增 `AttendanceDataService` 接口 + Context 注入 +- ✅ P2-1 已修复:删除按钮 `usePermission` 控制显隐 +- ✅ P2-5 已修复:stats 数据结构统一为 `AttendanceStats` +- ✅ P2-6 已修复:导出 `AttendanceRepository` 接口类型 +- ✅ P2-7 已修复:keydown 限容器内 + 排除 Select/contenteditable +- ✅ P2-8 已修复:parent-attendance-warning/rate-card 改为 RSC +- ✅ P2-9 已修复:statusCounts 使用 `useMemo` - ✅ stats 独立拆分为 `data-access-stats.ts`(拆分范例) - ✅ DataScope 完整接入 6 种 scope 类型 - ✅ actions 层无直接 DB 访问 +- ✅ Server Action 均有 `trackEvent` 埋点 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `actions.ts` | 271 | 10 个 Server Action(含权限校验、Zod 校验;P2-2:错误消息 i18n 化) | -| `data-access.ts` | 340 | 考勤 CRUD + 班级学生查询 + 规则 upsert + 总览统计(P2-1:`getAttendanceStats` 改 SQL 聚合) | -| `data-access-stats.ts` | 180 | 学生/班级考勤汇总(P2-6:stats 改 SQL 聚合 + recentRecords 分页) | -| `export.ts` | 90 | Excel 导出(P2-11 新增:考勤明细 + 统计汇总双 Sheet) | -| `schema.ts` | 43 | Zod 校验(5 个 schema) | -| `types.ts` | 103 | 类型定义 + 状态标签/颜色常量(硬编码英文) | -| `components/attendance-sheet.tsx` | 353 | 批量点名表单(键盘快捷键、状态按钮组) | -| `components/attendance-record-list.tsx` | 130 | 考勤记录列表 + 删除对话框 | -| `components/attendance-filters.tsx` | 97 | URL 同步筛选器(班级/状态/日期) | -| `components/attendance-stats-card.tsx` | 81 | 单卡片统计(8 指标) | -| `components/attendance-stats-cards.tsx` | 80 | 管理员总览 6 卡片网格(硬编码中文) | +| `actions.ts` | 258 | 5 个写 Server Action(权限校验 + 归属校验 + Zod + 埋点;✅ L-6 读取 period) | +| `data-access.ts` | ~410 | 考勤 CRUD + 规则 upsert + 总览统计(SQL 聚合 + 委托 data-access;✅ L-6 period 过滤/写入/更新) | +| `data-access-stats.ts` | ~230 | 学生/班级考勤汇总(SQL 聚合 + 委托 data-access;✅ L-6 period 映射) | +| `export.ts` | 93 | Excel 导出(类型守卫 + i18n 列头) | +| `schema.ts` | ~60 | Zod 校验(6 个 schema + ✅ L-6 AttendancePeriodEnum) | +| `types.ts` | ~95 | 类型定义(状态常量已迁移至 shared;✅ L-6 AttendancePeriod + period 字段) | +| `constants.ts` | 19 | 重导出 `shared/constants/attendance-status` | +| `services/types.ts` | 166 | 服务接口抽象(AttendanceDataService/Repository) | +| `services/attendance-context.tsx` | 46 | React Context 注入(AttendanceProvider + hook) | +| `services/attendance-data-service.ts` | 162 | 服务实现 + 工厂函数 | +| `components/attendance-sheet.tsx` | ~480 | 批量点名表单(AlertDialog + 容器内 keydown + useMemo;✅ L-6 节次选择器) | +| `components/attendance-record-list.tsx` | ~165 | 记录列表 + 删除对话框(权限控制 + resolveActionError;✅ L-6 节次列) | +| `components/attendance-filters.tsx` | 94 | URL 同步筛选器 | +| `components/attendance-stats-card.tsx` | 82 | 单卡片统计(8 指标) | +| `components/attendance-stats-cards.tsx` | 83 | 管理员总览 6 卡片网格(统一 AttendanceStats) | | `components/attendance-stats-class-selector.tsx` | 27 | 班级筛选 ChipNav | -| `components/attendance-rules-form.tsx` | 148 | 考勤规则配置表单 | -| `components/student-attendance-view.tsx` | 104 | 学生/家长视图(统计 + 最近记录) | -| `components/attendance-page-layout.tsx` | 38 | 页面布局骨架(P2-3 新增:header/stats/filters/children 插槽) | +| `components/attendance-rules-form.tsx` | 155 | 规则配置表单(resolveActionError) | +| `components/student-attendance-view.tsx` | 111 | 学生只读视图 | +| `components/attendance-page-layout.tsx` | 38 | 页面布局骨架(插槽模式) | +| `correlation-compute.ts` | ~90 | L-9 纯函数: Pearson 相关系数 + 风险分级 + 汇总 (与 IO 解耦) | +| `data-access-correlation.ts` | ~130 | L-9 跨模块关联分析: 委托 classes/users/grades data-access | +| `components/attendance-grade-correlation-card.tsx` | ~220 | L-9 散点图 + 风险表格 (recharts ScatterChart) | --- @@ -1085,16 +1412,21 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" ## 2.12 dashboard(仪表盘模块) -**职责**:管理员/教师/学生/家长仪表盘数据聚合 + 权限校验 + i18n + 纯逻辑工具函数。 +**职责**:管理员/教师/学生/家长仪表盘数据聚合 + 权限校验 + i18n + 纯逻辑工具函数 + 数据服务接口抽象 + 配置驱动 Widget 渲染 + 监控埋点接口 + 时间范围筛选 + 数据对比 + 自定义仪表盘 + 通知集成 + 实时更新 + 数据导出 + 移动端优化。 **导出函数**: - Actions:`getAdminDashboardAction` / `getTeacherDashboardAction` / `getStudentDashboardAction` / `getParentDashboardAction`(均调用 `requirePermission()` 校验对应 `DASHBOARD_*_READ` 权限) -- Data-access:`getAdminDashboardData`(并行调用 6 个模块 stats 函数) -- Lib 纯函数:`toWeekday` / `countStudentAssignments` / `sortUpcomingAssignments` / `filterTodaySchedule` / `computeTeacherMetrics` / `getGreetingKey` -- Components:`AdminDashboardView` / `TeacherDashboardView` / `StudentDashboard` / `UserGrowthChart` / `DashboardGreetingHeader`(共享问候头部,V2 抽象)/ `DashboardSection`(分区 Error Boundary + Suspense + 骨架屏)(均接入 next-intl i18n) +- Data-access:`getAdminDashboardData`(并行调用 6 个模块 stats 函数,V4 添加趋势数据 TODO 注释) +- Streams:`getAdminDashboardStreams`(V4 新增:返回各分区独立 Promise 供 React `use()` 流式消费) +- Lib 纯函数:`toWeekday` / `countStudentAssignments` / `sortUpcomingAssignments` / `filterTodaySchedule`(V4 修复:移除 `as T[]` 断言,改为非泛型函数)/ `computeTeacherMetrics` / `getGreetingKey` / `timeToMinutes`(V4 新增)/ `getScheduleStatus`(V4 新增)/ `getDueUrgency`(V4 新增)/ `getActionLabelKey`(V4 新增)/ `getActionVariant`(V4 新增)/ `computeComparison`(L3 新增:数据对比计算)/ `toCSV`(L7 新增:CSV 导出)/ `downloadFile`(L7 新增:浏览器下载)/ `exportDashboardCSV`(L7 新增:仪表盘 CSV 导出便捷方法) +- Services(V4 新增):`DashboardService` 接口(抽象数据依赖,含 4 个角色方法签名)/ `DashboardAnalytics` 接口(监控埋点:Widget 点击 / 空状态 / 错误重试 / 页面停留)/ `DashboardServiceProvider`(React Context 依赖注入)/ `useDashboardService` / `useDashboardAnalytics` +- Config(V4 新增):`ADMIN_WIDGET_CONFIG` / `TEACHER_WIDGET_CONFIG` / `STUDENT_WIDGET_CONFIG` / `PARENT_WIDGET_CONFIG` / `getWidgetConfig`(配置驱动 Widget 布局,新增角色只需修改配置) +- Hooks(L4/L6 新增):`useDashboardPreferences`(L4 自定义仪表盘偏好,localStorage 持久化)/ `useDashboardRealtime`(L6 SSE 实时更新,指数退避重连) +- Components:`AdminDashboardView` / `TeacherDashboardView` / `StudentDashboard` / `ParentDashboard`(V4 P1-4 从 parent 模块迁移,使用 slots 组合注入)/ `UserGrowthChart` / `DashboardGreetingHeader`(共享问候头部,V2 抽象,V4 支持空用户名 fallback)/ `DashboardSection`(分区 Error Boundary + Suspense + 骨架屏,V4 P3-1 新增 ariaLabel prop 支持键盘导航)/ `DashboardTimeRangeFilter`(L2 时间范围筛选)/ `ComparisonBadge`(L3 数据对比徽章)/ `DashboardNotificationWidget`(L5 通知中心 Widget)/ `DashboardResponsiveLayout`(L8 移动端响应式布局)/ `MobileSwipeContainer`(L8 移动端水平滑动)/ `DesktopGrid`(L8 桌面端网格) +- Types(V4 新增):`DashboardRole` / `WidgetSkeletonVariant` / `DashboardWidgetConfig` / `DashboardLayoutConfig` / `ComparisonTrend` / `ComparisonResult`(L3)/ `TimeRange`(L2)/ `ConnectionStatus`(L6)/ `DashboardNotificationItem`(L5)/ `ExportRow` / `ExportColumn`(L7) **依赖关系**: -- 依赖:`shared/*`、`@/auth`、`classes`(通过 data-access)、`homework`(通过 data-access)、`users`(通过 data-access)、`parent`(通过 data-access.getParentDashboardData)、`textbooks`/`questions`/`exams`(通过各模块 dashboard stats 函数)、`recharts`、`next-intl` +- 依赖:`shared/*`、`@/auth`、`classes`(通过 data-access)、`homework`(通过 data-access)、`users`(通过 data-access)、`parent`(通过 data-access.getParentDashboardData + 组件 slots 注入 ChildCard/ParentAttentionBanner)、`textbooks`/`questions`/`exams`(通过各模块 dashboard stats 函数)、`recharts`、`next-intl` - 被依赖:无 **权限点**: @@ -1110,26 +1442,57 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P0 已修复(2026-06-22):所有仪表盘组件接入 next-intl(`useTranslations` / `getTranslations`),翻译文件 `messages/{zh-CN,en}/dashboard.json` - ✅ P1 已修复(2026-06-22):业务逻辑(weekday 转换、作业统计、教师指标计算、问候语时段)抽取至 `lib/dashboard-utils.ts` 纯函数,与 UI 分离 - ✅ P2 已修复(2026-06-22):新增 `components/dashboard-section.tsx`,每个独立数据区块用 Error Boundary + Suspense + 骨架屏包裹,单区块崩溃/加载不波及整页(5 种骨架变体:stats/card/chart/table/list) -- ℹ️ V1 新增:`AdminDashboardData` 类型含 `userGrowth`/`homeworkTrend` 字段,`data-access.ts` 当前返回空数组占位,待后续接入真实统计 -- ℹ️ parent 仪表盘组件仍位于 `modules/parent/components/parent-dashboard.tsx`,通过 `dashboard/actions.getParentDashboardAction` 调用(架构决策:保留在 parent 模块以避免移动文件破坏其他 import) -- ✅ P0 已修复(2026-06-22 V2):10 个子组件 i18n 遗漏全部补齐(teacher-quick-actions / teacher-classes-card / teacher-homework-card / teacher-schedule / recent-submissions / teacher-grade-trends / student-grades-card / student-today-schedule-card / student-upcoming-assignments-card / admin-dashboard),新增 ~50 个翻译键 +- ℹ️ V1 新增:`AdminDashboardData` 类型含 `userGrowth`/`homeworkTrend` 字段,`data-access.ts` 当前返回空数组占位(V4 P2-4 添加 TODO 注释),待后续接入真实统计 +- ✅ V4 P1-4 已修复:`ParentDashboard` 组件从 `modules/parent/components/` 迁移至 `modules/dashboard/components/parent-dashboard/`,使用 slots 组合注入 ChildCard 和 ParentAttentionBanner,parent 模块仅保留数据访问 +- ✅ P0 已修复(2026-06-22 V2):10 个子组件 i18n 遗漏全部补齐,新增 ~50 个翻译键 - ✅ P1 已修复(2026-06-22 V2):抽象共享组件 `DashboardGreetingHeader`,消除 teacher/student 头部 90% 重复代码 -- ✅ P2 已修复(2026-06-22 V2):为 6 个纯函数添加 31 个单元测试(`tests/integration/dashboard/dashboard-utils.test.ts`) -- ✅ P2 已修复(2026-06-22 V2):a11y 增强 — admin 表格 ``、teacher/student 视图语义化标签(`
` / `
` / `