# Dashboard 模块 V3 审计报告

**审计日期**：2026-06-22
**审计范围**：`src/modules/dashboard/` + 所有 dashboard 路由文件
**前置审计**：v1（P0 修复：跨模块 DB 查询、权限、i18n 容器组件）、v2（10 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签）

---

## 概览

v1/v2 审计解决了表层问题。v3 审计发现了**更深层次的问题**，涉及数据完整性、i18n 完整性、死代码、类型安全、流式架构和测试缺口。最严重的是 admin dashboard 中 ContentRow 标签与值完全错配的 **P0 数据展示 bug**。

| 严重度 | 数量 |
|--------|------|
| P0 | 3 |
| P1 | 10 |
| P2 | 9 |

---

## P0 问题（严重）

### P0-1：Admin Dashboard ContentRow 标签与值错配（数据完整性）

- **文件**：`src/modules/dashboard/components/admin-dashboard/admin-dashboard.tsx`
- **行号**：166-169（Content 区块）、180-181（Homework Activity 区块）
- **问题**："Content" 区块显示教材/章节/题目/考试数量，但使用了用户/班级/待批改/已发布作业的标签。图标正确（Library, BookOpen, FileText, ClipboardList），但标签错误：
  - 行 166：`label={t("stats.users")}` + `value={data.textbookCount}` → 应为 `t("stats.textbooks")`
  - 行 167：`label={t("stats.classes")}` + `value={data.chapterCount}` → 应为 `t("stats.chapters")`
  - 行 168：`label={t("stats.toGrade")}` + `value={data.questionCount}` → 应为 `t("stats.questions")`
  - 行 169：`label={t("stats.homeworkPublished")}` + `value={data.examCount}` → 应为 `t("stats.exams")`
  - 行 180：`label={t("stats.activeAssignments")}` + `value={data.homeworkAssignmentCount}` → 标签说"active"但值是总数
  - 行 181：`label={t("stats.submissionRate")}` + `value={data.homeworkSubmissionCount}` → 标签说"rate"（百分比）但值是原始计数
- **修复**：使用与值匹配的正确翻译键。新增缺失键（`stats.textbooks`、`stats.chapters`、`stats.questions`、`stats.exams`、`stats.totalAssignments`、`stats.totalSubmissions`）到 `messages/{zh-CN,en}/dashboard.json`。

### P0-2：admin/error.tsx 硬编码中文，无 i18n

- **文件**：`src/app/(dashboard)/admin/error.tsx`
- **行号**：12-14
- **问题**：此错误边界有硬编码中文字符串（`"页面加载失败"`、`"抱歉，页面加载时发生了意外错误。请稍后重试。"`、`"重试"`），未导入或使用 `useTranslations`。英文用户会看到中文文本。v2 审计遗漏了此文件，因为只关注了 `dashboard/` 模块而非 `admin/` 路由错误边界。其他 dashboard error.tsx（teacher、parent、root）都正确使用了 `useTranslations`。
- **修复**：导入 `useTranslations`，替换硬编码字符串为 `t("error.loadFailed")`、`t("error.loadFailedDesc")`、`t("error.retry")`。

### P0-3：userGrowth 和 homeworkTrend 永远返回空数组

- **文件**：`src/modules/dashboard/data-access.ts`
- **行号**：46-47
- **问题**：`getAdminDashboardData` 硬编码 `userGrowth: []` 和 `homeworkTrend: []`。`UserGrowthChart` 组件（admin-dashboard.tsx 行 123、133）渲染这些空数组，产生永久空图表且无空状态。架构图（行 973）标注为"待后续接入真实统计"，但至今未修复。用户看到两个空白图表区域，有标题但无数据也无说明。
- **修复**：为 `UserGrowthChart` 添加空状态（当 `data.length === 0` 时显示"暂无数据"），与其他图表组件的空状态保持一致。

---

## P1 问题（高）

### P1-1：admin/dashboard 路由缺失 loading.tsx

