Files
NextEdu/docs/architecture/audit/dashboard-audit-report-v3.md
SpecialX 27db170c0a docs: update architecture docs, audit reports, and bug tracking
- Update architecture impact map, data, feature checklist, gap audit

- Add audit reports for dashboard, exam-homework, grades-diagnostic, settings-profile, textbooks

- Update bug reports (admin, teacher, lesson-preparation, others, shared)

- Update coding standards, DR plan, design docs, and README
2026-06-23 17:36:18 +08:00

216 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dashboard 模块 V3 审计报告
**审计日期**2026-06-22
**审计范围**`src/modules/dashboard/` + 所有 dashboard 路由文件
**前置审计**v1P0 修复:跨模块 DB 查询、权限、i18n 容器组件、v210 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签)
---
## 概览
v1/v2 审计解决了表层问题。v3 审计发现了**更深层次的问题**涉及数据完整性、i18n 完整性、死代码、类型安全、流式架构和测试缺口。最严重的是 admin dashboard 中 ContentRow 标签与值完全错配的 **P0 数据展示 bug**
| 严重度 | 数量 |
|--------|------|
| P0 | 3 |
| P1 | 10 |
| P2 | 9 |
---
## P0 问题(严重)
### P0-1Admin Dashboard ContentRow 标签与值错配(数据完整性)
- **文件**`src/modules/dashboard/components/admin-dashboard/admin-dashboard.tsx`
- **行号**166-169Content 区块、180-181Homework 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-2admin/error.tsx 硬编码中文,无 i18n
- **文件**`src/app/(dashboard)/admin/error.tsx`
- **行号**12-14
- **问题**:此错误边界有硬编码中文字符串(`"页面加载失败"``"抱歉,页面加载时发生了意外错误。请稍后重试。"``"重试"`),未导入或使用 `useTranslations`。英文用户会看到中文文本。v2 审计遗漏了此文件,因为只关注了 `dashboard/` 模块而非 `admin/` 路由错误边界。其他 dashboard error.tsxteacher、parent、root都正确使用了 `useTranslations`
- **修复**:导入 `useTranslations`,替换硬编码字符串为 `t("error.loadFailed")``t("error.loadFailedDesc")``t("error.retry")`
### P0-3userGrowth 和 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-1admin/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-2admin/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-3UserGrowthChart 硬编码标签用于两个图表
- **文件**`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-4formatDate / 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-8dashboard-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-24 个组件不必要标记为 "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-3UserGrowthChart 无空状态
- **文件**`src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx`
- **问题**:当 `data` 为空(当前永远如此 — 见 P0-3recharts 渲染空图表有坐标轴但无线条无说明。其他图表组件(`TeacherGradeTrends``StudentGradesCard`)使用 `ChartCardShell` 有正确空状态。
- **修复**:添加空状态检查:`data.length === 0` 时渲染 `EmptyState`
### P2-4Student 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-5StudentTodayScheduleCard 过时数据 — 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-8TeacherTodoCard 排序逻辑晦涩
- **文件**`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-9TeacherSchedule 渲染两次(移动端 + 桌面端)— 重复服务端渲染
- **文件**`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**(增量改进)