# Next_Edu 架构影响地图 > 重写日期:2026-06-17 > 目标:一次阅读即可直观理解整个项目架构 > 规则:源码修改后须同步更新本文档与 `005_architecture_data.json` > 审查依据:`docs/architecture/audit/` 下 4 份审查报告 --- ## 目录 - [第一部分:全局架构概览](#第一部分全局架构概览) - [1.1 分层架构图](#11-分层架构图) - [1.2 模块依赖关系图](#12-模块依赖关系图) - [1.3 数据流向图(考试流程)](#13-数据流向图考试流程) - [1.4 核心调用链路](#14-核心调用链路) - [第二部分:模块清单](#第二部分模块清单) - [第三部分:已知架构问题和技术债](#第三部分已知架构问题和技术债) - [附录 A:模块间依赖矩阵](#附录-a模块间依赖矩阵) - [附录 B:关键参数影响链](#附录-b关键参数影响链) - [附录 C:核心函数签名索引](#附录-c核心函数签名索引) - [相关文档](#相关文档) --- # 第一部分:全局架构概览 ## 1.1 分层架构图 项目采用**严格三层架构**,依赖方向必须单向:`app → modules → shared`。 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ app/ (路由层) │ │ ───────────────────────────────────────────────────────────────── │ │ (auth)/ login / register / privacy / terms │ │ (dashboard)/ admin / teacher / student / parent / management │ │ api/ REST API (auth, ai, upload, files, search, ...) │ └──────────────────────────────┬──────────────────────────────────────┘ │ 调用 Server Actions / data-access ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ modules/ (业务模块层) │ │ ───────────────────────────────────────────────────────────────── │ │ 核心教学: exams · homework · questions · textbooks · grades │ │ · lesson-preparation │ │ 教学管理: classes · school · scheduling · attendance · course-plans│ │ 用户与沟通:users · messaging · notifications · parent · audit │ │ 扩展功能: elective · proctoring · diagnostic · dashboard │ │ 权限管理: rbac │ │ 其他: announcements · files · settings · auth · layout · student│ │ │ │ 每个模块标准结构:actions.ts (编排) → data-access.ts (DB) → schema.ts│ └──────────────────────────────┬──────────────────────────────────────┘ │ 依赖 ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ shared/ (基础设施层) │ │ ───────────────────────────────────────────────────────────────── │ │ db/ schema.ts (54 张表) + relations.ts + index.ts │ │ lib/ auth-guard · permissions · ai · audit-logger · │ │ change-logger · login-logger · password-policy · │ │ rate-limit · excel · file-storage · ... │ │ hooks/ use-permission · use-aria-live · ... │ │ components/ ui/ (shadcn) · a11y/ · global-search · ... │ │ types/ permissions · action-state │ └─────────────────────────────────────────────────────────────────────┘ ▲ │ 反向依赖(违规,见 1.2) ┌──────────────────────────────┴──────────────────────────────────────┐ │ 根模块:src/auth.ts (NextAuth 配置) · src/proxy.ts (中间件) │ │ ✅ shared/lib/{audit-logger, change-logger, auth-guard} 已通过 │ │ shared/lib/session.ts 单一入口获取 session,不再直依赖 @/auth │ │ (详见第三部分 P0-2) │ └─────────────────────────────────────────────────────────────────────┘ ``` **分层规则**: - `app/` 只能调用 `modules/` 的 Server Actions 和 data-access,不直接访问 DB - `modules/` 之间通过对方 data-access 通信,不直接查询对方表 - `shared/` 是被依赖方,不应反向依赖 `app/` 或 `modules/` - `src/auth.ts` 和 `src/proxy.ts` 位于根目录,属于应用层 --- ## 1.1.1 M7 移动端 PWA 支持(2026-07-01 新增) - **PWA 清单**:新增 `src/app/manifest.ts`(Next.js App Router 元数据路由),生成 `/manifest.webmanifest`,声明 `name`/`short_name=NextEdu`/`start_url=/`/`display=standalone`/`theme_color=#1976d2`/`icons=/icon.svg`。 - **根布局**:`src/app/layout.tsx` 新增 `viewport` 导出(`width=device-width`、`initialScale=1`、`maximumScale=5`、`userScalable=true`、`themeColor` 按明暗主题切换)+ `metadata` 新增 `manifest`/`appleWebApp` 配置,支持添加到主屏幕与独立显示。 - **触控友好**:`lesson-preparation` 模块 `node-editor.tsx` 与 `lesson-plan-readonly-view.tsx` 的 `` 组件新增 `panOnDrag`/`zoomOnPinch`/`zoomOnDoubleClick={false}`/`selectionOnDrag={false}` 属性,防止移动端双击误缩放、优化触控拖拽体验。 --- ## 1.1.2 设计令牌体系(2026-07-04 新增) 项目采用**双层令牌架构**(Primitive + Semantic),Tailwind v4 通过 `@theme inline` 暴露为业务可用的类。 ### 文件分布 ``` src/app/ ├─ globals.css # @import 入口 + base layer + 组件硬编码样式 └─ styles/tokens/ ├─ primitive.css # Layer 1: 原始色板(zinc/stone/indigo)、字号/间距/阴影/字体家族阶梯 ├─ semantic-light.css # Layer 2: :root 语义令牌(shadcn + 扩展 + chart/sidebar + diff/graph) ├─ semantic-dark.css # Layer 2: .dark 语义令牌(完整暗色) ├─ lesson-preparation.css # --lp-* 令牌(明暗双份,HEX→HSL) ├─ tailwind-theme.css # @theme inline 暴露所有令牌为 Tailwind 类 └─ index.css # 汇总 @import 入口 ``` ### 令牌分类 | 分类 | 命名 | 暴露为 Tailwind 类 | |---|---|---| | shadcn 标准 | `--background`/`--foreground`/`--primary`/... | `bg-background`/`text-foreground`/`bg-primary`/... | | 扩展层级 | `--background-elevated`/`--text-secondary`/`--border-strong`/... | `bg-background-elevated`/`text-text-secondary`/... | | chart | `--chart-1~5` | `bg-chart-1`/... | | sidebar | `--sidebar-*` | `bg-sidebar`/`text-sidebar-foreground`/... | | lp-* 命名空间 | `--lp-paper`/`--lp-inline-node-text`/`--lp-dot-*` | `bg-lp-paper`/`text-lp-inline-node-text`/... | | 字体家族 | `--font-family-sans/serif/mono` | `font-sans`/`font-serif`/`font-mono` | | 字号阶梯 | `--font-size-1~9`(含 `--font-size-0` 微字号) | `text-size-1`~`text-size-9` | | 间距阶梯 | `--space-0~18` | `w-[length:var(--space-N)]` 等 | | 阴影阶梯 | `--shadow-1~6` | `shadow-1`~`shadow-6` | | 圆角阶梯 | `--radius-sm/md/lg/xl/2xl/full` | `rounded-sm`/`rounded-md`/... | | 动效 | `--duration-*`/`--ease-*` | `duration-fast`/`ease-in`/... | | z-index | `--z-dropdown/sticky/modal/popover/toast` | `z-dropdown`/`z-modal`/... | ### 强制约束 - ESLint 规则 `no-restricted-syntax` 禁止 `#hex` 字面量 - 自定义规则 `design-tokens/no-hardcoded-fonts` 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符) - 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML)、`manifest.ts`(PWA) - 任意值 `w-[Npx]` 等需通过 `// eslint-disable-next-line no-restricted-syntax -- ` 注释豁免(如 Dialog 固定宽度、图表画布、loading.tsx 骨架等) --- ## 1.1.3 Phase 4.2 + Phase 4.5 状态管理优化(2026-07-05 新增) V5 状态管理统一的后续阶段,针对两类高频性能问题进行细粒度优化:**Zustand 整体订阅导致的级联 re-render** 与 **useState + 等待 Server Action 模式导致的 UI 不即时反馈**。 ### Phase 4.2:lesson-plan-editor Zustand selector 拆分 `useLessonPlanEditor` 是组合了 5 个 slice 的 zustand store。原消费方使用 `const editor = useLessonPlanEditor()` 整体订阅,导致任一字段变化都触发整体 re-render。 | 文件 | 修改要点 | |------|---------| | `modules/lesson-preparation/components/lesson-plan-editor.tsx` | 拆分为 9 个细粒度 selector(`isDirty`/`isOnline`/`doc`/`title`/`isSaving`/`saveError`/`lastSavedAt`/`setTitle` + `canUndo`/`canRedo` 派生 boolean),移除 `editor.canUndo()` 重复调用,更新 `useEffect` 依赖与 `printablePlan` useMemo 使用拆分后的变量 | | `modules/lesson-preparation/components/blocks/text-study-block.tsx` | `const { updateNode } = useLessonPlanEditor()` → `useLessonPlanEditor((s) => s.updateNode)` | | `modules/lesson-preparation/components/blocks/exercise-block.tsx` | 拆为 `useLessonPlanEditor((s) => s.updateNode)` + `useLessonPlanEditor((s) => s.planId)` | | `modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx` | 拆为 7 个细粒度 selector(`toggleExpand`/`expandedNodeIds`/`updateNode`/`removeNode`/`addAnchor`/`duplicateNode`/`nodes`),特别地将 `doc` 改为 `s.doc.nodes` 更精细订阅(`addAnchor` 等仅修改 anchors 不会触发本组件 re-render) | **关键决策**: - `canUndo`/`canRedo` 改为 `useLessonPlanEditor((s) => s.canUndo())` 订阅派生 boolean,避免每次渲染时调用函数 - 不使用 `useShallow` 包装多字段 selector,因为单字段 selector 已经够细,且避免了 `useShallow` 自身的浅比较开销 - 函数引用(`setTitle`/`updateNode` 等)天然稳定,无需 `useShallow` ### Phase 4.5:useOptimistic + useTransition 乐观更新 将 4 个组件从 `useState` + 等待 Server Action 模式迁移到 React 19 `useOptimistic` + `useTransition`,自动管理乐观状态与回滚。 | 文件 | 修改要点 | |------|---------| | `modules/messaging/components/message-detail.tsx` | 星标 `useState` → `useOptimistic` + `useTransition`。`handleToggleStar` 在 `startStarTransition` 内 `addOptimisticStarred(nextStarred)` + `await toggleMessageStarAction` + `router.refresh()`。JSX 中所有 `isStarred` 引用改为 `optimisticIsStarred`(Button variant/aria-pressed/Star className/星标徽章) | | `modules/messaging/components/message-list.tsx` | 星标 `starredOverride` useState → `useOptimistic>`(key=messageId, value=isStarred)。`getIsStarred` 改为 `optimisticStarredMap.get(m.id)`。撤回 `recalledOverride` 仍保留 useState(不需要乐观更新) | | `modules/grades/components/grade-record-list.tsx` | 编辑保存 `isSaving` useState → `useOptimistic` + `useTransition`。`handleEditSave` 构造 `optimisticRecord` 在 `startSaveTransition` 内 `addOptimisticRecord` + `await updateGradeRecordAction` + `router.refresh()`。两处 `records.map` 改为 `optimisticRecords.map`(桌面表格 + 移动端卡片视图)即时显示乐观记录 | | `modules/attendance/components/attendance-sheet.tsx` | `isSubmitting` useState → `useOptimistic`。保留 `
` 模式 + `useFormStatus` SubmitButton(progressive enhancement)。`setOptimisticSubmitting(true)` 在 form action 内调用,action 完成后 `optimisticSubmitting` 自动恢复 false,移除 `finally` 块 | **关键决策**: - attendance-sheet 保留 form action 模式而非改用 `useTransition` 包裹,因为 `useFormStatus().pending` 依赖 form action 的 promise 状态,若用 `startTransition` 包裹会导致 form action 立即返回、`useFormStatus` 不可靠 - `useOptimistic` 的 `addOptimistic` 必须在 transition 或 action 内调用(form action 也算 action) - 成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值 - 不为撤回(recall)操作使用 useOptimistic,因为撤回需要根据服务端返回判断是否成功(2 分钟窗口校验),失败时不应乐观显示已撤回 ### 验证结果 - `npx tsc --noEmit`:零新增错误(pre-existing 错误:`providers.tsx` 缺少 `@tanstack/react-query-devtools`、`web-vitals-reporter.tsx` Metric 导出、`homework/data-access-write.ts` MySqlTransaction.run、`paper-context-menu.tsx` 第 58 行 `duplicateNode` 调用签名不匹配——此为 pre-existing bug,`duplicateNode(id: string)` 只接受 1 参数但调用方传 2 个、`print-view.tsx` Object.entries 类型推断) - `npm run lint`:零新增错误(8 个 modified 文件均未出现在 lint 输出中,pre-existing 3 errors + 13 warnings 均位于未修改文件:`scripts/check-db-state.mjs`、`scripts/seed-grade5-chinese.ts`、`practice-result-view.tsx`、`exam-rich-form.tsx`、`use-exam-preview-state.ts`、`use-exam-preview-tasks.ts`、`homework/data-access-write.ts`、`unread-message-badge.tsx`) --- ## 1.1.4 Phase 3.6 + Phase 3.7 + Phase 3.8 DB 性能优化(2026-07-05 新增) 针对数据库查询性能的三阶段优化:**FULLTEXT 全文检索**、**公告 fan-out 批量化**、**高 LIMIT 默认值调低**。 ### Phase 3.6:searchQuestions 改 FULLTEXT 索引 将 questions 表的模糊查询从 `LIKE '%xxx%'` 升级为 MySQL FULLTEXT 索引 + `MATCH AGAINST ... IN BOOLEAN MODE`,提升大表检索性能。 | 文件 | 修改要点 | |------|---------| | `src/shared/db/schema.ts` | 添加 `import { sql } from "drizzle-orm"`;在 `questions` 表添加 STORED 生成列 `contentText`(`text("content_text").generatedAlwaysAs(sql\`CAST(content AS CHAR)\`, { mode: "stored" })`);补齐 5 个先前未同步的索引(typeDifficultyIdx/statusCreatedIdx/creatorIdx/examStatusIdx/submittedAtIdx) | | `drizzle/0001_questions_fulltext_search.sql` | drizzle-kit 生成的迁移:CREATE TABLE lesson_plan_schedules + content_text 生成列 + 8 个索引;末尾手动追加 `CREATE FULLTEXT INDEX questions_content_text_ft_idx ON questions (content_text)` | | `drizzle/meta/0001_snapshot.json` | questions 表 indexes 节点添加 `questions_content_text_ft_idx` 声明(type: "fulltext") | | `src/modules/search/data-access.ts` | `searchQuestions(kw, limit?)` → `searchQuestions(q, limit?)`;新增 `toBooleanModeQuery` 工具函数将原始查询转为 `+word1* +word2*` 格式;查询改用 `MATCH(questions.contentText) AGAINST(? IN BOOLEAN MODE)` | | `src/app/api/search/route.ts` | 本地 `searchQuestions` 函数同步改用 MATCH AGAINST + `toBooleanModeQuery`;调用点从 `searchQuestions(kw, pageSize)` 改为 `searchQuestions(q, pageSize)` | **关键决策**: - 使用 STORED 生成列 `content_text` 而非 VIRTUAL,因为 FULLTEXT 索引在 MySQL 5.7+ 仅支持 STORED 生成列 - `CAST(content AS CHAR)` 将 JSON 字段序列化为纯文本,避免 MySQL FULLTEXT 无法直接索引 JSON 列的限制 - drizzle-kit 无法生成 FULLTEXT 索引声明,迁移文件中需手动追加 `CREATE FULLTEXT INDEX` 语句,并同步更新 snapshot 的 indexes 节点 - `toBooleanModeQuery` 转义 BOOLEAN MODE 特殊字符(+ - < > ( ) ~ * " ')避免用户输入破坏查询语法 - 保留 textbooks/exams/announcements 的 LIKE 模糊匹配(FULLTEXT 索引需 InnoDB + utf8mb4,且这些表查询量较小) ### Phase 3.7:getAllUserIds 加 LIMIT + 公告 fan-out 批量化 | 文件 | 修改要点 | |------|---------| | `src/modules/users/data-access.ts` | `getAllUserIds()` → `getAllUserIds(limit=1000, offset=0)`,添加默认 LIMIT 防止 OOM + 分页参数支持 | | `src/modules/announcements/data-access.ts` | `resolveAnnouncementTargetUserIds` school 分支改为分页循环遍历 `getAllUserIds(PAGE_SIZE, offset)`(PAGE_SIZE=1000),聚合所有用户 ID | | `src/modules/notifications/data-access.ts` | 新增 `createNotifications(items)` 批量 INSERT 函数(单次 `db.insert().values([...])`),返回生成的 ID 数组 | | `src/modules/notifications/channels/in-app-channel.ts` | `InAppChannelSender.sendBatch` 从 `Promise.all(items.map(send))` 改为调用 `createNotifications(inputs)` 单次批量 INSERT;预校验 recipient/userId 一致性;失败时整体回滚并返回失败结果 | | `src/modules/notifications/dispatcher.ts` | `sendBatchNotifications` 重构:并行获取所有用户偏好+联系方式 → 收集 in_app 渠道 payload 单次 `sendBatch` → 其他渠道保持 per-payload 并行 → 按 payload 聚合日志 | | `src/modules/notifications/index.ts` | 导出 `createNotifications` | **关键决策**: - `getAllUserIds` 默认 LIMIT 1000 是平衡单次查询压力与 fan-out 完整性的折中值;调用方通过分页遍历获取全部用户 - `createNotifications` 使用 drizzle 的 `db.insert().values(array)` 一次 INSERT 多行,相比 N 次单行 INSERT 显著降低网络往返 - in_app 渠道批量化,其他渠道(sms/email/wechat)保持原并行模式,因为这些渠道的发送成本主要在网络 IO 而非 DB 写入 - 批量 INSERT 失败时整体回滚(drizzle 默认事务行为),每条记录返回失败结果 ### Phase 3.8:调低高 LIMIT 默认值 在 `src/modules/*/data-access*.ts` 中搜索 LIMIT > 100 的默认值并调低至 ≤100(保留分页场景)。 | 文件 | 原 LIMIT | 新 LIMIT | 函数 | |------|---------|---------|------| | `src/modules/attendance/data-access-correlation.ts` | 5000 | 100 | `getAttendanceGradeCorrelation` 调用 `getGradeRecords` | | `src/modules/adaptive-practice/data-access-strategy.ts` | 1000 | 100 | `getStudentAnsweredQuestionIds` | | `src/modules/questions/data-access.ts` | 1000 | 100 | `exportQuestions` | **未修改(不在 data-access*.ts 范围内)**: - `src/modules/homework/stats-service.ts:276`(limit: 200)和 `:345`(limit: 5000)不在 `data-access*.ts` glob 范围内,本次未修改 ### 验证结果 - `npx tsc --noEmit`:零新增错误(pre-existing 错误:`providers.tsx`、`web-vitals-reporter.tsx`、`homework/data-access-write.ts`、`paper-context-menu.tsx`、`print-view.tsx` 均为未修改文件) - `npm run lint`:零新增错误(16 个 pre-existing problems 均位于未修改文件:`scripts/check-db-state.mjs`、`scripts/seed-grade5-chinese.ts`、`practice-result-view.tsx`、`exam-rich-form.tsx`、`use-exam-preview-state.ts`、`use-exam-preview-tasks.ts`、`homework/data-access-write.ts`、`unread-message-badge.tsx`) --- ## 1.1.5 性能预算重构专项(2026-07-05 新增 + 2026-07-07 补漏) > 完整审计报告见 `docs/architecture/audit/performance-budget-audit-report.md`。 > 本章节记录本次重构对架构产生的关键变更,以便后续维护时定位。 > 2026-07-07 补漏:Phase 1 配置基线(next.config.ts optimizePackageImports + serverExternalPackages 扩展 + WebVitalsReporter 注入闭环)+ Phase 2 实际动态导入实现(TipTap 3 处 + ReactFlow 1 处之前仅文件拆分未真正 dynamic import,本次补齐 next/dynamic ssr:false)+ chart.tsx 具名导入 + 12 个 recharts 图表 CHART_MARGIN 提取 + layout.tsx Promise.all + metadata 增强。 ### 影响范围概览 | Phase | 改造内容 | 关键文件 | |-------|---------|---------| | 1.1 | next.config.ts optimizePackageImports | `next.config.ts` 新增 `experimental.optimizePackageImports` 覆盖 lucide-react/recharts/@xyflow/react/@tiptap/* /@radix-ui/*/date-fns | | 1.2 | serverExternalPackages 扩展 | `next.config.ts` 追加 `tencentcloud-sdk-nodejs` + `exceljs`(原仅 `mysql2`) | | 1.3 | Web Vitals 上报闭环 | `src/app/providers.tsx` 注入 ``(之前组件存在但未注入,链路断裂) | | 1.4 | Lighthouse CI | `.gitea/workflows/lighthouse.yml` + `lighthouserc.json`(PR + 每日凌晨 3 点采样,LCP/CLS error 级) | | 2.1 | tiptap 动态导入(3 处) | `shared/components/ui/rich-text-editor.tsx` + `rich-text-editor-inner.tsx`;`modules/exams/editor/exam-rich-editor.tsx` + `exam-rich-editor-inner.tsx`(forwardRef wrapper);`modules/lesson-preparation/components/blocks/rich-text-block.tsx` + `rich-text-block-inner.tsx` | | 2.2 | ReactFlow 动态导入 | `modules/textbooks/components/knowledge-graph.tsx` 改用 `next/dynamic` 加载 `knowledge-graph-inner.tsx`,ReactFlowProvider 留外层同步壳 | | 2.3 | exams/editor barrel 拆分 | 新增 `modules/exams/editor/types.ts` 作为纯类型入口,供 server-only 消费方引用 | | 2.4 | chart.tsx 具名导入 + status-badge 移除 use client | `shared/components/ui/chart.tsx` 改 `import { ResponsiveContainer, Tooltip, Legend } from "recharts"` 替代 `import * as RechartsPrimitive`;`status-badge.tsx` 移除 `"use client"`(纯展示组件) | | 2.5 | 4 个角色根路由 loading.tsx | 新增 `teacher/loading.tsx` + `student/loading.tsx` + `parent/loading.tsx`(admin 已有) | | 3.1 | unstable_cache 评估 | 评估后决定不引入,与项目现有 React `cache()` 模式一致 | | 3.2 | auth 函数 cache 包装 | `shared/lib/session.ts:getSession` + `shared/lib/auth-guard.ts:resolveDataScope` + `getAuthContext` + `modules/classes/data-access.ts:getSessionTeacherId` 全部用 React `cache()` 包装,`getSessionTeacherId` 改走 `getAuthContext()` | | 3.3 | copyCoursePlanToClasses 批量化 | `modules/course-plans/data-access.ts:347` 改为单事务 + 2 次批量 INSERT,O(N*M) → O(1) | | 3.4 | gradeHomeworkAnswers 批量 UPDATE | `modules/homework/data-access-write.ts:356` 改为单次 `UPDATE ... CASE WHEN` + `sql.join`,替代 N 次串行 UPDATE | | 3.5 | exams 表索引补齐 | `shared/db/schema.ts` 新增 5 个索引,迁移 `drizzle/0001_questions_fulltext_search.sql` | | 3.6 | searchQuestions FULLTEXT | `shared/db/schema.ts` 新增 `contentText` STORED 生成列 + `questions_content_text_ft_idx` FULLTEXT 索引;`search/data-access.ts` 改用 `MATCH(...) AGAINST(... IN BOOLEAN MODE)` | | 3.7 | 公告 fan-out 批量化 | `getAllUserIds` 加分页参数;新增 `notifications/data-access.ts:createNotifications` 批量 INSERT;`dispatcher.sendBatchNotifications` 重构为 in_app 单次批量 + 其他渠道并行 | | 3.8 | 高 LIMIT 调低 | attendance 5000→100、adaptive-practice 1000→100、questions 1000→100 | | 4.1 | SidebarProvider memoize | `modules/layout/components/sidebar-provider.tsx` resize debounce 200ms + useMemo context + useCallback toggleSidebar | | 4.2 | lesson-plan-editor selector 拆分 | `lesson-plan-editor.tsx` + 3 个子组件改为单字段 selector | | 4.3 | 题库表 + 考试表虚拟化 | 新增 `@tanstack/react-virtual` 依赖;`question-data-table.tsx` + `exam-data-table.tsx` 引入 `useVirtualizer` | | 4.4 | recharts props 模块级常量 | 12 个 chart 文件提取 `CHART_MARGIN` 模块级常量(parent/grades/dashboard/homework 模块下) | | 4.5 | useOptimistic 改造 | `message-detail.tsx` + `message-list.tsx` + `grade-record-list.tsx` + `attendance-sheet.tsx` 改用 `useOptimistic` + `useTransition` | | 4.6 | 高频子组件 React.memo | `AssignmentCard` + `StatusBadge` + `EmptyState` 添加 React.memo(ClassCard/GraphKpNode 已有) | | 4.7 | AiAssistantWidget 懒加载 | 新增 `modules/ai/components/ai-assistant-widget-inner.tsx`,外层改为 lazy wrapper | | 4.8 | scan-image-viewer aspect-ratio | `modules/homework/components/scan-image-viewer.tsx` `` 加 `aspectRatio: "4 / 3"` + width/height 属性 | | 4.9 | metadata 增强 + Promise.all | `app/layout.tsx` 补 `metadataBase`/`openGraph`/`twitter`/`robots`/`applicationName`/`authors`/`creator`;`getLocale()`+`getMessages()`+`auth()` 串行 await 改 `Promise.all` 并行 | ### 未完成项(待后续专项) - **React Compiler**:`babel-plugin-react-compiler` 仍为 RC 状态,暂不引入。待 React 19 稳定后评估。 - **force-dynamic 96 处评估**:全项目 96 处 `export const dynamic = "force-dynamic"`,需逐个评估页面数据变更频率,低频变更页面改 `revalidate = 3600`。 - **Playwright 性能断言**:待 e2e 测试基础设施完善后接入。 ### 关键架构决策 1. **动态导入模式标准化**:重 UI 库(tiptap / @xyflow/react / AI SDK)统一采用 `xxx-inner.tsx` + `xxx.tsx` lazy wrapper 模式。ReactFlowProvider 等 context provider 留在外层同步渲染,保证 lazy 子组件的 `useReactFlow()` 等 hook 可用。 2. **请求级去重优先于跨请求缓存**:auth 相关 4 个核心函数(getSession/resolveDataScope/getAuthContext/getSessionTeacherId)全部用 React `cache()` 包装。不引入 `unstable_cache`,因权限数据易变,跨请求缓存风险高于收益。 3. **批量 SQL 优于循环 SQL**: - INSERT 场景:用 `db.insert().values([...])` 批量 - UPDATE 场景:用 `sql.join` + `CASE WHEN` 单次执行 - 复杂事务:用 `db.transaction` 包裹多个批量操作 4. **React 19 useOptimistic 替代手动 isPending**:消息星标/出勤/成绩录入统一改为 `useOptimistic` + `useTransition`,移除手动 `useState` 跟踪 pending。 5. **细粒度 Zustand selector**:单字段 selector 优先于 `useShallow` 多字段 selector,因为函数引用天然稳定。 --- ## 1.1.6 缓存策略落地专项重构(2026-07-05 新增) > **目标**:全栈缓存策略一致性重构 — 服务端 `cacheFn` 双层包装 + 集中式 `INVALIDATION_MAP` 失效编排 + 客户端 `queryKeys` 工厂 + `useActionQuery`/`useActionMutation` 向后兼容双模式 Hook。 ### 架构总览 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Client(TanStack Query + useActionQuery/useActionMutation) │ │ queryKeys.{module}.{resource}(...args) → queryKey │ │ useActionMutation({ mutationFn, actionId, params }) │ │ → 成功后按 CLIENT_INVALIDATION_MAP[actionId].queryKeys 自动失效 │ └──────────────────────────────┬──────────────────────────────────────┘ │ mutationFn = Server Action ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Server Actions(编排层) │ │ mutation 成功分支末尾: │ │ await invalidateFor("module.action", { id }) │ │ → 1) CacheStore.invalidateTags 2) revalidateTag × N 3) revalidatePath × N │ └──────────────────────────────┬──────────────────────────────────────┘ │ 调用 data-access ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Data-access(数据访问层) │ │ export const fnRaw = async (...) => { ... } │ │ export const fn = cacheFn(fnRaw, { tags, ttl, keyParts }) │ │ → 外层 react.cache(请求级 memoization) │ │ → 内层 cacheStore.getOrSet(跨请求/跨实例,按 tag 失效) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ CacheStore(CACHE_DRIVER 切换) │ │ memory(默认,单实例,Map + LRU + tag 反查索引) │ │ redis(@upstash/redis,多实例共享,故障降级) │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 核心基础设施(9 个文件) | 文件 | 职责 | |------|------| | `shared/lib/cache/types.ts` | `CacheFnOptions` / `CacheStore` / `InvalidationRule` 接口 | | `shared/lib/cache/memory-store.ts` | `MemoryCacheStore`(Map + LRU maxEntries=500 + tag 反查索引) | | `shared/lib/cache/redis-store.ts` | `RedisCacheStore`(key=`next-edu:cache:{key}`,故障降级) | | `shared/lib/cache/store-factory.ts` | `getCacheStore()` 单例工厂(按 `env.CACHE_DRIVER` 切换) | | `shared/lib/cache/cache-fn.ts` | `cacheFn` 双层包装器(外层 react.cache + 内层 cacheStore.getOrSet) | | `shared/lib/cache/invalidation-map.ts` | `INVALIDATION_MAP` 集中式失效映射(203 个 actionId)+ `fillTemplate` | | `shared/lib/cache/client-invalidation-map.ts` | `CLIENT_INVALIDATION_MAP` 客户端可见子集(仅 queryKeys) | | `shared/lib/cache/invalidate.ts` | `invalidateFor` 三步编排函数 | | `shared/lib/cache/index.ts` | barrel re-export | | `shared/lib/redis-client.ts` | 共享 Redis 单例(rate-limit + cache 共用) | | `shared/lib/query-keys.ts` | 客户端 queryKey 工厂(`queryKeys.classes.*`) | ### 迁移覆盖范围(28 个模块全部完成) **data-access 层迁移至 `cacheFn`**(83 个文件,250+ 个查询函数): | 组别 | 模块 | 文件数 | 备注 | |------|------|--------|------| | 标杆 | classes | 5 | `data-access-{teacher,admin,students,stats,schedule}.ts` | | A | users / school / rbac / textbooks / questions / course-plans / files / dashboard / parent / proctoring | 11 | 77 个函数 | | B | exams / grades / homework / diagnostic / error-book / adaptive-practice | 8 | 含跨模块接口 | | C | lesson-preparation / elective / announcements / messaging / notifications | 6 | 24 个函数 | **actions 层迁移至 `invalidateFor`**(43 个文件,247 处 `revalidatePath` 替换): | 组别 | 模块 | 文件数 | 替换数 | |------|------|--------|--------| | 标杆 | classes | 5 | 47 | | A | users / school / textbooks / questions / course-plans / standards / scheduling / audit / onboarding / invitation-codes / proctoring / i18n | 13 | 70 | | B | exams / grades / homework / attendance / leave-requests / diagnostic / error-book / adaptive-practice | 9 | 77 | | C | lesson-preparation / elective / announcements / messaging / notifications / settings / rbac | 8 | 100 | **INVALIDATION_MAP**:扩展至 **203 个 actionId**,覆盖全部 28 个模块。每个 actionId 声明 `{ tags, queryKeys, paths }` 三段副作用。 ### 强制规则(已通过 ESLint 落地) | 规则 | 落地方式 | |------|----------| | Server Actions 中禁止直接调用 `revalidatePath` / `revalidateTag` | `no-restricted-syntax` ESLint 规则,仅 `shared/lib/cache/invalidate.ts` 豁免 | | data-access 查询函数必须用 `cacheFn` 包装 | 双导出 raw 模式(`fnRaw` + `cacheFn(fnRaw, opts)`),单测可 mock | | 未知 actionId 必须抛错 | `invalidateFor` 内部强校验,提示更新 INVALIDATION_MAP | | Tag 命名规范 | `{module}` / `{module}:{resource}` / `{module}:{resource}:{id}` | ### 向后兼容 Hook 双模式 - **`useActionQuery`**:传 `queryKey` 走 `useQuery`(跨页共享缓存 + 自动 invalidate);不传走旧 `useEffect + useState`(向后兼容) - **`useActionMutation`**:传 `mutationFn + actionId` 自动 invalidate 相关 queryKeys;旧 `mutate(action)` 模式仍可用 ### 验证结果 - `npx tsc --noEmit`:零错误 - `npm run lint`:零 `no-restricted-syntax` 错误(仅 3 个 pre-existing `react-hooks/refs` 错误位于未修改文件) - `npx vitest run src/shared/lib/cache/`:25/25 测试通过(5 个测试文件:types/memory-store/redis-store/cache-fn/invalidate/invalidation-map) --- ## 1.1.7 日志系统重构(2026-07-07 新增) ### 目标 引入 pino 结构化日志、Request ID 贯穿、错误边界上报、静默失败修复、track-event 去重,统一全项目日志基础设施。仅应用层 SDK 改造,不部署外部可观测性服务。 ### 架构 ``` proxy.ts (Edge Runtime) Node.js Runtime │ 生成 requestId (crypto.randomUUID) │ │ 注入 x-request-id 请求头 │ headers() 读取 x-request-id ▼ ▼ NextResponse.next({ request: { headers } }) withRequestContext(fn) │ requestContextStorage.run({ requestId }) ▼ createModuleLogger("module") │ pino mixin: getRequestContext() ▼ stdout (JSON / pino-pretty) 客户端 error.tsx 服务端 /api/client-error │ useErrorReport(error) │ createModuleLogger("client-error") │ navigator.sendBeacon │ log.error({ message, stack, digest, path }) ▼ ▼ /api/client-error (POST) pino 日志流 ``` ### 核心组件 | 文件 | 职责 | |------|------| | `shared/lib/logger.ts` | pino 实例 + `createModuleLogger(module)` 工厂;`mixin` 自动混入 requestId/userId;开发环境 pino-pretty,生产环境 JSON | | `shared/lib/request-context.ts` | `AsyncLocalStorage`;`getRequestContext()` 读取当前上下文;仅 Node.js Runtime | | `shared/lib/with-request-context.ts` | `withRequestContext(fn)` 高阶函数;从 `headers()` 读取 `x-request-id` 或生成 UUID,注入 AsyncLocalStorage | | `shared/hooks/use-error-report.ts` | 客户端错误上报 Hook;navigator.sendBeacon 上报到 /api/client-error;sessionStorage 节流(1 分钟/digest) | | `src/proxy.ts` | Edge Runtime;生成/复用 requestId,通过 `NextResponse.next({ request: { headers } })` 注入 x-request-id | | `src/app/api/client-error/route.ts` | 接收客户端错误,用 `createModuleLogger("client-error")` 记录到 pino 日志流 | ### Request ID 贯穿链路 1. `proxy.ts`(Edge Runtime)生成 `requestId = crypto.randomUUID()`(Web Crypto API,非 Node.js crypto) 2. 通过 `NextResponse.next({ request: { headers: injectRequestId(request, requestId) } })` 注入到下游请求 3. `withRequestContext(fn)` 在 Server Action / Route Handler 中从 `headers()` 读取 `x-request-id` 4. `requestContextStorage.run({ requestId }, () => fn(...))` 注入 AsyncLocalStorage 5. `pino` 的 `mixin: () => getRequestContext()` 自动将 requestId 混入每条日志 ### Edge Runtime 限制 - `proxy.ts` 和原 `web-vitals/route.ts` 声明 `runtime = "edge"` - Edge Runtime(V8 Isolate)不支持 `node:async_hooks`(AsyncLocalStorage)和 `node:stream`(pino 依赖) - **解决方案**: - `proxy.ts` 仅用 Web Crypto API(`crypto.randomUUID()`),不导入 Node.js 模块 - `web-vitals/route.ts` 从 `runtime = "edge"` 改为 `runtime = "nodejs"`(pino 依赖 Node.js stream) - 客户端组件不能导入服务端 pino logger(`env.LOG_LEVEL` 是 server-only) ### 替换范围 - **34 个服务端 .ts 文件**:86 处 `console.*` 替换为 `createModuleLogger("module")` 的 `log.*` - **129 个 error.tsx**:接入 `useErrorReport(error)` Hook,移除 7 处 `useEffect + console.error` - **3 个 audit-logger**:catch 块 `/* Silently fail */` → `log.warn({ err, action, module }, "...")` - **4 个 track-event**:`console.*` 替换为 `log.*`(客户端 track-event 改为纯 no-op) - **ESLint `no-console: error`**:强制服务端 .ts 使用 logger;客户端 .tsx/hooks 暂时豁免(留待客户端错误上报机制处理) ### 环境变量 | 变量 | 默认值 | 说明 | |------|--------|------| | `LOG_LEVEL` | `info` | 日志级别(debug/info/warn/error) | ### 验证结果 - `npx tsc --noEmit`:零错误 - `npm run lint`:零错误(no-console 规则启用) --- ## 1.2 模块依赖关系图 下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。 ### 图例 - `───▶` 合理依赖(通过 data-access 或类型导入) - `═══▶` 违规依赖(直接查询对方 DB 表) - `⟳ ` 循环依赖 - `──▷` UI 组件组合(合理) ### 1.2.1 核心业务模块依赖 ``` ┌──────────────┐ │ textbooks │ ◀── 标杆模块(无跨模块 DB 访问) └──────┬───────┘ │ ──▷ UI 组合(knowledge-point-dialogs) │ ┌──────────────┼──────────────┐ │ │ │ ▼ ▼ ▼ ┌────────────┐ ┌──────────┐ ┌────────────┐ │ questions │ │ exams │ │ homework │ └─────┬──────┘ └────┬─────┘ └─────┬──────┘ │ ✅ P1-1 │ ✅ P1-1 │ ✅ P1-1 已修复 │ 通过 textbooks│ 通过 questions│ 通过 exams/classes/ │ data-access │ data-access │ school/users data-access │ │ 通过 classes │ │ │ data-access │ ┌─┴────────┐ │ │ │ grades │◀────┘ 仅外键引用(合理) │ (成绩) │◀──── exams data-access(✅ 2026-06-24 新增:getExamsForGradeEntry/getExamForGradeEntry 按试卷录入成绩) └────┬─────┘ │ ✅ P1-1 已修复 │ 通过 classes/school/users data-access ▼ ┌──────────┐ │ classes │ ✅ P1-1 已修复:通过 homework/data-access-classes └────┬─────┘ 获取作业数据,不再直查 homework/exams 表 │ ✅ P0-1 已修复:data-access.ts 已拆分为 5 文件 │ ✅ P0-5 已修复:classSchedule 写入口统一到 scheduling │ ┌─────────┼─────────┐ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────┐ ┌──────────┐ │scheduling│ │school│ │ attendance│ └────┬─────┘ └──────┘ └──────────┘ │ ✅ P0-5 已修复 │ classSchedule 写入口统一到 scheduling │ 通过 classes/data-access 校验归属 │ ✅ P1-1 已修复:通过 users data-access ``` ### 1.2.2 扩展模块依赖 ``` ┌─────────────┐ ✅ P0-3 已修复:通过各模块 data-access ┌──────────────┐ │ dashboard │ 获取数据(getUsersDashboardStats 等) │ users/classes│ │ (聚合层) │──────────────────────────────────────▶│ /exams/... │ └─────────────┘ ✅ P0-4 已修复:Promise.all 并行调用 └──────────────┘ ┌─────────────┐ ─── 调用 data-access(合理) ┌──────────────┐ │ parent │──────────────────────────────▶│ classes/ │ │ (聚合层) │ ✅ P1-1 已修复:通过各模块 │ homework/grades│ └─────────────┘ data-access 获取数据 └──────────────┘ ┌─────────────┐ ✅ P1-1 已修复:通过 exams/questions/ ┌──────────────┐ │ diagnostic │ classes/users data-access 获取数据 │ exams/questions│ │ │──────────────────────────────────────▶│ /classes/users│ └─────────────┘ └──────────────┘ ┌─────────────┐ ✅ P1-1 已修复:通过 exams/users ┌──────────────┐ │ proctoring │ data-access 获取数据 │ exams/users │ └─────────────┘──────────────────────────────────────▶└──────────────┘ ┌─────────────┐ ─── 调用 notifications dispatcher ┌──────────────┐ │ messaging │ (通知偏好/CRUD 已迁移至 notifications)│ notifications │ │ │──────────────────────────────────────▶│ (拥有 │ │ │ ✅ P0-4 / P1-5 已修复:单向依赖 │ messageNotif│ └─────────────┘ │ ications + │ │ preferences)│ └──────────────┘ ┌─────────────┐ ─── 调用 messaging Action ┌──────────────┐ │ settings │ (通知偏好表单) │ messaging │ └─────────────┘──────────────────────────────▶└──────────────┘ ┌─────────────────┐ ───▶ data-access(合理) ┌──────────────┐ │ lesson-prep │──────────────────────────────▶│ textbooks │ │ (备课聚合层) │ 只读章节/知识点树 │ (章节/KP 树) │ │ │──────────────────────────────▶└──────────────┘ │ │──────────────────────────────▶┌──────────────┐ │ │ 创建/查询题目 │ questions │ │ │──────────────────────────────▶└──────────────┘ │ │──────────────────────────────▶┌──────────────┐ │ │ 创建 exam 草稿 │ exams │ │ │──────────────────────────────▶└──────────────┘ │ │──────────────────────────────▶┌──────────────┐ │ │ 创建作业下发 │ homework │ │ │──────────────────────────────▶└──────────────┘ │ │──────────────────────────────▶┌──────────────┐ │ │ 查询教师班级 │ classes │ │ │──────────────────────────────▶└──────────────┘ │ │──────────────────────────────▶┌──────────────┐ │ │ 附件 │ files │ └─────────────────┘──────────────────────────────▶└──────────────┘ ``` ### 1.2.3 循环依赖详情 ✅ 已修复 ``` shared/lib/audit-logger.ts ──┐ shared/lib/change-logger.ts ──┼──▶ shared/lib/session.ts ──▶ (dynamic import) @/auth shared/lib/auth-guard.ts ──┘ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" ──▶ import { ... } from "@/shared/lib/login-logger" ──▶ import { ... } from "@/shared/lib/password-policy" ──▶ import { ... } from "@/shared/lib/rate-limit" ──▶ import { ... } from "@/shared/lib/role-utils" # P1-3 拆出 ──▶ import { ... } from "@/shared/lib/bcrypt-utils" # P1-3 拆出 ──▶ import { ... } from "@/shared/lib/http-utils" # P1-3 拆出 ──▶ import { ... } from "@/shared/lib/password-security-service" # P1-3 拆出 ──▶ import { db, schema } from "@/shared/db" ✅ 修复:shared/lib/* 不再静态 import @/auth,统一通过 session.ts 单一入口 session.ts 内部使用 dynamic import("@/auth") 打破模块级静态循环 运行时调用链保持不变,模块加载图无环 ``` --- ## 1.3 数据流向图(考试流程) 以"考试流程"为例,展示数据从创建到成绩统计的完整流向。 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ 阶段 1:教师创建考试 │ │ ───────────────────────────────────────────────────────────────── │ │ teacher/exams/create/page.tsx │ │ └─▶ exams/actions.createExamAction │ │ ├─▶ requirePermission(EXAM_CREATE) [shared/auth-guard] │ │ ├─▶ persistExamDraft() [exams/data-access] │ │ │ └─▶ db.insert(exams) [shared/db] │ │ │ └─▶ db.insert(examQuestions) [shared/db] │ │ └─▶ revalidatePath("/teacher/exams") │ │ │ │ 数据写入:exams 表 + examQuestions 表 │ │ ✅ P0-1 已修复:persistAiGeneratedExamDraft 改为调用 questions/data-access.createQuestionWithRelations,不再直查 questions 表 │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 阶段 2:学生作答(作业化考试) │ │ ───────────────────────────────────────────────────────────────── │ │ student/learning/assignments/[assignmentId]/page.tsx │ │ └─▶ homework/actions.startHomeworkSubmissionAction │ │ ├─▶ requirePermission(HOMEWORK_SUBMIT) │ │ ├─▶ data-access-write.startHomeworkSubmission ✅ P1-2 已修复 │ │ │ └─▶ db.insert(homeworkSubmissions) [shared/db] │ │ └─▶ 返回 submissionId │ │ │ │ └─▶ homework/actions.saveHomeworkAnswerAction │ │ └─▶ data-access-write.saveHomeworkAnswer ✅ P1-2 已修复 │ │ └─▶ db.transaction(insert homeworkAnswers) [shared/db] │ │ │ │ └─▶ homework/actions.submitHomeworkAction │ │ └─▶ data-access-write.submitHomework ✅ P1-2 已修复 │ │ └─▶ db.update(homeworkSubmissions.status="submitted") │ │ │ │ 数据写入:homeworkSubmissions 表 + homeworkAnswers 表 │ │ ✅ P1-2 已修复:actions 层不再直接 DB 操作,已下沉到 data-access-write│ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 阶段 3:教师批改 │ │ ───────────────────────────────────────────────────────────────── │ │ teacher/homework/submissions/[submissionId]/page.tsx │ │ └─▶ homework/actions.gradeHomeworkSubmissionAction │ │ ├─▶ requirePermission(HOMEWORK_GRADE) │ │ └─▶ data-access-write.gradeHomeworkSubmission ✅ P1-2 已修复│ │ └─▶ db.update(homeworkAnswers) 循环 [shared/db] │ │ │ │ 数据更新:homeworkAnswers.isCorrect / score / feedback │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 阶段 4:成绩统计与诊断 │ │ ───────────────────────────────────────────────────────────────── │ │ teacher/grades/page.tsx │ │ └─▶ grades/data-access.getGradeRecords │ │ └─▶ ✅ P1-1 已修复:通过 classes/school/users data-access │ │ │ │ teacher/diagnostic/page.tsx │ │ └─▶ diagnostic/data-access.updateMasteryFromSubmission │ │ ├─▶ ✅ P1-1 已修复:通过 exams data-access 获取提交 │ │ ├─▶ ✅ P1-1 已修复:通过 questions data-access 获取知识点 │ │ └─▶ 更新 knowledgePointMastery │ │ │ │ 数据读取:homeworkSubmissions → grades → knowledgePointMastery │ └─────────────────────────────────────────────────────────────────────┘ ``` **关键观察**:考试流程横跨 4 个模块(exams → homework → grades → diagnostic),✅ P1-1 已修复:所有跨模块查询已改为通过对方 data-access 接口,模块封装性已恢复。 --- ## 1.4 核心调用链路 ### 1.4.1 调用链路:创建考试(含 AI 出题) ``` [Client] exam-form.tsx │ FormData ▼ [Route] POST /teacher/exams/create (Server Action) │ ▼ [Action] exams/actions.createAiExamAction │ ├─▶ requirePermission(EXAM_CREATE) │ └─▶ shared/lib/auth-guard.getAuthContext() │ ├─▶ auth() [src/auth.ts] │ ├─▶ db.query.usersToRoles [shared/db] │ ├─▶ db.query.classSubjectTeachers │ └─▶ 返回 { userId, roles, permissions, dataScope } │ ├─▶ generateAiCreateDraftFromSource() │ └─▶ exams/ai-pipeline.ts (912 行) │ ├─▶ shared/lib/ai.createAiChatCompletion() │ │ └─▶ OpenAI SDK + db.query.aiProviders │ └─▶ JSON 解析 + Zod 校验 │ ├─▶ persistAiGeneratedExamDraft() │ └─▶ exams/data-access.ts │ ├─▶ db.insert(exams) ✅ 合理 │ ├─▶ db.insert(examQuestions) ✅ 合理 │ └─▶ questions/data-access.createQuestionWithRelations ✅ P0-1 已修复:通过 data-access │ └─▶ revalidatePath("/teacher/exams") ``` ### 1.4.2 调用链路:学生提交作业 ``` [Client] homework-take-view.tsx │ ▼ [Action] homework/actions.submitHomeworkAction │ ├─▶ requirePermission(HOMEWORK_SUBMIT) │ ├─▶ data-access-write.submitHomework ✅ P1-2 已修复 │ (校验 submission 归属 + 更新状态) │ └─▶ db.update(homeworkSubmissions) │ SET status = "submitted", submittedAt = now() │ └─▶ 返回 ActionState<{ submissionId }> ``` ### 1.4.3 调用链路:管理员仪表盘聚合 ``` [Route] /admin/dashboard/page.tsx (Server Component) │ ▼ [DataAccess] dashboard/data-access.getAdminDashboardData │ ├─▶ users/data-access.getUsersDashboardStats() ✅ 通过模块 data-access │ ├─ userCount / activeSessionsCount / userRoleCounts │ └─ recentUsers (含角色解析) ├─▶ classes/data-access.getClassesDashboardStats() ✅ 通过模块 data-access │ └─ classCount ├─▶ textbooks/data-access.getTextbooksDashboardStats() ✅ 通过模块 data-access │ └─ textbookCount / chapterCount ├─▶ questions/data-access.getQuestionsDashboardStats() ✅ 通过模块 data-access │ └─ questionCount ├─▶ exams/data-access.getExamsDashboardStats(scope?) ✅ 通过模块 data-access │ └─ examCount (含 scope 过滤) └─▶ homework/stats-service.getHomeworkDashboardStats(scope?) ✅ 通过模块 data-access ├─ homeworkAssignmentCount / homeworkAssignmentPublishedCount └─ homeworkSubmissionCount / homeworkSubmissionToGradeCount ✅ P0-4 已修复:dashboard 改为并行调用各模块 dashboard stats 函数,不再直查跨模块表 ``` ### 1.4.4 调用链路:admin 路由组统一权限守卫 ``` [Layout] app/(dashboard)/admin/layout.tsx (Server Component) │ ▼ [AuthGuard] shared/lib/auth-guard.getAuthContext() │ └─▶ getSession() → 校验已登录(未登录抛 PermissionDeniedError) │ ▼ [Children] 各 admin/* 页面(page.tsx)在函数体首行调用 requirePermission(XXX) │ ├─▶ /admin/school/schools → requirePermission(SCHOOL_MANAGE) ├─▶ /admin/school/academic-year → requirePermission(SCHOOL_MANAGE) ├─▶ /admin/school/classes → requirePermission(SCHOOL_MANAGE) ├─▶ /admin/school/departments → requirePermission(SCHOOL_MANAGE) ├─▶ /admin/school/grades → requirePermission(SCHOOL_MANAGE) ├─▶ /admin/school/grades/insights → requirePermission(SCHOOL_MANAGE) ├─▶ /admin/users/import → requirePermission(USER_MANAGE) ├─▶ /admin/users → requirePermission(USER_MANAGE) ├─▶ /admin/roles → requirePermission(ROLE_READ) ├─▶ /admin/roles/[id] → requirePermission(ROLE_READ) ├─▶ /admin/permissions → requirePermission(PERMISSION_READ) ├─▶ /admin/scheduling/auto → requirePermission(SCHEDULE_AUTO) ├─▶ /admin/scheduling/changes → requirePermission(SCHEDULE_ADJUST) ├─▶ /admin/scheduling/rules → requirePermission(SCHEDULE_ADJUST) ├─▶ /admin/announcements → requirePermission(ANNOUNCEMENT_MANAGE) ├─▶ /admin/announcements/[id] → requirePermission(ANNOUNCEMENT_MANAGE) ├─▶ /admin/audit-logs → requirePermission(AUDIT_LOG_READ) └─▶ /admin/invitation-codes → requirePermission(USER_MANAGE) ✅ audit-P2-3 新增 ✅ P0 安全修复:admin/layout.tsx 提供登录态统一守卫, 各页面 requirePermission() 提供细粒度权限校验 teacher/classes/* 路由权限校验(2026-06-22 审计修复): ├─▶ /teacher/classes/my → requirePermission(CLASS_READ) ✅ 已修复 ├─▶ /teacher/classes/my/[id] → requirePermission(CLASS_READ) ✅ 已修复 ├─▶ /teacher/classes/schedule → requirePermission(CLASS_READ) ✅ 已修复 └─▶ /teacher/classes/students → requirePermission(CLASS_READ) ✅ 已修复 student/learning + student/schedule 路由权限校验(2026-06-26 审计修复): ├─▶ /student/learning → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) ├─▶ /student/learning/courses → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) ├─▶ /student/learning/courses/[classId]→ requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) └─▶ /student/schedule → requirePermission(CLASS_READ) ✅ 已修复(原 homework:submit) ``` --- # 第二部分:模块清单 > 每个模块包含:职责 · 导出函数 · 依赖关系 · 已知问题 · 文件清单 ## 2.1 shared(基础设施层) **职责**:提供全项目共享的 DB Schema、工具函数、权限系统、UI 基础组件、通用 Hooks。 **导出函数**(核心): - `getAuthContext()` / `requirePermission(p)` / `requireAuth()` — 认证与权限 - `resolvePermissions(roles)` / `resolveDataScope(userId, roles)` — 权限解析 - `logAudit()` / `logLoginEvent()` / `logDataChange()` — 日志记录 - `createAiChatCompletion()` / `parseAiChatPayload()` — AI 调用 - `validatePassword()` / `isAccountLocked()` / `rateLimit()` — 安全策略 - `exportToExcel()` / `parseExcel()` / `generateTemplate()` — Excel 工具 - `cn()` / `formatDate()` / `formatFileSize()` — 通用工具 - `getInitials(name)` / `formatDateForFile(d?)` — 通用工具(P1-c / P1-a 重构新增:从 parent/lib/utils.ts、grades/export-button.tsx 等多处重复实现抽取) - `serializeDate(date)` / `serializeDateRequired(date)` — 日期序列化工具(✅ 审计 v1 P0-6 新增:位于 `lib/date-utils.ts`,S-04 DRY 修复;`serializeDate(date: Date | null | undefined): string | null` 与 `serializeDateRequired(date: Date): string`;messaging / course-plans / announcements / elective / audit / files / school / notifications 等 10+ data-access 文件迁移至共享 util,消除各模块重复定义 `const toIso = (d: Date) => d.toISOString()`) - `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`) - `apiSuccess(data, init?)` / `apiError(message, status, errorCode?, init?)` / `apiFromAction(result, options?)` / `handleApiError(error, init?)` / `withApiErrorHandler(handler)` / `parseJsonBody(req)` / `errorToStatus(error)` — API 路由层统一响应工具(✅ 2026-07-04 API 规范化重构新增:位于 `lib/api-response.ts`,仅用于 `app/api/.../route.ts`;统一响应信封 `{ success, message?, errorCode?, data? }` 与 `ActionState` 对齐;集中错误→HTTP 状态码映射替代字符串匹配推导;`withApiErrorHandler` HOF 统一异常处理消除每个 route 重复 try/catch;`apiFromAction` 通过 `errorCode` 驱动状态码替代 `result.message?.includes("not found")` 脆弱模式;`errorToStatus` 映射规则:PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500) - `formatSseEvent(data)` / `formatSseError(message)` / `formatSseDone()` / `createSseResponse(stream)` / `createSseError(message, status)` — SSE 共享工具(✅ 2026-07-04 API 规范化重构新增:位于 `lib/sse.ts`,统一 `app/api/.../stream/route.ts` 的事件格式与响应头;标准 SSE 头 `Content-Type: text/event-stream` + `Cache-Control: no-cache, no-transform` + `Connection: keep-alive`;客户端约定错误统一 `{ type: "error", message }`、流结束信号 `[DONE]`) **共享组件导出**(P0-b / P1-a / P1-b / P1-c / P2-a / P2-b / P3-a / P3-b / P3-c / P3-d / 第二轮 P0-1/P0-2/P0-3/P1-1/P1-2/P1-3/P1-4 重构新增,按类别组织): | 类别 | 组件 | 文件 | 用途 | 消费方数量 | |------|------|------|------|-----------| | **UI 组件** | `StatCard` | `components/ui/stat-card.tsx` | 统计卡片(标题+数值+图标+描述+跳转+骨架屏) | 8 个(P1-a) | | **UI 组件** | `StatItem` | `components/ui/stat-item.tsx` | 紧凑统计项(label+icon+value+hint,用于统计面板网格) | 8 个(P1-a) | | **UI 组件** | `ChipNav` | `components/ui/chip-nav.tsx` | 芯片导航组(通过 URL search params 切换筛选维度,Link 跳转) | 3 个(P1-b) | | **UI 组件** | `PageHeader` | `components/ui/page-header.tsx` | 页面头部(标题+描述+icon+actions,响应式布局) | 2 个(P2-b: profile/page.tsx, settings/security/page.tsx) | | **UI 组件** | `FilterBar` / `FilterSearchInput` / `FilterResetButton` | `components/ui/filter-bar.tsx` | 筛选栏容器+搜索框+重置按钮(统一布局壳,URL 状态由各模块处理) | 5 个(P3-b: exam/textbook/question/audit-log/login-log filters) | | **UI 组件** | `ConfirmDeleteDialog` | `components/ui/confirm-delete-dialog.tsx` | 通用删除确认对话框(AlertDialog 包装,支持自定义 confirmText/cancelText) | 5 个(P0-1: announcement-detail, message-detail, course-plan-detail, grade-classes-view, students-table) | | **UI 组件** | `Pagination` | `components/ui/pagination.tsx` | 通用分页 UI(Showing X-Y of Z + Page X of Y + 上一页/下一页按钮) | 3 个(P0-2: audit-log-table, login-log-table, data-change-log-table) | | **UI 组件** | `EmptyTableRow` | `components/ui/empty-table-row.tsx` | 表格空状态行(TableRow + TableCell 居中显示空状态文案) | 3 个(P0-3: audit-log-table, login-log-table, data-change-log-table) | | **UI 组件** | `ResizablePanel` | `components/ui/resizable-panel.tsx` | 自实现可拖拽分栏容器(左右两栏 + 中间分隔条 pointer 拖拽调整宽度,无新依赖;用于试卷富文本编辑器左编辑右预览、阅卷式批改左题目右扫描图等场景) | 2 个(exams/components/exam-rich-form, homework/components/homework-scan-grading-view) | | **UI 组件** | `ErrorState` | `components/error-state.tsx` | 通用错误边界展示组件("use client",统一 error.tsx 渲染逻辑:AlertTriangle 图标 + i18n title/description + retry 按钮;props:`{ error, reset, namespace }`;翻译键查找规则:在传入 namespace 下查找 `error.title`/`error.description`/`error.retry`) | 10+ 个(2026-06-26 审计修复新增:classes/student 等模块的 error.tsx) | | **UI 组件** | `StatusBadge` | `components/ui/status-badge.tsx` | 通用状态徽章(Badge + 状态→variant/label/className 映射表,修复 in_progress 颜色不一致 bug) | 9+ 个(P1-1: audit 3 文件, grades 2 文件, student/learning/assignments, parent/child-homework-summary, student-upcoming-assignments-card, question-columns) | | **表单字段** | `TextField` | `components/form-fields/text-field.tsx` | 通用文本字段(FormField + Input 包装,支持 text/number/password/datetime-local 类型 + value 转换器) | 3 个文件 16 处(P1-2: profile-settings-form 6, exam-basic-info-form 4, ai-provider-settings-card 4) | | **表单字段** | `SelectField` | `components/form-fields/select-field.tsx` | 通用选择字段(FormField + Select 包装,支持 toSelectValue/fromSelectValue 处理 number↔string) | 4 个文件 8 处(P1-2: exam-basic-info-form 3, ai-provider-settings-card 1, create-question-dialog 2, profile-settings-form 1) | | **表单字段** | `TextareaField` | `components/form-fields/textarea-field.tsx` | 通用多行文本字段(FormField + Textarea 包装) | 1 个(P1-2: create-question-dialog) | | **图表组件** | `ChartCardShell` | `components/charts/chart-card-shell.tsx` | 图表卡片外壳(Card+Header+EmptyState+Content 统一结构) | 8 个(P3-c) | | **图表组件** | `TrendLineChart` | `components/charts/trend-line-chart.tsx` | 趋势折线图(LineChart 统一配置,支持单/多系列) | 8 个(P3-c: grade-trend-chart 等) | | **图表组件** | `SimpleBarChart` | `components/charts/simple-bar-chart.tsx` | 柱状图(BarChart 统一配置,支持单/多 Bar + Cell 分桶着色 + defs 自定义 SVG 图案) | 8 个(P3-c: grade-distribution-chart 等) | | **图表组件** | `ComparisonRadarChart` | `components/charts/comparison-radar-chart.tsx` | 对比雷达图(RadarChart 统一配置,支持双 Radar 对比) | 8 个(P3-c: subject-comparison-chart, mastery-radar-chart 等) | | **课表组件** | `ScheduleList` / `ScheduleListItem` | `components/schedule/schedule-list.tsx` | 课表列表+列表项(课程+时间+地点+班级徽章,separator/card 两种变体) | 3 个(P3-a: student-today-schedule-card, child-schedule-card, student-schedule-view) | | **题库组件** | `QuestionBankFilters` | `components/question/question-bank-filters.tsx` | 题库筛选栏(搜索+题型+难度,default/compact 两种布局) | 2 个(P3-d: exam-assembly, question-bank-picker) | | **题库组件** | `QuestionCascadeFilter` | `modules/questions/components/question-cascade-filter.tsx` | ✅ 审计新增:教材→章节→知识点三级级联筛选(nuqs URL 状态同步,清除上级自动清除下级) | 1 个(question-filters) | | **题库组件** | `KnowledgePointSelector` | `modules/questions/components/knowledge-point-selector.tsx` | ✅ 审计新增:知识点选择器(支持搜索过滤,从 create-question-dialog 拆分) | 1 个(create-question-dialog) | | **题库组件** | `OptionsEditor` | `modules/questions/components/options-editor.tsx` | ✅ 审计新增:选项编辑器(单选/多选模式,增删改选项,从 create-question-dialog 拆分) | 1 个(create-question-dialog) | | **题库组件** | `QuestionBankResultsClient` | `modules/questions/components/question-bank-results-client.tsx` | ✅ 审计新增:RSC 与客户端 Hook 桥接(解决 useQuestionColumns Hook 在客户端组件中使用) | 1 个(teacher/questions/page) | | **题库组件** | `QuestionContentRenderer` | `modules/questions/components/question-content-renderer.tsx` | ✅ 审计新增:题目结构化渲染(题干/选项/答案/解析,单选圆点/多选方框,正确答案高亮) | 1 个(question-actions 查看详情) | | **题库组件** | `BatchOperations` | `modules/questions/components/batch-operations.tsx` | ✅ 审计新增:批量操作工具栏(批量删除,权限感知,事务原子性) | 1 个(question-data-table) | | **题库组件** | `ImportExportButtons` | `modules/questions/components/import-export-buttons.tsx` | ✅ 审计新增:导入导出组件(JSON 格式,导出下载/上传导入,导入前预览确认) | 2 个(teacher/admin questions page) | | **设置组件** | `SettingsView` | `modules/settings/components/settings-view.tsx` | 统一设置页布局(5 标签页:General/Notifications/Appearance/Security/AI,角色差异通过 props 注入,Tab URL 持久化,登出二次确认) | 4 个(P2-a: admin/teacher/student/parent 设置页) | **共享 Hooks 导出**(第二轮 P1-4 重构新增): | Hook | 文件 | 签名 | 用途 | 消费方 | |------|------|------|------|--------| | `useActionMutation` | `hooks/use-action-mutation.ts` | `useActionMutation(options?): { isWorking, mutate }`。Options 联合类型:旧版 `{ successMessage?, errorMessage?, onSuccess?, onError? }` 或新版 `{ mutationFn?, actionId?, params?, invalidateQueryKeys?, ... }`。新版接入 QueryClient,成功后按 `CLIENT_INVALIDATION_MAP[actionId]` 自动 invalidateQueries。`mutate(action?)` 参数可选:传 action 走旧模式,不传则使用 options.mutationFn | 通用 Server Action mutation Hook,替代 50+ 文件中重复的 setIsWorking + try/catch/finally + toast 模式。V5 缓存策略重构新增 QueryClient + actionId 自动失效,向后兼容旧调用方 | 示范:P1-4 schools-view;Task 19 classes/class-invitation-manager(revoke mutation + actionId 自动失效)。潜在影响 50+ 文件 | | `useActionQuery` | `hooks/use-action-query.ts` | `useActionQuery(action, options?): { data, loading, error, refetch }`。Options 联合类型:旧版 `{ deps?, enabled?, errorMessage? }` 或新版 `UseActionQueryWithCacheOptions`(含 `queryKey` + `UseQueryOptions`)。传入 `queryKey` 走 useQuery 跨页共享缓存;不传走旧 useEffect + useState。Hook 内部始终声明 useQuery + useState/useEffect 以遵守 React Hooks 规则 | 通用 Server Action 查询 Hook,替代 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式,内置竞态防护。V5 缓存策略重构新增 queryKey 入参走 QueryClient,向后兼容旧调用方 | 示范:P1-4 create-question-dialog;Task 19 classes/class-invitation-manager(listClassInvitationCodesAction + queryKeys.classes.invitations(classId))。潜在影响 11 个文件 | **V5 状态管理统一专项基础设施**(阶段 1 新增,零业务行为变更): | 资产 | 文件 | 签名 | 用途 | 后续消费方 | |------|------|------|------|--------| | `createQueryClient` | `lib/query-client.ts` | `createQueryClient(): QueryClient` | TanStack Query 工厂,默认 staleTime 30s / retry 1 / refetchOnWindowFocus false / mutations 全局 onError 兜底 | `app/providers.tsx` | | `notify` + `rawToast` | `lib/notify.ts` | `notify.{success,error,info,warning,message,promise}(message: string \| undefined, opts?: ExternalToast); rawToast` | 统一 toast 接口,包装 sonner。**V5 Phase 4 重写**:移除 `configureNotify` + i18n resolver 注入,简化为直接接受 `string \| undefined`(兼容 `result.message`),通过 `ExternalToast` options 转发 sonner 高级特性(description/duration/action)。i18n 由调用方通过 `t()` 控制。**已完成 131 文件批量替换**(业务代码 `import { toast } from "sonner"` → `import { notify } from "@/shared/lib/notify"` + `toast.X(...)` → `notify.X(...)`) | `query-client.ts` 全局兜底 + 所有业务模块(exams/grades/lesson-preparation/settings/classes 等 131 文件) | | `useDialogState` | `hooks/use-dialog-state.ts` | `useDialogState(): readonly [boolean, () => void, () => void, () => void]` | Dialog/Sheet 开关状态 Hook,替换 30+ 处 `const [open, setOpen] = useState(false)` 模式 | 待迁移:30+ 处 Dialog 组件 | | `useUiStore` + `useModal` + `ModalRoot` | `stores/ui-store.ts` + `components/modal-root.tsx` | `useUiStore: { modals, openModal, closeModal, isOpen, closeAll }; useModal(name); ModalRoot({ registry }): ReactNode` | 全局 UI store(zustand),集中管理 modal registry。命令式 API + Hook 双入口。**V5 Phase 4 新增 `ModalRoot` 组件**:监听 ui-store modals 状态渲染已注册 modal,路由变化时自动 `closeAll`。与 sonner 的分工:toast 走 notify,modal 走本 store | 待迁移:新增跨组件触发 modal 推荐使用此模式;存量 Dialog 保持 useState | | `createServiceProvider` | `lib/create-service-provider.tsx` | `createServiceProvider(displayName): { Provider, useService, Context }` | Service DI Provider 工厂,替换 14 处重复的 `createContext(null)` + Provider + useService 抛错模板 | 待迁移:announcements / attendance / audit / dashboard / messaging / settings 等 14 处 Service DI Provider | | `Providers` | `app/providers.tsx` | `Providers({ children, locale, messages, session }): React.JSX.Element` | 客户端 Provider 聚合。嵌套顺序:QueryClientProvider → NextIntlClientProvider → ThemeProvider → SessionProvider → NuqsAdapter → children + Toaster。**V5 Phase 4 调整**:移除 `NotifyConfigurator`(notify.ts 简化为直接接受 string,不再依赖 i18n resolver 注入)。DevTools 仅 dev 环境 | `app/layout.tsx` | **V5 状态分层模型**(5 层): | 层 | 责任 | 选型 | |----|------|------| | URL 状态 | 列表过滤、tab 持久化、深链接 | `nuqs useQueryState`(单一来源,逐步替换 `useSearchParams` + `window.location.search`) | | 服务端状态 | 列表/详情/分页数据,mutation + 缓存失效 | TanStack Query(混合模式:RSC 拿首屏 props,useQuery 负责交互数据) | | 客户端业务状态 | 模块级状态机(如 exam preview、lesson editor) | zustand slice(按模块拆,参考 lesson-preparation 现有模式) | | 全局 UI 状态 | modal registry | zustand ui-store | | 表单状态 | 表单密集模块 | react-hook-form + zodResolver | **共享工具函数导出**(第二轮 P1-3 重构新增): | 函数 | 文件 | 签名 | 用途 | 消费方 | |------|------|------|------|--------| | `formatDateTime` | `lib/utils.ts` | `formatDateTime(date, locale?): string` | 国际化日期+时间格式化(含小时、分钟) | 4 个(P1-3: lesson-plan-card, version-history-drawer, proctoring-dashboard, exam-ai-generator) | | `formatLongDate` | `lib/utils.ts` | `formatLongDate(date, locale?): string` | 国际化长日期格式化(含星期、完整月份名),默认 zh-CN,weekday=short | 1 个(P1-3: teacher-dashboard-header) | > 注:`SettingsView` 位于 `modules/settings/components/`(非 shared 层),因仅被 settings 模块消费,未下沉到 shared。此处列出以完整反映本次重构的组件抽取范围。 **依赖关系**: - 被依赖方:**所有模块**依赖 shared - ✅ 反向依赖已修复:`shared/lib/{audit-logger, change-logger, auth-guard}` 通过 `shared/lib/session.ts` 单一入口获取 session,不再直接依赖 `@/auth` **已知问题**: - ✅ P0:~~`shared/lib/*` ↔ `@/auth` 循环依赖~~ 已修复(新增 `shared/lib/session.ts` 封装 session 获取,3 个文件改为 `import { getSession } from "@/shared/lib/session"`) - ⚠️ P1:`schema.ts` 1111 行(54 张表混合,超 1000 硬上限) - ✅ P1:~~`auth.ts` 293 行混合 5 类职责~~ 已拆分(4 个辅助函数组迁移至 `shared/lib/{role-utils,bcrypt-utils,http-utils,password-security-service}`,auth.ts 仅保留 NextAuth 配置) - ✅ P2-2 已修复:~~`ai.ts` 218 行混合 5 类职责~~ 已拆分为 `ai/` 目录(payload-parser.ts/api-key-crypto.ts/provider-config.ts/client.ts/errors.ts/index.ts),原 `ai.ts` 保留为向后兼容的重导出文件(9 行) - ✅ P2-4 已修复:~~`onboarding-gate.tsx` 业务逻辑泄漏到 shared~~ 已迁移至 `modules/onboarding/`(actions/data-access/schema/types/components),引导流程改为独立路由 `/onboarding` + middleware 重定向 + Server Action - ✅ P0-2/P0-3/P0-4/P0-5/P1-1/P1-2/P1-4/P1-5 已修复(v3 对标 PowerSchool/Veracross/Auth0):家长绑定三因子验证(邮箱+生日+手机号后4位)、教师多科目循环绑定、审计日志、服务端幂等、URL query 持久化步骤、局部错误收集、家长多子女动态行、跳过机制明确化 - ✅ v3 i18n 体系引入:采用 next-intl 4.x(without i18n routing 模式),cookie 驱动 locale 切换,字典放在 `shared/i18n/messages/{locale}/`,支持 zh-CN/en 两种语言,不破坏现有路由组结构 - ✅ v3 班级邀请码体系引入(对标 Google Classroom / 钉钉教育 / 智学网):新增 `class_invitation_codes` 表(独立表,支持有效期/次数限制/审计/多码并存),6 位字母数字(剔除歧义字符 0/O/1/I/L,空间 1.13 亿),rate limit 防爆破(10 次/5 分钟),审计日志全链路记录,懒清理过期码 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `db/schema.ts` | 1111 | 54 张表定义(超硬上限) | | `db/relations.ts` | - | 表关系定义 | | `db/index.ts` | - | Drizzle 客户端 | | `lib/auth-guard.ts` | - | 认证上下文 + 权限校验 + DataScope | | `lib/permissions.ts` | - | 角色-权限映射 | | `lib/session.ts` | 38 | session 获取单一入口(getSession,server-only,dynamic import 打破循环) | | `lib/ai.ts` | 9 | 向后兼容重导出(P2-2 已拆分到 `ai/` 目录) | | `lib/ai/payload-parser.ts` | 78 | 请求负载解析 | | `lib/ai/api-key-crypto.ts` | 28 | API Key 加密/解密 | | `lib/ai/provider-config.ts` | 132 | Provider 配置查询(V3.1 增强:基于 session 用户的可见性/所有权校验,public + own private 过滤) | | `lib/ai/client.ts` | 58 | AI 客户端创建与调用 | | `lib/ai/errors.ts` | 8 | 错误格式化 | | `lib/ai/index.ts` | 5 | 聚合导出 | | `lib/audit-logger.ts` | - | 操作日志(通过 session.ts 获取 session,http-utils 获取 IP/UA) | | `lib/change-logger.ts` | - | 数据变更日志(通过 session.ts 获取 session,http-utils 获取 IP) | | `lib/login-logger.ts` | - | 登录日志(通过 http-utils 获取 IP/UA) | | `lib/password-policy.ts` | - | 密码策略纯函数 | | `lib/rate-limit/` | - | 速率限制器抽象(audit-P1-6 新增:抽象 `RateLimiter` 接口;默认内存实现 `MemoryRateLimiter`;可选 Redis 实现 `RedisRateLimiter` 基于 @upstash/ratelimit;通过 `RATE_LIMIT_DRIVER=memory\|redis` 环境变量切换;`rateLimit()` / `resetRateLimit()` 改为 Promise 返回以支持异步后端;`rateLimitKey` / `RATE_LIMIT_RULES` / `rateLimitHeaders` 保持同步纯函数) | | `lib/role-utils.ts` | 51 | 角色规范化纯函数(normalizeRole / resolvePrimaryRole;✅ P2-8 修改:保留 grade_head/teaching_head 独立性,不再映射为 teacher;优先级 admin>grade_head>teaching_head>teacher>student>parent) | | `lib/bcrypt-utils.ts` | 18 | bcrypt 哈希前缀规范化纯函数 | | `lib/breached-password.ts` | 125 | 已泄露密码检测(✅ audit-P2-4 新增:HIBP k-anonymity API 范围查询;仅发送 SHA-1 前 5 字符到 `api.pwnedpasswords.com`,本地比对完整哈希后缀,不泄露用户密码;3 秒超时 + AbortController;fail-open 策略:API 不可用时返回 `checkSkipped=true` 不阻断注册/改密;server-only) | | `lib/http-utils.ts` | 44 | 请求头解析(resolveClientIp / getUserAgent,server-only) | | `lib/password-security-service.ts` | 84 | 密码安全 DB 操作(账户锁定/失败登录追踪,server-only) | | `lib/route-resolver.ts` | 21 | 角色默认路径解析纯函数 `resolveDefaultPath(roles: string[]): string`(✅ audit-P0-6 新增:合并 `proxy.ts` 与 `onboarding/data-access.resolveDefaultPathByRoles` 重复实现;纯函数无 `server-only`,可在 edge/proxy 运行时使用;优先级 admin>grade_head/teaching_head>teacher>student>parent>fallback `/dashboard`) | | `lib/route-permissions.ts` | 82 | 路由权限配置常量(✅ audit-P1-10 + audit-P1-12 新增:从 proxy.ts 抽取,配置驱动设计;导出 4 个 `Record` 常量:`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/logger.ts` | 32 | pino 结构化日志实例(✅ 2026-07-07 日志重构新增:`createModuleLogger(module)` 工厂创建模块级 child logger;开发环境 pino-pretty 彩色输出,生产环境 JSON 到 stdout;`mixin` 自动从 AsyncLocalStorage 混入 requestId/userId;`LOG_LEVEL` 环境变量控制级别;server-only,不兼容 Edge Runtime) | | `lib/request-context.ts` | 22 | AsyncLocalStorage 请求上下文(✅ 2026-07-07 日志重构新增:`RequestContext` 接口含 requestId/userId/module;`requestContextStorage` 存储;`getRequestContext()` 读取当前上下文;仅 Node.js Runtime 可用,proxy.ts 不导入) | | `lib/with-request-context.ts` | 16 | Server Action 高阶函数包装(✅ 2026-07-07 日志重构新增:`withRequestContext(fn)` 从 `headers()` 读取 `x-request-id` 或生成 UUID,注入 AsyncLocalStorage;保留泛型类型推断) | | `lib/track-event.ts` | - | 监控埋点接口(✅ 2026-07-07 日志重构:`console.info` 替换为 `createModuleLogger("track")` 的 `log.info`;EventName 联合类型覆盖 11 个模块事件) | | `lib/excel.ts` | - | Excel 导入导出 | | `lib/file-storage.ts` | - | 文件存储抽象 | | `hooks/use-permission.ts` | - | 客户端权限 Hook | | `hooks/use-error-report.ts` | 72 | 客户端错误上报 Hook(✅ 2026-07-07 日志重构新增:在 error.tsx 中使用,通过 navigator.sendBeacon 上报到 /api/client-error;sessionStorage 节流:同一 digest 1 分钟内只上报一次;降级到 fetch keepalive) | | `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) | | `lib/date-utils.ts` | - | ✅ 审计 v1 P0-6 新增(S-04 DRY):日期序列化工具 `serializeDate(date: Date \| null \| undefined): string \| null` + `serializeDateRequired(date: Date): string`;messaging / course-plans / announcements / elective / audit / files / school / notifications 等 10+ data-access 文件迁移至此共享 util | | ~~`components/onboarding-gate.tsx`~~ | ~~312~~ | ✅ audit-P0-4 已删除(引导流程业务逻辑已迁移至 `modules/onboarding/`,原文件无任何 import 引用,删除以避免文件膨胀) | | `components/global-search.tsx` | 221 | 全局搜索(业务泄漏) | | `types/permissions.ts` | 157 | 67 个权限点常量 + Role/DataScope/AuthContext 类型(✅ rbac 新增:`Role` 类型从固定联合类型改为 `string` 支持动态角色;新增 `BUILTIN_ROLES` 常量、`BuiltinRole` 类型、`isBuiltinRole()` 类型守卫;`isRole()` 标记为 deprecated) | | `lib/api-response.ts` | 212 | ✅ 2026-07-04 API 规范化重构新增:API 路由层统一响应工具(apiSuccess/apiError/apiFromAction/handleApiError/withApiErrorHandler/parseJsonBody/errorToStatus);统一响应信封 + 集中错误→HTTP 状态码映射,消除每个 route 重复 try/catch 与字符串匹配推导状态码 | | `lib/sse.ts` | 81 | ✅ 2026-07-04 API 规范化重构新增:SSE 共享工具(formatSseEvent/formatSseError/formatSseDone/createSseResponse/createSseError);统一 stream 路由的事件格式与响应头,消除 `ai/chat/stream` 与 `notifications/stream` 重复实现 | --- ## 2.2 exams(考试模块) **职责**:考试全生命周期管理(创建/编辑/预览/发布/删除/复制)+ AI 辅助出题。 **导出函数**: - Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度3,按 gradeId 查询年级下所有考试 + 提交统计,EXAM_READ 权限)/ `autoMarkExamAction`(✅ 2026-06-24 新增:AI 自动标记试卷文本,将粘贴的试卷文本交给 AI 解析为带题目块/分组/填空/加点字标记的 Tiptap JSONContent 文档,EXAM_AI_GENERATE 权限,返回 `{ doc, title }` 供富文本编辑器载入。✅ 2026-06-24 拆分:迁移至 `ai-pipeline/auto-mark.ts`,actions.ts re-export 保持向后兼容)/ `createExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器保存试卷草稿,将 Tiptap JSONContent 通过 `editor/editor-to-structure.editorDocToStructure` 转换为 questions + structure 后复用 `persistAiGeneratedExamDraft` 持久化,EXAM_CREATE 权限;✅ 2026-06-24 升级:递归 `convertStructureNode` 支持 section/group/question 三级层级,section 数据不再丢失。✅ 2026-06-24 拆分:迁移至 `actions-rich-editor.ts`,actions.ts re-export 保持向后兼容)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access) / `updateExamFromRichEditorAction`(✅ 2026-06-24 新增:从富文本编辑器更新已有试卷,校验所有权后递归转换 EditorDoc→structure,为无 questionId 的题目创建 questions 记录,调用 `data-access.updateExamWithRichEditorData` 持久化,EXAM_UPDATE 权限,revalidatePath 列表页与组卷页。✅ P0-4 修复(2026-06-24):原直接 db.transaction 写 exams/examQuestions 表,现已下沉到 data-access.updateExamWithRichEditorData。✅ 2026-06-24 拆分:迁移至 `actions-rich-editor.ts`,actions.ts re-export 保持向后兼容) - Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)/ `getExamsForGradeEntry`(✅ 2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷,供成绩录入页试卷选择器使用,返回 id/title/subjectName/gradeName/questionCount/totalScore)/ `getExamForGradeEntry`(✅ 2026-06-24 新增:获取单个试卷详情含题目列表,innerJoin questions 获取 type,含 scope 校验,返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}],供 grades 模块按试卷录入成绩使用)/ `updateExamWithRichEditorData`(✅ 2026-06-24 新增,P0-4 修复:从富文本编辑器更新已有试卷的 DB 事务,单事务内更新 exams.title/structure + 删除旧 examQuestions + 插入新 examQuestions,原 updateExamFromRichEditorAction 直接 db.transaction 的逻辑下沉至此) - AI Pipeline:`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` / `loadAiDraftQuestionsAndStructure`(✅ 2026-06-24 新增:从 actions.ts 下沉,加载 AI 草稿题目与结构,优先从 rawAiQuestions JSON 解析,否则从源文本生成) - Utils:`normalizeStructure`(v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取;✅ 2026-06-24 升级:支持 section/group/question 三级层级递归校验,section 节点含 title/level/children,group 节点含 title/instruction/children) - Lib(✅ 类型守卫专项重构 2026-07-07 新增 `lib/type-guards.ts`):`isRecord`(unknown → `Record`,消除 `as Record` 断言)/ `isSimpleTextContent`(校验可选 text 字段)/ `isQuestionContentObj`(校验题目内容 text+options 结构,options 内仅校验 id/text 字段)/ `isJSONContent` + `isJSONContentArray`(Tiptap JSONContent 守卫,返回类型为 `v is JSONContent`)/ `toSortableId`(dnd-kit `UniqueIdentifier`(string|number)→ string,消除 `as string` 断言)/ `isQuestionBlockAttrs`(QuestionBlock 节点 attrs 守卫 { questionId, type, score })。**用途**:消除 ai-pipeline/components/editor 子目录 13 个文件中的违规 `as` 断言;合规 `as`(`as unknown as` 从 unknown 转换、`as const` 字面量常量)保留未改。**使用者**:`ai-pipeline/request.ts`、`components/exam-preview-utils.ts`、`components/exam-actions.tsx`、`components/assembly/{structure-editor,question-bank-list,exam-paper-preview}.tsx`、`editor/selection-toolbar.tsx`、`editor/exam-nodes-to-editor-doc.ts`、`editor/exam-rich-editor-inner.tsx`、`editor/extensions/question-block.tsx`、`components/exam-rich-form.tsx` - Preview Utils(`components/exam-preview-utils.ts`,✅ 2026-06-24 补充记录):`buildPreviewNodes` / `parseEditableContent` / `flattenPreviewQuestions` / `findPreviewQuestionNode` / `updatePreviewQuestionNodeInList` / `buildPreviewSignature` / `buildPreviewPayload` / `buildPreviewRequestData`(以上 8 个为预览纯函数,供 Hook 与组件复用)+ ✅ 2026-06-24 新增 5 个(从 use-exam-preview-tasks Hook 提取以控制行数):`buildTaskFormValues`(提取表单字段子集供任务回填)/ `isPreviewBackgroundTask`(类型守卫,校验 localStorage 恢复数据)/ `persistPreviewTasksToStorage`(持久化任务到 localStorage)/ `parseStoredPreviewTasks`(解析 localStorage 任务,queued/running 标记为 failed 防僵尸)/ `executeBackgroundPreviewTask`(执行单个后台预览任务的纯函数) - Hooks(✅ 2026-06-24 拆分记录):`useExamPreview`(主组合器,68 行,组合三个子 Hook + handlePreview 编排,返回 API 与拆分前一致)/ `usePreviewState`(状态子 Hook,67 行,10 个 useState + 3 个更新函数)/ `usePreviewBackgroundTasks`(后台任务子 Hook,80 行,PQueue 队列 + localStorage 持久化 + 入队/打开任务)/ `usePreviewRewrite`(AI 重写子 Hook,79 行,handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction) - Stats-service(V3-8 新增):`getExamAnalytics`(cache 包装,聚合考试所有作业的已批改提交,计算平均分/及格率/分数段分布/逐题错误率与难度等级,对标智学网考试分析)+ `ExamAnalyticsSummary` 类型 - Types(✅ 2026-06-24 新增成绩录入相关类型):`ExamQuestionItem`(试卷中单个题目的精简结构 { id, order, score, type })/ `ExamForGradeEntry`(成绩录入用的试卷详情,含题目列表)/ `ExamOptionForEntry`(成绩录入页试卷选择器选项 { id, title, subjectName, gradeName, questionCount, totalScore }) - Editor 子模块(✅ 2026-06-24 新增 `editor/` 目录,基于 Tiptap 的试卷富文本编辑器): - 组件:`ExamRichEditor`(富文本编辑器主体,forwardRef 暴露 `ExamRichEditorHandle` 以便父组件获取 JSONContent)/ `SelectionToolbar`(浮动选择工具栏,对选中文本应用 DottedMark 等标记) - 自定义 Tiptap 扩展(`editor/extensions/`):`DottedMark`(加点字标记,拼音注音题)/ `BlankNode`(填空空位节点,原子节点)/ `ImageNode`(图片节点,含 fileId/url/alt 属性)/ `QuestionBlock`(题目块节点,含 type/score 等 attrs)/ `GroupBlock`(大题分组节点,含 title)/ `SectionBlock`(✅ 2026-06-24 新增:分卷节点,含 title/level attrs,isolating: true,对应第Ⅰ卷/第Ⅱ卷层级) - 类型(`editor/exam-rich-editor-types.ts`):`RichQuestionType`(`single_choice | multiple_choice | judgment | text | composite`)/ `RichQuestionContent`(题干文本+选项+填空+图片+子题+正确答案)/ `EditorQuestion` / `EditorStructureNode`(✅ 2026-06-24 升级:section|group|question 三级递归,section 含 title/level/children,group 含 title/instruction/children)/ `EditorDoc`({ title, questions, structure })/ `EditorJSONContent`(JSONContent 别名) - 转换工具:`editorDocToStructure`(`editor/editor-to-structure.ts`,Tiptap JSONContent → EditorDoc,供 `createExamFromRichEditorAction`/`updateExamFromRichEditorAction` 持久化使用)/ `structureToEditorDoc`(`editor/structure-to-editor.ts`,EditorDoc → Tiptap JSONContent,✅ 2026-06-24 升级支持 section 节点回填,供编辑器回填已保存试卷使用)/ `examNodesToEditorDoc`(✅ 2026-06-24 新增:`editor/exam-nodes-to-editor-doc.ts`,BuildExam 的 `ExamNode[]` + `Question[]` → EditorDoc 桥梁函数,递归转换 section/group/question,供 edit-rich 页面加载已有试卷到富文本编辑器) - Barrel 导出:`editor/index.ts` 聚合导出上述所有组件、扩展、类型与转换函数 - Components(V3-8 新增):`ExamAnalyticsDashboard`(考试分析仪表盘:汇总卡片+分数段分布+逐题分析表) - Components(✅ 2026-06-24 新增):`ExamRichForm`(富文本编辑器试卷创建/编辑表单,使用 `ResizablePanel` 左编辑右预览布局,集成 `autoMarkExamAction` 一键 AI 标记;✅ 2026-06-24 升级支持 `mode="create"|"edit"` 双模式:create 模式调用 `createExamFromRichEditorAction` 保存草稿,edit 模式调用 `updateExamFromRichEditorAction` 更新已有试卷,编辑模式隐藏科目/年级/难度等字段并显示"Back to Build"按钮) - Components(✅ P3 新增记录):`ExamPreview`(试卷结构预览组件,从 exam-rich-form.tsx 提取,渲染 section/group/question 三级嵌套;P3 修改:题目图片 `` → `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` 获取作业与提交数据,合理跨模块调用) - 被依赖:`homework`(通过 sourceExamId 外键,合理)、`dashboard`(通过 data-access,P0-4 已修复)、`proctoring`(✅ P1-1 已修复:通过 exams data-access)、`diagnostic`(✅ P1-1 已修复:通过 exams data-access)、`grades`(✅ 2026-06-24 新增:通过 data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用) **已知问题**: - ✅ P0-1 已修复:~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`,通过 ID 映射保持 structure 引用一致 - ✅ P0-2 已修复:~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds` - ✅ P1-1 已修复:~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions` - ✅ P1-2 已修复:~~`actions.ts` 832 行(超 800 建议),多处直接 DB 操作~~ DB 操作已下沉到 data-access,actions.ts 现 691 行 - ✅ P0-4 已修复(2026-06-24):~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData - ✅ actions.ts 拆分(2026-06-24):~~`actions.ts` 1525 行(超 1000 硬上限)~~ 拆分为 4 个文件:actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action),actions.ts re-export 保持向后兼容 - ✅ P0-8 已修复:~~`data-access.ts` 1036 行(超 1000 硬上限)~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口),文末 re-export 保持向后兼容 - ⚠️ P1:`ai-pipeline.ts` 857 行(超 800 建议),混合 4 类职责 - ✅ P2 已修复:`ai-pipeline.ts` 中 3 处非空断言清理(`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`) - ✅ V3-5/V3-8/V3-12:`exam-actions.tsx` 增强:V3-5 角色化菜单(按角色显示不同操作项)、V3-8 新增 analytics 菜单项(BarChart3 图标,跳转 `/teacher/exams/[id]/analytics`)、V3-12 移动端触摸目标尺寸优化 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 633 | 10 个核心 Server Action(✅ 2026-06-24 拆分:从 1525 行降至 633 行;re-export autoMarkExamAction/createExamFromRichEditorAction/updateExamFromRichEditorAction 保持向后兼容;P0-4 已修复:无直接 DB 操作) | | `actions-helpers.ts` | 96 | ✅ 2026-06-24 新增:跨文件共享辅助函数(getStringValue/getBoolValue/parseExamModeConfig/failState/successState/invalidFormState/prepareExamCreateContext) | | `actions-rich-editor.ts` | 250 | ✅ 2026-06-24 新增:富文本编辑器 Server Action(createExamFromRichEditorAction/updateExamFromRichEditorAction)+ 共享辅助 convertStructureNode + RichExamCreateSchema/RichExamUpdateSchema。P0-4 修复:updateExamFromRichEditorAction 调用 data-access.updateExamWithRichEditorData 不再直访 DB | | `ai-pipeline/auto-mark.ts` | 356 | ✅ 2026-06-24 新增:autoMarkExamAction Server Action + 纯转换辅助(buildTiptapDocFromAiResponse/splitByDottedTexts/extractTitleFromAiResponse/AutoMarkSchema/AutoMarkResult)。从 actions.ts 迁出 | | `ai-pipeline.ts` | 857 | AI 出题管线(超限) | | `data-access.ts` | 542 | 考试 CRUD(✅ 2026-06-24 拆分:跨模块接口迁移至 data-access-cross-module.ts,原 1036 行降至 542 行;含 P1-2 写/查询函数,P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信;✅ P0-4 修复:新增 updateExamWithRichEditorData 将富文本编辑器更新事务下沉;导出 parseExamMeta/getString/getNumber/toExamStatus 供 cross-module 复用;文末 re-export 跨模块接口保持向后兼容) | | `data-access-cross-module.ts` | 462 | ✅ 2026-06-24 新增:跨模块查询接口(供 homework/grades/proctoring/diagnostic/dashboard/lesson-preparation 等模块调用,避免直查 exams/examSubmissions 表)。包含 getExamsDashboardStats/getExamIdsByGradeIds/getExamWithQuestionsForHomework/getExamSubjectIdMap/getExamTitleById/getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionWithAnswers/getExamSubmissionsForExam/addExamQuestions/getExamsByGradeId/getExamsForGradeEntry/getExamForGradeEntry,标记 `server-only`,从 data-access.ts 导入 parseExamMeta/getString/getNumber/toExamStatus 复用 | | `stats-service.ts` | - | V3-8 新增:考试分析数据聚合(`getExamAnalytics` + `ExamAnalyticsSummary` 类型) | | `types.ts` | 50+ | 类型定义(2026-06-24 新增:ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用。注:`AutoMarkResult`/`AutoMarkSchema` 已迁移至 `ai-pipeline/auto-mark.ts`,`RichExamCreateSchema`/`RichExamUpdateSchema` 已迁移至 `actions-rich-editor.ts`) | | `editor/index.ts` | 23 | 2026-06-24 新增:富文本编辑器 barrel 导出(聚合组件/扩展/类型/转换函数) | | `editor/exam-rich-editor.tsx` | - | 2026-06-24 新增:ExamRichEditor 富文本编辑器主体(forwardRef + ExamRichEditorHandle) | | `editor/selection-toolbar.tsx` | - | 2026-06-24 新增:SelectionToolbar 浮动选择工具栏 | | `editor/exam-rich-editor-types.ts` | 42 | 2026-06-24 新增:富文本编辑器类型定义(RichQuestionType/RichQuestionContent/EditorQuestion/EditorStructureNode/EditorDoc/EditorJSONContent) | | `editor/editor-to-structure.ts` | - | 2026-06-24 新增:editorDocToStructure(Tiptap JSONContent → EditorDoc) | | `editor/structure-to-editor.ts` | - | 2026-06-24 新增:structureToEditorDoc(EditorDoc → Tiptap JSONContent) | | `editor/extensions/index.ts` | 5 | 2026-06-24 新增:Tiptap 扩展 barrel 导出 | | `editor/extensions/dotted-mark.ts` | - | 2026-06-24 新增:DottedMark 加点字标记扩展 | | `editor/extensions/blank-node.tsx` | - | 2026-06-24 新增:BlankNode 填空空位节点扩展 | | `editor/extensions/image-node.ts` | - | 2026-06-24 新增:ImageNode 图片节点扩展 | | `editor/extensions/group-block.tsx` | - | 2026-06-24 新增:GroupBlock 大题分组节点扩展 | | `editor/extensions/question-block.tsx` | - | 2026-06-24 新增:QuestionBlock 题目块节点扩展 | | `hooks/use-exam-preview.ts` | 68 | 预览 Hook 主组合器(✅ 2026-06-24 拆分:从 318 行降至 68 行,组合 state/tasks/rewrite 三个子 Hook,保留 handlePreview 编排逻辑,返回 API 与拆分前完全一致,调用方 exam-form.tsx 无需修改) | | `hooks/use-exam-preview-state.ts` | 67 | ✅ 2026-06-24 新增:预览状态子 Hook(10 个 useState + updatePreviewQuestionNode/updateSelectedQuestionFromAi/applyPreviewResult 三个更新函数) | | `hooks/use-exam-preview-tasks.ts` | 80 | ✅ 2026-06-24 新增:预览后台任务子 Hook(PQueue 并发队列 + localStorage 持久化恢复 + handleBackgroundPreview/handleOpenPreviewTask) | | `hooks/use-exam-preview-rewrite.ts` | 79 | ✅ 2026-06-24 新增:预览 AI 重写子 Hook(handleRewriteSelectedQuestion 调用 regenerateAiQuestionAction) | | `components/exam-preview-utils.ts` | 293 | 预览纯函数工具集(buildPreviewNodes/parseEditableContent/flattenPreviewQuestions/findPreviewQuestionNode/updatePreviewQuestionNodeInList/buildPreviewSignature/buildPreviewPayload/buildPreviewRequestData + ✅ 2026-06-24 新增 5 个:buildTaskFormValues/isPreviewBackgroundTask/persistPreviewTasksToStorage/parseStoredPreviewTasks/executeBackgroundPreviewTask,从 use-exam-preview-tasks Hook 提取以控制行数) | | `components/exam-preview.tsx` | - | 试卷结构预览组件(use client):从 exam-rich-form.tsx 提取,渲染试卷富文本编辑器的结构预览,支持分卷(section)/大题分组(group)/题目(question)三级嵌套递归渲染。✅ P3 修改:题目块中图片将原生 `` 替换为 `next/image` 的 `` 组件(自动优化、懒加载、防布局偏移),新增导入 `next/image`;使用 `useTranslations('examHomework')` 国际化 section/group 标题与统计文案 | | `utils/normalize-structure.ts` | 57 | v3 新增:exam.structure 运行时校验与归一化(从 build/page.tsx 提取) | | `lib/type-guards.ts` | 80 | ✅ 类型守卫专项重构(2026-07-07)新增:exams 模块通用类型守卫集合。导出 `isRecord` / `isSimpleTextContent` / `isQuestionContentObj` / `isJSONContent` / `isJSONContentArray` / `toSortableId` / `isQuestionBlockAttrs` 共 7 个守卫/转换函数。消除 ai-pipeline/components/editor 子目录 13 个文件中的违规 `as` 断言(`as Record` / `as { text?: string }` / `as JSONContent[]` / `as string` / `as QuestionBlockAttrs` 等),保留合规 `as unknown as`(从 unknown 转换)与 `as const`(字面量常量)未改 | | `components/exam-analytics-dashboard.tsx` | - | V3-8 新增:考试分析仪表盘组件 | | `components/exam-actions.tsx` | - | V3-5/V3-8/V3-12 增强:角色化菜单+analytics 链接+移动端触摸优化 | | `components/exam-rich-form.tsx` | - | 2026-06-24 新增:富文本编辑器试卷创建/编辑表单(ResizablePanel 左编辑右预览 + AI 自动标记 + 保存草稿)。✅ 2026-06-24 拆分后:import 路径直连 `ai-pipeline/auto-mark.ts`(autoMarkExamAction)和 `actions-rich-editor.ts`(createExamFromRichEditorAction/updateExamFromRichEditorAction),支持 create/edit 双模式 | | `components/exam-boundaries.tsx` | 189 | ✅ 2026-06-25 新增(P2-3):模块统一边界组件。导出 `ExamErrorBoundary`(薄包装 SectionErrorBoundary,固定 examHomework 命名空间)、`ExamEmptyState`(i18n 键驱动的空状态)、`ExamSkeleton`(list/card/detail/analytics/editor 五种变体)。复用 shared 层 EmptyState/Skeleton/SectionErrorBoundary,避免每页重复实现。新增 i18n 键 `exam.error.boundaryTitle/boundaryDescription/retry` | | `components/*` | 19+ 文件 | 考试表单/组卷/预览/分析组件 | | `services/exam-service-port.ts` | 95 | ✅ 2026-06-25 新增(P2-4):模块对内的服务端口契约。导出 `ExamServicePort` 接口(listExams/getExam/createExam/updateExam/deleteExam/duplicateExam/autoMarkExam/getExamPreview/getAnalytics/getSubjects/getGrades 共 12 个方法)+ `ExamDetail`/`ExamCreateInput`/`ExamUpdateInput` 类型。区别于 shared 层的 ExamHomeworkServicePort(跨模块调用方契约,单例注册),本接口面向模块内部组件,通过 React Context 注入 | | `services/exam-service-context.tsx` | 72 | ✅ 2026-06-25 新增(P2-4):ExamServicePort 的 React Context 注入点。导出 `ExamServiceProvider`(Provider 组件)、`useExamService`(消费者 Hook,未注入时抛错)、`useTryExamService`(测试用,未注入时返回 null)。当前为骨架,具体实现将在 P2-6+ 落地 | | `services/index.ts` | 23 | ✅ 2026-06-25 新增:services 子模块 barrel 导出 | | `config/exam-widgets.ts` | 192 | ✅ 2026-06-25 新增(P2-5):配置驱动的 Widget 布局。导出 `ExamWidgetConfig`/`ExamWidgetLayoutConfig` 类型、四个角色的默认配置(TEACHER_EXAM_WIDGET_CONFIG/ADMIN_EXAM_WIDGET_CONFIG/STUDENT_EXAM_WIDGET_CONFIG/PARENT_EXAM_WIDGET_CONFIG)、`getExamWidgetConfig(role)` 与 `getWidgetsBySlot(config, slot)` 工具函数。与 dashboard/config/widget-configs.ts 模式对齐 | **已知问题**: - ✅ P0-1 已修复:~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`,通过 ID 映射保持 structure 引用一致 - ✅ P0-2 已修复:~~`getExams`/`getExamById`/`getExamsDashboardStats` 直查 `classes` 表~~ 改为调用 `classes/data-access.getClassGradeIdsByClassIds` - ✅ P1-1 已修复:~~`getSubjectsAction`/`getGradesAction` 直查 `subjects`/`grades` 表~~ 改为调用 `school/data-access.getSubjectOptions` / `getGradeOptions` - ✅ P1-2 已修复:~~`actions.ts` 832 行(超 800 建议),多处直接 DB 操作~~ DB 操作已下沉到 data-access,actions.ts 现 691 行 - ✅ P0-4 已修复(2026-06-24):~~`updateExamFromRichEditorAction` 直接 db.transaction 写 exams/examQuestions 表~~ DB 事务下沉到 data-access.updateExamWithRichEditorData - ✅ actions.ts 拆分(2026-06-24):~~`actions.ts` 1525 行(超 1000 硬上限)~~ 拆分为 4 个文件:actions.ts(633 行,核心 Action) + actions-helpers.ts(96 行,共享辅助) + actions-rich-editor.ts(250 行,富文本编辑器 Action) + ai-pipeline/auto-mark.ts(356 行,AI 自动标记 Action),actions.ts re-export 保持向后兼容 - ✅ P0-8 已修复:~~`data-access.ts` 1036 行(超 1000 硬上限)~~ 拆分为 `data-access.ts`(542 行,考试 CRUD) + `data-access-cross-module.ts`(462 行,13 个跨模块查询/写接口),文末 re-export 保持向后兼容 - ⚠️ P1:`ai-pipeline.ts` 857 行(超 800 建议),混合 4 类职责 - ✅ P2 已修复:`ai-pipeline.ts` 中 3 处非空断言清理(`draft.sections!.forEach` → 安全守卫、`aiParsed.sections!.flatMap` → `?? []`、`aiParsed.sections!.map` → `?? []`) - ✅ V3-5/V3-8/V3-12:`exam-actions.tsx` 增强:V3-5 角色化菜单(按角色显示不同操作项)、V3-8 新增 analytics 菜单项(BarChart3 图标,跳转 `/teacher/exams/[id]/analytics`)、V3-12 移动端触摸目标尺寸优化 - ✅ 2026-06-25 P2 落地(exams 审计报告实施): - ✅ P2-1:在 `editor/exam-rich-editor-types.ts` 新增 `isRichQuestionType`/`isStandaloneQuestionType`/`toRichQuestionType`/`toStandaloneQuestionType` 类型守卫;`editor/editor-to-structure.ts` 与 `editor/exam-nodes-to-editor-doc.ts` 改用 `toRichQuestionType`;`actions-rich-editor.ts` 两处 `as "single_choice"|"multiple_choice"|"text"|"judgment"` 断言改为 `toStandaloneQuestionType` 安全收窄(消除 4 处 `as` 违规) - ✅ P2-3:新增 `components/exam-boundaries.tsx`(ExamErrorBoundary/ExamEmptyState/ExamSkeleton 三组合单元,5 种骨架变体) - ✅ P2-4:新增 `services/exam-service-port.ts` + `services/exam-service-context.tsx`(ExamServicePort 接口 + Context 注入骨架,与 shared 层 ExamHomeworkServicePort 区分定位) - ✅ P2-5:新增 `config/exam-widgets.ts`(四角色 Widget 配置 + getExamWidgetConfig/getWidgetsBySlot) --- ## 2.3 homework(作业模块) **职责**:作业全生命周期(创建/发布/作答/批改/分析)。 **导出函数**: - Actions:`createHomeworkAssignmentAction` / `startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `submitHomeworkAction` / `gradeHomeworkSubmissionAction` / `batchAutoGradeSubmissionsAction`(V3-7 新增:批量自动批改,HOMEWORK_GRADE 权限+非管理员仅可批改自己创建的作业)/ `getScansAction`(✅ 2026-06-24 新增:获取某次提交的所有答题扫描图,扫描图存储在 fileAttachments 表中 targetType="homework"/targetId=submissionId;支持两类访问者——教师 HOMEWORK_GRADE 仅可访问自己创建的作业的提交,学生 HOMEWORK_SUBMIT 仅可访问自己的提交)/ `deleteScanAction`(✅ 2026-06-24 新增:删除答题扫描图,仅允许提交者本人删除且仅在提交状态为 started 时允许,调用 `files/data-access.deleteFileAttachment` 删除 DB 记录)/ `getExcellentSubmissionsAction`(✅ P3-1 新增:查询优秀作业列表,HOMEWORK_GRADE 权限,按 ctx.dataScope 过滤,调用前埋点 `homework.excellent_viewed`)/ `remindUnsubmittedAction`(✅ P3-2 新增:催交提醒,HOMEWORK_GRADE 权限,查询未提交学生后通过动态 import 调用 `notifications/data-access.createNotification` 创建 type=homework/priority=high 站内通知,Promise.allSettled 容错统计,埋点 `homework.remind_unsubmitted`)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access/data-access-write) - Data-access:`getHomeworkAssignments` / `getHomeworkAssignmentById` / `getHomeworkSubmissions` / `getStudentHomeworkAssignments` / `getStudentHomeworkTakeData` / `getHomeworkAssignmentReviewList` / `getHomeworkSubmissionDetails` / `getDemoStudentUser`(已迁移至 users 模块 `getCurrentStudentUser`,此处为 re-export 向后兼容)/ `toQuestionContent` / `getAssignmentMaxScoreById`(后两者供 stats-service 使用;`isRecord` 已于 2026-06-24 迁移至 `@/shared/lib/type-guards` 统一复用)/ `getHomeworkAssignmentsByExamId`(V3-8 新增:按考试 ID 查作业+目标/提交/批改计数)/ `getGradedSubmissionsByExamId`(V3-8 新增:按考试 ID 查已批改提交,按学生去重)/ `getStudentSubmissionResult`(V3-9 新增:查学生指定作业最新提交,用于结果页)/ `getStudentExamResults`(V3-11 新增:查学生考试结果列表,供家长端展示)/ `getExcellentSubmissions`(✅ P3-1 新增:查优秀作业列表,按得分率过滤 minPercentage 默认 80,同一学生取最高分,按得分率降序,支持 scope 过滤)/ `getUnsubmittedStudents`(✅ P3-2 新增:查未提交作业的学生列表,对比 targets 与已提交学生集合返回差集,供催交提醒使用) - Data-access-classes:`getAssignmentIdsForStudents` / `getHomeworkAssignmentsWithSubject` / `getHomeworkAssignmentsByIds` / `getAssignmentTargetCounts` / `getHomeworkSubmissionsForStudents` / `getPublishedHomeworkAssignmentsWithSubject` / `getHomeworkSubmissionsForAssignments`(P0-7 新增,供 classes 模块跨模块调用;✅ P1-1 已修复:内部不再直查 exams/subjects 表,改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`) - Data-access-write:11 个写操作函数(P1-2 新增 10 个从 actions 下沉 + V3-7 新增 `batchAutoGradeSubmissions`) - Stats-service:`getTeacherGradeTrends` / `getHomeworkAssignmentAnalytics` / `getStudentDashboardGrades`(从 data-access.ts re-export 以保持向后兼容) - Components(V3-7/V3-9 新增):`HomeworkBatchGradingView`(批量批改视图:勾选+一键批改+toast 反馈)/ `HomeworkSubmissionResult`(提交后即时反馈:分数汇总+对错分布+错题预览) - Components(✅ 2026-06-24 新增扫描图相关):`ScanUploader`(学生扫描图上传组件,调用 /api/upload 上传图片,targetType="homework",支持增删/排序,导出 `ScanImage` 类型)/ `ScanImageViewer`(扫描图查看器,支持翻页/缩放/旋转/全屏,用于阅卷式批改时查看学生答题图片)/ `HomeworkScanGradingView`(教师阅卷式批改视图,使用 `ResizablePanel` 左侧题目+批改表单、右侧扫描图查看器,集成 `getScansAction` 拉取扫描图、`gradeHomeworkSubmissionAction` 批改,支持上一份/下一份提交快速切换) - Components(✅ P3-1 新增):`ExcellentSubmissions`(优秀作业展示 async 服务端组件,按得分率降序,前三名金/银/铜徽章,迟交 Clock 标注,仅展示姓名不展示学号保护隐私,直接调用 `getExcellentSubmissions` 避免多余 action 往返)/ `ExcellentSubmissionsWithBoundary`(带 `SectionErrorBoundary` + `Suspense` 包裹版本,提供 Skeleton 骨架屏与局部错误降级,避免本区块故障影响整个作业详情页) - Types(✅ 2026-06-24 新增):`ScanAttachment`(扫描图附件结构 { fileId, url, filename, originalName, page },由 `getScansAction` 返回)/ `ScanImage`(扫描图 UI 结构,由 `ScanUploader` 导出供 `ScanImageViewer` 复用) - Types(✅ P3-1 新增):`ExcellentSubmissionItem`(优秀作业展示项 { submissionId, assignmentId, assignmentTitle, studentName, totalScore, maxScore, percentage, submittedAt, isLate },percentage 为得分率 0-100,isLate 标注迟交但质量优秀样例)/ `ExcellentSubmissionQuery`(优秀作业查询参数 { assignmentId, minPercentage?, limit? },minPercentage 默认 80,limit 默认 10) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamIdsByGradeIds/getExamSubjectIdMap/getExamWithQuestionsForHomework/getExamForProctoringCrossModule)、`classes`(✅ P1-1 已修复:通过 classes data-access.getStudentIdsByClassId 等 7 个函数)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getSubjectNameMapByIds)、`users`(✅ P1-1 已修复:通过 users data-access.getUserWithRole/getUserNamesByIds)、`files`(✅ 2026-06-24 新增:`deleteScanAction` 调用 `files/data-access.deleteFileAttachment` 删除扫描图 DB 记录,合理跨模块调用)、`notifications`(✅ P3-2 新增:`remindUnsubmittedAction` 通过动态 import 调用 `notifications/data-access.createNotification` 为未提交学生创建催交通知,合理跨模块调用) - 被依赖:`dashboard`(通过 data-access,合理)、`parent`(通过 data-access,合理;V3-11 新增 `getStudentExamResults` 供 parent 调用)、`classes`(✅ P0-7 已修复:classes 通过 `homework/data-access-classes` 获取作业数据,不再反向直查 homework/exams 表)、`exams`(V3-8 新增:`exams/stats-service.getExamAnalytics` 调用 `getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId`,合理跨模块调用) **已知问题**: - ✅ P0 已解决:`data-access.ts` 已拆分至 598 行(原 1038 行超 1000 硬上限),统计函数迁移至 `stats-service.ts` - ✅ P0 已解决:`getStudentDashboardGrades` 排名计算逻辑迁移至 `stats-service.ts` - ✅ P0 已解决:`getHomeworkAssignmentAnalytics` 错误率统计逻辑迁移至 `stats-service.ts` - ✅ P0-7 已修复:新增 `data-access-classes.ts`,将 classes 模块对 homework/exams 表的直查封装为 homework 模块的导出函数,恢复三层架构 - ✅ P1-1 已修复:~~5 处直查 `exams` 表~~ 改为调用 `exams/data-access.getExamIdsByGradeIds` / `getExamSubjectIdMap` / `getExamWithQuestionsForHomework`;`data-access-classes.ts` 内部 JOIN exams+subjects 也改为调用 `exams/data-access.getExamSubjectIdMap` + `school/data-access.getSubjectNameMapByIds`,彻底消除跨模块直查 - ✅ P0-竞品已修复:新增 `hooks/use-exam-countdown.ts` 倒计时 Hook(对标智学网/猿题库),`homework-take-view.tsx` 集成限时/监考模式倒计时显示与到时自动提交;`data-access.ts` 的 `getStudentHomeworkTakeData` 新增 `examModeConfig` + `startedAt` 字段供客户端计算截止时间 - ✅ P1-2 已修复:~~`actions.ts` 多处直接 DB 操作(`createHomeworkAssignmentAction` 157 行)~~ DB 操作已下沉到 `data-access-write.ts`,actions.ts 现 239 行 - ✅ V3-7:新增 `batchAutoGradeSubmissionsAction` + `batchAutoGradeSubmissions` + `HomeworkBatchGradingView`,提交列表页接入批量批改 - ✅ V3-8:新增 `getHomeworkAssignmentsByExamId` + `getGradedSubmissionsByExamId`,供 exams 模块跨模块调用 - ✅ V3-9:新增 `getStudentSubmissionResult` + `HomeworkSubmissionResult` + 路由 `/student/learning/assignments/[assignmentId]/result`,`homework-take-view.tsx` 提交后跳转结果页 - ✅ V3-12:`homework-take-view.tsx` 移动端触摸目标尺寸优化 - ✅ 2026-06-23 审计修复:data-access 层 4 个查询函数新增 `scope?: DataScope` 参数(`getHomeworkAssignments`/`getHomeworkAssignmentById`/`getHomeworkSubmissions`/`getHomeworkAssignmentReviewList`),教师仅查看自己创建的作业,学生/家长仅查看相关作业;Server Action catch 块改用 `handleActionError` 统一错误处理 - ✅ 2026-06-24 i18n 完成:11 个文件(6 个客户端组件 + 3 个服务端页面 + 2 个已有 `t` 的页面)全部接入 `next-intl`,硬编码中文/英文文案替换为 `examHomework.homework.*` 翻译键(zh-CN + en 双语);客户端组件使用 `useTranslations("examHomework")`,服务端页面使用 `getTranslations("examHomework")` - ✅ 2026-06-25 审计修复 P0:13 个缺失的 `loading.tsx`/`error.tsx` 路由文件全部补全(teacher/homework 根 + assignments + submissions + submissions/[submissionId] + scan-grading;student/learning/assignments + result);`homework-take-view.tsx` 拆分为 5 个组件(`HomeworkTakeView` 主容器 396 行 + `HomeworkTakeSidebar` 侧边栏 + `HomeworkTakeConfirmDialog` 提交确认对话框);`schema.ts` 错误消息改为 i18n 键 `homework.form.error.*`,Server Action 通过 `translateZodErrors` 翻译后返回 `fieldErrors`;`ScanAttachment` 类型从 `actions.ts` 迁移至 `types.ts` - ✅ 2026-06-25 审计修复 P1:`homework-grading-view.tsx` 删除本地 wrapper 函数(`isAutoGradable`/`applyAutoGrades`/`getCorrectnessState`/`extractQuestionText`),直接调用 `lib/question-content-utils`;`data-access.ts` 末尾 re-export(`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId`/`getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData`)已删除,调用方改为直接 import 子模块;`teacher/homework/assignments/[id]/page.tsx` Analytics 区块接入 `SectionErrorBoundary + Suspense + Skeleton`;`getScansAction` 中的 DB 查询下沉到新建的 `data-access-scans.ts::getScansBySubmissionId` - ✅ 2026-06-25 审计修复 P2:`startHomeworkSubmissionAction` / `saveHomeworkAnswerAction` / `deleteScanAction` 补全 `trackExamEvent` 埋点(`homework.started` / `homework.answer_saved` / `homework.scan_deleted`);`extractQuestionText` 从 `homework-grading-view.tsx` 迁移到 `lib/question-content-utils.ts` **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `data-access.ts` | 716 | 作业 CRUD + 批改(✅ 2026-06-25:删除末尾 re-export,调用方改为直接 import 子模块;✅ P3-1 新增 `getExcellentSubmissions`:按得分率过滤优秀作业,同一学生取最高分;✅ P3-2 新增 `getUnsubmittedStudents`:查询未提交作业学生供催交提醒使用) | | `data-access-write.ts` | 455 | 作业写操作(P1-2 新增 10 个写函数从 actions 下沉;V3-7 新增 `batchAutoGradeSubmissions`) | | `data-access-classes.ts` | 285 | 跨模块查询封装(P0-7 新增;✅ P1-1 已修复:内部通过 exams/school data-access 获取考试科目信息,不再直查 exams/subjects 表) | | `data-access-student.ts` | 293 | 学生端只读查询(✅ 2026-06-25:从 data-access.ts 拆出 `getStudentSubmissionResult`/`getStudentExamResults`/`getStudentHomeworkAssignments`/`getStudentHomeworkTakeData`) | | `data-access-exam-cross.ts` | 119 | 考试跨模块查询(`getHomeworkAssignmentsByExamId`/`getGradedSubmissionsByExamId`) | | `data-access-scans.ts` | 44 | ✅ 2026-06-25 审计新增:扫描图查询(`getScansBySubmissionId`,从 actions.ts 下沉的 DB 查询) | | `data-access-error-collection.ts` | 121 | 错题采集查询 | | `data-access-utils.ts` | 8 | 内部工具函数 | | `stats-service.ts` | 414 | 统计分析(教师趋势/作业分析/学生仪表盘成绩) | | `actions.ts` | 688 | 10 个 Server Action(P1-2 已修复,无直接 DB 操作;✅ 2026-06-25:`ScanAttachment` 类型迁移至 types.ts;schema.ts 错误消息改为 i18n 键,新增 `translateZodErrors` 翻译辅助函数;补全 `trackExamEvent` 埋点;✅ P3-1 新增 `getExcellentSubmissionsAction`:查询优秀作业列表,HOMEWORK_GRADE 权限 + scope 过滤 + 埋点 `homework.excellent_viewed`;✅ P3-2 新增 `remindUnsubmittedAction`:催交提醒,调用 `notifications/data-access.createNotification` 创建站内通知,埋点 `homework.remind_unsubmitted`) | | `types.ts` | 282 | 类型定义(✅ 2026-06-25:新增 `ScanAttachment` 接口,从 actions.ts 迁入;✅ P3-1 新增 `ExcellentSubmissionItem`/`ExcellentSubmissionQuery` 类型) | | `schema.ts` | 63 | Zod 校验(✅ 2026-06-25:错误消息改为 i18n 键 `homework.form.error.*`) | | `lib/question-content-utils.ts` | 321 | 题目内容工具函数(✅ 2026-06-25:`extractQuestionText` 从 grading-view 迁入) | | `lib/question-content-utils.test.ts` | 415 | 工具函数单测 | | `hooks/use-exam-countdown.ts` | 106 | P0-竞品新增:考试倒计时 Hook(每秒更新、紧急状态高亮、到时回调自动提交) | | `hooks/use-debounced-auto-save.ts` | 182 | 答案防抖自动保存 Hook(localStorage 离线缓存) | | `components/homework-take-view.tsx` | 396 | ✅ 2026-06-25 重构:从 520 行拆分为 3 个组件(主容器 + Sidebar + ConfirmDialog);V3-9/V3-12:提交后跳转结果页+移动端触摸优化;P0-竞品:集成限时/监考模式倒计时(useExamCountdown)+ 到时自动提交 | | `components/homework-take-sidebar.tsx` | 169 | ✅ 2026-06-25 新增:从 homework-take-view 拆出(自动保存状态/作业信息/截止时间/尝试次数/描述/进度网格/提交按钮) | | `components/homework-take-confirm-dialog.tsx` | 62 | ✅ 2026-06-25 新增:从 homework-take-view 拆出(提交确认对话框,含未作答数量警告) | | `components/homework-grading-view.tsx` | 472 | ✅ 2026-06-25:删除 4 个本地 wrapper 函数,直接调用 lib/question-content-utils | | `components/homework-batch-grading-view.tsx` | 165 | V3-7 新增:批量批改视图(use client) | | `components/homework-submission-result.tsx` | 224 | V3-9 新增:提交后即时反馈页 | | `components/homework-scan-grading-view.tsx` | 262 | 2026-06-24 新增:教师阅卷式批改视图(ResizablePanel 左题目右扫描图 + 上一份/下一份切换) | | `components/scan-uploader.tsx` | 254 | 2026-06-24 新增:学生扫描图上传组件(调用 /api/upload,targetType="homework",导出 `ScanImage` 类型) | | `components/scan-image-viewer.tsx` | 194 | 2026-06-24 新增:扫描图查看器(翻页/缩放/旋转/全屏) | | `components/question-renderer.tsx` | 339 | 题目渲染器(支持单选/多选/判断/文本题型) | | `components/excellent-submissions.tsx` | 206 | P3-1:优秀作业展示(服务端组件 + SectionErrorBoundary + Suspense + 骨架屏;✅ P3-5 修复:`getTranslations` 从 `next-intl/server` 导入) | | `components/student-homework-review-view.tsx` | 206 | 学生作业回顾视图 | | `components/homework-assignment-form.tsx` | 219 | 作业创建/编辑表单 | | `components/assignment-filters.tsx` | 46 | 作业列表筛选器(use client):基于 nuqs 的 URL 状态管理(✅ 2026-06-24 i18n:接入 `useTranslations`) | | `components/homework-assignment-exam-content-card.tsx` | 29 | 作业考试内容卡片(✅ 2026-06-24 i18n:接入 `useTranslations`) | | `components/homework-assignment-exam-preview-pane.tsx` | 39 | 作业考试预览面板(✅ 2026-06-24 i18n:接入 `useTranslations`) | | `components/homework-assignment-exam-error-explorer.tsx` | 39 | 作业错误分析主组件 | | `components/homework-assignment-exam-error-explorer-lazy.tsx` | 76 | 作业错误分析懒加载组件(✅ 2026-06-24 i18n:Fallback 接入 `useTranslations`) | | `components/homework-assignment-question-error-detail-panel.tsx` | 132 | 题目错误详情面板(✅ 2026-06-24 i18n:接入 `useTranslations`,`formatAnswer` 移入组件内闭包以访问 `t`) | | `components/homework-assignment-question-error-overview-card.tsx` | 89 | 题目错误概览卡片(✅ 2026-06-24 i18n:接入 `useTranslations`) | --- ## 2.4 questions(题库模块) **职责**:题库管理(题目 CRUD、知识点关联、题型支持、教材→章节→知识点级联筛选)。 **导出函数**: - Actions(11 个,全部调用 `requirePermission()`):`getQuestionsAction` / `createQuestionAction` / `updateQuestionAction` / `deleteQuestionAction` / `getKnowledgePointOptionsAction` / `getTextbookOptionsAction`(✅ 审计新增:级联第一级)/ `getChapterOptionsAction(textbookId)`(✅ 审计新增:级联第二级)/ `getKnowledgePointOptionsByChapterAction(chapterId)`(✅ 审计新增:级联第三级)/ `deleteQuestionsBatchAction`(✅ 审计新增:批量删除)/ `exportQuestionsAction`(✅ 审计新增:导出)/ `importQuestionsAction`(✅ 审计新增:导入) - Data-access:`getQuestions`(✅ 审计升级:支持 `textbookId`/`chapterId` 级联筛选参数,通过 textbooks data-access 获取知识点 ID 集合)/ `createQuestionWithRelations` / `updateQuestionById` / `deleteQuestionByIdRecursive` / `getKnowledgePointOptions` / `getTextbookOptions`(✅ 审计新增)/ `getChapterOptions(textbookId)`(✅ 审计新增)/ `getKnowledgePointOptionsByChapter(chapterId)`(✅ 审计新增)/ `deleteQuestionsBatch`(✅ 审计新增:批量删除,事务原子性)/ `exportQuestions`(✅ 审计新增:导出,权限范围过滤)/ `importQuestions`(✅ 审计新增:批量导入,事务原子性) - Types:`Question` / `QuestionWithRelations` / `GetQuestionsParams`(✅ 审计升级:新增 `textbookId`/`chapterId` 字段)/ `CreateQuestionInput` / `KnowledgePointOption` / `TextbookOption`(✅ 审计新增)/ `ChapterOption`(✅ 审计新增)/ `QuestionContent`(✅ 审计新增:类型守卫接口)/ `QuestionOption`(✅ 审计新增)/ `QUESTION_TYPE_I18N_KEY`(✅ 审计新增:题型 i18n 键映射)/ `DIFFICULTY_I18N_KEY`(✅ 审计新增:难度 i18n 键映射) - Utils:`parseQuestionContent(raw)`(✅ 审计新增:类型守卫,替代 `as` 断言)/ `getQuestionPreview` / `getQuestionOptions` / `getQuestionText` / `trackQuestionEvent`(✅ 审计新增:埋点接口 no-op)/ `trackQuestionCreated` / `trackQuestionUpdated` / `trackQuestionDeleted` / `trackQuestionSearched` - Components:`QuestionFilters`(✅ 审计重写:组合 QuestionCascadeFilter)/ `QuestionCascadeFilter`(✅ 审计新增:三级级联筛选,nuqs 管理 URL 状态)/ `QuestionDataTable`(✅ 审计重写:i18n + 集成 BatchOperations;✅ Phase 4.3:引入 @tanstack/react-virtual 虚拟滚动替换分页)/ `useQuestionColumns`(✅ 审计改为 Hook:因 useTranslations 是 Hook)/ `CreateQuestionButton`(✅ 审计重写:权限感知)/ `QuestionActions`(✅ 审计重写:权限感知 + 类型守卫 + QuestionContentRenderer 结构化预览)/ `CreateQuestionDialog`(✅ 审计重写:拆分为 281 行)/ `KnowledgePointSelector`(✅ 审计新增:从 dialog 拆分)/ `OptionsEditor`(✅ 审计新增:从 dialog 拆分)/ `QuestionBankResultsClient`(✅ 审计新增:RSC 与客户端 Hook 桥接)/ `QuestionContentRenderer`(✅ 审计新增:题目结构化渲染)/ `BatchOperations`(✅ 审计新增:批量删除工具栏)/ `ImportExportButtons`(✅ 审计新增:JSON 导入导出) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`textbooks`(✅ 审计升级:通过 textbooks data-access 的 `getTextbooks` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` 实现级联筛选) - 被依赖:`exams`(通过类型导入,合理)、`textbooks`(UI 组合,合理)、`error-book`(通过 data-access,合理)、`adaptive-practice`(通过 data-access,合理) **已知问题**: - ✅ P1-2 已修复:~~写操作函数错放在 `actions.ts`~~ 已下沉到 data-access - ✅ P1-1 已修复:~~`getKnowledgePointOptionsAction` 直查 textbooks 模块表~~ 改为调用 textbooks data-access - ✅ P2 已解决:~~`data-access.ts` 仅 129 行~~ 扩充至 260+ 行 - ✅ 审计 P0 已修复:~~按名字搜索等于无用~~ 新增教材→章节→知识点三级级联筛选(QuestionCascadeFilter + URL 状态同步) - ✅ 审计 P0 已修复:~~前端权限硬编码~~ 全部改用 `usePermission().hasPermission()` - ✅ 审计 P0 已修复:~~`as` 断言违反 TypeScript 严格模式~~ 提取 `parseQuestionContent` 类型守卫函数 - ✅ 审计 P0 已修复:~~i18n 完全缺失~~ 新增 `questions.json` 翻译文件(zh-CN + en),所有展示文本使用 i18n 键 - ✅ 审计 P0 已修复:~~缺 Error Boundary~~ 新增 `app/(dashboard)/teacher/questions/error.tsx` - ✅ 审计 P1 已修复:~~create-question-dialog.tsx 453 行超规范~~ 拆分为 KnowledgePointSelector + OptionsEditor + 主对话框(281 行) - ✅ 审计 P1 已修复:~~列定义为纯函数无法使用 useTranslations~~ 改为 `useQuestionColumns` Hook,通过 `QuestionBankResultsClient` 客户端桥接 - ✅ 审计 P1 已修复:~~缺监控埋点~~ 预留 `trackQuestionEvent` no-op 接口 - ✅ 审计 P2 已修复:~~Server Action 缺权限校验~~ 全部 8 个 Action 均调用 `requirePermission()` - ✅ 审计 P2 已实施:~~仅支持教师角色~~ 新增 admin/questions 页面,复用全部组件,权限感知自动适配 - ✅ 审计 P2 已实施:~~无题目结构化预览~~ 新增 QuestionContentRenderer 组件(题干/选项/答案/解析,单选圆点/多选方框,正确答案高亮) - ✅ 审计 P2 已实施:~~无批量操作~~ 新增 deleteQuestionsBatch data-access + deleteQuestionsBatchAction + BatchOperations 组件(事务原子性,权限感知) - ✅ 审计 P2 已实施:~~无导入导出~~ 新增 exportQuestions/importQuestions data-access + exportQuestionsAction/importQuestionsAction + ImportExportButtons 组件(JSON 格式,权限范围过滤,导入预览确认) - ✅ 2026-07-07 S-06:`data-access.ts` 7 个公开导出函数(getQuestionsRaw/getQuestionsDashboardStatsRaw/createQuestionWithRelations/updateQuestionById/deleteQuestionByIdRecursive/getKnowledgePointOptions/getKnowledgePointsForQuestionsRaw)补全 JSDoc(@param + @returns + @throws) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 380+ | 11 个 Server Action(全部 requirePermission,含 3 个级联筛选 + 批量删除 + 导入导出) | | `data-access.ts` | 600+ | 题目 CRUD + 知识点选项 + 级联筛选 + 批量删除 + 导入导出 | | `schema.ts` | 18 | Zod 校验 | | `types.ts` | 60+ | 类型定义(含 TextbookOption/ChapterOption/QuestionContent/i18n 键映射) | | `utils/parse-content.ts` | 60+ | 类型守卫(parseQuestionContent + 便捷方法) | | `utils/track-event.ts` | 30+ | 埋点接口(no-op 实现) | | `components/question-cascade-filter.tsx` | 130+ | 三级级联筛选(nuqs URL 状态) | | `components/question-filters.tsx` | 40+ | 筛选栏(组合 QuestionCascadeFilter) | | `components/question-data-table.tsx` | 180+ | 数据表格(i18n + 集成 BatchOperations + ✅ Phase 4.3:@tanstack/react-virtual 虚拟滚动,替换分页避免大数据渲染卡顿) | | `components/question-columns.tsx` | 100+ | 列定义 Hook(useQuestionColumns) | | `components/create-question-button.tsx` | 30+ | 创建按钮(权限感知) | | `components/question-actions.tsx` | 200+ | 行操作(权限感知 + 类型守卫 + QuestionContentRenderer) | | `components/create-question-dialog.tsx` | 281 | 创建对话框(组合 KnowledgePointSelector + OptionsEditor) | | `components/knowledge-point-selector.tsx` | 80+ | 知识点选择器(从 dialog 拆分) | | `components/options-editor.tsx` | 120+ | 选项编辑器(从 dialog 拆分) | | `components/question-bank-results-client.tsx` | 30+ | RSC 与客户端 Hook 桥接 | | `components/question-content-renderer.tsx` | 110+ | 题目结构化渲染(✅ 审计新增) | | `components/batch-operations.tsx` | 110+ | 批量操作工具栏(✅ 审计新增) | | `components/import-export-buttons.tsx` | 180+ | 导入导出组件(✅ 审计新增) | --- ## 2.5 textbooks(教材模块)— 标杆模块(data-access 层) **职责**:教材与知识体系管理(教材/章节树形结构、知识点 CRUD、Markdown 内容编辑、知识图谱)。 **导出函数**: - Actions(14 个,均为写操作;读操作由 RSC 页面直接调用 data-access):`createTextbookAction` / `updateTextbookAction` / `deleteTextbookAction` / `createChapterAction` / `updateChapterContentAction` / `deleteChapterAction` / `reorderChaptersAction` / `createKnowledgePointAction` / `updateKnowledgePointAction` / `deleteKnowledgePointAction` / `getKnowledgePointsByChapterAction` / `getKnowledgeGraphDataAction`(✅ Task 7 新增:知识图谱数据查询,支持 structure/student-mastery/class-mastery 三种视图)/ `createPrerequisiteAction`(✅ Task 7 新增:声明前置依赖,含循环检测)/ `deletePrerequisiteAction`(✅ Task 7 新增:删除前置依赖) - Data-access:`getTextbooks` / `getTextbookById` / `getChaptersByTextbookId` / `getKnowledgePointsByChapterId` / `getKnowledgePointsByTextbookId` / `createTextbook` / `updateTextbook` / `deleteTextbook` / `createChapter` / `updateChapterContent` / `deleteChapter` / `createKnowledgePoint` / `updateKnowledgePoint` / `deleteKnowledgePoint` / `reorderChapters` / `getTextbooksDashboardStats` / `getKnowledgePointOptions`(跨模块接口,供 questions 调用)/ `getTextbooksWithScope`(P1-1 新增:按数据范围获取教材列表,学生端强制按年级过滤)/ `verifyChapterBelongsToTextbook`(P0-4 新增:资源归属校验)/ `verifyKnowledgePointBelongsToTextbook`(P0-4 新增:资源归属校验)/ `verifyKnowledgePointBelongsToChapter`(P0-4 新增:校验知识点是否属于指定章节)/ `createPrerequisite`(✅ Task 7 新增:创建前置依赖)/ `deletePrerequisite`(✅ Task 7 新增:删除前置依赖)/ `getPrerequisiteEdgesForTextbook`(✅ Task 7 新增:获取教材下所有前置依赖边,用于循环检测)/ `getSubjectLabelKey` / `getGradeLabelKey`(i18n 标签键) - Data-access-graph(✅ Task 5 新增,图谱只读查询):`getKnowledgePointsWithRelations`(知识点+依赖+题目数聚合查询)/ `getStudentKpMastery`(学生掌握度)/ `getClassKpMastery`(班级平均掌握度)/ `getPrerequisitesForKp` / `getSuccessorsForKp` - Constants(✅ 新增):`SUBJECTS` / `GRADES` / `SUBJECT_COLORS` / `getSubjectColor` / `getSubjectLabelKey` / `getGradeLabelKey` - ✅ v1 测试修复:`SUBJECTS` 新增 `Chinese` 学科、`GRADES` 新增 `Grade 1`/`Grade 2`,与 seed 数据对齐 - ✅ v1 测试修复:`SUBJECT_COLORS` 新增 `Chinese` 主题色(rose) - Utils(✅ 新增,纯函数 + 单测):`sortChapters` / `buildChapterTree` / `buildChapterIndex` / `findChapterParent` / `filterKnowledgePointsByChapter` / `normalizeOptional` / `highlightKnowledgePoints` / `hasCycleAfterAddingEdge`(Task 4 新增:循环依赖检测,DFS 算法)/ `findChapterById`(V3 续审计新增:递归查找章节树中指定 ID 的章节,替代各模块本地重复实现) - Graph-layout(✅ 新增,纯函数 + 单测):`computeGraphLayout` - Lib(✅ v6 审计修复新增,类型守卫集合):`isKpWithRelations` / `isGraphNodeData` / `isGraphLayoutNodeData` / `isGraphEdgeData` / `isKpGraphNode` / `isKpGraphLink`(位于 `lib/type-guards.ts`,替代 components/* 中针对 React Flow `node.data` 与 react-force-graph-2d `NodeObject`/`LinkObject` 的 `as` 断言,从 unknown 安全收窄到具体业务类型) - Components(✅ Task 13 新增):`GraphNodeDetailPanel` — 知识图谱节点详情侧边面板,展示知识点名称/描述/掌握度/关联题目/前置后置知识点,支持跳转与前置关系增删 - Analytics(✅ 新增):`TextbookAnalytics` / `TextbookAnalyticsProvider` / `useTextbookAnalytics` **依赖关系**: - 依赖:`shared/*`、`@/auth`、`@xyflow/react`(知识图谱可视化)、`@dagrejs/dagre`(图谱分层布局算法)、`react-force-graph-2d`(v4 新增:Obsidian 风格力导向图;v6 types.ts 导入其 NodeObject/LinkObject 类型供 KpGraphNode/KpGraphLink 接口继承) - 被依赖:`questions`(✅ P1-1 已修复:通过 textbooks data-access)、`exams`(通过类型)、`dashboard`(通过 data-access,P0-4 已修复) - ✅ UI 层跨模块依赖已解耦:`textbooks/components/knowledge-point-dialogs.tsx` 不再直接 import questions 模块,改为通过 render prop 注入创建题目入口 **已知问题**: - ✅ 无跨模块 DB 访问(data-access 层) - ✅ actions 层编排模式标杆(权限校验 → 调用 data-access → revalidatePath) - ✅ data-access 层职责单一 - ✅ P2 已修复:`data-access.ts` 中 `byId.get(pid)!.children.push` 非空断言清理为安全守卫;`or(...)!` 非空断言清理为条件 push - ✅ P0 跨模块 UI 依赖已修复:`knowledge-point-dialogs.tsx` 通过 render prop 解耦,不再直接 import questions 模块 - ✅ P0 前端权限硬编码已修复:改用 `usePermission().hasPermission()` - ✅ P0 i18n 已基本接入:`chapter-sidebar-list.tsx` / `actions.ts` / `section-error-boundary.tsx`(默认值改英文)均已接入 next-intl - ✅ P1 Server Action 资源归属校验已修复:新增 `verifyChapterBelongsToTextbook` / `verifyKnowledgePointBelongsToTextbook` - ✅ P1 data-access 数据范围过滤已修复:新增 `getTextbooksWithScope`,学生端强制按年级过滤 - ✅ P1 Error Boundary 已补齐:新增 `section-error-boundary.tsx` - ✅ P1 知识点列表/弹窗重复实现已清理:移除无调用方的 `knowledge-point-panel.tsx` - ✅ P1 学科/年级选项统一:抽取到 `constants.ts`(`SUBJECTS` / `GRADES` / `SUBJECT_COLORS`) - ✅ v1 测试修复:`textbook-reader.tsx` 移除 `SheetTrigger`(错误置于 `Sheet` 外部导致 `DialogTrigger must be used within Dialog`),改用受控 `Button` + `onClick` 打开移动端抽屉 - ✅ v1 测试修复:新增 `teacher-textbook-reader.tsx` 客户端包装组件,解决 Server Component 向 Client Component 传递函数 prop(`renderQuestionCreator`)违反 Next.js 序列化约束的问题 - ✅ v1 测试修复:`scripts/seed.ts` 教材 subject/grade 改为英文 value(与 `constants.ts` 对齐),消除 i18n `MISSING_MESSAGE` 错误 - ✅ P1 纯逻辑已导出并补单测:新增 `utils.ts` / `graph-layout.ts` 及对应 `.test.ts` - ✅ v3 审计修复(2026-06-24):学生端两个页面添加 `requirePermission(Permissions.TEXTBOOK_READ)` 显式权限校验 - ✅ v3 审计修复(2026-06-24):`getKnowledgeGraphDataAction` 添加学生年级 scope 过滤,防止跨年级越权查看图谱数据 - ✅ v3 审计修复(2026-06-24):`getKnowledgeGraphDataAction` 的 class-mastery 模式限制为教师/管理员使用 - ✅ v3 审计修复(2026-06-24):`deleteChapter` 级联删除 `knowledgePointPrerequisites` 孤儿记录,保证数据完整性 - ✅ v3 审计修复(2026-06-24):`knowledge-graph.tsx` 前置依赖操作添加 try/catch/finally,防止 loading 状态卡死 - ✅ v3 审计修复(2026-06-24):`textbook-reader.tsx` `onCreateKnowledgePoint` 添加 try/finally - ✅ v3 审计修复(2026-06-24):`section-error-boundary.tsx` 添加 `componentDidCatch` 错误日志记录 + `role="alert"` a11y - ✅ v3 审计修复(2026-06-24):4 个 error.tsx 抽取为共享 `RouteError` 组件,消除重复代码 - ✅ v3 审计修复(2026-06-24):所有 loading.tsx 添加 `role="status"` / `aria-busy` a11y 标记 - ✅ v3 审计修复(2026-06-24):`chapter-sidebar-list.tsx` 章节项添加 `role="button"` / `tabIndex` / `onKeyDown` 键盘支持 - ✅ v3 审计修复(2026-06-24):`graph-node-detail-panel.tsx` 图标按钮添加 `aria-label` - ✅ v3 审计修复(2026-06-24):`data-access.ts` `as [string, string]` 断言改为返回类型标注 - ✅ v3 审计修复(2026-06-24):`knowledge-point-dialogs.tsx` `as` 断言改为 void 包装 - ✅ v3 审计修复(2026-06-24):`graph-kp-node.tsx` 双重 `as unknown as` 断言改为类型守卫函数 `extractNodeData` - ✅ v3 审计修复(2026-06-24):`graph-layout.ts` `GraphLayoutNodeData` 新增 `graphData` 字段类型定义 - ✅ v3 审计修复(2026-06-24):删除死代码 `verifyKnowledgePointBelongsToChapter`、`ReorderChaptersSchema`、deprecated `canEdit` prop - ✅ v3 审计修复(2026-06-24):`textbook-settings-dialog.tsx` 新增 `redirectAfterDelete` prop 消除硬编码 URL - ✅ v3 审计修复(2026-06-24):`teacher/textbooks/[id]/loading.tsx` 骨架屏布局改为匹配阅读器布局 - ✅ v3 P2 修复(2026-06-24):`data-access.ts` 3 处硬编码英文异常文案改为 `NotFoundError`(教材/章节),消息可安全返回客户端 - ✅ v3 P2 修复(2026-06-24):`graph-layout.ts` O(n²) `some()` 查找改为 `Set` 查找,性能降为 O(n) - ✅ v3 P2 修复(2026-06-24):抽取 `textbook-form-fields.tsx` 共享组件,消除 `textbook-form-dialog` 与 `textbook-settings-dialog` 表单字段重复 - ✅ v3 P2 修复(2026-06-24):`textbook-content-panel.tsx` 17 个 props 按 `chapter`/`editing`/`selection`/`highlight` 四组重构,提升可读性 - ✅ v3 P2 修复(2026-06-24):`textbook-reader.tsx` 移除命令式 `classList` 操作,视觉高亮改为 `isHighlighted` 状态驱动,仅保留 `scrollIntoView` - ✅ v3 P2 修复(2026-06-24):`use-kp-crud.ts`(122 行)拆分为 `use-kp-create`/`use-kp-delete`/`use-kp-update` 三个子 Hook + 门面,均在 80 行限制内 - ✅ v6 审计修复(2026-07-07):`graph-kp-node.tsx` 2 处 `as` 断言(`data.kp as KpWithRelations`、`data.graphData as GraphNodeData | undefined`)改用 `lib/type-guards.isKpWithRelations`/`isGraphNodeData` 守卫,`extractNodeData` 返回 `| null`,校验失败组件渲染 null - ✅ v6 审计修复(2026-07-07):`graph-prerequisite-edge.tsx` `data as unknown as GraphEdgeData | undefined` 双重断言改用 `lib/type-guards.isGraphEdgeData` 守卫 - ✅ v6 审计修复(2026-07-07):`knowledge-graph-node.tsx` `node.data as GraphLayoutNodeData` 断言改用 `lib/type-guards.isGraphLayoutNodeData` 守卫 - ✅ v6 审计修复(2026-07-07):`force-graph.tsx` 6 处 `as` 断言(`node as KpGraphNode` ×3、`link as KpGraphLink`、`kpLink.source as KpGraphNode`、`kpLink.target as KpGraphNode`)改用 `lib/type-guards.isKpGraphNode`/`isKpGraphLink` 守卫;`KpGraphNode`/`KpGraphLink` 接口从 force-graph.tsx 迁移至 types.ts 供守卫模块引用;保留 Line 80 `as unknown as ComponentType` 第三方库(next/dynamic)类型限制并加注释 - ✅ 2026-07-07 S-06:`data-access.ts` 全部公开导出函数(getTextbooksRaw/getTextbookByIdRaw/getChaptersByTextbookIdRaw/createTextbook/updateTextbook/deleteTextbook/createChapter/updateChapterContent/deleteChapter/getKnowledgePointsByChapterIdRaw/getKnowledgePointsByTextbookIdRaw/createKnowledgePoint/updateKnowledgePoint/deleteKnowledgePoint/reorderChapters/getTextbooksDashboardStatsRaw/createPrerequisite/deletePrerequisite/getTextbookTitlesByIdsRaw/getChapterTitlesByIdsRaw/getKnowledgePointNamesByIdsRaw 共 22 个)补全 JSDoc(@param + @returns + @throws) - ✅ 2026-07-07 修复:`getKnowledgePointsByTextbookIdRaw` 箭头函数缺失 `=>`(pre-existing 语法错误,tsc 在添加后续 JSDoc 时暴露),补全后 tsc 通过 - ⚠️ i18n 覆盖率约 99%(v3 新增 `notFound`/`noClassMasteryPermission`/`close`/`prerequisiteAddFailed`/`prerequisiteRemoveFailed` 等键,data-access 异常文案已错误码化) - ⚠️ P2 图谱方向键导航未实现 - ⚠️ P2 data-access 函数未导出非缓存版本,单测难以 mock **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 502 | 14 个 Server Action(写操作,含 Zod 校验 + 资源归属校验 + 知识图谱查询/前置依赖管理 + class-mastery 视图) | | `data-access.ts` | 593 | 教材/章节/知识点 CRUD + 跨模块查询接口 + 资源归属校验 + 数据范围过滤 + 前置依赖 CRUD(v3 P2:硬编码异常改为 NotFoundError) | | `data-access-graph.ts` | 188 | 知识图谱只读查询(✅ Task 5 新增:知识点关联聚合、学生/班级掌握度、前置后置知识点,标记 `server-only`;v2 修复:前置依赖双向 IN 过滤防跨教材污染) | | `types.ts` | 133 | 类型定义(含知识图谱类型:GraphViewMode/GraphLayoutMode/MasteryInfo/KpWithRelations/GraphNodeData/GraphEdgeData/KnowledgeGraphData/MasteryLevel;v6 审计修复新增:KpGraphNode/KpGraphLink 从 force-graph.tsx 提取至此,供 lib/type-guards.ts 引用) | | `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 查找) | | `lib/type-guards.ts` | 122 | v6 审计修复新增:教材模块类型守卫集合(isKpWithRelations/isGraphNodeData/isGraphLayoutNodeData/isGraphEdgeData/isKpGraphNode/isKpGraphLink),替代 components/* 中针对 React Flow `node.data` 与 react-force-graph-2d `NodeObject`/`LinkObject` 的 `as` 断言 | | `analytics.tsx` | 37 | 教材分析 Context/Provider/Hook(✅ 新增) | | `hooks/use-knowledge-point-actions.ts` | 45 | 知识点操作门面 Hook(v2 拆分:组合 useKpDialogState + useKpCrud,对外 API 不变) | | `hooks/use-kp-dialog-state.ts` | 33 | 知识点对话框状态管理 Hook(v2 新增:从 use-knowledge-point-actions 拆分) | | `hooks/use-kp-crud.ts` | 52 | 知识点 CRUD 门面 Hook(v3 P2 拆分:组合 useKpCreate/useKpDelete/useKpUpdate,对外 API 不变) | | `hooks/use-kp-create.ts` | 43 | 知识点创建 Hook(v3 P2 新增:从 use-kp-crud 拆分) | | `hooks/use-kp-delete.ts` | 49 | 知识点删除 Hook(v3 P2 新增:从 use-kp-crud 拆分) | | `hooks/use-kp-update.ts` | 36 | 知识点更新 Hook(v3 P2 新增:从 use-kp-crud 拆分) | | `hooks/use-text-selection.ts` | 49 | 文本选区捕获 Hook | | `hooks/use-graph-data.ts` | 65 | 知识图谱数据加载 Hook(✅ Task 11 新增:派生值模式避免 effect 中 setState;v2 修复:区分 isLoading/isRefreshing 避免 UI 闪烁) | | `components/knowledge-graph.tsx` | 408 | 知识图谱主组件(✅ Task 10/15 重写:全书视图 + 搜索高亮 + 关联节点高亮 + 章节着色;v2 新增:添加/删除前置依赖 Dialog;v4 新增:hierarchical/force 布局模式切换,根据布局模式渲染分层 ReactFlow 或 Obsidian 风格力导向图,状态/详情面板/依赖增删对话框共享) | | `components/graph-kp-node.tsx` | 105 | React Flow 自定义节点(✅ Task 9 新增:知识点名称+题目数徽章+掌握度进度条;v2 修复:节点宽度常量化 NODE_WIDTH;v3 修复:类型守卫替代 as 断言;v6 审计修复:extractNodeData 改用 lib/type-guards.isKpWithRelations/isGraphNodeData 守卫,校验失败返回 null) | | `components/graph-prerequisite-edge.tsx` | 47 | React Flow 自定义边(✅ Task 9 新增:虚线+箭头表示前置依赖;v2 修复:GraphEdgeData 类型安全转换;v6 审计修复:改用 lib/type-guards.isGraphEdgeData 守卫替代 `as unknown as` 双重断言) | | `components/graph-toolbar.tsx` | 121 | 图谱工具栏(✅ Task 9 新增:视图模式切换 + 搜索 + 重置视图;v2 新增:isRefreshing 轻量指示器;v4 新增:hierarchical/force 布局模式切换 Select) | | `components/force-graph.tsx` | 446 | Obsidian 风格力导向图谱组件(v4 新增:基于 react-force-graph-2d + d3-force;v5 视觉对齐 Obsidian Graph View:小圆点节点+极细半透明边线+标签右侧带背景+hover/选中邻居高亮+紫色外圈焦点环;通过 next/dynamic ssr:false + import type 加载避免 SSR canvas/window 报错;v6 审计修复:KpGraphNode/KpGraphLink 接口迁移至 types.ts,6 处 `as` 断言改用 lib/type-guards.isKpGraphNode/isKpGraphLink 守卫;保留 Line 80 `as unknown as ComponentType` 第三方库类型限制) | | `components/graph-node-detail-panel.tsx` | 171 | 节点详情侧边面板(✅ Task 13 新增:描述/掌握度/关联题目/前置后置列表;v2 修复:移除未使用 textbookId prop;v3 修复:aria-label) | | `components/chapter-sidebar-list.tsx` | 325 | 章节侧边栏列表(v2 修复:canEdit 默认 false + orderUpdateFailed i18n key;v3 修复:键盘导航) | | `components/section-error-boundary.tsx` | 53 | 章节内容错误边界(v2 修复:默认值改为空字符串 + 条件渲染;v3 修复:componentDidCatch + a11y) | | `components/textbook-form-fields.tsx` | 103 | 教材表单字段共享组件(v3 P2 新增:消除 form-dialog 与 settings-dialog 表单重复) | | `components/textbook-content-panel.tsx` | 199 | 章节内容面板(v3 P2:17 props 按 chapter/editing/selection/highlight 四组重构) | | `components/textbook-form-dialog.tsx` | 68 | 教材创建对话框(v3 P2:复用 TextbookFormFields) | | `components/textbook-settings-dialog.tsx` | 136 | 教材设置对话框(v3 P2:复用 TextbookFormFields) | | `components/textbook-reader.tsx` | 458 | 教材阅读器主组件(v3 P2:移除命令式 classList,高亮改为状态驱动;v5 桌面端改用 ResizablePanel 可拖拽分栏,图谱 tab 时图谱初始占 60%,切换 tab 用 key 重置比例) | | `components/*` | 17 文件 | 教材编辑/知识图谱组件(新增 `textbook-form-fields.tsx`、5 个 graph-* 组件、3 个 use-kp-* 子 Hook;`teacher-textbook-reader.tsx` 已移至 app 层) | --- ## 2.6 grades(成绩模块)— 标杆模块(拆分范例) **职责**:成绩分析(录入/查询/统计/导出/趋势对比分析)。 **导出函数**: - Actions:`getGradeRecordsAction` / `createGradeRecordAction`(v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`)/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`(v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度1,按 gradeId 查询年级整体 + 按班级拆分的成绩分布,GRADE_RECORD_READ 权限)/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`(v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `batchCreateGradeRecordsByExamAction`(✅ 2026-06-24 新增,✅ 2026-06-25 增强错题采集:按试卷批量录入每题得分,流程 requirePermission → getExamForGradeEntry scope 校验 → assertClassInScope → safeJsonParse → BatchGradeEntryByExamSchema 校验 → batchCreateGradeRecordsByExam 单事务写入(返回 {gradeRecordId, studentId, submissionId}[])→ Promise.allSettled 并行执行 updateMasteryFromExamScore + collectFromExamSubmission(考试错题采集,失败不阻断)→ notifyGradeEntered → revalidatePath,返回 gradeRecordId 列表供撤销)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions,分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限)。注:`assertClassInScope` 原位于 actions.ts(✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用),✅ v4-P2-6 修复:因 "use server" 文件要求所有 export 为 async,而 `assertClassInScope` 是同步函数,已迁移至独立文件 `lib/scope-check.ts`,actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入 - Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats 纯函数,返回整体分布 + 按班级拆分)/ `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`)/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUD,upsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总)/ `batchCreateGradeRecordsByExam`(✅ 2026-06-24 新增,✅ 2026-06-25 更新返回类型:按试卷批量录入成绩,单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩,返回 `BatchGradeRecordResult[]`({ gradeRecordId, studentId, submissionId })供调用方触发错题采集等后置钩子) - Types(✅ v3-P2 新增,✅ v4-P2-7 新增年级分布类型,✅ 2026-06-24 新增按试卷录入类型):`SchoolWideGradeSummaryItem` / `SchoolWideGradeSummary` / `GradeDraftData`(草稿数据接口)/ `GradeDistributionByGradeResult`(✅ v4-P2-7 新增:年级维度成绩分布结果,含 overall 整体分布 + stats 统计 + byClass 按班级拆分数组)/ `GradeDistributionByGradeClassItem`(✅ v4-P2-7 新增:按班级拆分的分布项:classId/className/distribution/stats)/ `GradeRecordAnswer`(✅ 2026-06-24 新增:成绩记录-题目得分明细,对应 grade_record_answers 表)/ `BatchGradeEntryByExamQuestion`(✅ 2026-06-24 新增:按试卷录入时单个题目得分 { questionId, score })/ `BatchGradeEntryByExamItem`(✅ 2026-06-24 新增:按试卷录入时单个学生所有题目得分 { studentId, answers }) - Lib(✅ P1-2 新增,✅ P3 更新签名,✅ P3-26 拆分,✅ v4-P2-6 新增 scope-check):`toNumber` / `normalize`(位于 `lib/grade-utils.ts`);`buildScopeClassFilter(scope, currentUserId?)`(P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`,P3 修复:`class_members` scope 内置 studentId 过滤,需传入 currentUserId 参数);`assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 actions.ts 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因:actions.ts 是 "use server" 文件要求所有 export 为 async,而 assertClassInScope 是同步函数) - Stats-service(✅ P1-1 新增):`computeGradeStats` / `computeAverageScore` / `buildGradeTrendPoints` / `computeTrendAverage` / `computeClassComparisonStats` / `computeSubjectComparisonStats` / `computeGradeDistribution` / `buildRankingTrendPoints`(从 3 个 data-access 文件抽取的纯函数,使数据层专注 DB I/O,统计逻辑可独立测试) - Export(✅ v4-P1-12 新增):`exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` / `exportStudentGradeRecordsToExcel`(v4-P1-12 新增:导出单个学生成绩单家长视角,仅含成绩明细 + 个人统计,不含班级数据,scope 为 children 自动按 studentId 过滤)/ `formatDateForFile`(已迁移至 shared/lib/utils) - Components(✅ P1-5 新增,✅ Phase 4.6 拆分扩展):`WidgetBoundary`(Error Boundary + Suspense + Skeleton 组合,含 a11y 属性)/ `SchoolWideSummaryCard`(✅ v3-P2 新增:管理员全校成绩汇总卡片,4 个统计卡片 + 各年级对比表格)/ `ScoreCell`(✅ v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%/黄60-84%/绿≥85%,使用语义化 Tailwind 类名避免动态拼接,fullScore<=0 时不着色)/ `BatchGradeEntrySelectors`(✅ Phase 4.6 新增:批量录入试卷/班级选择器子组件)/ `BatchGradeEntryToolbar`(✅ Phase 4.6 新增:批量录入底部工具栏)/ `GradeRecordRow`(✅ Phase 4.6 新增:成绩记录行渲染:桌面表格+移动端卡片)/ `GradeRecordDetail`(✅ Phase 4.6 新增:成绩记录编辑/删除对话框集合,导出 EditableFields 类型与 toEditableFields 工具函数) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds)、`exams`(✅ 2026-06-24 新增:通过 exams data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用)、`error-book`(✅ 2026-06-25 新增:通过 error-book data-access.collectFromExamSubmission 在成绩录入后自动采集考试错题)、`diagnostic`(通过 diagnostic data-access.updateMasteryFromExamScore 更新掌握度) - 被依赖:`parent`(通过 data-access,合理)、`dashboard` **已知问题**: - ✅ P1-1 已修复:~~多处直查 `classes`/`classEnrollments`/`subjects`/`users` 表~~ 改为调用对应模块 data-access 函数(classes/school/users) - ✅ P1-1 已修复:~~统计计算业务逻辑混入 data-access(`getClassGradeStats` / `getGradeDistribution`)~~ 抽取为纯函数到 `stats-service.ts`,数据层专注 DB I/O - ✅ P1-2 已修复:~~`toNumber`/`normalize`/`buildScopeClassFilter` 在 3 个 data-access 文件中重复定义~~ 抽取到 `lib/grade-utils.ts` 统一维护 - ✅ P1-3 已修复:~~12 个查询/分析 Action 缺少 Zod 校验~~ 新增 12 个查询 schema(DeleteGradeRecordSchema/GetGradeRecordByIdSchema/GradeQuerySchema/ClassGradeStatsQuerySchema/StudentGradeSummaryQuerySchema/ClassRankingQuerySchema/ExportGradesSchema/GradeTrendQuerySchema/ClassComparisonQuerySchema/SubjectComparisonQuerySchema/GradeDistributionQuerySchema/RankingTrendQuerySchema),所有 Action 使用 safeParse 校验 - ✅ P1-4 已修复:~~`batch-grade-entry.tsx`/`grade-record-form.tsx`/`grade-distribution-chart.tsx` 中存在 `as` 断言~~ 改用类型守卫函数(isGradeType/isSemester/isDistributionTooltipPayload) - ✅ P1-5 已修复:~~teacher/grades 与 teacher/diagnostic 路由缺少 loading.tsx/error.tsx~~ 已为 7 个路由补齐 loading.tsx + error.tsx,并新增 `WidgetBoundary` 通用组件 - ✅ P2-1 已修复:~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 4 个成绩图表添加 `role="img"` + `aria-label`,2 个表格添加 ``,3 个图标按钮添加 `aria-label` - ✅ P2-2 已修复:~~diagnostic 组件中存在 Tailwind 任意值~~ 改用标准 Tailwind 类 - ✅ P2-4 已修复:~~`buildScopeClassFilter` 中 `grade_managed` scope 返回 `sql\`1=0\``(空数据)~~ 改为子查询 `classId IN (SELECT id FROM classes WHERE grade_id IN (...))` 过滤所管年级的班级 - ✅ P2-6 已修复:~~student/grades 和 management/grade/insights 各自重复定义 `SearchParams` 类型与 `getParam` 函数~~ 改为统一从 `@/shared/lib/search-params` 导入 - ✅ actions 层无直接 DB 访问(标杆) - ✅ data-access 按职责拆分为 3 个文件(标杆) - ✅ P2 已修复:`export.ts` 中 `scoreMap.get(r.studentId)!` 非空断言清理为安全守卫(`if (!subjMap) continue`) - ✅ P3 修复(2026-06-22):~~`schema.ts` 缺少输入边界约束~~ score 字段添加 `.max(1000)`,records 数组添加 `.max(500)`,查询 schema 补全 studentId/semester/examId 字段 - ✅ P3 修复(2026-06-22):~~`buildScopeClassFilter` 的 `class_members` scope 未应用 studentId 过滤~~ 新增 `currentUserId` 参数,`class_members` scope 内置 `eq(gradeRecords.studentId, currentUserId)` 过滤 - ✅ P3 修复(2026-06-22):~~`getGradeRecords` 内存分页(全量查询后切片)~~ 改为 DB 层分页(limit/offset),并行查询总数与当前页数据,返回 `PaginatedGradeRecords { records, total }` - ✅ P3 修复(2026-06-22):~~`batchCreateGradeRecords` 非原子操作~~ 包裹 `db.transaction` 确保原子性 - ✅ P3 修复(2026-06-22):~~`updateGradeRecord`/`deleteGradeRecord` 未检查记录是否存在~~ 新增存在性检查,不存在时抛 `NotFoundError("成绩记录")` - ✅ P3 修复(2026-06-22):~~`getClassGradeStats`/`getStudentGradeSummary`/`getClassRanking`/`getClassStudentsForEntry`/`getClassGradeStatsWithMeta` 缺少 scope 过滤~~ 所有函数新增 `scope?`/`currentUserId?` 参数,应用 `buildScopeClassFilter`;`getStudentGradeSummary`/`getClassStudentsForEntry` 对 `class_taught` scope 校验学生/班级归属 - ✅ P3 修复(2026-06-22):~~`getClassRanking` 未处理并列排名~~ 相同平均分获得相同名次,下一名次跳过占位 - ✅ P3 修复(2026-06-22):~~`getClassComparison`/`getRankingTrend` 缺少 scope 过滤~~ 应用 `buildScopeClassFilter`;`getRankingTrend` 对 `class_taught` scope 校验 - ✅ P3 修复(2026-06-22):~~actions.ts/actions-analytics.ts catch 块直接返回 `e.message`~~ 改用 `handleActionError(e)` 统一错误处理;`batchCreateGradeRecordsAction` 使用 `safeJsonParse` 解析 JSON - ✅ P3 修复(2026-06-22):~~actions-analytics.ts 缺少 class scope 校验~~ `getGradeTrendAction`/`getSubjectComparisonAction`/`getGradeDistributionAction` 新增 `assertClassInScope` 校验 - ✅ P3 修复(2026-06-22):~~组件直接 try/catch 调用 Server Action~~ 改用 `safeActionCall` 包装器(onError/onFinally 回调);`batch-grade-entry.tsx` localStorage 访问包裹 `typeof window !== "undefined"` 检查;区分"未录入"与"录入 0" - ✅ P3 修复(2026-06-22):~~`grade-trend-card.tsx` 排序未处理 NaN 日期、除法未检查 fullScore > 0~~ 新增日期有效性检查与 `fullScore > 0` 守卫 - ✅ P3 修复(2026-06-22):~~页面文件缺少 scope 传递~~ teacher/grades/page.tsx 使用 DB 层分页;analytics/page.tsx 添加 EmptyState;entry/page.tsx 与 stats/page.tsx 过滤 class_taught scope 班级;student/grades/page.tsx 与 parent/grades/page.tsx 传递 `ctx.dataScope` 到 data-access - ✅ v3-P2 改进(2026-06-23):新增 `grade_drafts` 表(成绩录入草稿,userId+classId+subjectId+type 唯一键,content JSON 存储 scores,24 小时过期,schema.ts 第 1444-1469 行);新增 `saveGradeDraft`/`getGradeDraft`/`deleteGradeDraft` data-access 函数 + 对应 3 个 Server Actions,支持成绩批量录入草稿服务端持久化 - ✅ v3-P2 改进(2026-06-23):新增 `getExamOptionsForGrades` data-access 函数,获取指定班级/科目下有成绩记录的考试列表;`AnalyticsFilters` 组件新增 `exams`/`currentExamId`/`currentSemester` props,添加学期和考试筛选 ChipNav;teacher/grades/analytics/page.tsx 新增 semester/examId 搜索参数解析 - ✅ v3-P2 改进(2026-06-23):`getGradeTrend`/`getGradeDistribution`/`getSubjectComparison`/`getClassComparison` 均新增 `semester` 和 `examId` 可选参数,支持按学期和考试筛选分析 - ✅ v3-P2 改进(2026-06-23):新增 `getSchoolWideGradeSummary` data-access 函数 + `SchoolWideSummaryCard` 组件 + `SchoolWideGradeSummary`/`SchoolWideGradeSummaryItem` 类型,管理员全校成绩汇总视图(按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总);admin/school/grades/insights/page.tsx 顶部新增 SchoolWideSummaryCard - ✅ v3-P2 改进(2026-06-23):parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`,渲染 GradeTrendCard - ✅ v3-P3-4 改进(2026-06-23):GradeTrendCard 新增日期范围选择器(全部/近7天/近30天/近90天),通过 nuqs `trendRange` URL 参数持久化,useEffect 中计算截止时间戳避免渲染阶段调用 Date.now() - ✅ v4-P3-4 改进(2026-06-23):GradeDistributionChart 色盲友好双重编码——每个分数段使用不同 SVG pattern(条纹/点状/交叉线/反向条纹/网格)+ 颜色,`SimpleBarChart` 新增 `defs` prop 支持自定义 SVG 图案 - ✅ P3 修复(2026-06-23):~~`lib/grade-utils.ts` 72 行超 40 行工具函数上限~~ P3-26 将 `buildScopeClassFilter` 迁移至 `lib/scope-filter.ts`,grade-utils.ts 仅保留 toNumber/normalize(20 行) - ✅ P3 修复(2026-06-23):~~`stats-service.ts` createDefaultBuckets 不必要导出~~ P3-10 移除 export 关键字,改为内部函数 - ✅ P3 修复(2026-06-23):~~`stats-service.ts` buildGradeTrendPoints 使用 as 断言~~ P3-24 新增 isGradeTrendType 类型守卫函数替代 as 断言 - ✅ P3 修复(2026-06-23):~~`export.ts` 局部 avg 函数与 stats-service.computeAverageScore 重复~~ P3-6 删除局部 avg,复用 computeAverageScore - ✅ P3 修复(2026-06-23):~~`export.ts` TYPE_LABELS 硬编码中文~~ P3-7 改用 next-intl getTranslations,新增 export.sheets/columns/metrics i18n 键(zh-CN/en 同步) - ✅ v4-P1-6 改进(2026-06-23):`createGradeRecordAction` / `batchCreateGradeRecordsAction` 成绩录入后通知学生和家长(调用 `notifyGradeEntered`,内部使用 `parent/data-access.getParentIdsByStudentIds` 批量查询家长 ID),通知失败不阻断成绩录入 - ✅ v4-P1-7 改进(2026-06-23):新增 `ScoreCell` 组件(components/score-cell.tsx),根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名避免动态拼接 - ✅ v4-P1-10 改进(2026-06-23):`grade-record-list.tsx` 使用 `ScoreCell` 替代纯文本分数展示 + 表格 `overflow-x-auto` 水平滚动 - ✅ v4-P1-12 改进(2026-06-23):`exportGradesAction` 新增可选 `studentId` 参数,支持按学生导出(家长视角);新增 `exportStudentGradeRecordsToExcel` 导出函数(仅含成绩明细 + 个人统计,不含班级数据);parent/grades/page.tsx 传 studentId 到 ParentExportButton;ParentExportButton 接入 exportGradesAction - ✅ v3-P3-1 改进(2026-06-23):`batch-grade-entry.tsx` 新增"下载模板"按钮,客户端生成 CSV 模板(含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8),教师可下载填好后粘贴到录入表格 - ✅ v3-P3-2 改进(2026-06-23):`grade-record-list.tsx` 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;新增 `bulkDeleteGradeRecords` data-access 函数(使用 inArray 一次性删除避免 N+1)+ `bulkDeleteGradeRecordsAction` Server Action(限制单次最多 500 条) - ✅ v4-P3-2 改进(2026-06-23):`batch-grade-entry.tsx` 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态避免重复显示 - ✅ v4-P2-6 修复(2026-06-23):~~`assertClassInScope` 是同步函数但位于 "use server" 文件 actions.ts 中~~ Next.js 要求 "use server" 文件中所有 export 必须为 async,同步 export 会导致构建错误。修复:将 `assertClassInScope` 迁移至独立文件 `lib/scope-check.ts`(含 `import "server-only"`),actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入 - ✅ 2026-06-24 重新设计:批量录入从"只录总分"改为"按试卷录入每题得分"。新增 `grade_record_answers` 表存储每题得分明细(迁移 0010_grade_record_answers.sql)。`batchCreateGradeRecordsByExam` data-access 单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩(学生只看到对错,不知道学生答案)。`BatchGradeEntryByExam` 组件完全重写为 Excel 式表格(行=学生,列=题目,末列=总分自动计算),支持多行多列粘贴、Enter 跳下一行、Tab 跳下一格、分数校验、撤销机制(sessionStorage 5 分钟有效)。新增对 `exams` 模块的依赖(getExamsForGradeEntry/getExamForGradeEntry)。i18n 新增 batchByExam 章节(zh-CN/en ~35 键) - ✅ 审计修复(2026-06-22):~~`school-wide-summary-card.tsx` 整个组件无 i18n(13 处硬编码中文)~~ 改为 async 组件使用 `getTranslations("grades")`,新增 `schoolWide` 翻译键节(zh-CN/en 同步 14 键) - ✅ 审计修复(2026-06-22):~~5 个 teacher/grades 页面文件大量硬编码中文/英文~~ `page.tsx`/`analytics/page.tsx`/`entry/page.tsx`/`stats/page.tsx`/`error.tsx` 全部使用 `getTranslations("grades")` 或 `useTranslations("grades")`,新增 `page` 翻译键节(zh-CN/en 同步 ~30 键) - ✅ 审计修复(2026-06-22):~~`actions.ts` 15+ 处硬编码中文错误消息/通知内容~~ `notifyGradeEntered` 使用 `getTranslations("grades")` 获取通知标题和内容;`batchCreateGradeRecordsByExamAction` 错误消息改用 `t("action.*")` 翻译键;`exportGradesAction` 文件名和错误消息改用翻译键,新增 `action` 翻译键节(zh-CN/en 同步 ~20 键) - ✅ 审计修复(2026-06-22):~~`data-access.ts` 2 处硬编码中文 throw Error~~ 改为英文错误消息(data-access 层为防御性检查,action 层已先行校验) - ✅ 审计修复(2026-06-22):~~`data-access.ts:782` `as` 断言无类型守卫~~ 新增 `isGradeDraftData` 类型守卫函数(lib/type-guards.ts),从 unknown 安全验证 DB JSON 结构 - ✅ 审计修复(2026-06-22):~~`data-access-analytics.ts:360-362` 非空断言 `!`~~ 改用类型谓词 `filter((r): r is typeof r & { stats: NonNullable } => 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` | 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` | 23 | **G2-005 / S-01 治理(2026-07-07)**:barrel 重新导出,原 831 行超过 800 行建议上限,拆分为 4 个按分析维度划分的子文件(trend/class/student/overview),保持向后兼容,所有消费者无需修改 import 路径 | | `data-access-analytics-trend.ts` | 135 | **G2-005 / S-01 新增(2026-07-07)**:成绩趋势分析(getGradeTrendRaw/getGradeTrend + getExamOptionsForGradesRaw/getExamOptionsForGrades + GradeTrendParams 接口),从 data-access-analytics.ts 拆分 | | `data-access-analytics-class.ts` | 340 | **G2-005 / S-01 新增(2026-07-07)**:班级/科目/年级维度分析(getClassComparison/getClassComparisonWithSignificance + getSubjectComparison + getGradeDistribution + getGradeDistributionByGradeId + 4 个 Params 接口),从 data-access-analytics.ts 拆分 | | `data-access-analytics-student.ts` | 225 | **G2-005 / S-01 新增(2026-07-07)**:学生维度分析(getStudentPositionInClassDistribution + getStudentGrowthArchive),从 data-access-analytics.ts 拆分 | | `data-access-analytics-overview.ts` | 135 | **G2-005 / S-01 新增(2026-07-07)**:全校汇总分析(getSchoolWideGradeSummary,管理员视图,按年级聚合),从 data-access-analytics.ts 拆分 | | `data-access-ranking.ts` | 166 | 排名查询(P3 修复:getRankingTrend 接受 scope 参数 + class_taught 校验) | | `stats-service.ts` | 285 | 统计计算纯函数(P1-1 新增:8 个纯函数 + 2 个常量 + 2 个接口;P3-10:createDefaultBuckets 改为内部函数;P3-24:buildGradeTrendPoints 使用 isGradeTrendType 类型守卫替代 as 断言) | | `export.ts` | 290+ | Excel 导出(v2-P1-5 修复:传递 currentUserId 到 data-access;P3 修复:适配 PaginatedGradeRecords 结构 + 传递 scope;P3-6:复用 stats-service.computeAverageScore 替代局部 avg;P3-7:硬编码中文改用 next-intl getTranslations;v4-P1-12 新增:exportStudentGradeRecordsToExcel 家长视角单学生导出) | | `schema.ts` | 237 | Zod 校验(含 12 个查询 schema;P3 修复:score .max(1000) + records .max(500) + 补全查询字段;v3-P2 新增:grade_drafts 表定义;2026-06-24 新增:BatchGradeEntryByExamSchema;**P3-10 新增(2026-06-26)**:ExcelImportRowSchema/ExcelImportGradesSchema + 导出 ExcelImportRowInput/ExcelImportGradesInput 类型;**P3-7 新增(2026-06-26)**:GradeAppealStatusEnum/CreateGradeAppealSchema/ReviewGradeAppealSchema + 导出 CreateGradeAppealInput/ReviewGradeAppealInput 类型) | | `lib/grade-utils.ts` | 20 | 公共工具函数(toNumber/normalize;P3-26:buildScopeClassFilter 迁移至 scope-filter.ts) | | `lib/scope-filter.ts` | 56 | DB 行级权限过滤(buildScopeClassFilter;P3-26 从 grade-utils.ts 迁移;v2-P2-2 修复:改用 classes data-access 子查询;P3 修复:新增 currentUserId 参数) | | `lib/scope-check.ts` | 34 | v4-P2-6 新增:班级 scope 校验工具(assertClassInScope 同步函数,从 actions.ts 迁移至此独立文件以避开 "use server" 文件要求 export 必须为 async 的限制;含 `import "server-only"`;**P1-4 重构(2026-06-25)**:返回类型从 string \| null 改为 ScopeCheckErrorCode \| null(\"class_taught\" \| \"class_members\" \| \"denied\"),消除硬编码英文字符串,调用方通过 i18n 键 grades.action.scopeError.{code} 翻译) | | `lib/notify.ts` | 60 | **P1-1 新增(2026-06-25)**:成绩录入通知工具(notifyGradeEntered),从 actions.ts 抽取。使用 next-intl getTranslations 获取通知标题和内容(i18n);**P2-7 改进**:使用 Promise.allSettled 并行化通知写入,避免批量录入 30+ 学生时串行等待;依赖 notifications/data-access.createNotification + parent/data-access.getParentIdsByStudentIds + school/data-access.getSubjectNameById | | `lib/report-card.ts` | 380 | **P3-1 新增(2026-06-26)**:成绩报告卡数据访问层。getReportCardData(cached)聚合学生成绩+班级信息+班主任+学年+科目排名+总体统计,computeSubjectRank 计算单科排名。三层 scope 校验:owned(学生自己)/children(家长子女)/class_taught(教师班级学生)。静态导入 data-access(getStudentRankInClass/getClassNamesByIds)+ stats-service(computeAverageScore) | | `lib/type-guards.ts` | 25 | 类型守卫纯函数(isGradeType/isSemester/isGradeDraftData;**P2-9 修复(2026-06-25)**:isGradeType/isSemester 使用 GRADE_TYPES.some((t) => t === v) 替代 (GRADE_TYPES as readonly string[]).includes(v),消除 as 断言;isGradeDraftData 使用 in 运算符替代 as Record 断言) | | `types.ts` | 200+ | 类型定义(v3-P2 新增:SchoolWideGradeSummaryItem/SchoolWideGradeSummary;2026-06-24 新增:GradeRecordAnswer/BatchGradeEntryByExamQuestion/BatchGradeEntryByExamItem) | | `components/widget-boundary.tsx` | 136 | Widget 边界组件(P1-5 新增,v2-P1-1 已在 3 个页面应用;**P1-9(2026-06-25)**:teacher/grades/page.tsx 列表与统计卡片新增 WidgetBoundary 包裹,避免单数据区块失败导致整页崩溃) | | `components/school-wide-summary-card.tsx` | - | v3-P2 新增:管理员全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格) | | `components/score-cell.tsx` | 41 | v4-P1-7 新增:成绩单元格组件,根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名 | | `components/grade-trend-card.tsx` | 69 | 趋势卡片(v2-P2-9 修复:a11y;v2-P1-4:i18n;P3 修复:NaN 日期检查 + fullScore > 0 守卫) | | `components/grade-distribution-chart.tsx` | 100 | 分数分布图(v2-P1-4:i18n) | | `components/subject-comparison-chart.tsx` | 62 | 科目对比图(v2-P1-4:i18n) | | `components/class-comparison-chart.tsx` | 194 | 班级对比图(v2-P1-4:i18n;v3-P3-5 新增:显著性分析区域,基于极差和样本量的经验规则判断班级间差异,含可折叠详细分析) | | `components/grade-trend-chart.tsx` | 59 | 趋势图(v2-P1-4:i18n) | | `components/grade-record-form.tsx` | 177 | 录入表单(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall 包装提交) | | `components/batch-grade-entry.tsx` | 376 | 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);**Phase 4.6 重构(2026-07-06)**:进一步拆出 BatchGradeEntrySelectors(试卷/班级选择器含未选态)+ BatchGradeEntryToolbar(保存/清除按钮+粘贴提示),主组件从 457 行降至 376 行,容器仅保留状态编排与 Server Action 调用。撤销数据使用 isUndoData 类型守卫替代 as { ids: string[]; timestamp: number } 断言;window.confirm() 已替换为声明式 AlertDialog。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId? | | `components/batch-grade-entry-selectors.tsx` | 124 | **Phase 4.6 新增(2026-07-06)**:批量录入试卷/班级选择器子组件(含未选试卷态引导)。从 batch-grade-entry.tsx 拆分,承接试卷选择器、班级选择器、试卷信息徽章三个区块的渲染 | | `components/batch-grade-entry-toolbar.tsx` | 53 | **Phase 4.6 新增(2026-07-06)**:批量录入底部工具栏子组件(清除按钮 + 保存按钮 + 粘贴提示)。从 batch-grade-entry.tsx 拆分 | | `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/student-grade-summary.tsx` | 107 | 学生成绩摘要(v2-P1-4:i18n) | | `components/export-button.tsx` | 79 | 导出按钮(v2-P1-4:i18n;P3 修复:safeActionCall 包装导出操作;**P2-2(2026-06-25)**:handleExport 补全 : Promise 返回值类型) | | `components/stats-class-selector.tsx` | 40 | 统计班级选择器(v2-P1-4:i18n) | | `components/class-grade-report.tsx` | 130 | 班级成绩报告(v2-P1-4:i18n;**Phase 4.6 重构(2026-07-06)**:内联原 GradeStatsCard 实现,使用 StatsGrid + 8 个 StatCard(average/median/max/min/stdDev/passRate/excellentRate/count),含 count=0 空态 Card) | | `components/grade-record-list.tsx` | 232 | 成绩记录列表(v2-P1-4:i18n;P3 修复:safeActionCall 包装删除操作;v4-P1-7:使用 ScoreCell;v4-P1-10:overflow-x-auto;v3-P3-2 新增:多选复选框 + 全选 + 批量删除工具栏 + 批量删除确认对话框;**Phase 4.6 重构(2026-07-06)**:行渲染(桌面表格+移动端卡片)拆分至 GradeRecordRow,编辑/删除/批量删除对话框拆分至 GradeRecordDetail,主组件从 523 行降至 232 行) | | `components/grade-record-row.tsx` | 175 | **Phase 4.6 新增(2026-07-06)**:成绩记录行渲染子组件,承接桌面端表格行 + 移动端卡片堆叠视图,含 v3-P3-2 多选复选框 + 全选 + v4-P1-7 ScoreCell 颜色编码 | | `components/grade-record-detail.tsx` | 215 | **Phase 4.6 新增(2026-07-06)**:成绩记录详情/编辑/删除对话框集合,含编辑表单、单条删除确认、批量删除确认三个对话框。导出 EditableFields 类型与 toEditableFields 工具函数 | | `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 | > 架构变更(2026-07-05,Phase 4.5 — grade-record-list.tsx 编辑保存改用 useOptimistic + useTransition): > - **grade-record-list.tsx**:编辑保存 `isSaving` useState → `useOptimistic` + `useTransition`。`handleEditSave` 构造 `optimisticRecord` 在 `startSaveTransition` 内 `addOptimisticRecord` + `await updateGradeRecordAction` + `router.refresh()`;两处 `records.map` 改为 `optimisticRecords.map`(桌面表格 + 移动端卡片视图)即时显示乐观记录 > - **关键决策**:成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值;不为删除操作使用 useOptimistic,因为删除需要服务端确认(外键约束、关联记录检查等) > 架构变更(2026-07-06,Phase 4.6 — grades 模块组件化重构闭环): > - **batch-grade-entry.tsx**(457→376 行):拆出 `BatchGradeEntrySelectors`(试卷/班级选择器含未选态)+ `BatchGradeEntryToolbar`(保存/清除按钮+粘贴提示),容器仅保留状态编排与 Server Action 调用 > - **grade-record-list.tsx**(523→232 行):拆出 `GradeRecordRow`(行渲染:桌面表格+移动端卡片)+ `GradeRecordDetail`(编辑/单条删除/批量删除对话框集合) > - **删除 4 个重复组件**:`grade-stats-card.tsx`(迁移至 `StatsGrid`,由 class-grade-report.tsx 直接使用)/ `grade-filters.tsx`(迁移至调用方 `student/grades/student-grade-filters.tsx`,复用 `FilterBar` + `FilterSearchInput`)/ `grade-query-filters.tsx`(迁移至调用方 `teacher/grades/teacher-grade-query-filters.tsx`,复用 `FilterBar` + Select)/ `analytics-filters.tsx`(迁移至调用方 `teacher/grades/analytics/analytics-grade-filters.tsx`,复用 `FilterBar` + `ChipNav`) --- ## 2.7 classes(班级模块)— 耦合最严重 **职责**:班级 CRUD + 学生/教师管理 + 邀请码注册。 **导出函数**: - Actions:`createTeacherClassAction` / `updateTeacherClassAction` / `deleteTeacherClassAction` / `createAdminClassAction` / `updateAdminClassAction` / `deleteAdminClassAction` / `createGradeClassAction` / `updateGradeClassAction` / `deleteGradeClassAction` - Data-access:`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `verifyTeacherOwnsClass` / `getTeacherIdsByClassIds`(获取多个班级的所有教师 ID:班主任 + 任课教师,跨模块接口,供 messaging 模块调用)/ `getClassIdByScheduleId(scheduleId: string): Promise`(✅ 2026-06-26 审计修复新增:根据课表项 ID 获取所属班级 ID,供 actions-schedule 的 create/update/delete 三个 Action 做越权校验,防止教师通过 scheduleId 修改他人班级课表)(✅ P0-5 已修复:classSchedule 写函数 createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem 已迁移至 scheduling/data-access-class-schedule.ts,classes 模块仅保留 classSchedule 读函数;✅ P2 已修复:`getAccessibleClassIdsForTeacher` 使用 `Promise.all` 并行化 ownedIds 与 assignedIds 查询) - Schema:`CreateTeacherClassSchema` / `UpdateTeacherClassSchema` / `DeleteTeacherClassSchema` / `CreateAdminClassSchema` / `UpdateAdminClassSchema` / `DeleteAdminClassSchema` / `CreateGradeClassSchema` / `UpdateGradeClassSchema` / `DeleteGradeClassSchema` / `CreateClassScheduleItemSchema` / `UpdateClassScheduleItemSchema` / `DeleteClassScheduleItemSchema` / `EnrollStudentByEmailSchema` - Components:`AdminClassesClient` / `GradeClassesClient`(P1-D 重构:降至 ≤160 行,组合使用共享组件)/ `ClassListTable`(admin/grade 共享班级列表表格,P1-D 抽取)/ `ClassFormDialog`(admin/grade 共享创建/编辑表单弹窗,`schools` 可选控制学校选择显隐,P1-D 抽取)/ `ClassDeleteDialog`(admin/grade 共享删除确认弹窗,基于 ConfirmDeleteDialog,P1-D 抽取)/ `ClassListToolbar`(admin/grade 共享工具栏:新建按钮+计数徽章,P1-D 抽取)/ `class-form-utils`(`formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型,P1-D 抽取)/ `ClassErrorBoundary`(P1-C 新增:class component Error Boundary + i18n + router.refresh 重试)/ `ClassListSkeleton` / `ClassCardSkeleton` / `ClassGridSkeleton` / `StudentListSkeleton` / `ScheduleGridSkeleton`(P1-C 新增:5 个骨架屏组件)/ `MyClassesGrid`(P0-A 修复:已接入 i18n)/ `ScheduleView`(✅ P2 修复:已拆分出 `ScheduleCreateDialog`/`ScheduleEditDialog`/`ScheduleDeleteDialog` 3 个对话框子组件,主组件从 464 行降至 175 行;不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态,下沉到子对话框;WEEKDAYS 常量改用从 `./schedule-utils` 导入的 `SCHEDULE_WEEKDAYS`)/ `ScheduleCreateDialog`(P2 修复新增:课表新增对话框,从 schedule-view 拆分)/ `ScheduleEditDialog`(P2 修复新增:课表编辑对话框,从 schedule-view 拆分)/ `ScheduleDeleteDialog`(P2 修复新增:课表删除确认对话框,从 schedule-view 拆分)/ `ScheduleFilters` / `StudentsTable` / `StudentsFilters`(P0-A 修复:已接入 i18n)/ `ClassHeader` / `ClassOverviewStats` / `ClassQuickActions` / `ClassScheduleWidget` / `ClassAssignmentsWidget` / `ClassStudentsWidget` / `ClassTrendsWidget` / `EditClassDialog`(P0-A 修复:class-detail 下 7 个子组件 + 编辑对话框已接入 i18n) - 工具函数(components/schedule-utils.ts,2026-06-26 审计修复新增):`getSubjectColor(subject: string): string`(根据科目名返回对应颜色类名,中英文双语关键词匹配;修复原实现仅匹配英文关键词导致中文 `DEFAULT_CLASS_SUBJECTS` 全部落到 default 分支的颜色分组失效 bug)/ `getPositionStyle(startTime: string, endTime: string): { top: string; height: string }`(根据课程开始/结束时间计算课表块在 8:00-18:00 时间轴上的定位百分比)/ `timeToMinutes(time: string): number`(将 "HH:MM" 时间字符串转换为当天从 0 点起的分钟数)/ `SCHEDULE_WEEKDAYS`(✅ P2 修复新增:课表周常量数组,供 schedule-view 及拆分出的 3 个对话框子组件复用,替换原 schedule-view.tsx 内联的 WEEKDAYS 常量) - Hooks:`useClassData`(admin/grade 共享对话框+表单状态管理+重置逻辑,P1-E 抽取)/ `useClassFilters`(按学校筛选年级,admin 模式使用,P1-E 抽取) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`school`(✅ P1-1 已修复:通过 school data-access.isGradeHead/isGradeManager/findGradeIdByHeadAndName)、`homework`(✅ P0-7 已修复:通过 `homework/data-access-classes` 暴露的函数获取作业数据,不再直查 homework/exams 表) - 被依赖:`exams`/`homework`/`grades`/`attendance`/`scheduling`/`dashboard`(通过 data-access,P0-4 已修复)/`parent`/`course-plans`/`users`(✅ P1-1 已修复:8+ 处直查 classes 表改为通过 classes data-access)/`messaging`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId,支持学生/家长给班级教师发消息) **已知问题**: - ✅ P0-1 已修复:`data-access.ts` 已拆分为 5 个文件(data-access/data-access-stats/data-access-schedule/data-access-students/data-access-admin),所有文件均 ≤800 行 - ✅ P0-5 已修复:classSchedule 写函数(createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem)已迁移至 scheduling/data-access-class-schedule.ts,classes 模块仅保留 classSchedule 读函数(getStudentSchedule/getClassSchedule);新增 verifyTeacherOwnsClass 供 scheduling 模块跨模块校验教师班级归属 - ✅ P0-7 已修复:`data-access-stats.ts` 和 `data-access-students.ts` 不再直查 `homeworkAssignmentQuestions`/`homeworkAssignmentTargets`/`homeworkAssignments`/`homeworkSubmissions`/`exams` 表,改为调用 `homework/data-access-classes.ts` 暴露的函数(`getAssignmentIdsForStudents`/`getHomeworkAssignmentsWithSubject`/`getHomeworkAssignmentsByIds`/`getAssignmentMaxScoreById`/`getAssignmentTargetCounts`/`getHomeworkSubmissionsForStudents`/`getPublishedHomeworkAssignmentsWithSubject`/`getHomeworkSubmissionsForAssignments`) - ✅ P1-1 已修复:~~`actions.ts` 直查 `grades` 表做权限校验~~ 改为调用 `school/data-access` 函数 - ✅ P1-1 已修复:~~`getSessionTeacherId` 在 data-access 调用 `auth()`~~ 改为通过 `shared/lib/auth-guard.getAuthContext()` 获取 - ✅ P2 已修复:`data-access.ts` 中 `idByName.get(name)!` 非空断言清理为 `flatMap` 安全过滤;`data-access-admin.ts` 中同类非空断言清理 - ✅ P0-3 修复(2026-06-22):~~`actions.ts` 974 行接近 1000 行硬上限~~ 拆分为 6 个文件(actions-teacher/actions-admin/actions-grade/actions-invitations/actions-schedule/actions-shared),原 `actions.ts` 改为 50 行 barrel re-export - ✅ P1-1 修复(2026-06-22):~~`ctx.roles.includes("admin"/"teacher"/"student")` 角色硬编码~~ 改为 `hasAdminScope(ctx)`/`hasTeacherScope(ctx)`/`hasStudentScope(ctx)` 基于 `dataScope.type` 判断 - ✅ P1-4 修复(2026-06-22):`types.ts` 中 ClassHomeworkInsights 等跨领域类型保留在 classes 模块(因为是 classes 对 homework 数据的视图),添加注释说明归属决策 - ✅ P1-D 修复(2026-06-24):~~`admin-classes-view.tsx` 533 行与 `grade-classes-view.tsx` 439 行 ~80% 代码重复~~ 拆分为组合模式:抽取 `ClassListTable` / `ClassFormDialog` / `ClassDeleteDialog` / `ClassListToolbar` 4 个共享组件 + `class-form-utils`(`formatSubjectTeachers` 工具函数 + `ClassFormGrade`/`SubjectTeacherState` 类型);admin-classes-view 降至 159 行,grade-classes-view 降至 153 行;admin 删除弹窗统一改用 `ConfirmDeleteDialog`(与 grade 一致) - ✅ P1-E 修复(2026-06-24):抽取 `useClassData` hook(对话框开关+表单字段+提交中标记+重置逻辑,admin/grade 共享)+ `useClassFilters` hook(按学校筛选年级,admin 模式使用) - ✅ P0-A 修复(2026-06-24):~~classes 模块 13/14 个组件未接入 i18n~~ 全部 13 个组件(admin-classes-view/grade-classes-view/my-classes-grid/schedule-view/schedule-filters/students-filters/students-table/class-detail 下 7 个子组件/edit-class-dialog)已接入 `useTranslations("classes")`;my-classes-grid 中 5 处硬编码英文("Failed to regenerate"/"Failed to copy"/"No Room"/"vs last week")已替换为翻译键 - ✅ P0-B 修复(2026-06-24):~~classes.json 仅 55 行~~ 扩充至 ~370 行,覆盖 invitation/class/list/form/delete/filters/myClasses/students/schedule/detail(header/overview/trends/assignments/widgets/edit/empty)/grade/bulk/errors 全部场景;en 镜像同步 - ✅ P1-C 修复(2026-06-24):新增 `class-error-boundary.tsx`(class component Error Boundary + i18n + router.refresh 重试)和 `class-skeleton.tsx`(ClassListSkeleton/ClassCardSkeleton/ClassGridSkeleton/StudentListSkeleton/ScheduleGridSkeleton);6 个页面均已包裹 Error Boundary:admin/school/classes + teacher/classes/my + teacher/classes/my/[id] + teacher/classes/schedule + teacher/classes/students(ClassErrorBoundary)、management/grade/dashboard(SchoolErrorBoundary);schedule/students 页面 EmptyState 硬编码英文替换为 i18n - ✅ 2026-06-26 审计修复(i18n 全覆盖 + 越权校验 + 角色硬编码消除 + 错误处理统一 + 路由错误边界完善): - **i18n 翻译键扩充**:classes.json 新增 `actions` 命名空间(42 个键,覆盖全部 Server Action 错误/成功消息)、`error` 命名空间(3 个键:title/description/retry,供 ErrorState 组件使用)、`filters` 命名空间新增 10 个键、`detail` 命名空间新增 12 个键 - **越权校验加固**:`actions-schedule.ts` 新增 `assertClassOwnership` 辅助函数(调用 `getClassIdByScheduleId` + `verifyTeacherOwnsClass`),create/update/delete 三个 Action 增加班级归属校验,防止教师通过 scheduleId 修改他人班级课表;`actions-teacher.ts` 的 `updateTeacherClassAction` / `deleteTeacherClassAction` 增加越权校验 - **角色硬编码消除**:`data-access.ts` / `data-access-teacher.ts` / `data-access-admin.ts` 中所有 `roles.name, "teacher"` / `roles.name, "student"` 改为 `ROLE_NAMES.TEACHER` / `ROLE_NAMES.STUDENT`(共 7 处),统一引用 `@/shared/types/permissions` 常量 - **错误处理统一**:`actions-admin.ts` / `actions-grade.ts` / `actions-invitations.ts` / `actions-schedule.ts` / `actions-teacher.ts` 所有 catch 块改为使用 `handleActionError`(来自 `@/shared/lib/action-utils`),所有 message 改为使用 `await getTranslations("classes")` + t(),移除 `PermissionDeniedError` 直接 import;`actions-invitations.ts` 的 `assertClassOwnership` 签名变更:新增 `t` 参数;`actions-shared.ts` 的 `toWeekday` / `parseSubjectTeachers` 改为 async,使用 `ValidationError` 代替 `throw new Error` - **DEFAULT_CLASS_SUBJECTS 回退逻辑**:`data-access.ts` 的 `getClassSubjects()` 新增回退逻辑——subjects 表为空时返回 `DEFAULT_CLASS_SUBJECTS`;`data-access-teacher.ts` / `data-access-admin.ts` 中 `inArray(subjects.name, DEFAULT_CLASS_SUBJECTS)` 改为 `inArray(subjects.name, await getClassSubjects())`,确保筛选条件与回退数据源一致 - **DB 错误处理修正**:`data-access-teacher.ts` 的 `getTeacherClasses()` DB 错误时 throw 而非返回空数组(避免静默吞错) - **路由错误边界完善**(10 个 error.tsx + 1 个 loading.tsx 新增):teacher/classes/my/error.tsx、teacher/classes/my/[id]/error.tsx + loading.tsx、teacher/classes/students/error.tsx、teacher/classes/schedule/error.tsx、student/learning/error.tsx + loading.tsx、student/learning/courses/error.tsx、student/learning/courses/[classId]/error.tsx、student/schedule/error.tsx;统一使用 shared 层 `ErrorState` 组件 - **新增工具函数文件**:`components/schedule-utils.ts` 抽取 `getSubjectColor` / `getPositionStyle` / `timeToMinutes` 3 个纯函数(修复中文科目颜色分组失效 bug + 提升可测试性) - **新增权限点使用**:`student/learning/page.tsx` / `student/learning/courses/page.tsx` / `student/learning/courses/[classId]/page.tsx` / `student/schedule/page.tsx` 接入 `requirePermission(CLASS_READ)` - **generateMetadata 接入**(✅ P2 修复新增):8 个 page.tsx 新增 `generateMetadata` 导出(通过 next-intl `getTranslations` 生成页面标题):`teacher/classes/my/page.tsx`、`teacher/classes/my/[id]/page.tsx`、`teacher/classes/schedule/page.tsx`、`teacher/classes/students/page.tsx`、`student/learning/page.tsx`、`student/learning/courses/page.tsx`、`student/learning/courses/[classId]/page.tsx`、`student/schedule/page.tsx` > 架构变更(2026-07-07,S-03 重复 Helper 提取): > - **G3-020 admin 班级列表去重**:新增 `lib/admin-class-mappers.ts`(115 行),抽出 `buildSubjectsByClassIdMap`(subjectTeachers 行 → Map>)+ `composeAdminClassList`(classRows + subjectsByClassId → AdminClassListItem[],含 compareClassLike 排序);消除 `data-access-admin.ts` 中 145+124 行重复的 subjectsByClassId 构建与 list 排序逻辑,data-access-admin.ts 从 406 行降至 353 行 > - **G3-036 isClassSubject/toClassSubject 去重**:从 `data-access-admin.ts`/`data-access-teacher.ts`/`actions-shared.ts` 移除本地 `isClassSubject`/`toClassSubject`,统一从 `lib/admin-class-mappers.ts` 导入;`actions-shared.ts` 通过 `export { isClassSubject } from "./lib/admin-class-mappers"` re-export 保持向后兼容 > - **lib/admin-class-mappers.ts 接口**:`isClassSubject`/`toClassSubject`/`buildSubjectsByClassIdMap`/`composeAdminClassList` 4 个函数 + `SubjectTeacherRow`/`AdminClassRow` 2 个接口(供 data-access-admin 类型标注) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `data-access.ts` | 548 | 核心班级 CRUD + 邀请码 + 教师班级管理(含 re-export 向后兼容) | | `data-access-stats.ts` | 513 | 作业统计查询(班级/年级作业洞察,通过 homework/data-access-classes 获取数据) | | `data-access-schedule.ts` | 93 | 课表查询(学生/班级课表只读,P0-5 已修复:写函数已迁移至 scheduling 模块) | | `data-access-students.ts` | 253 | 学生相关查询(科目成绩、学生名单、学生班级,通过 homework/data-access-classes 获取数据) | | `data-access-admin.ts` | 353 | 管理员班级管理(管理员班级 CRUD、年级管理班级查询;2026-07-07 S-03:移除 145+124 行重复的 subjectsByClassId 构建与 list 排序,改调 `lib/admin-class-mappers`) | | `lib/admin-class-mappers.ts` | 115 | **admin 班级列表共享辅助(2026-07-07 S-03 新增)**:`isClassSubject`/`toClassSubject`/`buildSubjectsByClassIdMap`/`composeAdminClassList` 4 个函数 + `SubjectTeacherRow`/`AdminClassRow` 2 个接口;消除 data-access-admin/data-access-teacher/actions-shared 三处重复定义 | | `actions.ts` | 50 | Barrel re-export(P0-3 修复:从 974 行拆分为 6 个文件) | | `actions-teacher.ts` | 100 | 教师班级 CRUD(3 个 Action) | | `actions-admin.ts` | 120 | 管理员班级 CRUD(3 个 Action) | | `actions-grade.ts` | 110 | 年级组长班级 CRUD(3 个 Action) | | `actions-invitations.ts` | 502 | 邀请码与注册 + 批量操作(10 个 Action,P2-4 新增批量导入学生/批量分配教师) | | `actions-schedule.ts` | 90 | 班级课表 CRUD(3 个 Action) | | `actions-shared.ts` | 60 | 共享工具(hasAdminScope/hasTeacherScope/hasStudentScope/parseSubjectTeachers/toWeekday) | | `schema.ts` | 152 | Zod 校验(13 个 schema:教师/管理员/年级班级 CRUD + 课表 CRUD + 邮箱注册) | | `types.ts` | 201 | 类型定义(含跨领域类型说明注释,P1-4 修复) | | components/admin-classes-view.tsx | 159 | 管理员班级客户端(P1-D 重构:组合共享组件) | | components/grade-classes-view.tsx | 153 | 年级班级客户端(P1-D 重构:组合共享组件) | | components/class-list-table.tsx | 115 | 班级列表表格(admin/grade 共享,P1-D 抽取) | | components/class-form-dialog.tsx | 200 | 班级创建/编辑表单弹窗(admin/grade 共享,P1-D 抽取) | | components/class-delete-dialog.tsx | 35 | 班级删除确认弹窗(admin/grade 共享,P1-D 抽取) | | components/class-list-toolbar.tsx | 30 | 班级列表工具栏(admin/grade 共享,P1-D 抽取) | | components/class-form-utils.ts | 35 | formatSubjectTeachers + 表单类型(P1-D 抽取) | | components/schedule-utils.ts | 89 | 课表纯函数工具集(2026-06-26 审计修复新增:getSubjectColor/getPositionStyle/timeToMinutes;✅ P2 修复新增 SCHEDULE_WEEKDAYS 周常量;修复中文科目颜色分组失效 bug + 提升可测试性) | | components/class-error-boundary.tsx | 45 | 班级模块 Error Boundary(P1-C 新增) | | components/class-skeleton.tsx | 80 | 班级模块骨架屏组件(5 个:List/Card/Grid/StudentList/ScheduleGrid,P1-C 新增) | | components/my-classes-grid.tsx | 418 | 教师班级卡片网格(P0-A 修复:已接入 i18n) | | components/schedule-view.tsx | 175 | 课表视图(P0-A 修复:已接入 i18n;✅ P2 修复:拆分出 ScheduleCreateDialog/ScheduleEditDialog/ScheduleDeleteDialog 3 个对话框子组件,主组件从 464 行降至 175 行;不再持有 isWorking/handleCreate/handleUpdate/handleDelete 状态,下沉到子对话框;WEEKDAYS 常量改用从 ./schedule-utils 导入的 SCHEDULE_WEEKDAYS) | | components/schedule-create-dialog.tsx | - | 课表新增对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleCreate 状态) | | components/schedule-edit-dialog.tsx | - | 课表编辑对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleUpdate 状态) | | components/schedule-delete-dialog.tsx | - | 课表删除确认对话框(✅ P2 修复新增:从 schedule-view.tsx 拆分,承接原 isWorking/handleDelete 状态) | | components/schedule-filters.tsx | 60 | 课表筛选器(P0-A 修复:已接入 i18n) | | components/students-filters.tsx | 60 | 学生筛选器(P0-A 修复:已接入 i18n) | | components/students-table.tsx | 120 | 学生表格(P0-A 修复:已接入 i18n) | | components/class-detail/class-header.tsx | 100 | 班级详情头部(P0-A 修复:已接入 i18n) | | components/class-detail/class-overview-stats.tsx | 80 | 班级概览统计(P0-A 修复:已接入 i18n) | | components/class-detail/class-quick-actions.tsx | 60 | 快捷操作(P0-A 修复:已接入 i18n) | | components/class-detail/class-schedule-widget.tsx | 120 | 课表 Widget(P0-A 修复:已接入 i18n) | | components/class-detail/class-assignments-widget.tsx | 100 | 作业 Widget(P0-A 修复:已接入 i18n) | | components/class-detail/class-students-widget.tsx | 120 | 学生 Widget(P0-A 修复:已接入 i18n) | | components/class-detail/class-trends-widget.tsx | 150 | 趋势 Widget(P0-A 修复:已接入 i18n) | | components/class-detail/edit-class-dialog.tsx | 145 | 编辑班级对话框(P0-A 修复:已接入 i18n) | | hooks/use-class-data.ts | 95 | 对话框+表单状态管理 hook(P1-E 抽取) | | hooks/use-class-filters.ts | 30 | 按学校筛选年级 hook(P1-E 抽取) | **路由文件清单**(`app/(dashboard)/`,2026-06-26 审计修复新增错误边界 + 加载骨架): | 文件 | 职责 | |------|------| | `teacher/classes/my/error.tsx` | 教师我的班级错误边界(使用 shared `ErrorState` + namespace="classes") | | `teacher/classes/my/[id]/error.tsx` | 班级详情错误边界(使用 shared `ErrorState`) | | `teacher/classes/my/[id]/loading.tsx` | 班级详情加载骨架(ClassGridSkeleton) | | `teacher/classes/students/error.tsx` | 学生管理错误边界(使用 shared `ErrorState`) | | `teacher/classes/schedule/error.tsx` | 课表错误边界(使用 shared `ErrorState`) | | `student/learning/error.tsx` | 学生学习中心错误边界(使用 shared `ErrorState`) | | `student/learning/loading.tsx` | 学生学习中心加载骨架 | | `student/learning/courses/error.tsx` | 学生课程列表错误边界(使用 shared `ErrorState`) | | `student/learning/courses/[classId]/error.tsx` | 学生课程详情错误边界(使用 shared `ErrorState`) | | `student/schedule/error.tsx` | 学生课表错误边界(使用 shared `ErrorState`) | --- ## 2.8 school(学校模块) **职责**:学校/学年/部门/年级的 CRUD。 **导出函数**: - Actions:`createSchoolAction` / `updateSchoolAction` / `deleteSchoolAction` / `createAcademicYearAction` / `updateAcademicYearAction` / `deleteAcademicYearAction` / `createDepartmentAction` / `updateDepartmentAction` / `deleteDepartmentAction` / `createGradeAction` / `updateGradeAction` / `deleteGradeAction` / `promoteGradesAction`(编排层:权限校验 + Zod 校验 + 调用 data-access + revalidatePath + after(logAudit);`promoteGradesAction` 年级升级,审计日志 `grade.promote`) - Data-access:只读查询(`getSchools` / `getGrades` / `getDepartments` / `getAcademicYears` / `getStaffOptions` / `getGradesForStaff` / `getGradesByIds`(✅ G3-007 / S-03 新增:按 ID 列表批量获取年级,供 lib/school-permissions teacher 分支使用)/ `getOrgTree` / `getSubjectOptions` / `getGradeOptions` / `getSubjectNameMapByIds`(P1-1 新增:批量科目名称映射,供 homework/data-access-classes 调用)/ `getGradeOverviewStats`(✅ v4-P2-6 新增:年级概览统计,返回 `GradeOverviewStats[]`,每个年级的 classCount/studentCount/teacherCount,供年级管理卡片视图使用,动态导入 classes/data-access 避免循环依赖))+ 写操作(`create/update/delete` × `Department/School/Grade/AcademicYear`)+ `promoteGrades(schoolId)` 年级升级(order +1 + 名称升级,辅助函数 `promoteGradeName`;✅ G3-012 / F-09:使用 db.transaction 包裹循环 UPDATE) - Data-access 文件结构(✅ G3-007 / S-01 修复:从 938 行单文件拆分为 barrel + 6 个子文件):`data-access.ts`(barrel)→ `data-access-departments.ts` / `data-access-grades.ts` / `data-access-subjects.ts` / `data-access-classrooms.ts` / `data-access-semesters.ts` / `data-access-schools.ts` - Lib(✅ G3-011 / A-02 新增):`lib/school-permissions.ts` 包含 `getSchoolsForUser(userId)` / `getGradesForUser(userId)` 角色感知查询(admin/grade_head/teaching_head/teacher 分支),从 data-access 层迁出 - Types(✅ v4-P2-6 新增):`GradeOverviewStats`(年级概览统计接口:{ gradeId, classCount, studentCount, teacherCount },位于 data-access-grades.ts) - Components:`SchoolsClient` / `SchoolFormDialog` / `SchoolDeleteDialog` / `SchoolListToolbar` / `SchoolErrorBoundary` / `SchoolListSkeleton` / `SchoolCardSkeleton` / `OrgTreeNav` / `GradesClient`(✅ v4-P2-6 更新:新增 `gradeStats: GradeOverviewStats[]` prop,渲染年级概览卡片视图——每个年级卡片展示班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口)/ `GradeInsightsFilters`(✅ v4-P2-6 新增:年级洞察筛选器,使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新)/ `GradeDistributionPanel` / `GradeHomeworkPanel` / `GradeExamsPanel` / `GradeProgressPanel`(✅ v4-P2-7 新增:年级仪表盘 4 个维度面板组件,位于 `components/grade-dashboard/` 子目录,服务端组件直接渲染数据,无客户端交互) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`users`(⚠️ `getStaffOptions` 直查 users/roles,可接受) - 被依赖:`exams`(✅ P1-1 已修复:通过 school data-access)、`homework`(✅ P1-1 已修复:通过 school data-access)、`grades`(✅ P1-1 已修复:通过 school data-access)、`questions`(✅ P1-1 已修复:通过 textbooks data-access)、`classes`(✅ P1-1 已修复:通过 school data-access)、`course-plans`(合理) **已知问题**: - ✅ P0-8 已修复:`actions.ts` 不再直接导入 `db` 和 schema,所有 DB 写操作下沉到 `data-access.ts`,符合三层架构 - ✅ P2 已修复:`logAudit()` 通过 Next.js `after()` 异步非阻塞执行 - ✅ G3-010 / A-10 修复(2026-07-07):~~`data-access.ts` 中 8 处 catch 块添加 `console.error` 输出错误上下文~~ 改为移除所有 try-catch 错误吞没 + log.error,让错误向上抛出(data-access 层不应吞没错误) - ✅ G3-007 / S-01 修复(2026-07-07):~~`data-access.ts` 938 行超过 800 行上限~~ 拆分为 6 个按职责划分的子文件 + 1 个 lib(school-permissions),单文件均 < 460 行 - ✅ G3-011 / A-02 修复(2026-07-07):~~角色判断逻辑(getSchoolsForUser / getGradesForUser)混在 data-access 层~~ 迁出至 `lib/school-permissions.ts`,data-access 层只接受显式参数 - ✅ G3-012 / F-09 修复(2026-07-07):~~`promoteGrades` 循环 UPDATE 无事务~~ 改用 `db.transaction(async (tx) => {...})` 包裹整个循环,保证原子性 - ✅ S-03 修复(2026-07-07):抽取 `fetchGradesWithHeads` helper,消除 getGrades / getGradesForStaff / getGradesForUser 三处重复的"查询年级+主任用户+映射"代码 - ⚠️ P2:审计日志不一致(仅 school 实体记录,department/academicYear/grade 未记录) - ⚠️ P2:`getStaffOptions`/`getGrades` 直查 users/roles(展示用,可接受) - ✅ P0-2 修复(2026-06-22):~~年级 CRUD 逻辑与 `grade-management` 模块重复定义~~ `grade-management` 死模块已删除,年级 CRUD 统一由 school 模块负责 - ✅ P0-5 修复(2026-06-22):~~`school/components/*` 4 个组件缺少 i18n~~ 全部 4 个组件(schools-view/grades-view/departments-view/academic-year-view)已接入 `useTranslations("school")`;`school.json` i18n 文件已创建并扩充 - ✅ P1-3 修复(2026-06-22):新增 school-error-boundary.tsx(class component Error Boundary + i18n + router.refresh 重试)和 school-skeleton.tsx(SchoolListSkeleton 表格骨架 + SchoolCardSkeleton 卡片骨架);4 个页面(schools/grades/departments/academic-year)均已包裹 SchoolErrorBoundary;school.json 补充 errors.boundary.* 翻译键 - ✅ P1-5 修复(2026-06-22):~~`schools-view.tsx` 硬编码 Table+Dialog+AlertDialog~~ 拆分为组合模式:SchoolListToolbar + SchoolFormDialog + SchoolDeleteDialog + useSchoolData hook - ✅ P1-6 修复(2026-06-22):新增 `getSchoolsForUser(userId)` / `getGradesForUser(userId)` 权限感知查询函数,根据用户角色返回可见数据范围 - ✅ P2-1 修复(2026-06-22):抽取 `use-school-data` hook,将对话框状态管理逻辑与 UI 分离 - ✅ P2-2 修复(2026-06-23):新增 `getOrgTree()` data-access 函数(学校→年级→班级三级树,通过 `import("@/modules/classes/data-access")` 动态导入获取班级数据避免循环依赖)+ `OrgTreeNode` 类型 + `OrgTreeNav` 组件(三级树形导航,支持搜索过滤/选中高亮/展开折叠/不同节点类型图标);school.json 补充 `orgTree.*` 翻译键 - ✅ P2-3 修复(2026-06-23):新增 `promoteGradesAction` + `promoteGrades(schoolId)` + `promoteGradeName(name)` 辅助函数(中文数字 一→二…十二、阿拉伯数字 1→2…12 识别),按 order 降序逐条 +1 避免唯一约束冲突;含 `after(() => logAudit({ action: "grade.promote" }))` 审计日志 - ✅ P2-4 修复(2026-06-23):新增 `bulkEnrollStudentsAction`(CSV 批量导入学生,复用 enrollStudentByEmail)+ `bulkAssignSubjectTeachersAction`(CSV 批量分配教师,简化实现含 TODO 待完善查找逻辑);classes/actions.ts barrel 导出 - ✅ P2-5 修复(2026-06-23):为 department/academicYear/grade 的 9 个 CRUD Action 补充 `after(() => logAudit(...))` 审计日志(action: department.create/update/delete、academicYear.create/update/delete、grade.create/update/delete),与 school 实体审计日志策略一致 - ✅ v4-P2-6 改进(2026-06-23):年级管理体验增强——新增 `getGradeOverviewStats()` data-access 函数(返回 `GradeOverviewStats[]`:每个年级的 classCount/studentCount/teacherCount,动态导入 classes/data-access 避免循环依赖);`GradesClient` 新增 `gradeStats` prop,渲染年级概览卡片视图(班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口);admin/school/grades/page.tsx 新增 `getGradeOverviewStats()` 查询并传入 GradesClient - ✅ v4-P2-6 改进(2026-06-23):新增 `GradeInsightsFilters` 组件(使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新);admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写,使用 `GradeInsightsFilters` 替代原生 form get,添加 i18n,表格添加 `overflow-x-auto` 水平滚动 - ✅ P1-A 修复(2026-06-24):~~`grades-view.tsx` 860 行超出组件行数限制~~ 拆分为组合模式:`GradeFormDialog`(创建/编辑表单对话框,含客户端校验+去重+isDirty 检测)+ `GradeDeleteDialog`(删除确认)+ `GradeListToolbar`(搜索/筛选/排序工具栏)+ `GradeOverviewCards`(年级概览卡片视图)+ `useGradeData` hook(对话框状态管理);主容器 `grades-view.tsx` 降至 290 行,参照 `schools-view.tsx` 组合模式 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 457 | 13 个 Server Action(编排层,无 DB 直访;含 `promoteGradesAction` 年级升级) | | `data-access.ts` | 33 | barrel 重新导出(G3-007 / S-01 修复:从 938 行拆分为 6 个子文件 + 1 个 lib) | | `data-access-departments.ts` | 65 | 部门 CRUD(G3-007 / S-01 拆分;G3-010 / A-10 移除 try-catch 错误吞没) | | `data-access-grades.ts` | 452 | 年级 CRUD + promoteGrades(G3-012 / F-09 事务包裹)+ 跨模块年级接口 + 年级概览统计 + fetchGradesWithHeads helper(S-03 抽取)+ getGradesByIds(新增供 school-permissions 使用) | | `data-access-subjects.ts` | 98 | 科目只读接口(跨模块:getSubjectOptions / getSubjectNameById / getSubjectNameMapByIds) | | `data-access-classrooms.ts` | 42 | 教室只读接口(跨模块:getClassroomsForScheduling 供排课使用) | | `data-access-semesters.ts` | 86 | 学年 CRUD(createAcademicYear / updateAcademicYear 已用 db.transaction) | | `data-access-schools.ts` | 161 | 学校 CRUD + getStaffOptions 员工选项 + getOrgTree 组织架构树 | | `lib/school-permissions.ts` | 145 | 角色感知查询(G3-011 / A-02 迁出:getSchoolsForUser / getGradesForUser,根据用户角色返回可见数据范围) | | `schema.ts` | 56 | Zod 校验(含 `PromoteGradesSchema`) | | `types.ts` | 103 | 类型定义(含 Insert/Update 入参类型 + `OrgTreeNode`) | | components/schools-view.tsx | 132 | 学校列表容器(组合模式,P1-5 修复) | | components/school-form-dialog.tsx | 80 | 学校创建/编辑对话框(P1-5 修复) | | components/school-delete-dialog.tsx | 50 | 学校删除确认对话框(P1-5 修复) | | components/school-list-toolbar.tsx | 30 | 学校列表工具栏(P1-5 修复) | | components/school-error-boundary.tsx | 72 | 共享 Error Boundary(P1-3 修复) | | components/school-skeleton.tsx | 69 | 共享骨架屏(P1-3 修复) | | components/org-tree-nav.tsx | 134 | 学校→年级→班级三级树形导航(P2-2 修复:搜索过滤 + 选中高亮 + 展开折叠 + 节点类型图标) | | components/grades-view.tsx | 290 | 年级管理客户端(P1-A 修复:拆分为组合模式,仅负责列表渲染+筛选+组合子组件) | | components/grade-form-dialog.tsx | 280 | 年级创建/编辑表单对话框(P1-A 新增:含客户端校验+去重+isDirty 检测,useActionMutation 管理 mutation) | | components/grade-delete-dialog.tsx | 65 | 年级删除确认对话框(P1-A 新增) | | components/grade-list-toolbar.tsx | 115 | 年级列表工具栏(P1-A 新增:搜索+学校筛选+主任筛选+排序+新建按钮) | | components/grade-overview-cards.tsx | 165 | 年级概览卡片视图(P1-A 新增:班级/学生/教师数+年级主任/教学主任+快捷操作) | | components/grade-insights-filters.tsx | 48 | v4-P2-6 新增:年级洞察筛选器(ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新) | | hooks/use-school-data.ts | 40 | 学校数据管理 hook(P2-1 修复) | | hooks/use-grade-data.ts | 42 | 年级数据管理 hook(P1-A 新增:对话框状态管理,参照 use-school-data 模式) | --- ## 2.8b grade-management(年级管理模块)— ✅ 已删除 > **2026-06-22 审计发现**:该模块拥有完整的理想架构(Service 接口 + Context DI + 角色配置 + Error Boundary + Skeleton + i18n + hooks 分离),但 **13 个相关页面中无任何一个导入此模块**。`management/grade/*` 页面实际依赖 `classes` 和 `school` 模块的 data-access。 > > **2026-06-22 处置决策(P0-1/P0-2 修复)**:该死模块已**完整删除**。年级 CRUD 统一由 `school` 模块负责(`school/actions.ts` + `school/data-access.ts`),避免两套重复实现。详见 `docs/architecture/audit/school-grade-class-audit-report.md`。 --- ## 2.9 scheduling(排课模块) **职责**:自动排课算法 + 课表调整 + 排课规则管理。 **导出函数**: - Actions:`autoScheduleAction` / `applyAutoScheduleAction` / `getSchedulingRulesAction` / `updateSchedulingRulesAction` / `getScheduleChangesAction` / `createScheduleChangeAction` / `updateScheduleChangeAction` / `deleteScheduleChangeAction` - Data-access:`getSchedulingRules` / `getScheduleChanges` / `getAdminClassesForScheduling` / `getTeachersForScheduling` / `getClassroomsForScheduling` / `getClassSubjectsForScheduling` - Data-access-class-schedule(✅ P0-5 新增):`createClassScheduleItem` / `updateClassScheduleItem` / `deleteClassScheduleItem`(从 classes 模块迁移,含教师班级归属校验,通过 `classes/data-access.verifyTeacherOwnsClass` 跨模块校验) - Data-access(低级写入):`insertClassScheduleItem` / `updateClassScheduleItemById` / `deleteClassScheduleItemById` / `replaceClassSchedule`(统一 classSchedule DB 写入口) - 算法:`findOptimalSlot` / `validateSchedule` / `autoSchedule` / `buildDefaultTimeSlots`(纯函数,标杆) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`classes`(✅ P0-5 已修复:通过 `classes/data-access.verifyTeacherOwnsClass` / `getTeacherIdForMutations` 校验教师班级归属,不再直写 classSchedule 表的写入口分散在 classes 模块)、`school`(⚠️ 排课辅助查询,可接受)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds) - 被依赖:`classes/actions.ts`(✅ P0-5 已修复:通过 `scheduling/data-access-class-schedule` 调用写函数) **已知问题**: - ✅ P0-5 已修复:~~`applyAutoScheduleAction` 直接 transaction 写 `classSchedule` 表(第三个写入口)~~ 改为调用 `replaceClassSchedule` 统一写入口;classSchedule 所有写函数统一在 scheduling 模块 - ✅ P1-1 已修复:~~`autoScheduleAction` 直查 `users` 表~~ 改为调用 `users/data-access.getUserNamesByIds` - ⚠️ P2:`actions.ts` 末尾 re-export data-access 函数(反模式) - ✅ P2 已修复:`data-access.ts` 中 3 处非空断言清理(`userIds[0]!`、`rows[i]!`、`rows[j]!`);`auto-scheduler.ts` 中 2 处非空断言清理(`schedule[i]!`、`schedule[j]!`) - ✅ `auto-scheduler.ts` 是算法独立化的最佳实践(纯函数、无 DB、可测试) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `auto-scheduler.ts` | 310 | 排课算法(纯函数,标杆) | | `actions.ts` | 302 | 8 个 Server Action | | `data-access.ts` | 398 | 排课辅助查询 + 规则/变更 CRUD + classSchedule 低级写入(insert/update/delete/replace) | | `data-access-class-schedule.ts` | 165 | classSchedule 业务写入(P0-5 新增,从 classes 模块迁移,含教师归属校验;A-02 修复:字段校验已抽离至 lib/schedule-validation.ts,仅保留 DB 归属校验 + CRUD) | | `lib/schedule-validation.ts` | 173 | 课表项纯校验函数(A-02 新增:isTimeHHMM/isValidWeekday/isTimeRangeValid/normalizeLocation/normalizeAndValidateCreateInput/buildValidatedScheduleUpdate) | | `schema.ts` | - | Zod 校验 | | `types.ts` | - | 类型定义(含 P0-5 迁移的 CreateClassScheduleItemInput / UpdateClassScheduleItemInput) | --- ## 2.10 attendance(考勤模块)— 结构典范 **职责**:考勤记录管理 + 统计分析 + 规则配置。✅ L-6 节次考勤:支持按节次(早读/上午/下午/晚自习/全日)记录与过滤。✅ L-9 关联分析: 考勤与成绩跨模块关联分析(Pearson 相关系数 + 风险分级)。 **导出函数**: - Actions(5 个写 Action):`recordAttendanceAction`(✅ L-6:formData 读取 period)/ `batchRecordAttendanceAction`(✅ L-2:保存成功后调用 `notifyParentsOfAbsence` 异步通知家长,失败不阻断主流程;✅ L-6:每条记录携带 period)/ `updateAttendanceAction`(✅ L-6:formData 读取 period)/ `deleteAttendanceAction` / `saveAttendanceRulesAction`(✅ 审计修复:均调用 `requirePermission(ATTENDANCE_MANAGE)` + 资源归属校验 + `trackEvent` 埋点) - Data-access:`getAttendanceRecords`(✅ 审计修复:移除跨模块 JOIN,委托 `getUserNamesByIds`/`getClassNamesByIds`;✅ L-6:支持按 period 过滤)/ `getClassAttendanceForDate`(✅ L-6:新增可选 period 参数)/ `buildPeriodFilter`(✅ L-6 新增:纯函数,full_day 匹配 NULL OR 'full_day' 向后兼容)/ `createAttendanceRecord`(✅ L-6:写入 period 默认 full_day)/ `batchCreateAttendanceRecords`(✅ L-6:批量写入 period)/ `updateAttendanceRecord`(✅ L-6:支持更新 period)/ `deleteAttendanceRecord` / `getClassStudentsForAttendance`(✅ 审计修复:委托 `classes/data-access.getClassActiveStudentsWithInfo`)/ `getAttendanceStats`(✅ 审计修复:SQL 聚合 + 返回统一 `AttendanceStats` 类型)/ `upsertAttendanceRules` / `getAttendanceRules` / `getAttendanceRecordClassId` / `syncExcusedFromLeaveRequest`(✅ L-5 新增:请假审批通过后遍历日期区间插入 excused 记录,幂等不覆盖已有记录;✅ L-6:插入时 period: full_day;被 `leave-requests/actions.reviewLeaveRequestAction` 调用) - Data-access-stats:`getStudentAttendanceSummary`(✅ 审计修复:SQL 聚合 + 委托 data-access;✅ L-6:recentRecords 映射 period)/ `getClassAttendanceStats`(✅ 审计修复:SQL 聚合 + 委托 data-access;✅ L-6:studentRecords 映射 period)/ `computeStats`(✅ 纯函数导出,可单测) - Notifications(✅ L-2 新增):`notifyParentsOfAbsence`(批量考勤录入后多渠道通知家长:in_app/sms/email/wechat,一次性批量查询 parent-student 映射避免 N+1) - Import-export:`exportAttendanceRecordsToExcel`(✅ 审计修复:类型守卫 + i18n 列头) - Services(✅ 审计新增 P1-12/P2-6):`AttendanceDataService` 接口 / `AttendanceReadService` / `AttendanceWriteService` / `AttendanceRepository` / `AttendanceProvider` / `useAttendanceService` / `createAttendanceDataService` / `createAttendanceReadService` - Components:`AttendanceSheet`(✅ 审计修复:AlertDialog + 常量统一 + 容器内 keydown + useMemo;✅ L-6:新增节次选择器,3 列布局,提交时携带 period)/ `AttendanceRecordList`(✅ 审计修复:`usePermission` 控制删除按钮 + `resolveActionError`;✅ L-6:新增节次列,null 视为 full_day 展示)/ `AttendanceFilters` / `AttendanceStatsCard` / `AttendanceStatsCards`(✅ 审计修复:统一 `AttendanceStats` 类型)/ `AttendanceStatsClassSelector` / `AttendanceRulesForm`(✅ 审计修复:`resolveActionError`)/ `StudentAttendanceView` / `AttendancePageLayout` - Data-access-correlation(✅ L-9 新增): `getAttendanceGradeCorrelation`(跨模块委托 classes/users/grades data-access 聚合数据) / `canAccessCorrelationAnalysis`(权限校验) - Correlation-compute(✅ L-9 新增纯函数, 与 IO 解耦便于单测): `computePearsonCorrelation` / `classifyRiskLevel` / `interpretCorrelation` / `computeCorrelationSummary` - Components L-9(✅ L-9 新增): `AttendanceGradeCorrelationCard`(散点图 + 风险表格, recharts ScatterChart 风险等级着色) **依赖关系**: - 依赖:`shared/*`(含 `shared/constants/attendance-status` ✅ 审计新增:状态类型与视觉常量下沉至 shared)、`@/auth`、`classes`(✅ 审计修复:通过 `classes/data-access` 获取班级学生/名称)、`users`(✅ 审计修复:通过 `users/data-access.getUserNamesByIds` 获取姓名)、`parent`(✅ L-2 新增:通过 `parent/data-access.getParentStudentMapByStudentIds` 获取家长-学生映射)、`notifications`(✅ L-2 新增:通过 `sendBatchNotifications` 多渠道分发)、`grades`(✅ L-9 新增:通过 `grades/data-access.getGradeRecords` 获取成绩数据用于关联分析)、`next-intl` - 被依赖:`parent`(✅ 审计修复:通过 `createAttendanceReadService` 接口消费,不再直接 import attendance 组件;常量改从 `shared/constants/attendance-status` 导入)、`app/api/export`(通过 `exportAttendanceRecordsToExcel`)、`leave-requests`(✅ L-5 新增:审批通过后通过 `syncExcusedFromLeaveRequest` 同步 excused 考勤记录) **L-6 节次考勤数据模型**: - DB:`attendanceRecords.period` enum('morning_reading','morning','afternoon','evening','full_day') nullable(向后兼容 NULL 视为 full_day) - 索引:`classDatePeriodIdx`(class_id + date + period 复合索引) - 迁移:`drizzle/0021_attendance_period.sql` - 类型:`AttendancePeriod`(types.ts),`AttendanceRecord`/`AttendanceListItem`/`AttendanceQueryParams` 新增 period 字段 - Schema:`AttendancePeriodEnum`(Zod),`RecordAttendanceSchema` 默认 full_day,`UpdateAttendanceSchema` 可选 period - i18n:`attendance.period.{label,morning_reading,morning,afternoon,evening,full_day,allPeriods}` 中英文翻译 - 过滤策略:`buildPeriodFilter` 对 full_day 使用 `or(isNull(period), eq(period, "full_day"))` 匹配旧数据 - 对外统一:mapListItem 中 `r.period ?? "full_day"`,前端展示 null 统一为 full_day **L-9 考勤与成绩关联分析**: - 类型: `AttendanceGradeRiskLevel`(high/medium/low) / `AttendanceGradeCorrelationItem` / `AttendanceGradeCorrelationSummary`(types.ts) - 纯函数(correlation-compute.ts): `computePearsonCorrelation`(Pearson r) / `classifyRiskLevel`(high=低出勤+低分, medium=中低出勤+中低分, low=其余) / `interpretCorrelation` / `computeCorrelationSummary` - 数据访问(data-access-correlation.ts): `getAttendanceGradeCorrelation` 跨模块委托 classes/users/grades data-access; `canAccessCorrelationAnalysis` scope 校验 - UI(attendance-grade-correlation-card.tsx): recharts ScatterChart 散点图, 风险等级着色(red/amber/emerald) - 集成: admin/attendance/page.tsx 指定班级时显示 - i18n: attendance.correlation.* 中英文翻译 **已知问题**(详见 `docs/architecture/audit/attendance-audit-report.md`): - ✅ P0-1 已修复:teacher/sheet、teacher/stats 页面增加 `requirePermission(ATTENDANCE_READ)` - ✅ P0-2 已修复:teacher 分页使用后端返回的 `totalPages` - ✅ P0-3 已修复:`safeParseDate` fieldName 改为中性 key,前端通过 `resolveActionError` 按 errorCode 本地化 - ✅ P0-4 已修复:error.tsx 重试按钮改用 `t("actions.retry")` - ✅ P0-5 已修复:data-access 移除跨模块 JOIN,改为委托 data-access - ✅ P1-1 已修复:teacher/student 主页面增加 `requirePermission(ATTENDANCE_READ)` - ✅ P1-2 已修复:parent 通过 `AttendanceReadService` 接口消费 - ✅ P1-3 已修复:parent-attendance-calendar 改从 `shared/constants/attendance-status` 导入 - ✅ P1-6 已修复:export.ts 类型守卫替代 `as` 断言 - ✅ P1-7 已修复:calendar 使用 `useLocale()` 替代硬编码 locale - ✅ P1-8 已修复:teacher 子路由新增 error.tsx - ✅ P1-9 已修复:常量统一从 `constants.ts`(re-export shared)导入 - ✅ P1-10 已修复:4 个 error.tsx 抽取为 shared `RouteErrorBoundary` - ✅ P1-11 已修复:`getClassAttendanceStats` 改用 SQL 聚合 - ✅ P1-12 已修复:新增 `AttendanceDataService` 接口 + Context 注入 - ✅ P2-1 已修复:删除按钮 `usePermission` 控制显隐 - ✅ P2-5 已修复:stats 数据结构统一为 `AttendanceStats` - ✅ P2-6 已修复:导出 `AttendanceRepository` 接口类型 - ✅ P2-7 已修复:keydown 限容器内 + 排除 Select/contenteditable - ✅ P2-8 已修复:parent-attendance-warning/rate-card 改为 RSC - ✅ P2-9 已修复:statusCounts 使用 `useMemo` - ✅ stats 独立拆分为 `data-access-stats.ts`(拆分范例) - ✅ DataScope 完整接入 6 种 scope 类型 - ✅ actions 层无直接 DB 访问 - ✅ Server Action 均有 `trackEvent` 埋点 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 258 | 5 个写 Server Action(权限校验 + 归属校验 + Zod + 埋点;✅ L-6 读取 period) | | `data-access.ts` | ~410 | 考勤 CRUD + 规则 upsert + 总览统计(SQL 聚合 + 委托 data-access;✅ L-6 period 过滤/写入/更新) | | `data-access-stats.ts` | ~230 | 学生/班级考勤汇总(SQL 聚合 + 委托 data-access;✅ L-6 period 映射) | | `export.ts` | 93 | Excel 导出(类型守卫 + i18n 列头) | | `schema.ts` | ~60 | Zod 校验(6 个 schema + ✅ L-6 AttendancePeriodEnum) | | `types.ts` | ~95 | 类型定义(状态常量已迁移至 shared;✅ L-6 AttendancePeriod + period 字段) | | `constants.ts` | 19 | 重导出 `shared/constants/attendance-status` | | `services/types.ts` | 166 | 服务接口抽象(AttendanceDataService/Repository) | | `services/attendance-context.tsx` | 46 | React Context 注入(AttendanceProvider + hook) | | `services/attendance-data-service.ts` | 162 | 服务实现 + 工厂函数 | | `components/attendance-sheet.tsx` | ~480 | 批量点名表单(AlertDialog + 容器内 keydown + useMemo;✅ L-6 节次选择器) | | `components/attendance-record-list.tsx` | ~165 | 记录列表 + 删除对话框(权限控制 + resolveActionError;✅ L-6 节次列) | | `components/attendance-filters.tsx` | 94 | URL 同步筛选器 | | `components/attendance-stats-card.tsx` | 82 | 单卡片统计(8 指标) | | `components/attendance-stats-cards.tsx` | 83 | 管理员总览 6 卡片网格(统一 AttendanceStats) | | `components/attendance-stats-class-selector.tsx` | 27 | 班级筛选 ChipNav | | `components/attendance-rules-form.tsx` | 155 | 规则配置表单(resolveActionError) | | `components/student-attendance-view.tsx` | 111 | 学生只读视图 | | `components/attendance-page-layout.tsx` | 38 | 页面布局骨架(插槽模式) | | `correlation-compute.ts` | ~90 | L-9 纯函数: Pearson 相关系数 + 风险分级 + 汇总 (与 IO 解耦) | | `data-access-correlation.ts` | ~130 | L-9 跨模块关联分析: 委托 classes/users/grades data-access | | `components/attendance-grade-correlation-card.tsx` | ~220 | L-9 散点图 + 风险表格 (recharts ScatterChart) | | `lib/type-guards.ts` | - | ✅ as 断言修复新增(2026-07-07):`isAttendancePeriod` 守卫,替代 components/attendance-sheet.tsx 中 Select `onValueChange` 的 `as AttendancePeriod` 断言 | > 架构变更(2026-07-05,Phase 4.5 — attendance-sheet.tsx 提交状态改用 useOptimistic): > - **attendance-sheet.tsx**:`isSubmitting` useState → `useOptimistic`。保留 `` 模式 + `useFormStatus` SubmitButton(progressive enhancement);`setOptimisticSubmitting(true)` 在 form action 内调用,action 完成后 `optimisticSubmitting` 自动恢复 false,移除 `finally` 块 > - **关键决策**:保留 form action 模式而非改用 `useTransition` 包裹,因为 `useFormStatus().pending` 依赖 form action 的 promise 状态,若用 `startTransition` 包裹会导致 form action 立即返回、`useFormStatus` 不可靠;`useOptimistic` 的 `addOptimistic` 必须在 transition 或 action 内调用(form action 也算 action) --- ## 2.11 users(用户模块) **职责**:用户资料管理 + 批量导入导出 + 管理员用户列表管理。 **导出函数**: - Actions:`getUserProfileAction` / `updateUserProfileAction` / `importUsersAction` / `exportUsersAction` / `downloadUserTemplateAction` / `updateUserRoleAction` / `deleteUserAction` - Data-access:`getUserProfile` / `getCurrentStudentUser`(✅ P2-20 已修复:从 homework 模块迁移而来,6 个 student 页面通过此函数获取学生身份,不再依赖 homework 模块)/ `getAdminUsers`(管理员用户列表分页查询,支持搜索+角色聚合)/ `getAdminUserRoles`(角色名列表,用于筛选下拉框)/ `getAllUserIds(limit=1000, offset=0)`(✅ P3-7:全校用户 ID 查询,默认 LIMIT 1000 防止 OOM + 分页参数支持)/ `getUserIdsByGradeId` / `getUserNamesByIds` - Import-export:`generateUserImportTemplate` / `parseUserImportData` / `exportUsersToExcel`(+ re-export `batchImportUsers` / `UserImportResult` 保持向后兼容) - User-service:`batchImportUsers`(用户创建 + 密码哈希 + 角色分配) - Class-registration:`registerStudentByInvitationCode`(委托 classes/data-access 完成班级注册) - Components:`UserImportDialog`(批量导入对话框)/ `AdminUsersView`(管理员用户列表客户端组件,搜索+筛选+分页+删除) **依赖关系**: - 依赖:`shared/*`(含 `shared/lib/role-utils`,✅ P2 已修复:删除本地 `normalizeRoleName`/`resolvePrimaryRole`/`rolePriority`,统一复用 `shared/lib/role-utils.resolvePrimaryRole`)、`@/auth`、`classes`(✅ P1-4 已修复:通过 `class-registration.ts` 调用 `classes/data-access.enrollStudentByInvitationCode`,不再直写 classEnrollments) - 被依赖:`dashboard`(通过 data-access,P0-4 已修复)、`grades`(✅ P1-1 已修复:通过 users data-access)、`homework`(✅ P1-1 已修复:通过 users data-access)、`scheduling`(✅ P1-1 已修复:通过 users data-access)、`diagnostic`(✅ P1-1 已修复:通过 users data-access)、`elective`(✅ P1-1 已修复:通过 users data-access)、`proctoring`(✅ P1-1 已修复:通过 users data-access)、`parent`(✅ P1-1 已修复:通过 users data-access) **已知问题**: - ✅ P1 已解决:`import-export.ts` 四重职责已拆分为 `import-export.ts`(解析/生成)+ `user-service.ts`(用户创建)+ `class-registration.ts`(班级注册) - ✅ P1 已解决:`batchImportUsers` 不再跨模块直写 `classEnrollments`,改为调用 `classes/data-access.enrollStudentByInvitationCode` - ✅ P2 已解决:删除本地 `normalizeRoleName`/`resolvePrimaryRole`/`rolePriority`,统一复用 `shared/lib/role-utils.resolvePrimaryRole`,消除重复代码 - ✅ P1-1 已修复:~~`updateUserProfile` 绕过 data-access 直接 DB 写~~ 已下沉到 data-access - ✅ P2-20 已修复:新增 `getCurrentStudentUser` 函数(从 homework 模块迁移),6 个 student 页面通过此函数获取学生身份,不再依赖 homework 模块 - ✅ P2 已解决:`data-access.ts` 已扩充写操作(updateUserProfile 已下沉) - ⚠️ 已知限制:`AdminUsersView` 客户端组件的删除操作通过 `fetch("/api/admin/users/:id")` 调用,对应 API 路由尚未实现(`deleteUserAction` Server Action 已就绪,可作为后续 API 路由的实现基础) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `import-export.ts` | 157 | 文件解析/生成(模板生成 + 解析校验 + Excel 导出)+ re-export 向后兼容 | | `user-service.ts` | 82 | 用户创建(批量导入 + 密码哈希 + 角色分配) | | `class-registration.ts` | 21 | 班级注册(委托 classes/data-access) | | `actions.ts` | 218 | 7 个 Server Action(profile 更新 + 模板下载/导入/导出 + 角色更新 + 删除) | | `data-access.ts` | 394 | getUserProfile + 用户查询 + 管理员用户列表分页查询(getAdminUsers/getAdminUserRoles) | | `components/admin-users-view.tsx` | 290 | 管理员用户列表客户端组件(搜索+筛选+表格+分页+删除对话框) | --- ## 2.12 dashboard(仪表盘模块) **职责**:管理员/教师/学生/家长仪表盘数据聚合 + 权限校验 + i18n + 纯逻辑工具函数 + 数据服务接口抽象 + 配置驱动 Widget 渲染 + 监控埋点接口 + 时间范围筛选 + 数据对比 + 自定义仪表盘 + 通知集成 + 实时更新 + 数据导出 + 移动端优化。 **导出函数**: - Actions:`getAdminDashboardAction` / `getTeacherDashboardAction` / `getStudentDashboardAction` / `getParentDashboardAction`(均调用 `requirePermission()` 校验对应 `DASHBOARD_*_READ` 权限) - Data-access:`getAdminDashboardData`(并行调用 6 个模块 stats 函数,V4 添加趋势数据 TODO 注释) - Streams:`getAdminDashboardStreams`(V4 新增:返回各分区独立 Promise 供 React `use()` 流式消费) - Lib 纯函数:`toWeekday` / `countStudentAssignments` / `sortUpcomingAssignments` / `filterTodaySchedule`(V4 修复:移除 `as T[]` 断言,改为非泛型函数)/ `computeTeacherMetrics` / `getGreetingKey` / `timeToMinutes`(V4 新增)/ `getScheduleStatus`(V4 新增)/ `getDueUrgency`(V4 新增)/ `getActionLabelKey`(V4 新增)/ `getActionVariant`(V4 新增)/ `computeComparison`(L3 新增:数据对比计算)/ `toCSV`(L7 新增:CSV 导出)/ `downloadFile`(L7 新增:浏览器下载)/ `exportDashboardCSV`(L7 新增:仪表盘 CSV 导出便捷方法) - Services(V4 新增):`DashboardService` 接口(抽象数据依赖,含 4 个角色方法签名)/ `DashboardAnalytics` 接口(监控埋点:Widget 点击 / 空状态 / 错误重试 / 页面停留)/ `DashboardServiceProvider`(React Context 依赖注入)/ `useDashboardService` / `useDashboardAnalytics` - Config(V4 新增):`ADMIN_WIDGET_CONFIG` / `TEACHER_WIDGET_CONFIG` / `STUDENT_WIDGET_CONFIG` / `PARENT_WIDGET_CONFIG` / `getWidgetConfig`(配置驱动 Widget 布局,新增角色只需修改配置) - Hooks(L4/L6 新增):`useDashboardPreferences`(L4 自定义仪表盘偏好,localStorage 持久化)/ `useDashboardRealtime`(L6 SSE 实时更新,指数退避重连) - Components:`AdminDashboardView` / `TeacherDashboardView` / `StudentDashboard` / `ParentDashboard`(V4 P1-4 从 parent 模块迁移,使用 slots 组合注入)/ `UserGrowthChart` / `DashboardGreetingHeader`(共享问候头部,V2 抽象,V4 支持空用户名 fallback)/ `DashboardSection`(分区 Error Boundary + Suspense + 骨架屏,V4 P3-1 新增 ariaLabel prop 支持键盘导航)/ `DashboardTimeRangeFilter`(L2 时间范围筛选)/ `ComparisonBadge`(L3 数据对比徽章)/ `DashboardNotificationWidget`(L5 通知中心 Widget)/ `DashboardResponsiveLayout`(L8 移动端响应式布局)/ `MobileSwipeContainer`(L8 移动端水平滑动)/ `DesktopGrid`(L8 桌面端网格) - Types(V4 新增):`DashboardRole` / `WidgetSkeletonVariant` / `DashboardWidgetConfig` / `DashboardLayoutConfig` / `ComparisonTrend` / `ComparisonResult`(L3)/ `TimeRange`(L2)/ `ConnectionStatus`(L6)/ `DashboardNotificationItem`(L5)/ `ExportRow` / `ExportColumn`(L7) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`classes`(通过 data-access)、`homework`(通过 data-access)、`users`(通过 data-access)、`parent`(通过 data-access.getParentDashboardData + 组件 slots 注入 ChildCard/ParentAttentionBanner)、`textbooks`/`questions`/`exams`(通过各模块 dashboard stats 函数)、`recharts`、`next-intl` - 被依赖:无 **权限点**: - `DASHBOARD_ADMIN_READ`(admin) - `DASHBOARD_TEACHER_READ`(teacher) - `DASHBOARD_STUDENT_READ`(student) - `DASHBOARD_PARENT_READ`(parent) **已知问题**: - ✅ P0-4 已修复:`getAdminDashboardData` 改为并行调用各模块 dashboard stats 函数,不再直查跨模块表 - ✅ P0 已修复(2026-06-22):所有仪表盘页面通过 `actions.ts` 调用 `requirePermission()` 进行权限校验,不再裸调 data-access - ✅ P0 已修复(2026-06-22):根重定向页 `/dashboard` 改用 `resolvePermissions()` + 权限点判断,不再 `role === "xxx"` 硬编码 - ✅ P0 已修复(2026-06-22):所有仪表盘组件接入 next-intl(`useTranslations` / `getTranslations`),翻译文件 `messages/{zh-CN,en}/dashboard.json` - ✅ P1 已修复(2026-06-22):业务逻辑(weekday 转换、作业统计、教师指标计算、问候语时段)抽取至 `lib/dashboard-utils.ts` 纯函数,与 UI 分离 - ✅ P2 已修复(2026-06-22):新增 `components/dashboard-section.tsx`,每个独立数据区块用 Error Boundary + Suspense + 骨架屏包裹,单区块崩溃/加载不波及整页(5 种骨架变体:stats/card/chart/table/list) - ℹ️ V1 新增:`AdminDashboardData` 类型含 `userGrowth`/`homeworkTrend` 字段,`data-access.ts` 当前返回空数组占位(V4 P2-4 添加 TODO 注释),待后续接入真实统计 - ✅ V4 P1-4 已修复:`ParentDashboard` 组件从 `modules/parent/components/` 迁移至 `modules/dashboard/components/parent-dashboard/`,使用 slots 组合注入 ChildCard 和 ParentAttentionBanner,parent 模块仅保留数据访问 - ✅ P0 已修复(2026-06-22 V2):10 个子组件 i18n 遗漏全部补齐,新增 ~50 个翻译键 - ✅ P1 已修复(2026-06-22 V2):抽象共享组件 `DashboardGreetingHeader`,消除 teacher/student 头部 90% 重复代码 - ✅ P2 已修复(2026-06-22 V2):为 6 个纯函数添加 31 个单元测试 - ✅ P2 已修复(2026-06-22 V2):a11y 增强 — admin 表格 ``、teacher/student 视图语义化标签 - ✅ P0 已修复(2026-06-22 V4):`filterTodaySchedule` 移除 `as T[]` 类型断言,改为非泛型函数 - ✅ P1 已修复(2026-06-22 V4):5 个组件内嵌纯函数抽取至 `lib/dashboard-utils.ts` - ✅ P1 已修复(2026-06-22 V4):`teacherName` 硬编码 "Teacher" fallback 改为空字符串 - ✅ P1 已修复(2026-06-22 V4):`teacher-schedule.tsx` 移除本地类型定义,从 `types.ts` 导入 - ✅ P2 已修复(2026-06-22 V4):新增 `services/dashboard-service.tsx`,定义 `DashboardService` 接口 + `DashboardAnalytics` 监控埋点接口 + React Context 依赖注入 - ✅ P2 已修复(2026-06-22 V4):新增 `config/widget-configs.ts`,4 个角色的 Widget 布局配置,支持配置驱动渲染 - ✅ V4 P2-4 已修复:admin 趋势数据占位添加 TODO 注释,标记后续实现 - ✅ V4 P2-5 已修复:4 个角色 StatCard 使用模式统一(均传 color + valueClassName="tabular-nums") - ✅ V4 P3-1 已修复:`DashboardSection` 新增 `ariaLabel` prop,渲染 `
`,admin/teacher/student 视图全部接入 - ✅ V4 P3-2 已修复:`AdminTrendCharts` 添加 TODO 注释标记趋势数据待接入 - ✅ V4 P3-3 已修复:`TeacherTodoCard` 排序逻辑改用 `VARIANT_PRIORITY` 数值映射,提升可读性 - ✅ L1 已实施:Widget 下钻导航 — admin StatCard 添加 href 跳转,ContentRow 支持可选 href 下钻 - ✅ L2 已实施:时间范围筛选 — 新增 `DashboardTimeRangeFilter` 客户端组件,URL search param 持久化,集成到 admin dashboard - ✅ L3 已实施:数据对比 — 新增 `ComparisonBadge` 组件 + `computeComparison` 纯函数 - ✅ L4 已实施:自定义仪表盘 — 新增 `useDashboardPreferences` Hook,localStorage 持久化 Widget 显示/隐藏偏好 - ✅ L5 已实施:通知中心集成 — 新增 `DashboardNotificationWidget` 组件 - ✅ L6 已实施:实时更新 — 新增 `useDashboardRealtime` Hook(SSE + 指数退避重连) - ✅ L7 已实施:数据导出 — 新增 `lib/export-utils.ts`(CSV 导出 + 浏览器下载) - ✅ L8 已实施:移动端优化 — 新增 `DashboardResponsiveLayout` / `MobileSwipeContainer` / `DesktopGrid` 组件 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 167 | 4 个 Server Action(编排层,含 `requirePermission()` 权限校验) | | `data-access.ts` | 51 | admin 仪表盘数据聚合(并行调用各模块 stats 函数,V4 添加趋势数据 TODO) | | `streams.ts` | 34 | admin 流式数据源(返回未解析 Promise 供 React `use()` 消费) | | `lib/dashboard-utils.ts` | 279 | 纯逻辑工具函数(weekday / 统计 / 排序 / 指标计算 / 问候语 / 课表状态 / 作业紧急度 / 操作按钮 V4 新增 5 个函数) | | `lib/export-utils.ts` | 73 | L7 新增:CSV 导出工具(toCSV / downloadFile / exportDashboardCSV) | | `types.ts` | 107 | Admin / Teacher / Student 类型定义 + V4 新增 Widget 配置类型 | | `services/dashboard-service.tsx` | 110 | V4 新增:DashboardService 接口 + DashboardAnalytics 接口 + React Context 依赖注入 | | `config/widget-configs.ts` | 62 | V4 新增:4 个角色的 Widget 布局配置(配置驱动渲染) | | `hooks/use-dashboard-preferences.ts` | 82 | L4 新增:自定义仪表盘偏好 Hook(localStorage 持久化) | | `hooks/use-dashboard-realtime.ts` | 73 | L6 新增:SSE 实时更新 Hook(指数退避重连) | | `components/dashboard-section.tsx` | 186 | 分区 Error Boundary + Suspense + 骨架屏(V4 P3-1 新增 ariaLabel prop) | | `components/dashboard-greeting-header.tsx` | 36 | 共享问候头部组件(V2 抽象,V4 支持空用户名 fallback) | | `components/dashboard-time-range-filter.tsx` | 57 | L2 新增:时间范围筛选器(URL search param 持久化) | | `components/comparison-badge.tsx` | 75 | L3 新增:数据对比徽章 + computeComparison 纯函数 | | `components/dashboard-notification-widget.tsx` | 85 | L5 新增:通知中心 Widget | | `components/dashboard-responsive-layout.tsx` | 72 | L8 新增:移动端响应式布局组件 | | `components/parent-dashboard/parent-dashboard.tsx` | 115 | V4 P1-4 迁移:家长仪表盘视图(slots 组合注入) | | `components/admin-dashboard/admin-dashboard.tsx` | 180 | 管理员仪表盘视图(V4 接入时间范围筛选 + ariaLabel) | | `components/admin-dashboard/admin-sections.tsx` | 245 | 管理员 6 个分区组件(V4 StatCard 下钻 + ContentRow href) | | `components/admin-dashboard/user-growth-chart.tsx` | 65 | recharts 折线图(i18n) | | `components/teacher-dashboard/*.tsx` | 9 文件 | 教师仪表盘组件(V4 修复 teacher-schedule 类型导入 + TeacherTodoCard 排序优化 + ariaLabel) | | `components/student-dashboard/*.tsx` | 6 文件 | 学生仪表盘组件(V4 抽取纯函数 + ariaLabel) | | `tests/integration/dashboard/dashboard-utils.test.ts` | 408 | 6 个纯函数的 31 个单元测试(V2 新增) | --- ## 2.13 messaging(私信模块) **职责**:站内私信(messages 表 CRUD)+ 消息草稿(message_drafts 表 CRUD)。 **导出函数**: - Actions:`sendMessageAction`(✅ 审计 V1-P0-1:新增 `isReceiverAllowed` 二次校验收件人是否在 sender DataScope 内 + ✅ 审计 V1-P0-6:校验 `parentMessageId` 属于当前用户会话 + ✅ 审计 V1-P1-9:失败埋点 `message.send_failed`)/ `markMessageAsReadAction` / `deleteMessageAction` / `getMessagesAction`(✅ 审计 V1:参数新增 `starredOnly?: boolean`)/ `getRecipientsAction` / `getUnreadMessageCountAction`(✅ P1-4 已修复:通知 CRUD Action 已迁移至 notifications 模块;✅ V3-P1-3 已修复:通知偏好 Action 已迁移至 notifications 模块,messaging 仅保留私信 Action;✅ V2-P2-13c 新增:`toggleMessageStarAction` 星标切换 / `getMessageDraftsAction` 草稿列表 / `saveMessageDraftAction` 草稿保存(✅ V3-P0-1:使用 `MessageDraftSchema` Zod 校验替代 `as` 断言)/ `deleteMessageDraftAction` 草稿删除;✅ 审计 V1-P0-4 已删除:`getMessageDetailAction`(V2-P1-3 重构遗留死代码,详情数据由 RSC 通过 `getMessageDetailPageData` 直接获取);✅ 审计 V1-P1-3 新增:`getMessageThreadAction` 获取消息线程(替代组件直接调用 data-access,统一权限边界)) - Data-access:`getMessages`(✅ V2-P2-13c 新增 starredOnly 星标筛选)/ `getMessageById` / `getMessageThread`(✅ 审计 V1-P0-2:签名变更为 `(messageId, userId)`,内部校验当前用户对根消息的访问权)/ `createMessage` / `markMessageAsRead` / `deleteMessage` / `toggleMessageStar`(✅ V2-P2-13c 新增:接收方星标切换)/ `getUnreadMessageCount` / `getRecipients`(✅ 审计 V1-P1-6:返回 `role` 收紧为 `RecipientRole` 类型,admin 分支补 role)/ `isReceiverAllowed`(✅ 审计 V1-P0-1 新增:校验收件人是否在 sender DataScope 内)/ `getMessageDetailPageData`(✅ V2-P1-3 新增:消息详情页编排函数,获取详情并自动标记已读;✅ 审计 V1-P0-3:`getMessagesPageData` 已删除,编排迁回 messages/page.tsx 页面层 `Promise.all` 调用 messaging + notifications data-access,保持模块独立性)/ `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`(✅ V2-P2-13c 新增:消息草稿 CRUD,操作 message_drafts 表) - Hooks:`useMessageSearch`(✅ P1-7 新增:消息搜索 hook,含防抖和请求竞态取消) - Lib:`buildReplyHref`(✅ 审计 V1-P1-7 新增:构建回复消息 URL 纯函数,使用 URLSearchParams 安全编码,替代组件内字符串拼接) - Notification-preferences:~~re-export shim(实际逻辑在 `notifications/preferences.ts`)~~ ✅ P0-b 已修复:`notification-preferences.ts` 文件已删除(通知模块去重),消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences` **依赖关系**: - 依赖:`shared/*`、`@/auth`、`notifications`(✅ P0-4 / P1-5 已修复:通过 `sendNotification` dispatcher 发送通知,通知 CRUD 和偏好已迁移至 notifications 模块;✅ P1-4 已修复:通知 UI 组件已迁移至 notifications 模块;✅ 审计 V1-P0-3 已修复:data-access 不再动态 import notifications,编排迁回页面层)、`classes`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID,支持学生 class_members 和家长 children 数据范围)、`users`(通过 data-access.getUserNamesByIds 获取用户显示名称) - 被依赖:`notifications`(✅ 已消除反向依赖)、`settings`(通知偏好表单)、`layout`(✅ P1-4 已修复:通知下拉组件改为从 notifications 模块导入) **已知问题**: - ✅ 审计 v1 P0-5 已修复:~~`data-access.ts` 1089 行超 1000 行硬限(规则 S-01),混合 CRUD/批量/群组/模板/举报多职责~~ 拆分为 5 个子文件(`data-access-core.ts` / `data-access-bulk.ts` / `data-access-group.ts` / `data-access-templates.ts` / `data-access-reports.ts`),`data-access.ts` 改为 barrel 文件 `export * from "./data-access-*"` 保持向后兼容;同时迁移至 `shared/lib/date-utils.ts` 的 `serializeDate` 统一日期序列化(规则 S-04 DRY) - ✅ P0-4 已修复:~~`sendMessageAction` 绕过 notifications dispatcher 直接调用 `createNotification`~~ 改为调用 `notifications.sendNotification`,通知 CRUD 已迁移至 notifications 模块 - ✅ P0 已修复:~~与 notifications 双向依赖 + 职责重叠~~ 通知相关表(messageNotifications / notificationPreferences)所有权已移交 notifications 模块,messaging 仅保留 messages 表 - ✅ P1-5 已修复:~~同时管理 3 类数据(messages + messageNotifications + notificationPreferences)~~ 仅管理 messages 表,通知相关数据由 notifications 模块管理 - ✅ P1 已修复:~~通知相关 Action 使用 `requireAuth()` 而非 `requirePermission()`~~ 通知 Action 已迁移至 notifications 模块并改为 `requirePermission(Permissions.MESSAGE_READ)` - ✅ P1 已修复:~~`markMessageAsReadAction` / `deleteMessageAction` / `getMessageDetailAction` 缺少 Zod 校验~~ 已添加 `MessageIdSchema` 校验 messageId 参数 - ✅ P1 已修复:~~`updateNotificationPreferencesAction` 缺少 Zod 校验~~ 已添加 `UpdateNotificationPreferencesSchema` 校验 8 个布尔字段 - ✅ P2 已修复:`data-access.ts` 中 3 处 `or(...)!` 非空断言清理为安全守卫(条件 push) - ✅ P0-b 已修复:~~`notification-preferences.ts` re-export shim 文件~~ 已删除(通知模块去重),8 个消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`,消除 messaging 模块对通知偏好的冗余 re-export 层 - ✅ P1 已修复:~~全模块零 i18n,中英文案硬编码~~ 所有组件接入 next-intl(`useTranslations("messages")`),新增 `src/shared/i18n/messages/{zh-CN,en}/messages.json` 翻译字典(title/description/tabs/actions/form/status/meta/notificationType/search/empty/messages/error 共 13 个命名空间;✅ 审计 V1-P1-4 新增 `pagination` / `drafts` / `thread` 三个命名空间);所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata - ✅ P1 已修复:~~缺 Error Boundary~~ 新增 3 个 `error.tsx` 错误边界(`/messages`、`/messages/[id]`、`/messages/compose`),统一使用 `EmptyState` + i18n 错误文案 + 重试按钮 - ✅ P2 已修复:a11y 改进,`message-list.tsx` 添加 `aria-label` / `aria-hidden`;✅ 审计 V1-P1-4:分页 `aria-label` i18n 化(替代硬编码 "Previous page" / "Next page"),星标按钮加 `aria-pressed` - ✅ P1-4 已修复:~~通知组件(notification-list.tsx / notification-dropdown.tsx)放在 messaging/components 下,直接 import notifications 模块类型和 messaging/actions~~ 两个组件已迁移至 `notifications/components/`,通知 CRUD Action 已迁移至 `notifications/actions.ts`,messaging 模块仅保留私信组件 - ✅ 审计 V1-P0-3 已修复:~~`getMessagesPageData` 在 data-access 层动态 `await import("@/modules/notifications/data-access")` 跨模块编排~~ 编排迁回 `messages/page.tsx` 页面层 `Promise.all([getMessages(...), getNotifications(...)])`,保持 messaging data-access 模块独立性 - ✅ 审计 V1-P0-4 已修复:~~`getMessageDetailAction` 死代码(V2-P1-3 重构遗留,全代码库无引用)~~ 已彻底删除 - ✅ 审计 V1-P0-1 已修复:~~`sendMessageAction` 未校验 `receiverId` 是否在 sender DataScope 内~~ 新增 `isReceiverAllowed(userId, receiverId, scope)` data-access 函数,Server Action 二次校验,防止越权发送 - ✅ 审计 V1-P0-2 已修复:~~`getMessageThread(messageId)` 缺少 userId 参数和权限校验~~ 签名变更为 `(messageId, userId)`,where 子句加 `(senderId=userId OR receiverId=userId)`,纵深防御 - ✅ 审计 V1-P0-5 已修复:~~`MessageCompose` 自动保存 effect 在用户快速提交后仍会触发新草稿创建~~ 新增 `submittedRef` 标志位,提交后跳过自动保存,失败时重置 - ✅ 审计 V1-P0-6 已修复:~~`sendMessageAction` 未校验 `parentMessageId` 属于当前用户会话~~ 新增 `getMessageById(parentMessageId, ctx.userId)` 校验,防止将回复注入他人会话线程 - ✅ 审计 V1-P1-1 已修复:~~compose 页面无草稿列表 UI~~ 新增 `MessageDraftList` 组件,compose page 通过 `getMessageDraftsAction` 获取草稿并渲染,支持"继续编辑"和"删除草稿" - ✅ 审计 V1-P1-2 已修复:~~MessageList 缺少"星标" Tab,data-access `starredOnly` 筛选不可达~~ Tab 类型新增 `starred`,切换时调用 `getMessagesAction({type:"all", starredOnly:true})` - ✅ 审计 V1-P1-3 已修复:~~MessageDetail 不渲染消息线程,`getMessageThread` 已实现但 UI 缺失~~ 新增 `getMessageThreadAction` + 线程渲染区块,时间正序展示回复 - ✅ 审计 V1-P1-6 已修复:~~`RecipientOption.role` 为宽松的 `string?`~~ 新增 `RecipientRole = "student" | "teacher" | "admin" | "parent"` 类型,admin 分支补 role - ✅ 审计 V1-P1-7 已修复:~~`replyHref` 构建逻辑散落在 MessageDetail 组件内,URL 参数未安全编码~~ 抽取为 `lib/build-reply-href.ts` 纯函数,使用 `URLSearchParams` 编码 - ✅ 审计 V1-P1-9 已修复:~~`sendMessageAction` 失败路径无埋点~~ catch 块调用 `trackEvent({event:"message.send_failed"})`,权限拒绝除外避免噪音 - ✅ P1-7 已修复:~~消息列表客户端 `useEffect` + `setTimeout` 防抖搜索未取消已发出的请求~~ 搜索逻辑抽离为 `useMessageSearch` hook(含防抖 + 请求竞态取消);~~无分页 UI~~ 新增客户端分页 UI(PAGE_SIZE=20,ChevronLeft/ChevronRight 按钮) - ✅ P1-9 已修复:~~`deleteMessage` 两个独立 UPDATE 无事务~~ 改为 `db.transaction` 包裹 senderDeletedAt 和 receiverDeletedAt 更新,保证原子性 - ✅ P2-11 已修复:~~发送/删除消息无埋点~~ `sendMessageAction` / `markMessageAsReadAction` / `deleteMessageAction` 新增 `trackEvent` 埋点(message.sent / message.marked_read / message.deleted) - ✅ V2-P0-2 已修复:~~通知标题硬编码~~ `sendMessageAction` 通过 `getTranslations('messages')` 生成 i18n 通知标题(`notification.messageTitle` / `messageTitleNoSubject`) - ✅ V2-P1-2 已修复:~~MessageList 客户端过滤冗余~~ 客户端过滤仅在初始数据(type=all)时执行,搜索结果已由服务端按 tab 过滤 - ✅ V2-P1-3 已修复:~~消息详情页分散编排~~ 新增 `getMessageDetailPageData` 编排函数,替代 page.tsx 中 `after()` + `getMessageById` + `markMessageAsRead` 的分散编排 - ✅ V2-P1-4 已修复:~~表单无服务端校验错误展示~~ `message-compose.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示(receiverId/subject/content) - ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `unread-message-badge.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量(~~60_000ms~~ → 30_000ms,✅ V2-P3 与通知组件保持一致) - ✅ V2-P2-13c 已实现:消息星标和草稿功能。`messages` 表新增 `isStarred` 字段(接收方可标记重要消息)+ `messages_receiver_starred_idx` 复合索引;新增 `message_drafts` 表(userId/receiverId/subject/content/parentMessageId + updatedAt/createdAt,含 `message_drafts_user_idx` 和 `message_drafts_user_updated_idx` 索引);data-access 新增 `toggleMessageStar` / `getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById`;actions 新增 `toggleMessageStarAction` / `getMessageDraftsAction` / `saveMessageDraftAction` / `deleteMessageDraftAction`;`getMessages` 支持 `starredOnly` 筛选;i18n 新增 actions.star/unstar/saveDraft/deleteDraft + messages.draftSaved/draftDeleted/starToggled + empty.noDrafts/noStarred 翻译键;迁移文件 `drizzle/0008_message_star_draft.sql` - ✅ V3-P0-1 已修复:~~`saveMessageDraftAction` 中 5 处 `formData.get("xxx") as string \| null` 断言~~ 改为使用 `MessageDraftSchema` Zod 校验安全转换 FormData 值 - ✅ V3-P1-3 已修复:~~`getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 放在 messaging 模块~~ 已迁移至 `notifications/actions.ts`,messaging 仅保留私信 Action - ✅ V3-P1-6 已修复:~~`getMessageDraftsAction` 和 `getMessagesPageData` 使用内联 `import("./types").XXX` 类型导入~~ 改为顶部 `import type` 语句 - ✅ V3-P2-7 已修复:~~`message-list.tsx` 中 `setTab(v as Tab)` 断言~~ 改为 `isTab` 类型守卫 **未实施项(P1-5 / P1-8 / P2-* 中长期)**: - ⏳ P1-5:`UnreadMessageBadge` 接入 SSE 实时推送(需扩展 `/api/notifications/stream` 端点 payload) - ✅ P1-8 已实施:`MessageList` 依赖注入化(定义 `MessageListService` 接口通过 `MessageListServiceProvider` Context 注入,`default-message-list-service.ts` 绑定现有 Server Actions) - ✅ P2-1 已实施:批量操作(`bulkMarkMessagesAsRead` / `bulkDeleteMessages` / `bulkToggleMessagesStar`,使用 inArray + 事务) - ✅ P2-2 已实施:群组消息(fan-out on write 策略,`messages.groupMessageId` 列 + `sendGroupMessage` data-access + `sendGroupMessageAction`,教师→全班家长,DataScope 校验) - ✅ P2-3 已实施:附件支持(复用 `fileAttachments` 表,`targetType="message"` + `getMessageAttachments` data-access + `getMessageAttachmentsAction`,UI 含 `MessageAttachmentUploader` 上传 + `MessageAttachmentList` 展示下载) - ✅ P2-4 已实施:消息撤回(`messages.recalledAt` 列 + `recallMessage` data-access,2 分钟窗口校验,UI 显示"已撤回"占位) - ✅ P2-5 已实施:举报+屏蔽(新增 `message_reports` 表 + `user_blocks` 表,`reportMessage` / `blockUser` / `unblockUser` / `isEitherUserBlocked` data-access,`reportMessageAction` / `blockUserAction` / `unblockUserAction` / `getUserBlocksAction`;`sendMessageAction` 集成 `isEitherUserBlocked` 屏蔽校验) - ✅ P2-6 已实施:快捷回复模板(`message_templates` 表 + CRUD data-access + `MessageTemplatePicker` UI 组件集成到 compose) - ✅ P2-7 已实施:配置驱动的角色组合(`RecipientResolver` 注册表模式,`RECIPIENT_RESOLVERS` 按 DataScope.type 索引,新增角色仅新增配置项) - ✅ P2-8 已实施:Error Boundary 细化(列表区/详情区/草稿区分别 `SectionErrorBoundary` 包裹) - ✅ P2-9 已实施:React Suspense 流式渲染(RSC 使用 `Suspense` 包裹 MessageList + NotificationList,骨架屏 fallback) - ✅ P2-10 已实施:跨设备草稿同步(`message_drafts.deviceId` + `version` 列 + `userParentIdx` 索引,`updateMessageDraft` 带乐观锁返回 `"ok" | "not_found" | "conflict"`,`saveMessageDraftAction` 处理冲突与 not_found 重建) - ✅ P2-11 已实施:组合搜索(`getMessages` 扩展 `senderId` / `dateFrom` / `dateTo` / `isRead` 参数) - ✅ P1-9 已实施:失败埋点(`message.send_failed` / `message.group_send_failed` / `message.report_failed` 等) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | ~360 | 11 个私信 Server Action(✅ 审计 V1-P0-4:删除 `getMessageDetailAction` 死代码;✅ 审计 V1-P1-3:新增 `getMessageThreadAction`;✅ 审计 V1-P0-1/P0-6/P1-9:sendMessageAction 增加 receiverId 二次校验 + parentMessageId 校验 + 失败埋点;✅ V3-P1-3:通知偏好 Action 已迁移至 notifications;✅ V3-P0-1:`saveMessageDraftAction` 使用 Zod 校验;✅ V2-P0-2:通知标题 i18n 化) | | `data-access.ts` | - | ✅ 审计 v1 P0-5 拆分为 barrel 文件:`export * from "./data-access-*"`,向后兼容所有现有 import 路径(原单文件 1089 行超 1000 硬限,规则 S-01) | | `data-access-core.ts` | - | ✅ 审计 v1 P0-5 新增:helpers(`resolveUserNames` / `toIso` via shared util)+ 核心 CRUD(`getMessages` / `getMessageById` / `getMessageThread` / `createMessage` / `markMessageAsRead` / `deleteMessage` / `toggleMessageStar` / `recallMessage` / `getUnreadMessageCount` / `getRecipients` / `isReceiverAllowed` / `getMessageDetailPageData`) | | `data-access-bulk.ts` | - | ✅ 审计 v1 P0-5 新增:批量操作(`bulkMarkMessagesAsRead` / `bulkDeleteMessages` / `bulkToggleMessagesStar`) | | `data-access-group.ts` | - | ✅ 审计 v1 P0-5 新增:群组消息 fan-out(`sendGroupMessage` + `SendGroupMessageInput` / `SendGroupMessageResult` 接口) | | `data-access-templates.ts` | - | ✅ 审计 v1 P0-5 新增:草稿 + 模板 CRUD(`getMessageDrafts` / `createMessageDraft` / `updateMessageDraft` / `deleteMessageDraft` / `getMessageDraftById` + `getMessageTemplates` / `createMessageTemplate` / `updateMessageTemplate` / `deleteMessageTemplate`) | | `data-access-reports.ts` | - | ✅ 审计 v1 P0-5 新增:举报 + 屏蔽 + 附件(`reportMessage` / `hasUserReportedMessage` / `blockUser` / `unblockUser` / `isUserBlocked` / `isEitherUserBlocked` / `getBlockedUserIds` / `getMessageReports` / `getUserBlocks` / `getMessageAttachments` + `MESSAGE_ATTACHMENT_TARGET_TYPE`) | | `schema.ts` | 40 | 私信发送校验 + messageId 校验 + 消息草稿校验(✅ V3-P0-1 新增 `MessageDraftSchema`;✅ V3-P1-3:`UpdateNotificationPreferencesSchema` 已迁移至 notifications/schema.ts) | | `types.ts` | 85 | 私信类型 + ✅ 审计 V1-P1-6 新增 `RecipientRole` 类型 + re-export 通知类型(向后兼容) | | `hooks/use-message-search.ts` | ~60 | ✅ P1-7 新增:消息搜索 hook(防抖 + 请求竞态取消) | | `lib/build-reply-href.ts` | ~30 | ✅ 审计 V1-P1-7 新增:构建回复 URL 纯函数(URLSearchParams 编码) | | `lib/type-guards.ts` | ~40 | ✅ 类型守卫专项重构(2026-07-04)新增:`isRecipientRole` / `isMessageReportReason` / `isMessageReportStatus` 守卫 + `toMessageReportReason` / `toMessageReportStatus` 安全窄化函数(fallback 默认值),替代 `data-access.ts` 与 `message-report-block.tsx` 中的 `as` 断言 | **组件清单**: | 组件 | 职责 | |------|------| | `components/message-list.tsx` | 消息列表(✅ P1-7:使用 `useMessageSearch` hook + 客户端分页 UI,PAGE_SIZE=20;✅ V2-P1-2:客户端过滤仅在初始数据时执行;✅ 审计 V1-P1-2:新增 `starred` Tab + 调用 `getMessagesAction({starredOnly:true})`;✅ 审计 V1-P1-4:分页 aria-label i18n 化) | | `components/message-detail.tsx` | 消息详情(含回复;✅ 审计 V1-P1-3:渲染消息线程;✅ 审计 V1-P1-7:使用 `buildReplyHref` 纯函数;✅ 审计 V1-P1-4:星标按钮加 `aria-pressed`) | | `components/message-compose.tsx` | 撰写新消息(✅ V2-P1-4:fieldErrors + aria-invalid 字段级错误展示;✅ 审计 V1-P0-5:`submittedRef` 防止提交后自动保存竞态) | | `components/message-draft-list.tsx` | ✅ 审计 V1-P1-1 新增:草稿列表组件(继续编辑 + 删除草稿 + 乐观更新) | | `components/unread-message-badge.tsx` | 未读消息计数徽章(侧边栏,每 30 秒轮询 `getUnreadMessageCountAction`;✅ V2-P2-1:POLL_INTERVAL_MS 常量;✅ V2-P3:间隔从 60s 缩短为 30s 与通知一致) | **客户端行为**: - `message-list.tsx`:客户端调用 `getMessagesAction` 搜索消息(useMessageSearch hook,400ms 防抖,请求竞态取消);V2-P1-2 优化:客户端过滤仅在初始数据(type=all)时执行;✅ 审计 V1-P1-2:starred Tab 单独加载 `getMessagesAction({type:"all", starredOnly:true})` - `unread-message-badge.tsx`:每 `POLL_INTERVAL_MS`(30_000ms)轮询 `getUnreadMessageCountAction` 刷新未读计数 - `message-detail.tsx`:✅ 审计 V1-P1-3 客户端调用 `getMessageThreadAction(messageId)` 加载回复链 - `message-compose.tsx`:✅ 审计 V1-P0-5 `submittedRef` 标志位阻止发送后自动保存 effect 触发新草稿 > 架构变更(2026-07-05,Phase 4.5 — 消息星标改用 useOptimistic + useTransition): > - **message-detail.tsx**:星标 `useState` → `useOptimistic` + `useTransition`。`handleToggleStar` 在 `startStarTransition` 内 `addOptimisticStarred(nextStarred)` + `await toggleMessageStarAction` + `router.refresh()`。JSX 中所有 `isStarred` 引用改为 `optimisticIsStarred`(Button variant/aria-pressed/Star className/星标徽章) > - **message-list.tsx**:星标 `starredOverride` useState → `useOptimistic>`(key=messageId, value=isStarred)。`getIsStarred` 改为 `optimisticStarredMap.get(m.id)`;`handleToggleStar` 重写为 `startStarTransition` + `addOptimisticStar` + `await router.refresh()`。撤回 `recalledOverride` 仍保留 useState(不需要乐观更新) > - **关键决策**:成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值;不为撤回(recall)操作使用 useOptimistic,因为撤回需要根据服务端返回判断是否成功(2 分钟窗口校验),失败时不应乐观显示已撤回 --- ## 2.14 notifications(通知分发模块) **职责**:多渠道通知分发(SMS/Email/WeChat/InApp)+ 站内通知 CRUD + 通知偏好管理 + 通知 UI 组件。 **导出函数**: - Actions:`sendNotificationAction` / `sendClassNotificationAction` / `getNotificationsAction` / `getUnreadNotificationCountAction` / `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` / `archiveNotificationAction`(✅ P1-4 新增:后 4 个通知 CRUD Action 从 messaging 模块迁移;✅ V2-P2-13b 新增:archiveNotificationAction 归档 Action;✅ V3-P1-3 新增:`getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 从 messaging 模块迁移) - Dispatcher:`sendNotification(payload)` / `sendBatchNotifications(payloads)`(✅ P3-7:`sendBatchNotifications` 重构为 in_app 单次批量 INSERT,其他渠道保持 per-payload 并行) - Data-access:`createNotification` / `createNotifications(items)`(✅ P3-7 新增:批量 INSERT 多行)/ `getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `getUnreadNotificationCount` / `archiveNotification` / `unarchiveNotification` / `getUserContactInfo` / `logNotificationSend` / `logNotificationSendBatch`(✅ P0-4 / P1-5 修复后从 messaging 迁移;✅ V2-P2-13b 新增:archiveNotification / unarchiveNotification 归档函数) - Preferences:`getNotificationPreferences` / `upsertNotificationPreferences`(✅ P0-4 / P1-5 修复后从 messaging 迁移) - Channels:`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender` - Components:`NotificationList` / `NotificationDropdown`(✅ P1-4 新增:从 messaging/components 迁移;✅ V2-P2-13b:NotificationList 支持优先级 Badge 显示和归档操作;✅ V2-P2-13c:NotificationList 支持按类型筛选) - Hooks:`useNotificationStream`(✅ V2-P3 新增:SSE 实时推送 + 轮询降级 Hook)、`useDesktopNotifications`(✅ V2-P2-13c 新增:浏览器桌面推送 Hook,支持权限管理和新通知桌面提醒) - Types:`NotificationPriority`(✅ V2-P2-13b 新增:通知优先级类型 low/normal/high/urgent) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getStudentIdsByClassId) - 被依赖:`messaging`(✅ P0-4 / P1-5 已修复:messaging 通过 `sendNotification` dispatcher 发送通知;✅ P1-4 已修复:通知 UI 组件由 notifications 模块自持,messaging 不再反向依赖)、`layout`(✅ P1-4 已修复:通知下拉组件直接从 notifications 模块导入)、`app/(dashboard)/messages`(✅ P1-4 已修复:通知列表组件直接从 notifications 模块导入) **已知问题**: - ✅ P0-4 已修复:~~不拥有任何数据,全部依赖 messaging 模块~~ messageNotifications 和 notificationPreferences 表所有权已从 messaging 迁移至 notifications 模块 - ✅ P0 已修复:~~与 messaging 双向依赖~~ notifications 不再反向依赖 messaging,in-app-channel 改为静态导入本地 createNotification - ✅ P1-1 已修复:~~`sendClassNotificationAction` 直查 `classes`/`classEnrollments`~~ 改为调用 `classes/data-access.getClassExists` / `getStudentIdsByClassId` - ✅ P1-4 已修复:~~通知 UI 组件放在 messaging/components 下,直接 import notifications 类型和 messaging/actions~~ `notification-list.tsx` 和 `notification-dropdown.tsx` 已迁移至 `notifications/components/`,通知 CRUD Action 已从 messaging 迁移至 `notifications/actions.ts`,消除 UI 层跨模块耦合 - ✅ P2-11 已修复:~~通知标记已读无埋点~~ `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` 新增 `trackEvent` 埋点(notification.marked_read / notification.marked_all_read) - ✅ V2-P0-1 已修复:~~通知 i18n 键混在 messages.json 中~~ 新增独立的 `notifications.json` 命名空间(zh-CN/en),通知组件 `useTranslations` 从 `"messages"` 切换到 `"notifications"`;`src/i18n/request.ts` 新增 notifications 命名空间加载 - ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `notification-dropdown.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量(30_000ms) - ✅ V2-P3 已优化:~~30 秒轮询~~ `notification-dropdown.tsx` 改为 SSE 实时推送(`/api/notifications/stream`),SSE 不可用时自动降级为轮询(调用 Server Actions,间隔 30 秒) - ✅ V2-P2-13b 新增:通知优先级和归档功能。schema.ts `messageNotifications` 表新增 `priority`(low/normal/high/urgent,默认 normal)和 `isArchived`(默认 false)字段 + 2 个索引;types.ts 新增 `NotificationPriority` 类型;data-access.ts 新增 `archiveNotification` / `unarchiveNotification` 函数,`getNotifications` 支持归档和优先级筛选(默认仅返回未归档),`createNotification` 支持 priority;actions.ts 新增 `archiveNotificationAction`(含 trackEvent 埋点 notification.archived);NotificationList 组件支持优先级 Badge 显示和归档按钮;i18n 新增 priority/actions.archive/messages.archiveFailed 翻译键 - ✅ V2-P2-13c 新增:通知分类筛选 + 浏览器桌面推送。NotificationList 组件新增按类型筛选按钮组(all/message/announcement/homework/grade),空状态区分"无通知"和"无筛选结果";新增 `useDesktopNotifications` Hook(浏览器 Notification API,支持权限管理、自动请求权限、新通知桌面推送、点击跳转);NotificationDropdown 集成桌面推送(监听新通知并触发桌面提醒,首次加载不批量推送);i18n 新增 filter.all / empty.noFilterResults / empty.noFilterResultsDesc 翻译键 - ⚠️ P1:发送日志仅 console,无 `notification_logs` 表 - ✅ 渠道抽象优秀(接口 + 工厂 + Mock 实现) - ✅ V3-P1-3 已修复:~~通知偏好 Action 放在 messaging 模块~~ `getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 已迁移至 `notifications/actions.ts`,新增 `notifications/schema.ts`(`UpdateNotificationPreferencesSchema` 从 messaging 迁移) - ✅ V3-P1-4 已修复:~~`sendBatchNotifications` 重复日志~~ 移除 `sendBatchNotifications` 中的重复 `logNotificationSendBatch` 调用(日志已在 `sendNotification` 内部记录) - ✅ V3-P1-5 已修复:~~腾讯云短信 `SmsSdkAppId` 使用 `templateCode`~~ 新增 `SMS_SDK_APP_ID` 环境变量和 `smsSdkAppId` 配置字段,`SmsSdkAppId` 使用独立配置值 - ✅ V3-P2-7 已修复:~~`notification-list.tsx` 中 `Object.keys(TYPE_ICON) as NotificationType[]` 断言~~ 改为 `TYPE_KEYS` 常量数组 - ✅ V3-P2-8 已标记:`logNotificationSend` 中 `console.info` / `console.error` 添加 TODO 注释,预留统一日志服务接入点 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `dispatcher.ts` | ~225 | 渠道选择 + 并行分发(✅ V3-P1-4:移除 `sendBatchNotifications` 重复日志;✅ P3-7:`sendBatchNotifications` 重构为 in_app 批量 INSERT + 其他渠道并行) | | `data-access.ts` | ~290 | 站内通知 CRUD + 用户联系方式 + 发送日志持久化(notification_logs 表;P0-4 / P1-5 修复后新增通知 CRUD;✅ V2-P2-13b:新增归档函数 + 优先级/归档筛选;✅ V3-P2-8:console 日志添加 TODO 统一日志服务标记;✅ P3-7:新增 `createNotifications` 批量 INSERT 函数) | | `preferences.ts` | 166 | 通知偏好 CRUD(P0-4 / P1-5 修复后从 messaging 迁移) | | `actions.ts` | ~380 | 9 个 Server Action(✅ P1-4:新增 4 个通知 CRUD Action;✅ V2-P2-13b:新增 archiveNotificationAction;✅ V3-P1-3:新增 getNotificationPreferencesAction / updateNotificationPreferencesAction 从 messaging 迁移) | | `schema.ts` | 22 | ✅ V3-P1-3 新增:通知偏好校验 Schema(从 messaging/schema.ts 迁移) | | `types.ts` | ~130 | 通知负载 + 渠道配置 + 通知记录 + 偏好类型(P0-4 / P1-5 修复后扩充;✅ V2-P2-13b:新增 NotificationPriority 类型 + priority/isArchived 字段) | | `index.ts` | ~85 | 对外导出入口(✅ P1-4:新增组件和 CRUD Action 导出;✅ V2-P2-13b:新增归档函数/Action/类型导出;✅ V2-P2-13c:新增 useNotificationStream / useDesktopNotifications Hook 导出;✅ V3-P1-3:新增通知偏好 Action 和 Schema 导出) | | `channels/*` | 5 文件 | 4 个渠道实现 | | `components/notification-list.tsx` | ~190 | ✅ P1-4 新增(从 messaging 迁移):通知列表组件;✅ V2-P2-13b:支持优先级 Badge 显示和归档操作;✅ V2-P2-13c:支持按类型筛选 + 空状态区分 | | `components/notification-dropdown.tsx` | ~180 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件;✅ V2-P3:改用 SSE 实时推送 + 轮询降级;✅ V2-P2-13c:集成 useDesktopNotifications 桌面推送 | | `hooks/use-notification-stream.ts` | ~195 | ✅ V2-P3 新增:SSE 实时推送 Hook(EventSource + 轮询降级) | | `hooks/use-desktop-notifications.ts` | ~100 | ✅ V2-P2-13c 新增:浏览器桌面推送 Hook(Notification API + 权限管理 + 点击跳转) | | `lib/type-guards.ts` | - | ✅ as 断言修复新增(2026-07-07):`isStringRecord` 守卫,替代 channels/wechat-channel.ts 中 `metadata.wechatKeywords as Record` 断言 | **组件清单**: | 组件 | 职责 | |------|------| | `components/notification-list.tsx` | 通知列表(消息页底部,展示所有通知,支持标记已读;✅ V2-P0-1:useTranslations 命名空间从 "messages" 切换到 "notifications";✅ V2-P2-13b:支持优先级 Badge 显示和归档操作;✅ V2-P2-13c:支持按类型筛选 + 空状态区分"无通知"/"无筛选结果") | | `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,✅ V2-P3:改用 SSE 实时推送 `/api/notifications/stream` + 轮询降级;✅ V2-P0-1:useTranslations 命名空间切换;✅ V2-P2-1:POLL_INTERVAL_MS 常量;✅ V2-P2-13c:集成 useDesktopNotifications 桌面推送,监听新通知并触发桌面提醒) | **客户端行为**: - `notification-dropdown.tsx`:✅ V2-P3 改用 `useNotificationStream` Hook 消费 SSE 实时推送(`/api/notifications/stream`),SSE 不可用时自动降级为轮询(调用 `getNotificationsAction` + `getUnreadNotificationCountAction`,间隔 30 秒);✅ V2-P2-13c 集成 `useDesktopNotifications` Hook,通过 `prevNotificationIds` 对比检测新通知并触发桌面推送(首次加载不批量推送) - `hooks/use-notification-stream.ts`:✅ V2-P3 新增,管理 SSE 连接生命周期(EventSource onopen/onmessage/onerror),降级时通过 `pollFnRef` 调用 Server Actions 轮询 - `hooks/use-desktop-notifications.ts`:✅ V2-P2-13c 新增,封装浏览器 Notification API(权限管理、自动请求权限、桌面推送、点击跳转回调),SSR 安全(`typeof window !== "undefined"` 检查) --- ## 2.15 audit(审计模块) **职责**:操作日志 + 登录日志 + 数据变更日志查询与导出 + 审计概览仪表盘 + 数据保留策略(含 i18n + Error Boundary + 骨架屏 + 埋点 + Service 接口抽象)。 **导出函数**: - Actions:`getDataChangeLogsAction` / `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` / `getAuditRetentionConfigAction` / `saveAuditRetentionConfigAction`(✅ 审计 v1 P0-3:权限点由 `AUDIT_LOG_READ` 提升为 `AUDIT_RETENTION_MANAGE`,修复读权限执行写操作的提权漏洞)/ `purgeAuditLogsAction`(✅ 审计 v1 P0-3:权限点由 `AUDIT_LOG_READ` 提升为 `AUDIT_LOG_PURGE`,修复读权限执行物理删除的提权漏洞;✅ 审计重构:导出逻辑下沉到 `export.ts`,actions 仅编排;新增 `trackEvent` 埋点 `audit.exported`;✅ P2-5 新增保留策略 3 个 Action;✅ v2 修正:保留策略操作 event 改为 `audit.retention`;✅ v2 新增 `revalidatePath` 精确刷新缓存) - Data-access:`getAuditLogs` / `getLoginLogs` / `getDataChangeLogs` / `getAuditModuleOptions` / `getDataChangeStats` / `getDataChangeTableOptions` / `getAuditLogsForExport` / `getLoginLogsForExport` / `getDataChangeLogsForExport` / `getAuditOverviewStats` / `getAuditTrend` / `getDataChangeActionStats`(✅ P2-4 新增概览统计 3 个函数) - Export(✅ 审计重构新增):`buildAuditLogExport` / `buildLoginLogExport` / `buildDataChangeLogExport`(Excel 列定义 + 行映射纯函数,从 actions 抽取) - Retention(✅ P2-5 新增):`getAuditRetentionConfig` / `saveAuditRetentionConfig` / `purgeExpiredAuditLogs` / `calculateRetentionThreshold` / `isValidRetentionDays`(保留策略配置读写 + 清理 + 纯函数) - Hooks(✅ 审计重构新增):`useLogPagination`(分页 URL 状态管理,三处重复逻辑抽取) - Services(✅ P2-8 新增,✅ v2 接线完成):`AuditService` 接口 + `AuditServiceProvider` Context + `useAuditService` / `useAuditAnalytics` hook + `adminAuditService` 默认实现(v2 改造为 Client-safe:查询方法抛错强制走 RSC props,写操作委托 Server Action)+ `mockAuditService` 测试用实现(v2 修正:消除 9 处 `as` 断言,为所有方法添加显式返回类型标注) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`next-intl`(i18n)、`shared/lib/track-event`(埋点)、`settings`(保留策略配置存储于 `system_settings` 表 `audit_retention` 分类) - 被依赖:无 **已知问题**: - ✅ 审计 v1 P0-3 已修复:~~`purgeAuditLogsAction` 与 `saveAuditRetentionConfigAction` 用读权限 `AUDIT_LOG_READ` 执行物理删除/写配置(权限提权漏洞,规则 A-04)~~ 新增独立权限点 `AUDIT_LOG_PURGE`("audit_log:purge")与 `AUDIT_RETENTION_MANAGE`("audit_retention:manage"),两个 Action 改用对应专属权限点;权限点已全链路同步:`permissions.ts` 常量 → `ROLE_PERMISSIONS_SEED` admin 角色 → `permission-bitmap.ts` 注册 → `permission-catalog.ts` audit 分组 → i18n `rbac.json`(zh-CN/en)翻译键 - ✅ 审计重构 P0 已修复:~~Excel 导出逻辑内联在 actions 层~~ 已抽取到 `export.ts`(144 行),actions.ts 从 212 行精简到 139 行 - ✅ 审计重构 P0 已修复:~~三个导出 Action 结构高度重复~~ 列定义 + 行映射统一到 export.ts 的 `buildXxxExport` 函数 - ✅ 审计重构 P0 已修复:~~全模块零 i18n~~ 所有组件接入 `useTranslations("audit")` + `useLocale()`,formatDate 从硬编码 `zh-CN` 改为 locale 变量;新增 `src/shared/i18n/messages/{zh-CN,en}/audit.json`(含 table/filter/empty/export/error/detail/overview/retention 命名空间) - ✅ 审计重构 P0 已修复:~~缺 Error Boundary + 骨架屏~~ 新增 `AuditErrorBoundary`(class 组件,捕获错误展示 EmptyState + 重试按钮);4 个页面新增 `loading.tsx`(骨架屏)+ `error.tsx`(错误边界) - ✅ 审计重构 P1 已修复:~~DataChangeLogTable 281 行过大~~ 拆分为 `data-change-log-table.tsx`(146 行,仅表格)+ `data-change-log-filters.tsx`(138 行,独立筛选器)+ `data-change-log-view.tsx`(53 行,视图组合) - ✅ 审计重构 P1 已修复:~~三处分页逻辑重复~~ 抽取 `useLogPagination` hook(27 行) - ✅ 审计重构 P1 已修复:~~导出无埋点~~ `exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction` 新增 `trackEvent` 埋点(`audit.exported`) - ✅ 审计重构 P1 已修复:a11y 改进,筛选器添加 `aria-label`、`aria-expanded` 属性 - ✅ 审计重构 P1 已修复:~~api/export/route.ts 中 `as Record` 类型断言~~ 改为 for 循环 + 类型守卫 - ✅ P2 已修复:`data-access.ts` 中 6 处 catch 块添加 `console.error` 输出错误上下文;`toIso`/`clampPageSize`/`clampPage` 工具函数补齐显式返回类型 - ✅ P2-1 已修复:~~data-access 错误吞没~~ 所有 catch 块改为 `throw error`(不再返回空数组),UI 通过 ErrorBoundary 捕获 - ✅ P2-2 已修复:~~无日志详情视图~~ 新增 `AuditLogDetailDialog`(discriminated union 模式,支持 audit/login/dataChange 三种类型) - ✅ P2-3 已修复:~~无用户维度筛选~~ 三个筛选器均新增 `userId` Input + URL 状态管理 - ✅ P2-4 已修复:~~无审计概览仪表盘~~ 新增 `/admin/audit-logs/overview` 页面(统计卡片 + 趋势图 + 分布图 + 快捷入口 + 保留策略配置) - ✅ P2-5 已修复:~~无数据保留策略~~ 新增 `retention.ts`(配置读写 + 清理纯函数)+ 3 个 Server Action + cron API 路由 + `AuditRetentionSettings` 组件 - ✅ audit-P2-5(auth 审计)已修复:~~`login_logs` 与 `audit_logs`/`data_change_logs` 共用同一保留期(180 天),无法为登录日志配置更长的安全取证保留期~~ 新增独立 `loginLogRetentionDays` 配置项(`DEFAULT_LOGIN_LOG_RETENTION_DAYS = 365`),`AuditRetentionConfig` 类型新增 `loginLogRetentionDays` 字段;`getAuditRetentionConfig` / `saveAuditRetentionConfig` 读写新配置项;`purgeExpiredAuditLogs(retentionDays, loginLogRetentionDays?)` 支持为 login_logs 传入独立保留期(向后兼容,第二参数可选);`purgeAuditLogsAction` / `AuditService.purgeExpiredAuditLogs` 接口同步更新;`AuditRetentionSettings` 组件新增登录日志保留天数 Input;新增 `vercel.json` 配置 cron schedule `0 3 * * *`(每日凌晨 3 点触发 `/api/cron/audit-cleanup`);`env.mjs` 新增 `CRON_SECRET` 可选环境变量校验 - ✅ P2-6 已修复:~~无单测~~ 新增 `retention.test.ts`(9 个测试)+ `export.test.ts`(13 个测试),全部通过 - ✅ P2-8 已修复:~~无 Service 接口抽象~~ 新增 `services/audit-service.tsx`(接口 + Context + hook)+ `admin-audit-service.ts`(默认实现)+ `mock-audit-service.ts`(测试用) - ✅ v2 P0-1 已修复:~~Service 抽象层定义但从未接线(死代码)~~ `admin-audit-service.ts` 改造为 Client-safe(移除 `server-only` + data-access 依赖;查询方法抛错强制走 RSC props;写操作委托 Server Action);`overview/page.tsx` 注入 `AuditServiceProvider`;`AuditRetentionSettings` 与 `AuditLogExportButton` 改用 `useAuditService()` + `useAuditAnalytics()` hook - ✅ v2 P0-2 已修复:~~`mock-audit-service.ts` 使用 9 处 `as` 类型断言违规~~ 为所有方法对象字面量添加显式返回类型标注 `Promise`,消除全部 `as` 断言 - ✅ v2 P0-3 已修复:~~保留策略 Server Action 无 `revalidatePath`~~ `saveAuditRetentionConfigAction` 与 `purgeAuditLogsAction` 末尾新增 `revalidatePath("/admin/audit-logs/overview")` 精确刷新缓存 - ✅ v2 P1-1 已修复:~~`trackEvent` event 名称与业务不匹配(保留策略操作用了 `audit.exported`)~~ 改为 `audit.retention`(同步更新 `actions.ts` + `api/cron/audit-cleanup/route.ts` + `shared/lib/track-event.ts` EventName 联合类型新增 `audit.retention`) - ✅ v2 P1-2 已修复:~~`AuditErrorBoundary` 使用 `namespace="common"` 不符合模块约定~~ 改为 `namespace="audit"` + 新增 `error.boundaryTitle` / `error.boundaryDescription` 翻译键 - ✅ v2 P1-3 已修复:~~`AuditLogExportButton` 与 `AuditRetentionSettings` 未接入埋点 hook~~ `AuditLogExportButton` 调用 `analytics.trackExport(exportType, 0)`;`AuditRetentionSettings` 调用 `analytics.trackRetentionConfigChange(config)` + `analytics.trackPurge(result)` - ✅ v2 P1-4 已修复:~~`getAuditModuleOptions` / `getDataChangeTableOptions` catch 块吞错误返回空数组~~ 改为 `throw error`,UI 通过 ErrorBoundary 捕获 - ✅ v2 P1-5 已修复:~~日期筛选器两个 Input 共用 `aria-label={t("table.time")}` 无法区分起止~~ 改为 `aria-label={t("filter.startDate")}` 和 `aria-label={t("filter.endDate")}`(同步更新三个 filters 组件) - ✅ v2 P1-6 已修复:~~`AuditLogDetailDialog` 的 `DialogDescription` 与 DialogTitle 重复~~ 改为 `t("detail.description")` 并新增对应翻译键 - ✅ v2 P1-7 已修复:~~骨架屏缺少 `aria-hidden` 屏幕阅读器会朗读骨架~~ `audit-log-table-skeleton.tsx` 根 div 与 4 个 `loading.tsx` 添加 `aria-hidden="true" role="presentation"` - ✅ data-access 职责清晰,无跨模块问题 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 210 | 7 个 Server Action(编排层:3 导出 + 3 保留策略 + 1 查询;✅ v2 新增 `revalidatePath` + `audit.retention` 埋点;✅ audit-P2-5 `purgeAuditLogsAction` 新增 `loginLogRetentionDays` 参数 + trackEvent 埋点;✅ 审计 v1 P0-3:`purgeAuditLogsAction` 改用 `AUDIT_LOG_PURGE`,`saveAuditRetentionConfigAction` 改用 `AUDIT_RETENTION_MANAGE`,修复读权限执行写/删操作的提权漏洞) | | `data-access.ts` | 387 | 日志查询(12 个函数:3 查询 + 3 导出 + 3 选项/统计 + 3 概览统计;✅ v2 P1-4 错误处理统一为 throw) | | `export.ts` | 144 | Excel 导出纯函数(列定义 + 行映射 + buildXxxExport) | | `retention.ts` | 181 | 保留策略(配置读写 + 清理 + 纯函数,✅ P2-5 新增;✅ audit-P2-5 新增 `DEFAULT_LOGIN_LOG_RETENTION_DAYS=365` + `loginLogRetentionDays` 配置 + `purgeExpiredAuditLogs` 第二可选参数) | | `types.ts` | 167 | 类型定义(含概览统计 + 保留策略类型,✅ P2-4/P2-5 扩展;✅ audit-P2-5 `AuditRetentionConfig` 新增 `loginLogRetentionDays` 字段) | | `hooks/use-log-pagination.ts` | 27 | 分页 URL 状态管理 hook | | `services/audit-service.tsx` | 124 | Service 接口 + Context Provider + hook(✅ P2-8 新增) | | `services/admin-audit-service.ts` | 82 | 管理员默认实现(✅ v2 P0-1 改造为 Client-safe:查询抛错 + 写操作委托 Server Action) | | `services/mock-audit-service.ts` | 70 | 测试用 Mock 实现(✅ v2 P0-2 消除 9 处 `as` 断言) | | `retention.test.ts` | 85 | 保留策略纯函数单测(9 个测试,✅ P2-6 新增) | | `export.test.ts` | 175 | 导出映射纯函数单测(13 个测试,✅ P2-6 新增) | **组件清单**: | 组件 | 行数 | 职责 | |------|------|------| | `components/audit-error-boundary.tsx` | 19 | 审计模块错误边界(class 组件,✅ v2 P1-2 namespace 改为 audit) | | `components/audit-log-table-skeleton.tsx` | 42 | 骨架屏(✅ v2 P1-7 添加 `aria-hidden`) | | `components/audit-log-table.tsx` | 115 | 操作日志表格(i18n + a11y + 详情对话框) | | `components/audit-log-filters.tsx` | 97 | 操作日志筛选器(i18n + a11y + userId 筛选;✅ v2 P1-5 日期 aria-label 区分起止) | | `components/audit-log-view.tsx` | 51 | 操作日志视图(筛选+表格+分页) | | `components/audit-log-export-button.tsx` | 76 | 导出按钮(i18n;✅ v2 P1-3 接入 `useAuditAnalytics().trackExport`) | | `components/audit-log-detail-dialog.tsx` | 132 | 日志详情对话框(discriminated union,✅ P2-2 新增;✅ v2 P1-6 修复 DialogDescription 重复标题) | | `components/login-log-table.tsx` | 109 | 登录日志表格(i18n + a11y + 详情对话框) | | `components/login-log-filters.tsx` | 83 | 登录日志筛选器(i18n + a11y + userId 筛选;✅ v2 P1-5 日期 aria-label 区分起止) | | `components/login-log-view.tsx` | 49 | 登录日志视图 | | `components/data-change-log-table.tsx` | 150 | 数据变更日志表格(i18n + a11y + 详情对话框) | | `components/data-change-log-filters.tsx` | 139 | 数据变更日志筛选器(i18n + a11y + userId 筛选;✅ v2 P1-5 日期 aria-label 区分起止) | | `components/data-change-log-view.tsx` | 53 | 数据变更日志视图 | | `components/audit-overview-stats-bar.tsx` | 52 | 概览统计卡片栏(✅ P2-4 新增) | | `components/audit-activity-trend-chart.tsx` | 67 | 活动趋势折线图(✅ P2-4 新增) | | `components/data-change-distribution-chart.tsx` | 72 | 数据变更分布柱状图(✅ P2-4 新增) | | `components/audit-overview-view.tsx` | 113 | 概览页视图(统计+图表+快捷入口+保留配置,✅ P2-4 新增) | | `components/audit-retention-settings.tsx` | 179 | 保留策略配置组件(✅ P2-5 新增;✅ v2 P0-1 + P1-3 改用 `useAuditService` + `useAuditAnalytics`) | **页面路由**: - `/admin/audit-logs` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹 - `/admin/audit-logs/login-logs` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹 - `/admin/audit-logs/data-changes` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹 - `/admin/audit-logs/overview` → `requirePermission(AUDIT_LOG_READ)` + `AuditErrorBoundary` 包裹(✅ P2-4 新增) - `/api/cron/audit-cleanup` → CRON_SECRET 鉴权(✅ P2-5 新增) - ✅ 4 个页面均新增 `loading.tsx`(骨架屏)+ `error.tsx`(错误边界) --- ## 2.16 announcements(公告模块) **职责**:公告 CRUD + 发布/归档 + 发布通知。 **导出函数**: - Actions:`getAnnouncementsAction` / `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction`(✅ P1 已修复:actions 层不再直接访问 DB,全部下沉到 data-access;✅ 发布公告时触发通知模块 `sendBatchNotifications`)/ `toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`(✅ V2-P2-13d 新增:置顶切换、已读标记、批量已读状态查询;✅ Audit-P0-3:`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` 新增资源所有权/可见性二次校验;✅ Audit-P1-1:所有 Action 返回 i18n 化 message;✅ Audit-P1-7:`handleActionError` 集成 console.error + trackEvent("announcement.action_error") + 区分 PermissionDeniedError / FORBIDDEN_ANNOUNCEMENT) - Data-access:`getAnnouncements`(✅ Audit-P0-2:`audience` 字段从单值升级为数组 `{ gradeIds, classIds }`,过滤逻辑改用 `inArray`)/ `getAnnouncementById` / `getAnnouncementByIdForUser`(✅ Audit-P0-1 新增:用户端详情页专用,结合 status=published + 受众过滤 + 已读状态 + readCount 一次返回,防止越权)/ `isAnnouncementVisibleToAudience`(✅ Audit-P0-1 新增:纯函数,判断公告对受众是否可见,可独立单测)/ `resolveUserAudience`(✅ Audit-P0-2:从原 `resolveAudience` 重命名,遍历所有 childrenIds/classIds/gradeIds 而非仅首个)/ `resolveAnnouncementTargetUserIds`(✅ Audit-P1-5:从 actions.ts 下沉到 data-access;✅ P3-7:school 分支改为分页循环遍历 `getAllUserIds(PAGE_SIZE, offset)`,PAGE_SIZE=1000,避免单次查询超大学校全量用户导致 OOM)/ `insertAnnouncement` / `updateAnnouncementById` / `deleteAnnouncementById` / `publishAnnouncementById` / `archiveAnnouncementById`(后 5 个为 P1-2 新增)/ `toggleAnnouncementPin`(✅ Audit-P1-6:原子化 UPDATE ... SET isPinned = NOT isPinned)/ `markAnnouncementAsRead`(✅ Audit-P1-6:onDuplicateKeyUpdate 实现幂等 upsert)/ `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`(✅ V2-P2-13d 新增:置顶切换 + 已读回执 CRUD)/ `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData`(✅ P1-5 新增:管理端列表页和编辑页编排函数)/ `getUserAnnouncementsPageData`(✅ V3-P0-2 新增:用户端列表页编排函数)/ `getAdminAnnouncementDetailPageData`(✅ Audit-P1-2 新增:管理端详情页编排函数,并行 getAnnouncementById + getAnnouncementReadCount + classes.getClassOptions) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`school`(获取年级列表)、`classes`(获取班级列表 + 解析受众)、`users`(获取目标用户 ID 列表)、`notifications`(发布公告时发送通知)、`shared/i18n`(✅ Audit-P1-1:Actions 通过 `getTranslations('announcements')` 返回 i18n 化 message) - 被依赖:无 **已知问题**: - ✅ P1-2 已修复:~~所有写操作直接在 actions 层 `db.insert/update/delete`,未下沉到 data-access~~ 写操作已下沉到 data-access(5 个新函数) - ✅ P2 已修复:~~`getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission(ANNOUNCEMENT_READ)`~~ 改为 `requirePermission(Permissions.ANNOUNCEMENT_READ)` - ✅ 已修复:用户端列表页传入 `audience` 受众过滤(school/grade/class),管理端返回所有公告 - ✅ 已修复:用户端新增公告详情页 `/announcements/[id]`(只读模式) - ✅ 已修复:管理端列表页传递 `classes` 数据给 `AdminAnnouncementsView` - ✅ 已修复:发布公告时(`publishAnnouncementAction` / `createAnnouncementAction` 直接发布 / `updateAnnouncementAction` 状态变为 published)触发通知模块 `sendBatchNotifications` - ✅ 已修复:新增 `loading.tsx` 骨架屏(用户端 + 管理端) - ✅ P1 已修复:~~全模块零 i18n,中英文案硬编码~~ 所有组件接入 next-intl(`useTranslations("announcements")`),新增 `src/shared/i18n/messages/{zh-CN,en}/announcements.json` 翻译字典(title/description/filter/status/type/form/actions/messages/meta/empty/error 共 11 个命名空间);所有页面 `page.tsx` 使用 `generateMetadata` + `getTranslations` 替代硬编码 metadata - ✅ P1 已修复:~~缺 Error Boundary~~ 新增 4 个 `error.tsx` 错误边界(`/announcements`、`/announcements/[id]`、`/admin/announcements`、`/admin/announcements/[id]`),统一使用 `EmptyState` + i18n 错误文案 + 重试按钮 - ✅ P2 已修复:a11y 改进,`announcement-card.tsx` / `announcement-detail.tsx` 添加 `aria-label` - ✅ P1-5 已修复:~~页面层 `Promise.all` 编排 announcements/school/classes 三个模块的 data-access~~ 新增 `getAdminAnnouncementsPageData` 和 `getEditAnnouncementPageData` 编排函数,页面层仅调用单一函数 - ✅ P1-6 已修复:~~`targetGradeId` / `targetClassId` 为 optional,未根据 `type` 做条件必填校验~~ `CreateAnnouncementSchema` 和 `UpdateAnnouncementSchema` 添加 `superRefine(refineAudience)`,年级公告强制 `targetGradeId`,班级公告强制 `targetClassId` - ✅ P2-11 已修复:~~发布/归档/删除公告无埋点~~ `createAnnouncementAction` / `updateAnnouncementAction` / `deleteAnnouncementAction` / `publishAnnouncementAction` / `archiveAnnouncementAction` 新增 `trackEvent` 埋点(announcement.created / announcement.updated / announcement.published / announcement.deleted / announcement.archived) - ✅ V2-P0-2 已修复:~~通知标题硬编码~~ `createAnnouncementAction` / `updateAnnouncementAction` / `publishAnnouncementAction` 通过 `getTranslations('announcements')` 生成 i18n 通知标题(`notification.publishedTitle` / `publishedContent`) - ✅ V2-P1-1 已修复:~~AnnouncementList 客户端 useState/useMemo 过滤~~ 改为纯服务端过滤模式,Select 切换仅更新 URL `?status=` 触发 RSC 重新渲染 - ✅ V2-P1-4 已修复:~~表单无服务端校验错误展示~~ `announcement-form.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示(title/content/targetGradeId/targetClassId) - ✅ V2-P2-13d 已实现:公告置顶 + 已读回执。`announcements` 表新增 `isPinned` 字段(置顶优先排序,`getAnnouncements` orderBy 改为 `desc(isPinned), desc(createdAt)`);新增 `announcement_reads` 表(唯一索引 announcementId+userId 保证幂等);新增 3 个 Server Action(`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` / `getAnnouncementReadStatusAction`)+ 5 个 data-access 函数(`toggleAnnouncementPin` / `markAnnouncementAsRead` / `isAnnouncementReadByUser` / `getAnnouncementReadCount` / `getAnnouncementReadStatusForUser`);`Announcement` 类型新增 `isPinned` / `isReadByCurrentUser?` / `readCount?` 字段;新增 `AnnouncementRead` 类型;埋点新增 `announcement.pin_toggled` / `announcement.marked_read` - ✅ V3-P0-2 已修复:~~用户端公告列表页 `resolveAudience` 50 行业务逻辑在路由层~~ 新增 `getUserAnnouncementsPageData` 编排函数(含 `resolveAudience` 内部函数),将受众解析逻辑从 `page.tsx` 下沉到 `data-access.ts`,页面层仅调用单一函数 - ✅ Audit-P0-1 已修复:~~用户端详情页越权访问草稿/他人班级公告~~ 新增 `getAnnouncementByIdForUser`,结合 `status="published"` + 受众过滤(`isAnnouncementVisibleToAudience` 纯函数)+ 已读状态 + readCount 一次返回,`markAnnouncementAsReadAction` 调用此函数做资源可见性二次校验,不可见时抛 `FORBIDDEN_ANNOUNCEMENT` - ✅ Audit-P0-2 已修复:~~`resolveAudience` 仅取首个 child/class/grade 导致家长多孩子/教师多班级时只看到首个受众公告~~ audience 类型升级为 `{ gradeIds: string[]; classIds: string[] }`,`resolveAudience` 重命名为 `resolveUserAudience` 遍历全部;`getAnnouncements` 过滤逻辑改用 `inArray` 支持数组 - ✅ Audit-P0-3 已修复:~~`toggleAnnouncementPinAction` / `markAnnouncementAsReadAction` 缺资源所有权二次校验~~ 前者新增资源存在性校验(不存在返回 404);后者调用 `getAnnouncementByIdForUser` 做可见性二次校验 - ✅ Audit-P1-1 已修复:~~Actions 返回英文硬编码 message~~ 全部改用 `t("messages.created")` / `t("messages.updated")` / `t("messages.deleted")` / `t("messages.published")` / `t("messages.archived")` / `t("messages.pinned")` / `t("messages.unpinned")` / `t("messages.markedRead")` / `t("messages.invalidFormData")` / `t("messages.unexpectedError")` / `t("messages.permissionDenied")` / `t("messages.forbidden")` - ✅ Audit-P1-2 已修复:~~管理端 `/admin/announcements/[id]` 原为编辑表单页,缺独立详情页~~ 拆分为详情页(`AnnouncementDetail canManage=true`,调用 `getAdminAnnouncementDetailPageData` 含 readCount 统计)+ `/edit` 子路由(编辑表单);用户端 `AnnouncementList` 通过 `service.getReadStatus` 批量获取已读状态传入卡片做已读/未读视觉区分 - ✅ Audit-P1-3 已修复:~~组件直接 import actions 耦合~~ 引入 `AnnouncementsServiceContext` 依赖注入(`AnnouncementsServiceProvider` + `useAnnouncementsService()` + `DefaultAnnouncementsService` 默认实现代理到真实 Server Actions),所有交互型组件(Card/Detail/Form/List)通过 Hook 消费,便于测试 mock 和未来角色差异化实现 - ✅ Audit-P1-4 已修复:~~`AnnouncementForm` 硬编码 `router.push("/admin/announcements")` 路由~~ 新增 `onSuccess` / `onCancel` 回调优先于 `successHref` / `cancelHref` 兜底,便于在 Dialog 中复用 - ✅ Audit-P1-5 已修复:~~`resolveTargetUserIds` 业务逻辑位于 actions 层~~ 下沉到 data-access `resolveAnnouncementTargetUserIds` - ✅ Audit-P1-6 已修复:~~`toggleAnnouncementPin` read-then-write 竞态~~ 原子化 `UPDATE ... SET isPinned = NOT isPinned`;~~`markAnnouncementAsRead` select-then-insert 双查询~~ 改用 `onDuplicateKeyUpdate` 幂等 upsert - ✅ Audit-P1-7 已修复:~~`handleActionError` 缺日志/埋点/i18n~~ 重写为接收 `t` 函数,集成 `console.error` + `trackEvent("announcement.action_error")` + 区分 `PermissionDeniedError` / `FORBIDDEN_ANNOUNCEMENT` / 其他错误 - ✅ Audit-P1-8 已修复:~~表单与 Dialog 行为耦合(保存后路由跳转关闭不了 Dialog)~~ `AdminAnnouncementsView` Dialog 内表单通过 `onSuccess={handleFormSuccess}` 关闭弹窗 - ✅ Audit-P2-1 已修复:~~a11y:置顶按钮嵌套在 Link 内导致键盘导航异常~~ 改为绝对定位 + `onKeyDown` 处理 Enter/Space,脱离外层 Link 语义 - ✅ Audit-P2-2 已修复:~~`mapRow` 类型不安全~~ 改用 `typeof announcements.$inferSelect` 推导 - ✅ Audit-P2-3 已修复:~~i18n 死键与缺键~~ 清理 `description.detail` / `description.create` 等死键,补 `messages.invalidFormData` / `messages.unexpectedError` / `messages.permissionDenied` / `messages.forbidden` - ✅ Audit-P2-4 已修复:~~`AnnouncementList.detailHrefBuilder` 死 prop~~ 删除(已由 V3 的 `detailHrefPrefix` 替代,本次彻底清理) - ✅ Audit-P2-5 已修复:~~两处 loading.tsx 重复骨架屏 JSX~~ 抽取 `AnnouncementListSkeleton` 共享组件,支持 `showCreateButton` prop - ✅ Audit-P2-6 已修复:~~用户端公告列表无分页 UI~~ 新增 `AnnouncementPagination` 组件(URL `?page=N` 持久化、a11y、窗口策略、边界处理);新增 `countAnnouncements` data-access 函数;`getUserAnnouncementsPageData` 升级为接收 `page`/`pageSize` 并返回 `PaginatedAnnouncements` 类型 `{ items, total, page, pageSize }`,内部并行执行列表查询与 count 查询;`/announcements/page.tsx` 读取 `?page=` 查询参数并传入分页数据;新增 i18n 键 `pagination.label` / `prev` / `next` / `page` - ✅ Audit-P2-7 已修复:~~模块零测试覆盖~~ 新增 3 个测试文件:`schema.test.ts`(18 个测试,覆盖 `refineAudience` 条件校验矩阵:school/grade/class 类型与 target 必填组合 + transform 归一化);`is-announcement-visible.test.ts`(15 个测试,覆盖 `isAnnouncementVisibleToAudience` 纯函数,含 P0-2 多孩子/多班级场景);`components/announcement-card.test.tsx`(16 个测试,覆盖基础渲染、isRead 视觉区分、isPinned 交互、a11y 键盘导航 Enter/Space、toggling 禁用、错误回滚)。同时修复项目级 `@testing-library/dom` peer dependency 缺失(已影响 `dashboard-section.test.tsx` 等现有测试) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | ~400 | 9 个 Server Action + 通知触发逻辑 + trackEvent 埋点(P1-2 已修复,无直接 DB 操作;V2-P2-13d 新增置顶/已读/批量已读状态 3 个 Action;Audit-P0-3 新增资源所有权二次校验;Audit-P1-1 i18n 化 message;Audit-P1-7 `handleActionError` 集成日志/埋点/i18n) | | `data-access.ts` | ~580 | 公告 CRUD + 发布/归档 + 受众过滤 + `getAdminAnnouncementsPageData` / `getEditAnnouncementPageData` / `getUserAnnouncementsPageData` 编排函数 + `getAdminAnnouncementDetailPageData`(Audit-P1-2 新增)+ `countAnnouncements`(Audit-P2-6 新增);V2-P2-13d 置顶切换 + 已读回执 5 个函数;Audit-P0-1 新增 `getAnnouncementByIdForUser` / `isAnnouncementVisibleToAudience`;Audit-P0-2 `resolveAudience` → `resolveUserAudience` 遍历全部受众;Audit-P1-5 `resolveAnnouncementTargetUserIds` 下沉;Audit-P1-6 `toggleAnnouncementPin` / `markAnnouncementAsRead` 原子化;Audit-P2-2 `mapRow` 改用 `$inferSelect` 推导 | | `schema.ts` | ~95 | Zod 校验 + `refineAudience` 条件校验(✅ P1-6 新增 superRefine) | | `types.ts` | ~130 | 类型定义(`GetAnnouncementsParams.audience` 升级为数组 Audit-P0-2;新增 `UserAudience` Audit-P0-2;新增 `AnnouncementsService` 接口 Audit-P1-3;新增 `PaginatedAnnouncements` Audit-P2-6) | | `schema.test.ts` | ~200 | ✅ Audit-P2-7 新增:18 个测试,覆盖 `refineAudience` 条件校验矩阵 + transform 归一化 | | `is-announcement-visible.test.ts` | ~180 | ✅ Audit-P2-7 新增:15 个测试,覆盖 `isAnnouncementVisibleToAudience` 纯函数(含 P0-2 多孩子/多班级场景) | | `components/announcements-service-context.tsx` | ~40 | ✅ Audit-P1-3 新增:`AnnouncementsServiceProvider` + `useAnnouncementsService()` | | `components/default-announcements-service.ts` | ~60 | ✅ Audit-P1-3 新增:`AnnouncementsService` 默认实现,代理到真实 Server Actions | | `components/announcement-list-skeleton.tsx` | ~40 | ✅ Audit-P2-5 新增:共享骨架屏,支持 `showCreateButton` prop | | `components/announcement-pagination.tsx` | ~100 | ✅ Audit-P2-6 新增:分页组件,URL `?page=N` 持久化,a11y + 窗口策略 + 边界处理 | | `components/announcement-card.test.tsx` | ~300 | ✅ Audit-P2-7 新增:16 个测试,覆盖基础渲染、isRead 视觉、isPinned 交互、a11y 键盘导航、toggling 禁用、错误回滚 | **组件清单**: | 组件 | 职责 | |------|------| | `components/announcement-list.tsx` | 公告列表(用户端,支持状态筛选;✅ V2-P1-1:纯服务端过滤,Select 切换更新 URL ?status= 触发 RSC 重新渲染;✅ V3:新增 `detailHrefPrefix` prop 替代 `detailHrefBuilder` 函数 prop;✅ Audit-P1-2:用户端通过 `service.getReadStatus` 批量获取已读状态传入卡片;✅ Audit-P2-4:删除死 prop `detailHrefBuilder`;✅ Audit-P2-6:新增 `pagination` prop,传入时在列表底部渲染 `AnnouncementPagination`) | | `components/announcement-card.tsx` | 公告卡片(列表项;✅ Audit-P1-3:通过 `useAnnouncementsService()` 消费 togglePin;✅ Audit-P2-1:置顶按钮绝对定位 + onKeyDown 处理;✅ Audit-P1-2:新增 `isRead` prop 已读/未读视觉区分) | | `components/announcement-detail.tsx` | 公告详情(只读;✅ Audit-P1-3:所有 Action 调用改用 `useAnnouncementsService()`) | | `components/announcement-form.tsx` | 公告表单(创建/编辑,✅ P1-6:条件校验由 schema superRefine 保证;✅ V2-P1-4:fieldErrors + aria-invalid;✅ Audit-P1-3:通过 `useAnnouncementsService()` 消费 create/update;✅ Audit-P1-4/P1-8:新增 `onSuccess` / `onCancel` 回调优先于 `successHref` / `cancelHref` 兜底) | | `components/admin-announcements-view.tsx` | 管理端公告视图(列表 + 筛选;✅ Audit-P1-3:包裹 `AnnouncementsServiceProvider`;✅ Audit-P1-8:Dialog 内表单通过 `onSuccess` 关闭弹窗) | | `components/announcement-pagination.tsx` | ✅ Audit-P2-6 新增:分页导航(URL 持久化、a11y、窗口策略、边界处理) | --- ## 2.17 files(文件模块) **职责**:文件附件 CRUD + 批量删除 + 统计 + 上传/预览 UI。 **导出函数**: - Actions(✅ 2026-06-25 新增 `actions.ts` 编排层,P0 修复):`uploadFileAction`(FILE_UPLOAD 权限 + Zod 校验 + storageProvider.save + trackEvent + logAudit)/ `getFileAction`(FILE_READ 权限 + ownership 检查:非 FILE_DELETE 持有者只能读自己上传的)/ `deleteFileAction`(FILE_DELETE 权限 + storageProvider.delete)/ `batchDeleteFilesAction`(FILE_DELETE 权限 + Zod 校验 max 100 ids)/ `getFileListAction`(FILE_READ 权限)/ `getFileStatsAction`(FILE_READ 权限) - Data-access:`createFileAttachment` / `getFileAttachment`(cache)/ `getFileAttachmentsByTarget`(cache)/ `getFileAttachmentsByUploader`(cache)/ `getAllFileAttachments`(cache)/ `deleteFileAttachment` / `deleteFileAttachments`(批量)/ `getFileAttachmentsWithFilters`(cache)/ `getFileStats`(cache)/ `getFileByUrl`(cache,✅ 2026-06-25 新增:按 url 字段查询)/ `getFileAttachmentsByIds`(cache)(✅ P2 已修复:7 个读函数使用 `React.cache()` 包装实现请求级 memoization) - Types(✅ 2026-06-25 更新):`FileTargetType` 新增 `"user_avatar"` 枚举值,现为 `"exam" | "textbook" | "question" | "announcement" | "homework" | "user_avatar"`,用于用户头像上传场景(targetId = users.id);`FileUploadResult` / `FileAttachment` / `CreateFileAttachmentInput` / `FileAttachmentQueryParams` / `FileStats` / `BatchDeleteResult` - Schema(✅ 2026-06-25 新增 `schema.ts`):`UploadMetadataSchema` / `BatchDeleteSchema`(max 100 ids)/ `FileListQuerySchema` / `FileTargetTypeSchema` / `validateFileSize` - Components:`FileUpload` / `FilePreview` / `FilePreviewDialog`(✅ 2026-06-25 补录)/ `FileIcon` / `AdminFilesView` - Hooks(✅ 2026-06-25 新增 `hooks/` 目录):`useFileUpload`(XHR 上传、进度、状态机、校验)/ `useFileBatchOperations`(客户端筛选、选择、批量删除)/ `useFilePreview` + `useImageZoom`(图片缩放) **依赖关系**: - 依赖:`shared/*`、`@/auth` - 被依赖:`app/api/upload` / `app/api/files/[id]` / `app/api/files/batch-delete`(✅ 2026-06-25 重构:3 个 API 路由改为委托 `actions.ts`,仅保留速率限制和请求解析)/ `homework`(✅ 2026-06-24:`homework/actions.deleteScanAction` 调用 `deleteFileAttachment`;`homework/components/scan-uploader` 调用 `/api/upload`,targetType="homework")/ `settings`(✅ 2026-06-25 新增:`settings/actions-avatar.ts` 通过 `storageProvider` 抽象删除旧头像文件,targetType="user_avatar";`settings/components/avatar-upload.tsx` 上传头像走 `/api/upload`) **已知问题**: - ✅ P0 已修复:~~无 `actions.ts`,data-access 被路由直接调用~~ 新建 `actions.ts` 编排层,所有 API 路由(upload/files/[id]/files/batch-delete)改为委托 actions,路由层仅保留速率限制 - ✅ P0 已修复:~~`app/api/upload/route.ts` 仅 `requireAuth()` 无权限校验~~ 改为 `requirePermission(FILE_UPLOAD)` - ✅ P0 已修复:~~`app/api/files/[id]/route.ts` 横向越权(任意用户可读/删他人文件)~~ `getFileAction` 加入 ownership 检查(非 FILE_DELETE 持有者只能读自己上传的) - ✅ P1 已修复:~~i18n 缺失,组件硬编码英文~~ 补全 `en/files.json` + `zh-CN/files.json`(88 行,含 title/upload/list/preview/admin 等命名空间),所有组件接入 `useTranslations` - ✅ P1 已修复:~~缺少 `loading.tsx` 骨架屏和 `error.tsx` 错误边界~~ 新增 `app/(dashboard)/admin/files/loading.tsx` + `error.tsx` - ✅ P1 已修复:~~组件逻辑与 UI 耦合,不可单测~~ 抽取 3 个自定义 hooks:`useFileUpload` / `useFileBatchOperations` / `useFilePreview` - ✅ P1 已修复:~~`settings/actions-avatar.ts` 直接 import `fs/promises` 绕过 storageProvider 抽象~~ 改用 `storageProvider.delete` - ✅ P1 已修复:~~`FileTargetType` 缺 `"user_avatar"` 枚举值导致 settings 头像上传 targetType 未注册~~ 已加入枚举 - ✅ P2 已修复:~~`file-list.tsx` 125 行死代码~~ 已删除 - ✅ P2 已修复:~~所有函数 try-catch 吞错误返回空数组/null~~ 所有 catch 块已添加 `console.error` 输出错误上下文 - ✅ 审计治理已修复(G5-005/G5-006/G5-007/P-05/A-10/F-03):~~data-access.ts 12 处 `log.error` + try-catch 吞错误返回 null/[]/false + `db.select()` SELECT \*~~ 移除全部 try-catch 与 `log.error`,错误自然抛出由 actions 层捕获返回 `ActionState`;`db.select()` 改为显式列枚举 `db.select(fileAttachmentColumns)` - ✅ P2 已修复:`getFileAttachmentsWithFilters` 中 `or(...)!` 非空断言清理为安全守卫 - ✅ P2 已修复:~~`getFileAttachmentsWithFilters` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注 - ✅ P2 已修复:~~`admin-files-view.tsx` 中 `t(`filter.${key}` as never)` 类型断言~~ 改为 `renderTypeLabel` 辅助函数 + 字面量联合类型 + switch - ✅ P2 已修复:~~`file-preview-dialog.tsx` 中 `label` 变量赋值后未使用~~ 已移除 - ✅ P2 新增:监控埋点 — `actions.ts` 接入 `trackEvent`(file.uploaded/upload_failed/deleted/batch_deleted/viewed/action_error)+ `logAudit` - ✅ P2 新增:a11y — 所有图标加 `aria-hidden`,按钮加 `aria-label`,`AdminFilesView` 用三层 `WidgetBoundary` 包裹(StatsSection / UploadSection / ListSection) - ✅ 职责单一,不跨模块查询 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `data-access.ts` | 280 | 文件 CRUD + 批量删除 + 统计(11 个函数 + mapRow,审计治理后移除 try-catch/log.error + 显式列枚举) | | `actions.ts` | 330 | 6 个 Server Action(编排层,权限+Zod+storageProvider+trackEvent+logAudit) | | `schema.ts` | - | Zod 校验(UploadMetadata/BatchDelete/FileListQuery/FileTargetType) | | `types.ts` | 70 | 类型定义(FileTargetType 含 user_avatar) | | `components/*` | 5 文件 | 上传/预览/预览弹窗/图标/管理视图 | | `hooks/*` | 3 文件 | 上传/批量操作/预览 hooks | --- ## 2.18 course-plans(课程计划模块) **职责**:课程计划 CRUD + 周计划项 CRUD + 排序 + 批量操作 + 复制 + 年级进度统计 + 日历视图 + CSV 导出 + 模板库 + 拖拽排序 + 数据联动 + 组件级 Error Boundary。 **导出函数**: - Actions:`getCoursePlansAction` / `getCoursePlanAction` / `createCoursePlanAction` / `updateCoursePlanAction` / `deleteCoursePlanAction` / `createCoursePlanItemAction` / `updateCoursePlanItemAction` / `deleteCoursePlanItemAction` / `toggleCoursePlanItemCompletedAction` / `getGradeCoursePlanProgressAction`(年级仪表盘维度4)/ `reorderCoursePlanItemsAction`(✅ P1-7 新增:拖拽排序周计划条目)/ `bulkToggleItemsAction`(✅ P2-4 新增:批量标记完成状态)/ `copyCoursePlanAction`(✅ P2-4 新增:复制计划到其他班级,P2-5 复用为"从模板创建")/ `getTemplateCandidatesAction`(✅ P2-5 新增:获取模板候选列表,复用现有计划作为模板)/ `trackCoursePlanEvent`(✅ P2-8 新增:监控埋点接口预留,已在 plan_deleted/items_reordered/plan_exported/plan_created_from_template 四处调用) - Data-access:与 actions 对应 + `getGradeCoursePlanProgress` + `reorderCoursePlanItems`(✅ P1-7 新增)+ `bulkUpdateItemCompleted`(✅ P2-4 新增)+ `copyCoursePlanToClasses`(✅ P2-4 新增)+ `enrichPlanRows`(✅ P1-1 新增:批量解析班级/科目/教师名称,通过动态 import 对方 data-access,**不再直接 JOIN 其他模块表**)+ `buildScopeCondition`(✅ P0-3 新增:DataScope 权限过滤) - Lib(✅ P2 新增):`lib/export-utils.ts`(纯函数 `planToExportRows`/`buildExportColumns`/`exportCoursePlanReport`,CSV 导出不依赖 i18n,标签由调用方传入)/ `lib/calendar-utils.ts`(纯函数 `planToCalendarEvents`/`buildMonthGrid`/`eventsOnDay`/`filterEventsInRange` + 日期工具 `addDays`/`addMonths`/`startOfWeek`/`startOfMonth`/`endOfMonth`/`isSameDay`/`isWithinRange`/`formatDateISO`/`parseISODate`) - Types(✅ v4-P2-7 新增):`GradeCoursePlanProgressItem` / `GradeCoursePlanProgressResult` / `CoursePlanQueryScope`(✅ P0-3 新增:数据范围上下文,含 userId/isAdmin/classIds/teacherId/studentId)/ `ReorderCoursePlanItemInput`(✅ P1-7 新增)/ 类型守卫 `isCoursePlanStatus` / `isCoursePlanSemester`(✅ P1-4 新增:替代 `as` 断言)/ 配置驱动 `ROLE_WIDGET_CONFIG`(✅ P2 新增:admin/teacher/parent/student 四角色 Widget 配置)/ `CalendarEvent`(✅ P2-7 新增:日历事件类型)/ `CoursePlanExportColumnKey` + `CoursePlanColumnLabels`(✅ P2-6 新增:导出列类型与标签接口) - Components:`CoursePlanList`(✅ P1 修复:使用 `` + 类型守卫 + i18n)/ `CoursePlanDetail`(✅ P1 修复:批量选择 + `successHref` prop + 埋点;✅ P2-1 增强:`SectionErrorBoundary` 包裹进度条 + 周计划表 + 日历视图;✅ P2-3 增强:`textbooksHref`/`homeworkHref` props 支持数据联动跳转;✅ P2-6 增强:CSV 导出按钮;✅ P2-7 增强:嵌入 `CoursePlanCalendar`)/ `CoursePlanForm`(✅ P1 修复:类型守卫 + `successHref` prop;✅ P2-5 增强:嵌入 `TemplatePickerDialog`)/ `CoursePlanItemEditor`(✅ P1 修复:i18n)/ `CoursePlanProgress`(✅ P1 修复:a11y aria 属性 + i18n)/ `SortableWeekRow`(✅ P1-7/P2-3 新增:@dnd-kit/sortable 拖拽行 + 数据联动跳转链接 + 复选框)/ `CoursePlanCalendar`(✅ P2-7 新增:月历视图 6×7 网格 + 上一月/下一月/今天导航 + 事件列表)/ `TemplatePickerDialog`(✅ P2-5 新增:模板选择器 + 搜索过滤 + 克隆到目标班级) **依赖关系**: - 依赖:`shared/*`(含 ✅ P2-6 新增 `shared/lib/export-utils.ts` 通用 CSV 导出工具)、`@/auth`、`classes`(✅ P1-1 已修复:通过动态 import `getClassNamesByIds` 批量解析,不再直接 JOIN classes 表)、`school`(✅ P1-1 已修复:通过动态 import `getSubjectNameMapByIds` 批量解析;`getSubjectOptions` 已移至 school 模块)、`users`(✅ P1-1 已修复:通过动态 import `getUserNamesByIds` 批量解析)、`@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`(✅ P1-7 新增:拖拽排序依赖) - 被依赖:无(parent/student 路由通过 page.tsx 调用 data-access,✅ P2-2 已新增) **已知问题**: - ✅ P0-1 已修复:教师详情页信息泄露 — `getCoursePlanById` 增加 `scope` 参数,非 admin 仅能查看自己负责的计划 - ✅ P0-2 已修复:admin 页面无 requirePermission — 所有 admin 页面补充 `requirePermission(COURSE_PLAN_READ/MANAGE)` - ✅ P0-3 已修复:data-access 无 DataScope 过滤 — `getCoursePlans`/`getCoursePlanById`/`getGradeCoursePlanProgress` 增加可选 `scope` 参数 - ✅ P0-4 已修复:i18n 严重缺失 — 翻译键从 5 个扩展至 ~230 个,覆盖状态/筛选/列表/详情/表单/条目/进度/Toast/教师/家长/学生/错误/日历/导出/批量/模板/加载 + Error Boundary 文案 - ✅ P1-1 已修复:跨模块直接 JOIN — 移除 `buildPlanSelect` 的 `leftJoin(classes/subjects/users)`,改用 `enrichPlanRows` 动态 import 对方 data-access 批量查询 - ✅ P1-2 已修复:缺 loading.tsx/error.tsx — 12 个路由(含 P2-2 新增 parent/student)均补充骨架屏 + 错误边界(使用 `EmptyState` + i18n) - ✅ P1-3 已修复:原生 `` 标签 — 替换为 Next.js `` 组件 - ✅ P1-4 已修复:`as` 断言 — 替换为类型守卫函数 `isCoursePlanStatus`/`isCoursePlanSemester` - ✅ P1-5 已修复:URL 路径推断角色 — 改为 `successHref` prop 显式传入 - ✅ P1-6 已修复:Action 入参未验证 — `getCoursePlansAction`/`getGradeCoursePlanProgressAction`/`reorderCoursePlanItemsAction`/`bulkToggleItemsAction`/`copyCoursePlanAction` 增加 Zod schema 验证 - ✅ P1-7 已修复:死代码 `reorderCoursePlanItems` — 已补充对应 Action + UI 拖拽排序(`SortableWeekRow` + `DndContext` + `SortableContext`,PointerSensor + KeyboardSensor 支持 a11y) - ✅ P2-1 已修复:无 Error Boundary — `SectionErrorBoundary` 包裹进度条 + 周计划表 + 日历视图三个数据区块 - ✅ P2-2 已修复:parent/student 无路由 — 新增 parent/student 课程计划只读路由(按子女班级/学生班级 scope 过滤) - ✅ P2-3 已修复:无数据联动 — 周计划项章节文本渲染为教材跳转链接,行尾新增作业跳转按钮(通过 `textbooksHref`/`homeworkHref` props 按角色注入) - ✅ P2-4 已修复:无批量操作 — 批量标记完成 + 批量复制计划到其他班级 - ✅ P2-5 已修复:无模板复用 — `TemplatePickerDialog` 复用现有计划作为模板,搜索 + 选择 + 克隆到目标班级 - ✅ P2-6 已修复:无导出 — CSV 导出(UTF-8 BOM 兼容 Excel),通用工具迁移至 `shared/lib/export-utils.ts` - ✅ P2-7 已修复:无日历视图 — `CoursePlanCalendar` 月历视图,周计划项按 `plan.startDate` 映射到日期范围 - ✅ P2-8 已修复:无监控埋点 — `trackCoursePlanEvent` 预留接口,已在 plan_deleted/items_reordered/plan_exported/plan_created_from_template 四处调用 - ✅ actions 层使用 `handleActionError`/`revalidatePlanPaths` 辅助函数(良好范例) - ✅ `resolveScope` 函数正确处理 DataScope union 类型(admin/teacher/parent/student 四角色范围解析) **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `data-access.ts` | 507 | 课程计划 + 周计划项 CRUD + 批量 + 复制 + 名称解析解耦 | | `actions.ts` | 520 | 14 个 Server Action + 埋点接口 + scope 解析 + 模板候选 | | `schema.ts` | 213 | Zod 校验(含 Action 入参验证 schema) | | `types.ts` | 161 | 类型定义 + 类型守卫 + 角色配置驱动 + CalendarEvent + 导出列类型 | | `lib/export-utils.ts` | 91 | CSV 导出纯函数(planToExportRows + buildExportColumns + exportCoursePlanReport) | | `lib/calendar-utils.ts` | 175 | 日历视图纯函数(事件映射 + 月历网格 + 日期工具) | | `components/course-plan-list.tsx` | 162 | 列表组件(i18n + Link + 类型守卫) | | `components/course-plan-detail.tsx` | 384 | 详情组件(批量选择 + 埋点 + successHref + Error Boundary + 拖拽 + 导出 + 日历 + 数据联动) | | `components/course-plan-form.tsx` | 326 | 表单组件(类型守卫 + successHref + 模板选择器) | | `components/course-plan-item-editor.tsx` | 250 | 周计划编辑器(i18n) | | `components/course-plan-progress.tsx` | 42 | 进度条(a11y + i18n) | | `components/sortable-week-row.tsx` | 176 | 拖拽行(@dnd-kit + 数据联动跳转 + 复选框 + a11y) | | `components/course-plan-calendar.tsx` | 200 | 月历视图(6×7 网格 + 导航 + 事件列表 + a11y) | | `components/template-picker-dialog.tsx` | 195 | 模板选择器(搜索 + 列表 + 克隆 + a11y) | | `i18n/zh-CN/course-plans.json` | 233 | 中文翻译(~230 键) | | `i18n/en/course-plans.json` | 233 | 英文翻译(~230 键) | **页面路由**(✅ P1-2 全部补充 loading.tsx + error.tsx,✅ P2-2 新增 parent/student 路由): | 路由 | 角色 | 权限 | 说明 | |------|------|------|------| | `admin/course-plans` | admin | COURSE_PLAN_READ | 列表(全局视图) | | `admin/course-plans/[id]` | admin | COURSE_PLAN_READ | 详情(含日历/导出/拖拽) | | `admin/course-plans/[id]/edit` | admin | COURSE_PLAN_MANAGE | 编辑 | | `admin/course-plans/create` | admin | COURSE_PLAN_MANAGE | 创建(含从模板创建) | | `teacher/course-plans` | teacher | COURSE_PLAN_READ | 列表(按 teacherId 过滤) | | `teacher/course-plans/[id]` | teacher | COURSE_PLAN_READ | 详情(scope 过滤防越权 + 数据联动) | | `parent/course-plans` | parent | COURSE_PLAN_READ | 列表(按子女 classIds 过滤) | | `parent/course-plans/[id]` | parent | COURSE_PLAN_READ | 详情(按子女 classIds scope 过滤) | | `student/course-plans` | student | COURSE_PLAN_READ | 列表(按 class_members scope 过滤) | | `student/course-plans/[id]` | student | COURSE_PLAN_READ | 详情(按 class_members scope 过滤 + 教材联动) | --- ## 2.19 parent(家长模块) **职责**:家长视角的子女数据聚合与展示。 **导出函数**: - Actions(✅ 审计 v1 P0-4 新增 `actions.ts`):`getChildrenAction` / `getChildBasicInfoAction` / `getChildDashboardDataAction` / `getParentDashboardDataAction` / `getChildNameListAction` / `verifyParentChildRelationAction`(6 个 Server Action,每个调用 `requireAuth()` + 内部通过 `verifyParentChildRelation` 做亲子关系校验,替代 app/ 页面直接 import data-access) - Data-access:`getChildren` / `getChildBasicInfo` / `getChildDashboardData`(V3-11 增强:并行调用 `homework/data-access.getStudentExamResults` 获取考试结果)/ `getParentDashboardData` / `verifyParentChildRelation` / `getChildNameList`(✅ v4 新增:用于详情页头部多子女切换器,一次批量查询避免 N+1) - Types(V3-11 新增):`ChildExamResultItem`(单条考试结果:submissionId/examId/examTitle/assignmentId/assignmentTitle/score/maxScore/submittedAt/status);`ChildDashboardData` 扩展 `examResults: ChildExamResultItem[]` 字段 - Components:`ParentDashboard` / `ChildCard` / `ChildDetailHeader` / `ChildDetailPanel`(V3-11 增强:新增 exams Tab)/ `SiblingSwitcher` / `ChildHomeworkSummary` / `ChildHomeworkDetail`(v4 新增)/ `ChildGradeSummary` / `ChildGradeDetail`(v4 新增)/ `ChildScheduleCard` / `ChildExamDetail`(V3-11 新增:子女考试详情视图,汇总卡片+考试列表)/ `ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentAttentionBanner`(v4 新增)/ `ParentAttendanceWarning`(v4 新增)/ `ParentAttendanceRateCard`(v4 新增)/ `ParentAttendanceCalendar`(v4 新增)/ `ParentExportButton`(v4 新增) **v4 修复(产品/UX 维度)**: - ✅ FEAT-G01:新增 `/parent/leave` 请假申请占位页(含 loading.tsx) - ✅ FEAT-G02:详情页 Schedule Tab 支持完整周课表(新增 `weeklySchedule` 字段 + `ChildWeeklyScheduleItem` 类型 + `buildWeeklySchedule` 函数) - ✅ FEAT-G03:详情页 Grades Tab 新增 `ChildGradeDetail` 按科目分组展示(平均分、趋势、最近成绩) - ✅ FEAT-G04:详情页 Homework Tab 新增 `ChildHomeworkDetail` 展示完整作业信息(状态、截止、提交时间、尝试次数) - ✅ FEAT-G05:考勤页新增 `ParentAttendanceWarning` 异常预警横幅(聚合缺勤/迟到/低出勤率) - ✅ FEAT-G06:详情页底部新增"Contact Teacher"快捷入口 - ✅ FEAT-G07:详情页头部新增 `SiblingSwitcher` 多子女切换器 - ✅ LAYOUT-P01:仪表盘新增 `ParentAttentionBanner` 待办事项横幅 - ✅ LAYOUT-P02:`ChildCard` 突出 Overdue 异常(红色边框 + AlertTriangle 图标) - ✅ LAYOUT-P03:仪表盘快捷入口改为 4 宫格大图标卡片 - ✅ LAYOUT-P04:详情页改为 Tab 布局(Overview/Homework/Grades/Schedule/Attendance/Diagnostic) - ✅ LAYOUT-P05:详情页新增面包屑导航 - ✅ LAYOUT-P07:成绩趋势图 X 轴改用序号,避免日期重叠 - ✅ LAYOUT-P08:成绩页新增 `ParentExportButton` 导出按钮(占位) - ✅ LAYOUT-P09:考勤页新增 `ParentAttendanceCalendar` 月历视图(按状态着色,支持按月切换) - ✅ LAYOUT-P10:考勤异常高亮(与 FEAT-G05 同步实现) - ✅ NAV-P02:Grades/Attendance 页面描述明确职责(多子女对比 vs 单子女详情) - ✅ NAV-P03:详情页实现 `?tab=` 参数支持 - ✅ NAV-P04:所有 parent 路由新增 `loading.tsx` 骨架屏 + `error.tsx` 错误边界 - ✅ DATA-P02:成绩卡片新增 TrendIcon 进步/退步/持平标识 - ✅ DATA-P03:排名展示新增"Top X%"百分比 - ✅ DATA-P04:作业列表新增科目标识 Badge - ✅ DATA-P05:作业分数显示新增"pts"单位 - ✅ DATA-P06:考勤页新增 `ParentAttendanceRateCard` 出勤率汇总卡片 - ✅ HABIT-P01:仪表盘"一眼定位异常"能力(AttentionBanner 聚合) - ✅ HABIT-P02:待办横幅作业项直接跳转详情页 homework tab(1 次点击到达) - ✅ HABIT-P03:多子女切换无需返回仪表盘 - ✅ HABIT-P06:仪表盘展示未读/待办数量 - ✅ A11Y-P02:Overdue 状态增加 AlertTriangle 图标辅助 - ✅ A11Y-P04:成绩图表容器新增 aria-label - ✅ PERF-P01/P02:骨架屏 + 错误边界 - ✅ PERF-P03:空状态新增"Contact support"引导按钮 - ✅ PERF-P04:`ChildCard` Link 添加 `prefetch` - ✅ MOBILE-P03:移动端子女卡片改为水平滑动 Carousel(snap-x) - ✅ MOBILE-P04:作业/成绩列表项 `min-h-[44px]` 触摸区域 **V3-11 修复(家长端考试详情)**: - ✅ V3-11:`parent/types.ts` 新增 `ChildExamResultItem` 类型;`ChildDashboardData` 扩展 `examResults` 字段 - ✅ V3-11:`parent/data-access.ts` `getChildDashboardData` 并行调用 `homework/data-access.getStudentExamResults` 获取子女考试结果(加入现有 `Promise.all`) - ✅ V3-11:新增组件 `ChildExamDetail`(考试成绩汇总卡片:已参加考试数/平均分/最高分 + 考试成绩列表:考试标题/分数/得分率 Progress/提交时间) - ✅ V3-11:`child-detail-panel.tsx` 新增 exams Tab(对标智学网家长端考试详情) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`classes`(合理)、`homework`(合理;V3-11 新增 `getStudentExamResults` 调用)、`grades`(合理)、`users`(合理)、`school`(合理)、`attendance`(v4 新增:考勤页复用 `StudentAttendanceView`;⚠️ 跨模块 UI 类型依赖:3 个组件直接 import `@/modules/attendance/types`) - 被依赖:无 **已知问题**: - ✅ 审计 v1 P0-4 已修复:~~parent 模块缺失 `actions.ts`,3 个页面(`children/[studentId]/page.tsx` / `leave/page.tsx` / `elective/page.tsx`)直接 import `@/modules/parent/data-access`,绕过 Server Action 权限边界(规则 A-08)~~ 新建 `src/modules/parent/actions.ts`,封装 6 个 Server Action(`getChildrenAction` / `getChildBasicInfoAction` / `getChildDashboardDataAction` / `getParentDashboardDataAction` / `getChildNameListAction` / `verifyParentChildRelationAction`),每个 Action 调用 `requireAuth()` + 内部 `verifyParentChildRelation` 亲子关系校验;3 个页面改为调用对应 Server Action - ⚠️ P1:3 个 parent 组件(`parent-attendance-warning.tsx`/`parent-attendance-rate-card.tsx`/`parent-attendance-calendar.tsx`)直接 import `@/modules/attendance/types`,违反模块解耦原则(应通过 data-access 接口或 shared 类型抽象) - ⚠️ P1:parent-attendance-calendar.tsx 重新定义 `STATUS_DOT`/`STATUS_LABEL` 常量,与 attendance 模块重复 - ⚠️ P1:3 个 parent 组件的纯函数(`buildWarnings`/`aggregate`/`rateTone`/`formatDateKey`/`parseDateKey`/`buildCalendarDays`/`isSameDay`)未导出,无法单测 - ✅ P1 已修复:~~`app/(dashboard)/parent/children/[studentId]/page.tsx` 直接访问 DB(违反三层架构)~~ 改为调用 `verifyParentChildRelation` data-access 函数 - ✅ P1 已修复:~~权限校验未加 parentId 条件,存在信息泄露风险~~ `verifyParentChildRelation` 同时按 parentId + studentId 过滤 - ✅ P2 已修复:~~`getChildBasicInfo` 多次串行查询~~ 改为 `Promise.all` 并行化,并使用 `getStudentActiveClass` 一次 JOIN - ✅ P2 已修复:~~`getGradeOptions` 全量查询效率低~~ 改为 `getGradeNameById` 按 ID 查询 - ✅ P2 已修复:~~`buildHomeworkSummary` 中 `[...assignments].sort()` 不必要拷贝~~ 改为 `toSorted()` - ✅ P2 已修复:~~`in7Days` 死代码~~ 已删除 - ⚠️ v4 保留:`/parent/leave` 为占位页,待后端实现请假审批流后接入 - ✅ v4-P1-5 改进(2026-06-23):新增 `getParentIdsByStudentIds` data-access 函数,批量查询多个学生的家长 userId(去重),供 diagnostic/grades 模块通知场景调用 - ✅ v4-P1-9 改进(2026-06-23):parent/diagnostic/page.tsx 新增错误卡片展示子女诊断查询失败原因 - ✅ v4-P1-12 改进(2026-06-23):`ParentExportButton` 接入 `exportGradesAction`,支持按 studentId 导出单个子女成绩;parent/grades/page.tsx 传 studentId 到 ParentExportButton - ⚠️ v4 保留:详情页 Attendance/Diagnostic Tab 为占位提示,待对应功能实现后填充 - ✅ v3-P2 改进(2026-06-23):parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`,渲染 GradeTrendCard;parent/diagnostic/page.tsx 传入 `practiceHrefBase={null}` 隐藏练习按钮 - ✅ 职责单一,正确复用其他模块 data-access **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | - | ✅ 审计 v1 P0-4 新增:6 个 Server Action(`getChildrenAction` / `getChildBasicInfoAction` / `getChildDashboardDataAction` / `getParentDashboardDataAction` / `getChildNameListAction` / `verifyParentChildRelationAction`),每个调用 `requireAuth()` + `verifyParentChildRelation` 亲子关系校验,替代 app/ 页面直接 import data-access | | `data-access.ts` | 243+ | 子女关系 + 仪表盘数据聚合 + 关系校验 + 子女姓名列表(v4 新增 `getChildNameList` + `buildWeeklySchedule`;V3-11 `getChildDashboardData` 并行调用 `getStudentExamResults`) | | `types.ts` | 79+ | 类型定义(含 JSDoc,v4 新增 `ChildWeeklyScheduleItem`;V3-11 新增 `ChildExamResultItem` + `ChildDashboardData.examResults`) | | `components/parent-dashboard.tsx` | 110 | 仪表盘(v4 重构:待办横幅 + 宫格快捷入口 + 移动端水平滑动) | | `components/parent-attention-banner.tsx` | 128 | v4 新增:待办事项/异常聚合横幅(作业项直接跳转详情页 homework tab) | | `components/parent-attendance-warning.tsx` | 89 | v4 新增:考勤异常预警 | | `components/parent-attendance-rate-card.tsx` | 105 | v4 新增:考勤出勤率汇总卡片 | | `components/parent-attendance-calendar.tsx` | 175 | v4 新增:考勤月历视图(use client) | | `components/parent-export-button.tsx` | 50 | v4 新增:成绩导出按钮(v4-P1-12 已接入 exportGradesAction,支持按 studentId 导出单个子女成绩) | | `components/child-card.tsx` | 148 | 子女卡片(v4 增强:异常突出 + 趋势图标) | | `components/child-detail-header.tsx` | 78 | 详情页头部(v4 增强:面包屑) | | `components/child-detail-panel.tsx` | 200+ | 详情页 Tab 面板 + SiblingSwitcher(v4 重写,集成 Homework/Grade Detail;V3-11 新增 exams Tab;✅ i18n:硬编码英文提取为 `parent.childDetail.*` 翻译键,新增 parent.json;修复 P2-4:`as` 断言改用 `isTab` 类型守卫) | | `components/child-homework-summary.tsx` | 147 | 作业摘要(v4 增强:科目标识 + 触摸区域 + pts 单位) | | `components/child-homework-detail.tsx` | 145 | v4 新增:作业详情视图(完整作业信息) | | `components/child-grade-summary.tsx` | 159 | 成绩趋势(v4 增强:趋势图标 + aria-label) | | `components/child-grade-detail.tsx` | 165 | v4 新增:成绩详情视图(按科目分组分析) | | `components/child-exam-detail.tsx` | 165 | V3-11 新增 / ✅ P3-4 改进:子女考试详情视图(汇总卡片+考试列表,对标智学网家长端;P3-4:所有硬编码英文文案改为 i18n 翻译键 `examHomework.homework.parentExam.*`,新增 ChevronRight 导航图标,ul/li 语义化标签 + ARIA 属性,触控目标 ≥ 44px) | | `components/child-schedule-card.tsx` | 119 | 课表卡片(v4 增强:周课表视图) | | `components/parent-children-data-page.tsx` | 92 | 共享数据页(v4 增强:headerExtra) | **路由清单**: | 路由 | 文件 | 说明 | |------|------|------| | `/parent/dashboard` | `dashboard/page.tsx` + `loading.tsx` | 家长仪表盘 | | `/parent/grades` | `grades/page.tsx` + `loading.tsx` | 多子女成绩聚合(v3-P2:并行查询 getClassAverageTrend + GradeTrendCard) | | `/parent/diagnostic` | `diagnostic/page.tsx` + `loading.tsx` + `error.tsx` | P2-5 新增:多子女学情诊断聚合(v3-P2:practiceHrefBase={null} 隐藏练习按钮) | | `/parent/attendance` | `attendance/page.tsx` + `loading.tsx` | 多子女考勤聚合(v4 新增预警横幅) | | `/parent/leave` | `leave/page.tsx` + `loading.tsx` | v4 新增:请假申请(占位) | | `/parent/children/[studentId]` | `children/[studentId]/page.tsx` + `loading.tsx` | 子女详情页(v4 重构:Tab 布局 + 多子女切换) | | `error.tsx` | `error.tsx` | v4 新增:错误边界 | --- ## 2.20 elective(选课模块) **职责**:选修课程管理 + 学生选课 + 抽签。 **导出函数**: - Actions(11 个):`getElectiveCoursesAction` / `createElectiveCourseAction` / `updateElectiveCourseAction` / `deleteElectiveCourseAction` / `getStudentSelectionsAction` / `selectCourseAction` / `dropCourseAction` / `runLotteryAction` / `getAvailableCoursesForStudentAction` / `openSelectionAction` / `closeSelectionAction`(✅ P2-2:8 个写 Action 错误消息改用 `getTranslations("elective")` 国际化) - Data-access:`getElectiveCourses` / `getElectiveCourseById` / `createElectiveCourse` / `updateElectiveCourse` / `deleteElectiveCourse` / `openSelection` / `closeSelection` / `buildCourseSelect` / `mapCourseRow` / `resolveCourseDisplayNames`(✅ P2-7:使用 React `cache()` 包装 subject/grade options 请求级去重;✅ P2-8:通过 `resolvers.ts` 接口抽象跨模块依赖)/ `CourseCoreRow` - Data-access-operations:`selectCourse`(✅ P2-9:新增 `checkScheduleConflict` 时间冲突检测;✅ P2-10:新增 `checkCreditLimit` 学分上限校验;✅ P2-4:学分上限改用 `getElectiveCreditLimit` 配置化,按年级覆盖;✅ P2-4:FCFS 录取后触发 `notifyCapacityThresholdIfNeeded` 容量阈值通知)/ `dropCourse`(✅ P2-4:新增 `dropReason` 参数 + `dropDeadline` 校验)/ `runLottery` / `buildLotteryRankCase`(⚠️ 未导出,无法单测)/ `checkScheduleConflict`(P2-9 新增)/ `checkCreditLimit`(P2-10 新增,P2-4 重构为配置化) - Data-access-selections:`getCourseSelections` / `getStudentSelections` / `getStudentGradeId`(✅ P2-8:通过 `resolvers.ts` StudentGradeResolver 接口抽象;✅ P2-4:被 `selectCourse` 调用以获取年级用于学分上限)/ `getAvailableCoursesForStudent` / `resolveStudentDisplayNames`(✅ P2-8:通过 `resolvers.ts` CourseDisplayResolver 接口抽象) - Data-access-settings(✅ P2-4 新增):`getElectiveCreditLimit(gradeId?)`(按年级覆盖的学期学分上限,fallback 到全局,默认 10)/ `getCapacityNotifyThreshold()`(容量阈值通知比例,默认 0.9)/ `ELECTIVE_DEFAULTS` - Resolvers(✅ P2-8 新增):`CourseDisplayResolver` / `StudentGradeResolver`(接口)/ `getCourseDisplayResolver` / `getStudentGradeResolver` / `setCourseDisplayResolver` / `setStudentGradeResolver` / `resetResolvers`(可注入测试 mock) - Lib 纯函数(✅ P2-9 新增):`parseSchedule`(解析时间段字符串)/ `isScheduleConflict`(判断两个时间段是否冲突) - Import-export(✅ P2-11 新增):`exportElectiveCoursesToExcel`(课程列表导出)/ `exportCourseSelectionsToExcel`(选课名单导出) - Components:`ElectiveCourseList`(课程卡片网格 + 管理操作)/ `ElectiveCourseForm`(课程创建/编辑表单)/ `ElectiveFilters`(nuqs 筛选栏)/ `StudentMySelectionsSection` + `StudentAvailableCoursesSection`(✅ P2-3:由原 `StudentSelectionView` 拆分为两个独立客户端组件,支持 Suspense 流式渲染)/ `ElectivePageLayout`(✅ P2-4 新增:页面布局骨架,admin/teacher 选课页复用)/ `ElectiveStatsCards`(✅ P1-13 新增)/ `ElectiveCourseDetail`(✅ P2-2 新增:课程详情复用组件)/ `ParentSelectionView`(✅ P0-2 新增:家长只读视图) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`school`(✅ P3 已修复:通过 school data-access.getSubjectOptions/getGradeOptions 获取科目/年级名称;✅ P2-8:通过 resolvers.ts 接口抽象)、`users`(✅ P3 已修复:通过 users data-access.getUserNamesByIds;✅ P2-8:通过 resolvers.ts 接口抽象)、`classes`(通过 classes data-access.getStudentActiveGradeId;✅ P2-8:通过 resolvers.ts 接口抽象)、`next-intl`(✅ P2-2:Server Action 错误消息 i18n;✅ P2-4:容量阈值通知文案 i18n)、`notifications`(✅ P2-4 新增:通过 `sendNotification` 发送容量阈值通知给教师/管理员)、`systemSettings` 表(✅ P2-4:读取 elective 学分上限与容量阈值配置) - 被依赖:`app/api/export`(✅ P2-11:通过 `exportElectiveCoursesToExcel` / `exportCourseSelectionsToExcel`) **已知问题**(详见 `docs/architecture/audit/elective-audit-report.md` 与 `attendance-elective-audit-report.md`): - ✅ P2-2 已修复:8 个写 Action 错误消息改用 `getTranslations("elective")` 国际化 - ✅ P2-4 已修复:新增 `ElectivePageLayout` 组件,admin/teacher 选课页复用布局 - ✅ P2-7 已修复:`resolveCourseDisplayNames` 使用 React `cache()` 包装 subject/grade options 请求级去重 - ✅ P2-8 已修复:新增 `resolvers.ts`,跨模块依赖(users/school/classes)通过接口抽象,支持测试注入 mock - ✅ P2-9 已修复:`selectCourse` 新增时间冲突检测(`parseSchedule` + `isScheduleConflict` 纯函数 + `checkScheduleConflict` 异步查询) - ✅ P2-10 已修复:`selectCourse` 新增学分上限校验(`checkCreditLimit`,MAX_CREDIT_PER_TERM=10) - ✅ P2-11 已修复:新增 `export.ts`,课程列表/选课名单可导出 Excel - ✅ A-02 已修复:业务逻辑从 data-access 抽离至 lib/——`lib/schedule-conflict.ts`(normalizeDay/parseSchedule/isScheduleConflict/hasAnyScheduleConflict 纯函数)、`lib/lottery.ts`(buildLotteryRankCase/fisherYatesShuffle/partitionLottery 纯函数);data-access-operations.ts 仅保留 DB CRUD + 调用 lib 函数,并通过 re-export 保持向后兼容 - ✅ P0-1 已修复(2026-06-25):教师页面跳转 admin 路由修复,新增 `teacher/elective/create` 与 `teacher/elective/[id]/edit` 路由(含权限校验、教师越权防护) - ✅ P0-2 已修复(2026-06-25):新增 `parent/elective/page.tsx`(含 loading.tsx + error.tsx),使用 `ParentChildrenDataPage` 模式 + `parentId+studentId` 双重校验 - ✅ P0-3 已修复(2026-06-25):`admin/elective/page.tsx` 与 `student/elective/page.tsx` 入口添加 `requirePermission(Permissions.ELECTIVE_READ)` 纵深防御 - ✅ P0-4 已修复(2026-06-25):`data-access-operations.ts` 引入 `ElectiveBusinessError`(带 i18n code + params),由 `actions.ts` 通过 `translateBusinessError` 翻译后再返回 - ✅ P0-5 已修复(2026-06-25):`export.ts` 移除 `as` 类型断言,改用 `ElectiveCourseStatusEnum.safeParse()` 类型守卫 - ✅ P1-6 已修复(2026-06-25):`elective-course-form.tsx` 全量 i18n 化,所有硬编码英文替换为 `t("form.*")` 键 - ✅ P1-7 已修复(2026-06-25):`elective-course-list.tsx` 中 `` 替换为 Next.js `` - ✅ P1-8 已修复(2026-06-25):3 个 `error.tsx` 文案修正为 `t("errors.title")/t("errors.description")/t("actions.retry")` - ✅ P1-9 已修复(2026-06-25):`export.ts` 表头全量 i18n 化(statusHeader/priorityHeader/selectedAtHeader/enrolledAtHeader/indexHeader) - ✅ P1-10 已修复(2026-06-25):`getElectiveCourses` / `getElectiveCourseById` 移除 try-catch 静默吞错,让异常冒泡到 RSC 触发 error.tsx - ✅ P1-11 已修复(2026-06-25):`parseSchedule` 重写,支持完整英文星期(Monday/Tuesday 等)+ 中文(周一/星期一)+ 多时段(逗号/分号分隔) - ✅ P1-12 已修复(2026-06-25):`runLottery` 不再自动将课程置为 `closed`,保留 `status="open"` 支持重抽 - ✅ P1-13 已修复(2026-06-25):新增 `data-access-stats.ts` 与 `ElectiveStatsCards` 组件,集成到 admin 列表页顶部展示 4 项关键指标 - ✅ P2-1 已修复(2026-06-25):新增 `tests/integration/elective/elective-pure-functions.test.ts`(35 个单测,覆盖 `parseSchedule` / `isScheduleConflict` / `normalizeDay` / `buildLotteryRankCase` / `mapCourseRow`) - ✅ P2-2 已修复(2026-06-25):新增 `ElectiveCourseDetail` 复用组件 + admin/student 课程详情页(含 loading.tsx) - ✅ P2-3 已修复(2026-06-25):student 页面拆分"我的选课"与"可选课程"为两个独立 Suspense 边界;admin 列表与统计卡片分离 Suspense;`StudentSelectionView` 拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection`;新增 4 个异步 RSC loader(`MySelectionsLoader` / `AvailableCoursesLoader` / `StatsCardsLoader` / `CourseListLoader`)+ 分段骨架屏 - ✅ P2-4 已修复(2026-06-25):配置化学分上限(`data-access-settings.ts` 读取 systemSettings,按年级覆盖,默认 10);退课截止时间 `dropDeadline` 字段 + 校验;退课理由 `dropReason` 字段(可选填写,最长 255 字符);容量阈值通知(FCFS 录取后跨过 `capacityNotifyThreshold` 阈值时通知教师,fire-and-forget);新增 `ElectiveErrorCode.dropDeadlinePassed`;通知文案 i18n 化 - ✅ P1 已修复:~~`buildCourseSelect` 跨模块 join users/subjects/grades 表~~ 改为只查 electiveCourses 表,通过 `resolveCourseDisplayNames` 调用 school/users data-access 获取显示名称 - ✅ P1 已修复:~~`getSubjectOptions` 本地直查 subjects 表且与 school 模块重复~~ 删除本地实现,改用 `school/data-access.getSubjectOptions` - ✅ P1 已修复:~~`selectCourse`/`dropCourse` 缺事务包裹~~ 改为 `db.transaction` 包裹,FCFS 模式下使用 `FOR UPDATE` 行锁防止并发超卖 - ✅ P2 已修复:~~`mapCourseRow` 在 data-access.ts 与 data-access-selections.ts 重复定义~~ 抽取到 data-access.ts 统一导出,data-access-selections.ts 复用 - ✅ P2 已修复:~~`runLottery` 使用 `sort(() => Math.random() - 0.5)` 有偏 shuffle~~ 改为 Fisher-Yates 无偏洗牌算法 - ✅ P2 已修复:~~`selectCourse` FCFS 并发超卖风险~~ 使用 `db.transaction` + `.for("update")` 行锁 - ✅ 权限校验完整(ELECTIVE_MANAGE/SELECT/READ,所有页面入口均调用 `requirePermission`) - ⚠️ 无 P2 待办:所有审计报告 P0/P1/P2 项均已实施完毕 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `actions.ts` | 348 | 11 个 Server Action(P0-4:translateBusinessError 翻译 ElectiveBusinessError) | | `data-access.ts` | 258 | 课程 CRUD + scope 过滤 + 共享映射函数(P1-10:移除静默吞错) | | `data-access-operations.ts` | 374 | 选课操作(P0-4:ElectiveBusinessError + i18n code;P1-11:parseSchedule 支持完整星期+多时段;P1-12:抽签可重跑;A-02:纯函数抽离至 lib/,仅保留 DB CRUD + re-export 向后兼容) | | `lib/schedule-conflict.ts` | 108 | 时间冲突检测纯函数(A-02 新增:normalizeDay/parseSchedule/isScheduleConflict/hasAnyScheduleConflict) | | `lib/lottery.ts` | 72 | 抽签算法纯函数(A-02 新增:buildLotteryRankCase/fisherYatesShuffle/partitionLottery) | | `data-access-selections.ts` | 147 | 选课记录查询 + 学生可选课程 | | `data-access-stats.ts` | 88 | 管理员概览统计(P1-13 新增:getElectiveOverviewStats) | | `resolvers.ts` | 83 | 跨模块依赖接口抽象 | | `export.ts` | 102 | Excel 导出(P0-5:移除 as 断言;P1-9:表头全量 i18n) | | `schema.ts` | 179 | Zod 校验 | | `types.ts` | 73 | 类型定义 | | `constants.ts` | 55 | i18n key 映射 + Badge variant + 类型守卫 | | `components/elective-course-list.tsx` | 236 | 课程卡片网格 + 管理操作(P1-7:a 转 Link) | | `components/elective-course-form.tsx` | 301 | 课程创建/编辑表单(P1-6:全量 i18n 化) | | `components/elective-filters.tsx` | 51 | nuqs 筛选栏(搜索 + 模式) | | `components/student-selection-view.tsx` | 278 | 学生选课视图(✅ P2-3:拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection` 两个独立客户端组件,支持 Suspense 流式渲染) | | `components/elective-page-layout.tsx` | 30 | 页面布局骨架 | | `components/elective-stats-cards.tsx` | 73 | 管理员统计卡片网格(P1-13 新增) | | `components/parent-selection-view.tsx` | 80 | 家长查看子女选课只读视图(P0-2 新增) | | `components/elective-course-detail.tsx` | 130 | 课程详情复用组件(P2-2 新增:admin/student 共用) | | `app/(dashboard)/student/elective/_components/my-selections-loader.tsx` | 17 | 异步 RSC loader:我的选课(P2-3 新增) | | `app/(dashboard)/student/elective/_components/available-courses-loader.tsx` | 45 | 异步 RSC loader:可选课程 + 过滤逻辑(P2-3 新增) | | `app/(dashboard)/admin/elective/_components/stats-cards-loader.tsx` | 12 | 异步 RSC loader:统计卡片(P2-3 新增) | | `app/(dashboard)/admin/elective/_components/course-list-loader.tsx` | 30 | 异步 RSC loader:课程列表 + 状态过滤(P2-3 新增) | --- ## 2.21 proctoring(监考模块) **职责**:考试监考事件记录 + 防作弊监控 + 监考面板。 **导出函数**: - Actions:`recordProctoringEventAction` / `getProctoringDashboardAction` - Data-access:`recordProctoringEvent` / `getExamSubmissionForProctoring` / `getExamForProctoring` / `getExamProctoringSummary` / `getStudentProctoringStatuses` / `getRecentProctoringEvents`(✅ P2 已修复:`getExamProctoringSummary` 使用 `Promise.all` 并行化考试信息与提交记录查询、事件类型统计与学生事件统计查询;合并两次 filter 为单次循环统计 started/submitted) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamForProctoringCrossModule/getExamSubmissionForProctoringCrossModule/getExamSubmissionsForExam/getExamTitleById)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds) - 被依赖:无 **已知问题**: - ✅ P1-2 已修复:`exam-mode-config.tsx` 已集成到考试表单(`exam-form.tsx` 调用 ``),移除全部 10 处 `as` 类型断言,改用 `useFormContext` 替代 `Control` prop 避免 `Control` 不变型问题 - ✅ P1-3 已修复:`proctoring-dashboard.tsx` 用类型守卫函数 `isProctoringEventType` + `toProctoringEventTypes` 替代 `Object.keys(...) as ProctoringEventType[]` 断言 - ✅ P0-6 已修复:~~事件上报存在 Server Action 与 REST API 双通道重复~~ 删除 `/api/proctoring/event` REST 路由(移至 deletes/),Server Action `recordProctoringEventAction` 为唯一规范路径 - ✅ P1-1 已修复:~~跨模块直查 `exams`/`examSubmissions`/`users`~~ 改为通过 exams/users data-access 函数获取数据 - ✅ P2 已修复:`actions.ts` 不再直接 import `db` 和 `examSubmissions`,submission 归属校验已下沉到 data-access;`recordProctoringEventAction` 改用 `requirePermission(EXAM_SUBMIT)` 并增加 `revalidatePath` - ✅ P2 已修复:~~`getStudentProctoringStatuses` 串行查询(getUserNamesByIds 后再查事件)~~ 改为 `Promise.all` 并行拉取学生姓名与事件记录 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `data-access.ts` | 409 | 事件记录 + 查询 + 摘要统计 + submission 归属校验 | | `actions.ts` | 139 | 2 个 Server Action | | `types.ts` | 136 | 类型定义 + 标签常量 + 阈值常量 | | `components/anti-cheat-monitor.tsx` | - | 学生端防作弊监控 | | `components/exam-mode-config.tsx` | - | 考试模式配置(✅ P1-2 已修复:已集成到 exam-form,useFormContext 替代 Control prop,无 as 断言) | | `components/proctoring-dashboard.tsx` | - | 教师监考面板(✅ P1-3 已修复:类型守卫函数替代 as 断言) | --- ## 2.22 diagnostic(学情诊断模块) **职责**:知识点掌握度查询 + 诊断报告生成。 **导出函数**: - Actions:`generateStudentReportAction` / `generateClassReportAction` / `generateGradeReportAction`(v4-P2-3 新增:生成年级诊断报告)/ `publishReportAction`(v4-P1-4 + v4-P1-5 增强:班级报告发布时通知全班学生 + 全班学生家长,个人报告通知学生本人 + 其家长,调用 `parent/data-access.getParentIdsByStudentIds` 批量查询家长 ID)/ `deleteReportAction`(v2-P2-3 修复:删除死代码 `getDiagnosticReportsAction` / `getDiagnosticReportByIdAction`,页面直接调用 data-access 并自行权限校验)/ `exportDiagnosticReportAction`(v3-P2-4 新增:导出 Excel)/ `getClassStudentsByKnowledgePointAction`(v3-P2-5 新增:按知识点筛选学生) - Data-access:`updateMasteryFromSubmission`(v2-P1-8 修复:累积模式;v2-P2-5 修复:db.transaction 包裹)/ `getStudentMastery`(P3-19 修复:移除 export,改为模块内部函数)/ `getStudentMasterySummary`(P3-18 修复:getUserNamesByIds 与 getStudentMastery 并行查询)/ `getClassMasterySummary`(v2-P2-4 修复:totalStudents 语义 + 班级平均掌握度按学生平均)/ `getGradeMasterySummary`(v4-P2-3 新增:年级掌握度摘要)/ `getKnowledgePointStats`(v2-P1-7 修复:页面先查班级再传参)/ `getClassStudentsByKnowledgePoint`(v3-P2-5 新增) - Data-access-reports:`generateDiagnosticReport` / `generateClassDiagnosticReport`(v2-P2-6 修复:校验掌握度数据;P3-27 修复:使用 DiagnosticReportError 结构化错误码)/ `generateGradeDiagnosticReport`(v4-P2-3 新增:生成年级诊断报告)/ `getDiagnosticReports`(P3-15 修复:支持分页 limit/offset,返回 { reports, total } 结构;v4-P1-1 增强:新增可选 `scope?: DataScope` 参数,支持 children/class_taught 行级权限过滤,class_taught 通过 `getStudentIdsByClassIds` 解析所教班级学生 ID)/ `getDiagnosticReportById` / `publishDiagnosticReport` / `deleteDiagnosticReport`(✅ P2 已修复:使用 `React.cache()` 包装实现请求级 memoization;P3-1 修复:toNumber 从 grades 模块导入)/ `DiagnosticReportError`(P3-27 新增:结构化错误码类,v4-P2-3 新增 GRADE_NOT_FOUND/GRADE_NO_MASTERY_DATA 错误码) - Stats-service(✅ v2-P1-6 新增):`serializeMasteryWithKp` / `computeAverageMastery` / `classifyStrengthsWeaknesses`(P3-16 修复:弱项阈值从 <60 改为 <80,消除 60-79 盲区)/ `buildStudentMasterySummary` / `aggregateClassMastery` / `computeKpStats` / `computeClassAverageMastery` / `buildStudentsNeedingAttention` / `buildClassMasterySummary` / `buildGradeMasterySummary`(v4-P2-3 新增)/ `buildStudentReportContent` / `buildClassReportContent` / `buildGradeReportContent`(v4-P2-3 新增)/ `computeMasteryLevel` / `serializeMastery`(从 data-access / data-access-reports 抽取的纯函数) - Export(v3-P2-4 新增):`exportDiagnosticReportToExcel`(v4-P2-1 增强:班级报告导出新增知识点统计+需关注学生 Sheet;i18n 全覆盖)/ `buildDiagnosticReportFilename`(v4-P2 改为 async,使用 i18n 翻译文件名) - Role-config(v4-P2-2 新增):`DIAGNOSTIC_ROLE_CONFIG` / `getDiagnosticRoleConfig` / `DiagnosticRole` 类型(配置驱动角色差异,新增角色只需添加配置项) - Schema:`GenerateStudentReportSchema` / `GenerateClassReportSchema` / `GenerateGradeReportSchema`(v4-P2-3 新增)/ `PublishReportSchema` / `DeleteReportSchema`(v2-P2-3 修复:删除死代码 `GetDiagnosticReportsSchema` / `GetDiagnosticReportByIdSchema`) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamSubmissionWithAnswers)、`questions`(✅ P1-1 已修复:通过 questions data-access.getKnowledgePointsForQuestions)、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getActiveStudentIdsByClassId;v2-P1-7 新增 getStudentActiveClassId)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds/getUserIdsByGradeId)、`grades`(P3-1 修复:通过 grades/lib/grade-utils.toNumber 复用工具函数)、`school`(v4-P2-3 新增:通过 school data-access.getGradeNameById 获取年级名称) - 被依赖:无 **已知问题**: - ✅ P1-1 已修复:~~`updateMasteryFromSubmission` 跨模块直查 4 张表~~ 改为调用 `exams/data-access.getExamSubmissionWithAnswers` 和 `questions/data-access.getKnowledgePointsForQuestions` - ✅ P2 已修复:~~`data-access-reports.ts` 有未使用代码(`round2` + `void round2`)~~ 已删除死代码 - ✅ P2 已修复:~~`updateMasteryFromSubmission` 循环内串行 await upsert~~ 改为 `Promise.all` 并行执行所有 upsert - ✅ P2 已修复:~~`getClassMasterySummary` 串行查询~~ 改为两组 `Promise.all` 并行 - ✅ P2 已修复:~~`getDiagnosticReports` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注 - ✅ P0-2 已修复:~~`data-access-reports.ts` 直查 `users` 表获取姓名~~ 改为通过 `users/data-access.getUserNamesByIds` - ✅ P2-2 已修复:~~Tailwind 任意值~~ 改用标准 Tailwind 类 - ✅ P2-1 已修复:~~图表/表格/列表缺少 a11y ARIA 属性~~ 为 5 个图表添加 `role="img"` + `aria-label`,2 个表格添加 ``,3 个列表添加 `role="list"`,3 个图标按钮添加 `aria-label` - ✅ P2-3 已修复:~~班级报告将生成者 ID 存入 `studentId` 字段~~ schema 改为可空,班级报告 `studentId` 置空 - ✅ v2-P1-6 已修复:~~统计逻辑混在 data-access 层~~ 抽取 `stats-service.ts`(352 行,12 个纯函数 + 2 个常量 + 4 个接口) - ✅ v2-P1-7 已修复:~~`getKnowledgePointStats` 无参调用~~ 页面先查 `getStudentActiveClassId` 再传参 - ✅ v2-P1-8 已修复:~~`updateMasteryFromSubmission` 覆盖模式~~ 改为累积计算(读取已有记录后累加) - ✅ v2-P2-3 已修复:~~死代码 `getDiagnosticReportsAction` / `getDiagnosticReportByIdAction` 全局零调用~~ 已删除,页面直接调用 data-access - ✅ v2-P2-4 已修复:~~`totalStudents` 语义错误 + 班级平均掌握度计算偏差~~ 改为实际有掌握度记录的学生数;先算学生个人平均再取平均 - ✅ v2-P2-5 已修复:~~多 upsert 无事务包裹~~ 使用 `db.transaction()` 保证原子性 - ✅ v2-P2-6 已修复:~~生成报告未校验掌握度数据~~ 添加 `totalKnowledgePoints === 0` 和 `studentCount === 0` 校验 - ✅ v2-P1-4 已修复:~~4 个组件 i18n 完全未接入~~ 全部接入 `useTranslations("diagnostic")` - ✅ v2-P2-7 已修复:~~`report-list.tsx` 过滤器 Label 缺少 `htmlFor`~~ 添加 `htmlFor` 和 `id` - ✅ 与 grades 模块无职责重叠 - ✅ v3-P2 改进(2026-06-23):`StudentDiagnosticView` 新增 `practiceHrefBase` prop(string | null),null 时隐藏练习按钮;teacher/diagnostic/student/[studentId] 传入 `practiceHrefBase="/teacher/questions"`,parent/diagnostic 传入 `practiceHrefBase={null}` 隐藏练习按钮 - ✅ P3-1 已修复(2026-06-23):~~`data-access-reports.ts` 本地定义 `toNumber` 与 `grades/lib/grade-utils.ts` 重复~~ 改为从 grades 模块导入 - ✅ P3-15 已修复(2026-06-23):~~`getDiagnosticReports` 无分页,可能返回大量数据~~ 添加 limit/offset 分页支持,返回 `{ reports, total }` 结构,Promise.all 并行查询总数和数据 - ✅ P3-16 已修复(2026-06-23):~~强弱项分类存在 60-79 盲区~~ 弱项阈值从 <60 改为 <80,确保所有知识点都被分类 - ✅ P3-17 已修复:~~班级报告 strengths 无数量上限~~ `buildClassReportContent` 中 strengths 已限制为前 5 个(按掌握度降序) - ✅ P3-18 已修复(2026-06-23):~~`getStudentMasterySummary` 串行查询用户名和掌握度~~ 改为 Promise.all 并行查询 - ✅ P3-19 已修复(2026-06-23):~~`getStudentMastery` 使用 export 但仅内部使用~~ 移除 export,改为模块内部函数 - ✅ P3-27 已修复(2026-06-23):~~`generateDiagnosticReport` / `generateClassDiagnosticReport` 中 `throw new Error("...")` 直接暴露给用户~~ 改为使用 `DiagnosticReportError` 结构化错误码(继承 BusinessError) - ✅ P3-21 已修复(2026-06-23):`ClassDiagnosticView` View 按钮已有描述性 aria-label(`classDiagnostic.viewAriaLabel`,含学生名) - ✅ P3-22 已修复(2026-06-23):`StudentDiagnosticView` Practice 按钮添加 aria-label(`studentDiagnostic.practiceAriaLabel`,含知识点名) - ✅ v4-P3-6 已修复(2026-06-23):`MasteryRadarChart` 雷达图添加 `tabIndex={0}` 键盘可达性 + `focus-visible` 焦点样式(原有 `role="img"` + `aria-label` 保留) - ✅ v4-P1-1 改进(2026-06-23):`getDiagnosticReports` 新增可选 `scope?: DataScope` 参数,支持 children/class_taught 行级权限过滤;teacher/diagnostic/page.tsx 传入 dataScope;student/diagnostic/page.tsx 传 status:"published" + dataScope;parent/diagnostic/page.tsx 传 status:"published" + dataScope + 错误卡片 - ✅ v4-P1-3 改进(2026-06-23):`student-diagnostic-view.tsx` 移除草稿回退逻辑(不再展示 draft 状态报告) - ✅ v4-P1-4 + v4-P1-5 改进(2026-06-23):`publishReportAction` 班级报告发布时通知全班学生 + 全班学生家长(通过 `getStudentIdsByClassId` 获取全班学生,通过 `parent/data-access.getParentIdsByStudentIds` 获取家长 ID);个人报告通知学生本人 + 其家长 - ✅ v4-P1-8 + v4-P1-11 改进(2026-06-23):`class-diagnostic-view.tsx` 热力图添加图例 + 表格 `overflow-x-auto` 水平滚动 - ✅ v4-P1-9 改进(2026-06-23):parent/diagnostic/page.tsx 新增错误卡片(Error Card)展示子女诊断查询失败原因 - ✅ v4-P1 数据库变更(2026-06-23):`learningDiagnosticReports` 表新增 `classId` 字段(varchar(128), references classes.id, onDelete: set null)+ `diagnostic_class_idx` 索引;`DiagnosticReport` 接口新增 `classId: string | null` 字段(班级报告关联班级 ID,个人报告为 null) - ✅ v4-P1 师生关系校验(2026-06-23):teacher/diagnostic/student/[studentId]/page.tsx 新增 class_taught 师生关系校验 - ✅ v4-P2 审计修复(2026-06-24):P0-1 跨模块 import WidgetBoundary → 提升到 `shared/components/widget-boundary.tsx`,diagnostic 和 grades 模块统一从 shared 引用 - ✅ v4-P2 审计修复(2026-06-24):P0-2/P0-3 教师页面标题 + error.tsx 硬编码 → 接入 `getTranslations("diagnostic")` / `useTranslations("diagnostic")` - ✅ v4-P2 审计修复(2026-06-24):P1-1 parent 错误卡片中英文混用 → 提取完整 i18n 键 `error.childLoadFailed` / `error.childLoadFailedDesc` - ✅ v4-P2 审计修复(2026-06-24):P1-2 `as` 类型断言 → 改用类型守卫函数 `isReportType` / `isReportStatus` 返回值收窄 - ✅ v4-P2 审计修复(2026-06-24):P1-3 分享链接指向不存在路由 → 移除分享功能(无报告详情页需求) - ✅ v4-P2 审计修复(2026-06-24):P1-4 报告内容硬编码中文 → stats-service 接收 `ReportContentTranslations` 接口参数,actions 层从 next-intl 构建翻译注入 - ✅ v4-P2 审计修复(2026-06-24):P1-5 Excel 导出表头硬编码 → export.ts 使用 `getTranslations("diagnostic.exportContent")`,`buildDiagnosticReportFilename` 改为 async - ✅ v4-P2 审计修复(2026-06-24):P2-1 班级报告导出缺少明细 → 利用 `classId` 字段查询 `getClassMasterySummary`,导出知识点统计 Sheet + 需关注学生 Sheet - ✅ v4-P2 审计修复(2026-06-24):P2-2 角色差异通过 props 硬编码 → 新增 `role-config.ts`,`DIAGNOSTIC_ROLE_CONFIG` 配置对象驱动 `practiceHrefBase`,组件接受 `role` prop - ✅ v4-P2 审计修复(2026-06-24):P2-3 无年级诊断报告入口 → 新增 `gradeId` 字段(schema + 迁移 0012)、`GradeMasterySummary` 类型、`getGradeMasterySummary` / `buildGradeMasterySummary` / `buildGradeReportContent` / `generateGradeDiagnosticReport` / `generateGradeReportAction` / `GenerateGradeReportSchema`,i18n 新增 `gradeSummary` / `gradeRecommendation` / `gradeNoWeakness` - ✅ v4-P2 审计修复(2026-06-24):P2-4 热力图色块缺少键盘导航 → 添加 `tabIndex={0}` + `focus-visible:ring-2` 样式,外层容器 `role` 从 `img` 改为 `group` - ✅ v2-P0-1 审计修复(2026-06-25):3 个子路由 error.tsx 硬编码中文 → teacher/diagnostic/class/[classId]/error.tsx、teacher/diagnostic/student/[studentId]/error.tsx、parent/diagnostic/error.tsx 全部接入 `useTranslations("diagnostic")`,新增 `error.classLoadFailed/Desc`、`error.studentLoadFailed/Desc`、`error.parentLoadFailed/Desc` i18n 键 - ✅ v2-P1-1 审计修复(2026-06-25):~~`getDiagnosticReports` 未过滤 `grade_managed` DataScope~~ 在 data-access-reports.ts 中新增 `grade_managed` 分支,调用 `users/data-access.getUserIdsByGradeId` 解析年级学生 ID 后用 `inArray` 过滤,修复年级主任越权查看全校报告的安全漏洞 - ✅ v2-P1-2 审计修复(2026-06-25):i18n 弱项标签"弱项(<60%)"与代码逻辑(<80%)不一致 → 修正为"弱项(<80%)" - ✅ v2-P1-3 审计修复(2026-06-25):热力图 aria-label 硬编码中文标点 → 使用 i18n 模板键 `classDiagnostic.heatmapCellAriaLabel: "{name}:{level}%,{label},{mastered}/{total}"` - ✅ v2-P1-4 审计修复(2026-06-25):组件直接 import actions 无接口抽象 → 新增 `services/diagnostic-service.ts`(DiagnosticService 接口)、`services/diagnostic-service-context.tsx`(DiagnosticServiceProvider + useDiagnosticService)、`services/default-diagnostic-service.ts`(默认实现绑定 Server Actions),report-list.tsx 和 class-diagnostic-view.tsx 改用 Context 注入 - ✅ v2-P1-5 审计修复(2026-06-25):置信度计算过于简化(仅 null/非 null 两级)→ 改为基于知识点数量的多级判断(0 insufficient / 1-3 low / 4-8 medium / >8 high) - ✅ v2-P1-6 审计修复(2026-06-25):无 Error Boundary 包裹独立数据区块 → student-diagnostic-view(雷达图/强弱项/最新报告/历史报告)和 class-diagnostic-view(概览/热力图/筛选/排名/关注列表/生成报告)各数据区块均用 WidgetBoundary 包裹 - ✅ v2-P2-1 审计修复(2026-06-25):死 i18n 键未清理 → 移除 9 个 share 相关 i18n 键(share/shareAriaLabel/shareTitle 等) - ✅ v2-P2-2 审计修复(2026-06-25):教师报告列表页客户端过滤 class_members → 移除客户端 filter,DataScope 过滤已在 data-access 层完成 - ✅ v2-P2-3 审计修复(2026-06-25):export.ts 错误消息硬编码英文 → 新增 `DiagnosticExportError` 结构化错误码(继承 BusinessError),`throw new Error("Report not found")` 改为 `throw new DiagnosticExportError("REPORT_NOT_FOUND", ...)` - ✅ v2-P2-5 审计修复(2026-06-25):雷达图名称截断无 tooltip → 改用完整 `knowledgePoint` 作为 angleKey 使 Tooltip 显示完整名称,通过 `angleTickFormatter` 截断轴上显示文本(8 字符+省略号) - ✅ v2-P2-6 审计修复(2026-06-25):通知类型复用 "grade" → NotificationType 新增 "diagnostic" 类型,更新 notification-list/notification-dropdown TYPE_ICON 和 TYPE_KEYS,diagnostic/actions.ts 两处 createNotification 改用 `type: "diagnostic"` - ✅ v2-P2-7 审计修复(2026-06-25):无监控埋点接口 → 新增 `services/diagnostic-monitor.ts`(DiagnosticMonitor 接口 + noopDiagnosticMonitor 实现)、`services/diagnostic-monitor-context.tsx`(DiagnosticMonitorProvider + useDiagnosticMonitor)、`services/monitored-diagnostic-service.ts`(createMonitoredDiagnosticService 工厂函数,组合模式包装服务添加埋点),两个 teacher 页面接入 DiagnosticMonitorProvider - ✅ v2-P2-9 审计修复(2026-06-25):parent/diagnostic noRecordsTitle 与 noRecordsDescription 重复 → 新增 `parent.noReportsTitle` / `parent.noReportsDescription` 独立 i18n 键 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| | `data-access.ts` | 477 | 知识点掌握度查询 + 更新(v2-P1-8 累积模式;v2-P2-5 事务;v2-P2-4 语义修正;v2-P1-6 改用 stats-service 纯函数;v4-P2-3 新增 getGradeMasterySummary) | | `data-access-reports.ts` | 256+ | 诊断报告 CRUD(v2-P2-6 校验;v2-P1-6 改用 stats-service 纯函数;v4-P1-1:getDiagnosticReports 新增 scope 参数支持行级权限过滤;v4-P2-3 新增 generateGradeDiagnosticReport) | | `stats-service.ts` | 388+ | 统计计算纯函数(v2-P1-6 新增:12 个纯函数 + 2 个常量 + 4 个接口;v4-P2-3 新增 buildGradeMasterySummary/buildGradeReportContent) | | `actions.ts` | 274+ | 5 个 Server Action(v2-P2-3 删除 2 个死代码读 Action;v4-P1-4/v4-P1-5:publishReportAction 新增全班学生 + 家长通知;v4-P2-3 新增 generateGradeReportAction) | | `schema.ts` | 31+ | Zod 校验(4 个 schema,v2-P2-3 删除 2 个死代码 schema;v4-P2-3 新增 GenerateGradeReportSchema) | | `types.ts` | 109+ | 类型定义(v4-P2-3 新增 GradeMasterySummary 接口) | | `export.ts` | 165+ | Excel 导出(v3-P2-4 新增;v4-P2-1 班级报告导出新增知识点统计+需关注学生 Sheet;i18n 全覆盖) | | `role-config.ts` | 40+ | 角色配置驱动(v4-P2-2 新增:DIAGNOSTIC_ROLE_CONFIG 配置对象,通过 role prop 驱动 practiceHrefBase 等角色差异) | | `components/class-diagnostic-view.tsx` | 448+ | 班级诊断视图(v2-P1-6 热力图 a11y;v2-P1-4 i18n;v4-P1-8 热力图图例;v4-P1-11 表格 overflow-x-auto;v4-P2-4 热力图色块 tabIndex+focus-visible 键盘导航) | | `components/student-diagnostic-view.tsx` | 293+ | 学生诊断视图(v2-P1-4 i18n;v3-P2 新增:practiceHrefBase prop;v4-P1-3 移除草稿回退逻辑;v4-P2-2 改为 role prop 配置驱动) | | `components/mastery-radar-chart.tsx` | 85 | 雷达图(v2-P1-4 i18n) | | `components/report-list.tsx` | 445 | 报告列表(v2-P2-7 Label htmlFor;v2-P1-4 i18n;v4-P1-3 移除分享功能避免 404) | | `components/confidence-utils.ts` | 31 | 置信度计算(v4-P3-7 新增;v2-P1-5 改为基于知识点数量的多级判断) | | `services/diagnostic-service.ts` | 58 | DiagnosticService 接口(v2-P1-4 新增:抽象客户端可调用的诊断操作) | | `services/diagnostic-service-context.tsx` | 46 | DiagnosticServiceProvider + useDiagnosticService(v2-P1-4 新增:Context 注入) | | `services/default-diagnostic-service.ts` | 86 | 默认服务实现绑定 Server Actions(v2-P1-4 新增) | | `services/diagnostic-monitor.ts` | 95 | DiagnosticMonitor 接口 + noopDiagnosticMonitor(v2-P2-7 新增:监控埋点扩展点) | | `services/diagnostic-monitor-context.tsx` | 56 | DiagnosticMonitorProvider + useDiagnosticMonitor(v2-P2-7 新增) | | `services/monitored-diagnostic-service.ts` | 159 | createMonitoredDiagnosticService 工厂(v2-P2-7 新增:组合模式包装服务添加埋点) | --- ## 2.23 settings(设置模块)— V3 AI 配置统一入口 **职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页(学生/教师概览)+ 学校品牌配置(audit-P2-6 新增:登录/注册页 logo/标语/作者)。V3 将 AI Provider 配置统一到 `/admin/ai-settings` 独立页面,移除 `/settings?tab=ai` 标签页和考试页面内嵌弹窗。V3.1 新增 public/private 可见性模型:管理员发布 public Provider 全员可用,普通用户可创建 private Provider 仅本人可见。audit-P2-6 新增品牌配置:`brand-config.ts` 纯类型 + 默认值(无 `server-only`,可被 Client Component 导入)+ `data-access-brand.ts` 数据访问(`server-only`,从 `system_settings` 表 `brand` 分类读取 4 个键)+ `actions-brand.ts` Server Action + `BrandConfigCard` 管理员配置组件。 **导出函数**: - Actions:`getAiProviderSummaries`(V3.1 增强:按用户身份过滤 public + own private)/ `upsertAiProviderAction`(V3.1 增强:visibility 字段,非管理员强制 private)/ `testAiProviderAction` / `deleteAiProviderAction`(V3 新增删除能力)/ `canConfigurePublicAiProvider`(V3.1 新增:检查当前用户是否拥有 AI_CONFIGURE 权限) - Actions-password:`changePasswordAction`(✅ P1 已修复:使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access) - Actions-avatar:`updateUserAvatarAction` / `removeUserAvatarAction`(✅ P2-8 新增:头像上传/删除,复用 `/api/upload` 路由) - Actions-notifications:`sendTestNotificationAction`(✅ P2-10 新增:发送测试通知,占位实现待接入真实通知服务) - Actions-system-settings:`getAdminSystemSettingsAction` / `saveAdminSystemSettingsAction`(✅ P0-3 新增:管理员系统设置 CRUD,4 分类 Zod 校验) - Actions-brand:`getBrandConfigAction` / `saveBrandConfigAction`(✅ audit-P2-6 新增:品牌配置 CRUD,`requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath` 刷新 `/(auth)/login` / `/(auth)/register` / `/admin/settings`) - Actions-security:`getSecurityCenterAction` / `toggleTwoFactorAction`(✅ P2-9 新增:2FA 状态查询/切换 + 最近登录历史) - Data-access:`getAiProviderSummaries`(管理员视图,返回全部)/ `getAiProviderSummariesForUser`(V3.1 新增:用户视图,返回 public + own private)/ `countDefaultAiProviders` / `getAiProviderForUpdate`(V3.1 增强:可选 userId 做所有权校验)/ `updateAiProvider`(V3.1 增强:visibility 字段)/ `createAiProvider`(V3.1 增强:visibility 字段)/ `deleteAiProvider`(V3 新增:事务删除 + 自动转移默认;V3.1 增强:可选 userId 做所有权校验)/ `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`(P1 新增,从 actions 下沉) - Data-access-system-settings:`getSystemSettingsByCategory` / `getAllSystemSettings` / `getSystemSetting` / `upsertSystemSetting` / `upsertSystemSettings`(✅ P0-3 新增:system_settings 表 CRUD,键值对存储模式;✅ audit-P2-6 `SystemSettingCategory` 联合类型新增 `"brand"`) - Data-access-brand:`getBrandConfig` / `saveBrandConfig`(✅ audit-P2-6 新增:`server-only` 数据访问,从 `system_settings` 表 `brand` 分类并行读取 4 个键 schoolName/logoUrl/testimonialQuote/testimonialAuthor,未配置项使用 `DEFAULT_BRAND_CONFIG` 默认值) - Brand-config(纯类型,无 `server-only`):`BrandConfig`(接口:schoolName / logoUrl: string|null / testimonialQuote / testimonialAuthor)/ `DEFAULT_BRAND_CONFIG`(默认值:schoolName="Next_Edu" / logoUrl=null / 默认英文标语) - Components:`SettingsView`(统一设置页布局,V3 移除 AI 标签页后为 4 标签页 General/Notifications/Appearance/Security;角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入;Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏)、`SettingsServiceProvider` / `useSettingsService`(Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary,局部失败不影响整页)、`QuickLinksCard`(快捷链接卡片,i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`(✅ P0-3 已修复:从 mock 改为真实数据层;✅ v3 已拆分:编排 4 个子 Card)、`SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard`(✅ v3 新增:从 AdminSettingsView 拆分的 4 个独立 Card 组件,接收 values + onChange props)、`AvatarUpload`(✅ P2-8 新增:头像上传/预览/删除客户端组件,文件验证 + i18n)、`SecurityCenterCard`(✅ P2-9 新增;✅ v3 已拆分:改为编排器,加载数据 + 渲染两个子区块)、`SecurityTwoFactorSection`(✅ v3 新增:2FA 启用/关闭/重新生成备份码,从 SecurityCenterCard 拆分)、`SecurityRecentLoginsSection`(✅ v3 新增:最近登录历史 + 远程登出,从 SecurityCenterCard 拆分)、`ThemePreferencesCard`(✅ P2-11 已增强:集成 `LocaleSwitcher` 语言切换)、`AiProviderSettingsCard`(V3 增强:新增删除按钮 + AlertDialog 确认;V3.1 增强:visibility 选择器 + 可见性/归属 Badge;✅ v3 已拆分:提取 Selector + DeleteDialog 子组件)、`AiProviderSelector`(✅ v3 新增:服务商选择器 + 密钥状态展示,从 AiProviderSettingsCard 拆分)、`AiProviderDeleteDialog`(✅ v3 新增:删除确认对话框,从 AiProviderSettingsCard 拆分)、`BrandConfigCard`(✅ audit-P2-6 新增:管理员品牌配置卡片,独立表单 + 独立保存,通过 `getBrandConfigAction`/`saveBrandConfigAction` 调用,挂载在 `AdminSettingsView` 主表单之后以避免嵌套 ``) - Config:`ROLE_SETTINGS_CONFIG` / `resolveRoleSettingsConfig`(配置驱动角色 → 设置视图映射,新增角色只需添加条目)、`ProfileOverviewType` / `resolveProfileOverviewType`(✅ v3 新增:配置驱动角色 → 概览类型映射,消除 roles.includes 硬编码,优先级 student > teacher > none) - Lib:`buildStudentOverviewData` / `computeStudentStats` / `sortUpcomingAssignments` / `filterTodaySchedule` / `toWeekday`(纯数据计算函数,与 UI 分离,便于单元测试)、`toSettingItem` / `SettingItem`(✅ v3 新增:从 actions-system-settings.ts 提取的纯函数,将表单值序列化为数据库存储格式,便于单元测试) - Types:`AiProviderSummary`(V3.1 增强:新增 visibility/createdBy 字段)/ `AiProviderName`(V3.2 增强:新增 'ollama' 本地部署支持)/ `AiProviderVisibility`(V3.1 新增:'public' | 'private')/ `AiProviderExisting`(V3.1 增强:新增 visibility/createdBy 字段)/ `SettingsService` / `ProfileService` / `NotificationPreferenceService`(服务接口定义,用于依赖注入解耦) **依赖关系**: - 依赖:`shared/*`(含 `shared/lib/bcrypt-utils`)、`@/auth`、`messaging`(页面层通过 `SettingsService` 接口注入,组件层不直接 import)、`users`(页面层通过 `SettingsService` 接口注入)、`classes` / `homework` / `dashboard`(ProfileStudentOverview 异步组件获取学生概览数据)、`notifications`(页面层获取通知偏好) - 被依赖:无 **已知问题**: - ⚠️ P2:混合 5 类职责(AI Provider + 密码 + 资料 + 主题 + 通知偏好) - ✅ P1 已修复:~~无 `data-access.ts`,`actions.ts` 直接使用 `db`~~ 新建 `data-access.ts`,所有 DB 操作已下沉 - ✅ P1 已修复:~~`changePasswordAction` 使用 `requireAuth()` 无 Zod 校验~~ 改为 `requirePermission(USER_PROFILE_UPDATE)` + `ChangePasswordSchema` Zod 校验 + 并行查询优化 - ✅ audit-P2-4 已修复:~~`changePasswordAction` 未检测新密码是否在已知数据泄露中,用户可将弱密码(如 "Pass1234")改回使用~~ 在 `validatePassword` 校验通过后、DB 查询前调用 `checkBreachedPassword(newPassword)`,命中则拒绝改密并提示用户更换密码;fail-open 策略:HIBP API 不可用时跳过检查不阻断改密流程 - ✅ audit-P2-6 已修复:~~AuthLayout 学校品牌信息(学校名/logo/标语/作者)硬编码在组件内,无法由管理员动态配置~~ 新增品牌配置完整链路:`brand-config.ts`(纯类型 + `DEFAULT_BRAND_CONFIG` 默认值,无 `server-only` 可被 Client Component 导入)+ `data-access-brand.ts`(`server-only` 数据访问,从 `system_settings` 表 `brand` 分类读取 4 个键)+ `actions-brand.ts`(`requirePermission(SCHOOL_MANAGE)` + Zod 校验 + `revalidatePath`)+ `BrandConfigCard`(管理员配置卡片,独立表单 + 独立保存,挂载在 `AdminSettingsView` 主表单之后以避免嵌套 ``)。`SystemSettingCategory` 联合类型新增 `"brand"`。i18n 键 `settings.brand.*`(11 个键)已添加到 zh-CN + en - ✅ P2 已修复:`actions-password.ts` 删除本地 `normalizeBcryptHash`,统一复用 `shared/lib/bcrypt-utils.normalizeBcryptHash`,消除重复代码 - ✅ P2-a 已修复:~~admin/teacher/student/parent 四个设置视图重复布局~~ 新增 `SettingsView` 统一设置页布局 + `resolveRoleSettingsConfig` 配置驱动角色路由,删除 `parent-settings-view.tsx` / `student-settings-view.tsx` / `teacher-settings-view.tsx` - ✅ parent 角色路由已修复:通过 `ROLE_SETTINGS_CONFIG` 配置驱动,parent 用户正确渲染对应配置 - ✅ Tab URL 持久化已修复:`SettingsView` 改为受控模式,通过 `useSearchParams` 读取 `tab` 参数,`router.push` 更新 URL - ✅ 登出二次确认已修复:`SettingsView` 的 Log out 按钮使用 `AlertDialog` 包裹,点击时弹出确认对话框 - ✅ AiProviderSettingsCard 已集成:`SettingsView` 新增 AI 标签页,条件渲染需 `AI_CONFIGURE` 权限 - ✅ password-change-form 任意值 Tailwind 类已修复:~~`[&>div]:bg-red-500` 等任意值类~~ Progress 组件新增 `indicatorClassName` prop,使用标准颜色类 - ✅ P0 已修复:~~`notification-preferences-form.tsx` 跨模块直接 import messaging/actions~~ 改为通过 `useSettingsService().notifications.updatePreferences` 调用,页面层注入实现 - ✅ P0 已修复:~~`profile-settings-form.tsx` 跨模块直接 import users/actions~~ 改为通过 `useSettingsService().profile.updateProfile` 调用,页面层注入实现 - ✅ P0 已修复:~~i18n 完全缺失~~ 新增 `settings.json` 翻译文件(zh-CN + en),所有组件改用 `useTranslations` / `getTranslations` - ✅ P1 已修复:~~缺少 Error Boundary~~ 新增 `SettingsSectionErrorBoundary`,每个 TabsContent + profile 页面角色概览区块均包裹 - ✅ P1 已修复:~~缺少 Suspense 骨架屏~~ 每个 TabsContent 包裹 `Suspense` + `SettingsSectionSkeleton`;profile 页面包裹 `ProfileStudentOverviewSkeleton` / `ProfileTeacherOverviewSkeleton` - ✅ P1 已修复:~~profile/page.tsx 业务逻辑与 UI 混合~~ 抽取 `buildStudentOverviewData` 等纯函数到 `lib/student-overview-data.ts`;拆分 `ProfileStudentOverview` / `ProfileTeacherOverview` 异步组件 - ✅ 密码修改有速率限制 - ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验 - ✅ P0-3 已修复:~~AdminSettingsView 为 mock 实现,无数据持久化~~ 新增 `system_settings` 表(键值对存储)+ `data-access-system-settings.ts` + `actions-system-settings.ts`,AdminSettingsView 改为真实数据层 - ✅ P2-8 已修复:~~无头像上传~~ 新增 `updateUserAvatar` data-access + `actions-avatar.ts` + `AvatarUpload` 组件,复用 `/api/upload` 路由 - ✅ P2-9 已修复:~~无 2FA / 会话管理~~ 新增 `actions-security.ts` + `SecurityCenterCard` 组件(2FA 开关占位 + 最近登录历史来自 login_logs 表) - ✅ P2-10 已修复:~~通知偏好表单无测试通知按钮~~ 新增 `sendTestNotificationAction`,每个已启用渠道旁显示测试按钮 - ✅ P2-11 已修复:~~语言切换未集成到设置页~~ `ThemePreferencesCard` 集成 `LocaleSwitcher` 到 Appearance 标签页 - ✅ v1.1 已修复:~~`/settings` 页面 ErrorBoundary 触发(Functions cannot be passed directly to Client Components)~~ 新增 `actions-service.ts`("use server" 文件),导出 `updateProfileAction` + `updateNotificationPreferencesAction` 两个 Server Action wrapper;`page.tsx` 直接传递 Server Action 引用;`SettingsService` 接口的 `getProfile`/`getPreferences` 改为可选 - ✅ v1.1 已修复:~~i18n 键双重 `settings.` 前缀(MISSING_MESSAGE)~~ `role-settings-config.tsx` 中 `descriptionKey` 去掉 `settings.` 前缀 - ✅ v1.1 已修复:~~AI 标签页 FormLabel 在 Form 上下文外使用(getFieldState null)~~ `ai-provider-settings-card.tsx` 中两处 `` 改为 `