- **文件（缺失）**：`src/app/(dashboard)/admin/dashboard/loading.tsx`
- **问题**：admin dashboard 路由无路由级 `loading.tsx`，回退到 `admin/loading.tsx`（通用骨架屏，不匹配 admin dashboard 布局）。Teacher、student、parent 都有 dashboard 专属 `loading.tsx`。
- **修复**：创建 `admin/dashboard/loading.tsx`，骨架屏匹配 `AdminDashboardView` 布局。

### P1-2：admin/dashboard 和 student/dashboard 路由缺失 error.tsx

- **文件（缺失）**：`src/app/(dashboard)/admin/dashboard/error.tsx`、`src/app/(dashboard)/student/dashboard/error.tsx`
- **问题**：这些路由无路由级错误边界。Admin 回退到 `admin/error.tsx`（有硬编码中文 — 见 P0-2）。Student 回退到 `student/error.tsx`。Teacher 和 parent 都有 dashboard 专属 `error.tsx`（含 i18n + 重试按钮）。
- **修复**：为两个路由创建 dashboard 专属 `error.tsx`，使用 `useTranslations` 和 `reset()`。

### P1-3：UserGrowthChart 硬编码标签用于两个图表

- **文件**：`src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx`
- **行号**：44
- **问题**：`name` 属性硬编码为 `t("chart.newUsers")`。此组件在 `admin-dashboard.tsx` 中被复用于用户增长（行 123）和作业提交趋势（行 133）。作业趋势图错误地显示"新用户"作为图例/提示标签。
- **修复**：为 `UserGrowthChart` 添加 `labelKey` 或 `name` prop，让调用方指定正确标签。

### P1-4：formatDate / formatLongDate 总是使用 zh-CN locale

- **文件**：`src/shared/lib/utils.ts`（行 8、35），及所有不传 locale 的 dashboard 组件
- **问题**：`formatDate` 和 `formatLongDate` 默认 `locale = "zh-CN"`。所有 dashboard 组件调用时未传用户 locale：
  - `dashboard-greeting-header.tsx` 行 22
  - `admin-dashboard.tsx` 行 215
  - `teacher-homework-card.tsx` 行 69
  - `recent-submissions.tsx` 行 96
  - `student-grades-card.tsx` 行 23、105
  - `student-upcoming-assignments-card.tsx` 行 106
  
  英文用户看到中文格式日期（如"2026年6月22日 周一"而非"Monday, June 22, 2026"）。
- **修复**：客户端组件用 `useLocale()`（next-intl），服务端组件用 `getLocale()`（next-intl/server），传入 `formatDate`/`formatLongDate`。

### P1-5：死代码 — getCachedAdminDashboard 从未使用

- **文件**：`src/modules/dashboard/actions.ts`
- **行号**：146
- **问题**：`export const getCachedAdminDashboard = cache(getAdminDashboardAction)` 定义但从未被导入或调用。`data-access.ts` 中的 `getAdminDashboardData` 已用 `cache()` 包裹。此外，用 React `cache()` 包裹调用 `requirePermission()` 的 Server Action 语义上不正确。
- **修复**：删除行 146 及未使用的 `cache` 导入。

### P1-6：死代码 — AvatarImage src={undefined}

- **文件**：`src/modules/dashboard/components/teacher-dashboard/recent-submissions.tsx`
- **行号**：76
- **问题**：`<AvatarImage src={undefined} alt={item.studentName} />` 总是传 `undefined` 作为 `src`，`AvatarImage` 永远不会渲染实际图片，总是回退到 `AvatarFallback`。
- **修复**：移除 `AvatarImage` 行，仅保留 `AvatarFallback`。

### P1-7：死 prop — TeacherStats isLoading 从未传入

- **文件**：`src/modules/dashboard/components/teacher-dashboard/teacher-stats.tsx`
- **行号**：10、18、32、41、50、59
- **问题**：`TeacherStats` 接受 `isLoading` prop（默认 `false`）并传给所有 4 个 `StatCard`。但 `TeacherStats` 仅在 `DashboardSection` 中渲染（`teacher-dashboard-view.tsx` 行 53），未传 `isLoading`。prop 永远为 `false`。`StudentStatsGrid` 无此 prop，造成不一致。
- **修复**：移除 `TeacherStats` 的 `isLoading` prop 及 `StatCard` 调用。

### P1-8：dashboard-utils.ts 中的 `as` 类型断言违反项目规则

- **文件**：`src/modules/dashboard/lib/dashboard-utils.ts`
- **行号**：114、145
- **问题**：项目规则明确"禁止 `as` 断言"（除 `unknown` 转换或测试外）。两处违规：
  - 行 114：`})) as StudentTodayScheduleItem[] | TeacherTodayScheduleItem[]`
  - 行 145：`) as TeacherTodayScheduleItem[]`
  
  根因是 `filterTodaySchedule` 重载服务于学生和教师课表，但返回类型是联合类型。
- **修复**：将 `filterTodaySchedule` 改为泛型函数，或拆分为两个函数。

### P1-9：辅助函数缺失显式返回类型

- **文件**：
  - `teacher-schedule.tsx` 行 24：`const getStatus = (start: string, end: string) => {`
  - `student-upcoming-assignments-card.tsx` 行 30：`const getDueUrgency = (dueAt: string | null) => {`
- **问题**：项目规则要求"函数返回值必须显式标注"。
- **修复**：添加显式返回类型。

### P1-10：重复的 loading.tsx 和 error.tsx 文件

- **文件**：
  - `src/app/(dashboard)/dashboard/loading.tsx` 和 `src/app/(dashboard)/teacher/dashboard/loading.tsx` — 字节级完全相同
  - `src/app/(dashboard)/dashboard/error.tsx`、`teacher/dashboard/error.tsx`、`parent/dashboard/error.tsx` — 全部相同
- **问题**：这些文件是精确副本。任何修复必须应用到所有副本，容易产生漂移。
- **修复**：抽取共享 `DashboardLoadingSkeleton` 和 `DashboardErrorFallback` 组件到 `src/modules/dashboard/components/`，每个路由的 `loading.tsx`/`error.tsx` 渲染共享组件。

---

## P2 问题（中）

### P2-1：流式/Suspense 未生效 — 数据在页面级获取

- **文件**：所有 `page.tsx`（admin/teacher/student/parent dashboard）
- **问题**：所有页面用 `export const dynamic = "force-dynamic"` 和 `await getDashboardAction()` 在渲染任何子组件前获取所有数据。`DashboardSection` 包裹子组件于 `<Suspense>`，但数据已在页面级解析并作为 props 传入，Suspense 永远不会在初始渲染时触发。
- **修复**：将数据获取移入各卡片组件（使其成为异步服务端组件自行获取数据），或传入未解析的 promise 并用 React `use()` hook。这是较大的架构变更。

### P2-2：4 个组件不必要标记为 "use client"

- **文件**：
  - `dashboard-greeting-header.tsx` — 仅用 `useTranslations`、`formatLongDate`、`getGreetingKey`
  - `teacher-quick-actions.tsx` — 仅用 `useTranslations`、`Link`、`Button`
  - `teacher-dashboard-header.tsx` — 包裹上述两个
  - `student-dashboard-header.tsx` — 包裹 `DashboardGreetingHeader`
- **问题**：这些组件标记为 `"use client"` 但不含客户端 only hook（`useState`、`useEffect`、事件处理器等）。`useTranslations` 在服务端组件中可用。转为服务端组件（用 `getTranslations` 替代 `useTranslations`）可减少客户端包大小。
- **修复**：移除 `"use client"`，改 `useTranslations` 为 `getTranslations`（async），组件改为 `async function`。

### P2-3：UserGrowthChart 无空状态

- **文件**：`src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx`
- **问题**：当 `data` 为空（当前永远如此 — 见 P0-3），recharts 渲染空图表有坐标轴但无线条无说明。其他图表组件（`TeacherGradeTrends`、`StudentGradesCard`）使用 `ChartCardShell` 有正确空状态。
- **修复**：添加空状态检查：`data.length === 0` 时渲染 `EmptyState`。

### P2-4：Student dashboard 空状态缺少 CTA（与 teacher 不一致）

- **文件**：
  - `student-today-schedule-card.tsx` 行 58-63：`EmptyState` 无 `action`
  - `student-upcoming-assignments-card.tsx` 行 59-64：`EmptyState` 无 `action`
- **问题**：Teacher dashboard 空状态都含 CTA。Student dashboard 空状态无 CTA，用户无明确下一步。
- **修复**：为 student 空状态添加 `action` prop。

### P2-5：StudentTodayScheduleCard 过时数据 — useMemo 不随时间更新

- **文件**：`src/modules/dashboard/components/student-dashboard/student-today-schedule-card.tsx`
- **行号**：25-43
- **问题**：`useMemo(() => { ... }, [items])` 基于 `new Date()` 计算 `currentId` 和 `nextId`。依赖数组是 `[items]`，仅在 `items` 变化时重新计算。用户保持页面打开时，"进行中"和"下一个"徽章会过时。
- **修复**：添加基于时间的重渲染机制（如 `useEffect` + `setInterval` 每分钟更新 `now` state）。

### P2-6：仅图标按钮缺少 aria-label

- **文件**：`src/modules/dashboard/components/teacher-dashboard/teacher-homework-card.tsx`
- **行号**：22
- **问题**：`<Button asChild size="icon" variant="ghost" className="h-8 w-8" title={...}>` 用 `title` 作 tooltip 但无 `aria-label`。屏幕阅读器可能不播报按钮用途。
- **修复**：添加 `aria-label={t("quickActions.createNewAssignment")}`。

### P2-7：无组件测试 — 仅有纯函数测试

- **文件**：`tests/integration/dashboard/dashboard-utils.test.ts`（408 行，31 个测试覆盖 6 个纯函数）、`tests/integration/dashboard/dashboard-routing.test.ts`（6 个测试覆盖重定向逻辑）
- **问题**：v2 添加了纯函数单测，但零组件测试、零 Server Action 测试、零 data-access 测试、零错误边界测试。`dashboard-routing.test.ts` 在用户对象上 mock `permissions`（行 41），但实际代码用 `resolvePermissions(roles)` — mock 的 `permissions` 字段被忽略，测试设置有误导性。
- **修复**：添加组件测试（RTL）、Action 测试（mock data-access，验证权限调用）、修复路由测试。

### P2-8：TeacherTodoCard 排序逻辑晦涩

- **文件**：`src/modules/dashboard/components/teacher-dashboard/teacher-todo-card.tsx`
- **行号**：52
- **问题**：`.sort((a, b) => (a.variant === "urgent" ? -1 : 1) - (b.variant === "urgent" ? -1 : 1))` 难以阅读。布尔转数字的算术不透明。
- **修复**：重写为更可读的比较函数。

### P2-9：TeacherSchedule 渲染两次（移动端 + 桌面端）— 重复服务端渲染

- **文件**：`src/modules/dashboard/components/teacher-dashboard/teacher-dashboard-view.tsx`
- **行号**：63-67（移动端）、85-89（桌面端）
- **问题**：`TeacherSchedule`（异步服务端组件调用 `getTranslations`）在 React 树中渲染两次 — 一次在 `lg:hidden` div，一次在 `hidden lg:block` div。两个实例都在服务端渲染并发送到客户端，使此区块 HTML 负载翻倍。
- **修复**：渲染一次并用 CSS grid/flexbox 重排序实现响应式布局，或接受此重复为较小代价。

---

## 修复顺序

1. **P0-1**（ContentRow 标签）— 直接面向用户的数据 bug
2. **P0-2**（admin/error.tsx i18n）— 直接 i18n 回归
3. **P0-3 + P1-3 + P2-3**（空趋势数据 + 图表标签 + 空状态）— 一起修复
4. **P1-1、P1-2**（缺失 loading.tsx/error.tsx）— 一致性
5. **P1-4**（日期 locale）— 系统性 i18n 修复
6. **P1-5、P1-6、P1-7**（死代码）— 快速清理
7. **P1-8、P1-9**（类型安全）— 重构 `filterTodaySchedule`
8. **P1-10**（重复文件）— 抽取共享组件
9. **P2-2、P2-4、P2-6、P2-8**（增量改进）
