refactor: fix all P0/P1/P2 bugs and architecture issues

Bug fixes (from bugs/ directory):

- Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions

- Fix shared/lib <-> auth circular dependency via new session.ts module

- Fix divide-by-zero guard in grades data-access

- Fix audit export data truncation (paginated fetch for full datasets)

- Fix missing transactions in homework grading and elective lottery

- Fix missing revalidatePath in course-plans actions

- Fix frontend permission checks using requirePermission instead of requireAuth

- Fix dashboard role routing using session.user.roles

- Fix student auth pattern (migrate getDemoStudentUser to users module)

- Fix ActionState return type handling in components

Code quality fixes:

- Remove 60+ as type assertions (replace with type guards)

- Remove non-null assertions (use optional chaining or explicit checks)

- Convert dynamic imports to static imports (grades, diagnostic)

- Add React.cache() wrapping for read functions

- Parallelize independent queries with Promise.all

- Add explicit return types to 30+ arrow functions

- Replace any with unknown + type guards

- Fix import type for type-only imports

- Add Zod validation schemas for classes and diagnostic modules

- Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction)

- Add console.error to silent catch blocks

- Fix permission naming consistency (exam:proctor_read -> exam:proctor:read)

Architecture doc sync:

- Update 004_architecture_impact_map.md and 005_architecture_data.json

- Update management-modules-audit.md for P0-7 cross-module fix

Moved deleted proctoring event route to deletes/ folder.
This commit is contained in:
SpecialX
2026-06-19 05:13:09 +08:00
parent 063baffe4c
commit 49291fcc31
114 changed files with 12548 additions and 3395 deletions

711
bugs/student_bug.md Normal file
View File

@@ -0,0 +1,711 @@
# `src/app/(dashboard)/student` 前端规范核查报告
> 核查日期2026-06-18
> 核查范围:`src/app/(dashboard)/student/` 目录下所有前端文件(含 `loading.tsx`+ 关联模块组件 `src/modules/student/components/*`
> 依据文档:项目规则、编码规范 `docs/standards/coding-standards.md`、架构影响地图 004、架构数据 005
> 应用技能:`vercel-react-best-practices`、`web-artifacts-builder`、`web-design-guidelines`
---
## 一、核查文件清单
### 1.1 路由页面文件11 个)
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [dashboard/page.tsx](../src/app/(dashboard)/student/dashboard/page.tsx) | 88 | Server Component | 学生仪表盘 |
| [dashboard/loading.tsx](../src/app/(dashboard)/student/dashboard/loading.tsx) | 60 | Loading UI | 仪表盘骨架屏 |
| [attendance/page.tsx](../src/app/(dashboard)/student/attendance/page.tsx) | 40 | Server Component | 学生考勤 |
| [diagnostic/page.tsx](../src/app/(dashboard)/student/diagnostic/page.tsx) | 31 | Server Component | 学情诊断 |
| [elective/page.tsx](../src/app/(dashboard)/student/elective/page.tsx) | 49 | Server Component | 选课中心 |
| [grades/page.tsx](../src/app/(dashboard)/student/grades/page.tsx) | 40 | Server Component | 我的成绩 |
| [learning/assignments/page.tsx](../src/app/(dashboard)/student/learning/assignments/page.tsx) | 167 | Server Component | 作业列表 |
| [learning/assignments/[assignmentId]/page.tsx](../src/app/(dashboard)/student/learning/assignments/[assignmentId]/page.tsx) | 53 | Server Component | 作业作答/复习 |
| [learning/courses/page.tsx](../src/app/(dashboard)/student/learning/courses/page.tsx) | 39 | Server Component | 课程列表 |
| [learning/courses/loading.tsx](../src/app/(dashboard)/student/learning/courses/loading.tsx) | 28 | Loading UI | 课程骨架屏 |
| [learning/textbooks/page.tsx](../src/app/(dashboard)/student/learning/textbooks/page.tsx) | 71 | Server Component | 教材列表 |
| [learning/textbooks/[id]/page.tsx](../src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx) | 76 | Server Component | 教材阅读 |
| [schedule/page.tsx](../src/app/(dashboard)/student/schedule/page.tsx) | 54 | Server Component | 课表 |
| [schedule/loading.tsx](../src/app/(dashboard)/student/schedule/loading.tsx) | 31 | Loading UI | 课表骨架屏 |
### 1.2 关联模块组件3 个)
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [student-courses-view.tsx](../src/modules/student/components/student-courses-view.tsx) | 156 | Client Component | 课程视图 + 加入班级表单 |
| [student-schedule-filters.tsx](../src/modules/student/components/student-schedule-filters.tsx) | 32 | Client Component | 课表筛选器 |
| [student-schedule-view.tsx](../src/modules/student/components/student-schedule-view.tsx) | 88 | Server Component | 课表视图 |
### 1.3 缺失文件
| 缺失类型 | 影响范围 | 说明 |
|---------|---------|------|
| `layout.tsx` | 整个 student 路由组 | 无统一布局,每个页面重复写 `<div className="... p-8">` 容器 |
| `error.tsx` | 整个 student 路由组 | 无错误边界Server Component 抛错时显示 Next.js 默认错误页 |
| `loading.tsx` | attendance / diagnostic / elective / grades / learning/assignments / learning/assignments/[assignmentId] / learning/textbooks / learning/textbooks/[id] | 8 个路由无骨架屏,跳转时白屏 |
| `not-found.tsx` | 整个 student 路由组 | `notFound()` 调用时显示 Next.js 默认 404 |
---
## 二、违规问题清单
### 2.1 认证模式严重不一致 — 严重度:高
#### BUG-A01三种认证模式混用
- **位置**:整个 student 目录
- **问题**同一角色student的页面使用了 3 种不同的认证方式:
| 模式 | 使用页面 | 问题 |
|------|---------|------|
| `getAuthContext()` from `@/shared/lib/auth-guard` | attendance / diagnostic / grades | ✅ 推荐 |
| `auth()` from `@/auth` | elective | ⚠️ 直接调用 auth(),绕过 auth-guard 抽象 |
| `getDemoStudentUser()` from `@/modules/homework/data-access` | dashboard / learning/* / schedule | ⚠️ 依赖 homework 模块获取用户身份 |
- **规范依据**:编码规范 8.3「统一通过 `getAuthContext()` / `requirePermission()` 获取会话」
- **影响**
1. `elective/page.tsx` 直接 `import { auth } from "@/auth"`违反分层规则app 层不应直接依赖根模块 `@/auth`,应通过 `shared/lib/auth-guard`
2. `getDemoStudentUser()` 名为 "Demo" 实为真实查询,命名误导
3. `getDemoStudentUser()` 定义在 `homework/data-access.ts`,导致 6 个 student 页面为获取用户身份而依赖 homework 模块,违反模块封装
- **改进建议**
1.`getDemoStudentUser` 迁移到 `users/data-access.ts` 并重命名为 `getCurrentStudentUser()``getStudentSession()`
2. 所有 student 页面统一使用 `getAuthContext()` 获取 `ctx.userId`,再按需查询学生信息
3. 移除 `elective/page.tsx` 中的 `import { auth } from "@/auth"`
#### BUG-A02`getDemoStudentUser` 命名误导
- **位置**`src/modules/homework/data-access.ts:475`
- **问题**:函数名包含 "Demo",但实际执行真实 DB 查询JOIN users + usersToRoles + roles WHERE roles.name = "student"),并非演示用桩函数
- **影响**:开发者看到 "Demo" 会误以为是临时实现,不敢用于生产;或误以为返回固定演示数据
- **改进建议**:重命名为 `getCurrentStudentUser()``resolveCurrentStudent()`
#### BUG-A03`getDemoStudentUser` 放置在错误的模块
- **位置**`src/modules/homework/data-access.ts:475-490`
- **问题**:学生身份查询属于 `users` 模块职责,却放在 `homework` 模块
- **规范依据**:项目规则「模块标准结构」+ 编码规范 2.2「模块封装」
- **影响**`dashboard``learning/assignments``learning/courses``learning/textbooks``learning/textbooks/[id]``schedule` 6 个页面为获取用户身份而 `import` homework 模块,造成虚假依赖
- **改进建议**:迁移到 `src/modules/users/data-access.ts`,导出为 `getCurrentStudentUser()`
---
### 2.2 `learning/assignments/page.tsx` — 严重度:高
#### BUG-L01中英文混排
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:81, 122`
- **问题**:在英文 UI 中插入中文标签
```tsx
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">未答题</div>
...
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">已答题</div>
```
- **规范依据**:项目为 K12 中文教务系统,但本页面其余文案均为英文("Due"、"Attempts"、"Score"、"Start"、"Continue"),中英文混排显得不专业
- **改进建议**:统一为英文 "Pending" / "Completed",或整页改为中文
#### BUG-L02卡片渲染逻辑重复
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:83-116` 与 `124-157`
- **问题**:未答题区和已答题区的卡片 JSX 完全相同34 行 × 2 = 68 行重复),仅数据源不同
- **规范依据**编码规范「DRY 原则」+ 项目规则「单文件行数建议 ≤ 500 行」
- **改进建议**:抽取为 `<AssignmentCard assignment={a} />` 组件,循环调用
#### BUG-L03JSX 语法格式错误
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:162`
- **问题**:行尾 `})}` 多了一个 `)`
```tsx
)})}
```
应为:
```tsx
))}
```
- **影响**:虽然能编译通过(因外层有 `(`但格式混乱IDE 自动格式化后会变化,造成 git diff 噪音
- **改进建议**:修正为 `))}`
#### BUG-L04`getStatusVariant` 对 submitted 和 in_progress 返回相同值
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:13-18`
- **问题**
```tsx
if (status === "submitted") return "secondary"
if (status === "in_progress") return "secondary"
```
两种状态视觉上无法区分,学生无法快速辨别「已提交待批改」和「进行中」
- **改进建议**`in_progress` 改为 `"outline"` 或新增 `"in_progress"` 专属样式
#### BUG-L05状态判断函数参数类型过宽
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:13, 20, 27, 34, 39`
- **问题**`getStatusVariant(status: string)` 等函数参数为 `string`,但 `a.progressStatus` 实际类型是 `StudentHomeworkProgressStatus` 联合类型
- **规范依据**:编码规范 4.2「优先使用精确类型」
- **改进建议**:参数类型改为 `StudentHomeworkProgressStatus`,并在 switch 中穷举
#### BUG-L06`new Map<string, typeof assignments>()` 类型不优雅
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:63`
- **问题**:使用 `typeof assignments` 作为 Map 值类型,将类型绑定到变量,可读性差
- **改进建议**:导入显式类型 `new Map<string, StudentHomeworkAssignment[]>()`
---
### 2.3 `learning/textbooks/page.tsx` — 严重度:中
#### BUG-T01存在注释掉的代码
- **位置**`src/app/(dashboard)/student/learning/textbooks/page.tsx:42-50`
- **问题**
```tsx
{/* <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">Textbooks</h2>
<p className="text-muted-foreground">Browse your course textbooks.</p>
</div>
<Button asChild variant="outline">
<Link href="/student/dashboard">Back</Link>
</Button>
</div> */}
```
- **规范依据**:编码规范禁止提交注释掉的代码(应删除或用版本管理)
- **改进建议**:删除注释块
#### BUG-T02缺少页面标题
- **位置**`src/app/(dashboard)/student/learning/textbooks/page.tsx`
- **问题**:其他 student 页面都有 `<h2 className="text-2xl font-bold tracking-tight">` 标题,本页面因注释掉了标题块,直接渲染 `TextbookFilters`,与其他页面不一致
- **改进建议**:恢复标题(不带"Back"按钮),保持视觉一致性
#### BUG-T03`student` 变量未真正使用
- **位置**`src/app/(dashboard)/student/learning/textbooks/page.tsx:23-31`
- **问题**`student` 仅用于 "No user" 检查,`getTextbooks(q, subject, grade)` 不依赖学生身份
- **影响**:任何登录学生都能浏览所有教材,无年级/科目过滤;如设计如此,则 `student` 检查可简化为 `getAuthContext()` 仅做认证
- **改进建议**:若教材对所有学生开放,改用 `getAuthContext()` 仅做认证;若应按年级过滤,则 `getTextbooks` 增加 `grade` 参数
---
### 2.4 `learning/textbooks/[id]/page.tsx` — 严重度:中
#### BUG-TD01`student` 变量未使用
- **位置**`src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx:18-31`
- **问题**:同 BUG-T03`student` 仅用于 "No user" 检查,教材内容查询不依赖学生身份
- **改进建议**:同 BUG-T03
#### BUG-TD02错误处理不一致
- **位置**`src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx:19, 41`
- **问题**
- `if (!student)` 返回完整 `EmptyState` 页面
- `if (!textbook) notFound()` 抛出 404
同一文件内两种错误处理模式
- **改进建议**:统一为 `notFound()` 或都返回 `EmptyState`
#### BUG-TD03装饰性 `<span>` 缺少 `aria-hidden`
- **位置**`src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx:49`
- **问题**
```tsx
<span className="hidden sm:inline-block w-px h-4 bg-border" />
```
纯装饰性分隔线,屏幕阅读器会朗读空内容
- **规范依据**Web Interface Guidelines — Accessibility「装饰性元素应 `aria-hidden`」
- **改进建议**:添加 `aria-hidden="true"`
---
### 2.5 `dashboard/page.tsx` — 严重度:中
#### BUG-D01`as` 类型断言违规
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:11`
- **问题**
```tsx
return (day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7
```
- **规范依据**:编码规范 4.2.3「禁止 `as` 断言(除非从 `unknown` 转换)」
- **改进建议**:使用类型守卫或重写为:
```tsx
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
const day = d.getDay()
const weekday = day === 0 ? 7 : day
if (weekday < 1 || weekday > 7) throw new Error(`Invalid weekday: ${weekday}`)
return weekday
}
```
#### BUG-D02缺少页面标题容器
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:76-87`
- **问题**:其他 student 页面都有 `<div className="... p-8"><div><h2>...</h2><p>...</p></div>...</div>` 容器,本页面直接返回 `<StudentDashboard />`,无外层 padding 和标题
- **影响**:视觉上与其他页面不一致(无 padding无标题
- **改进建议**:添加统一的页面容器和标题,或将容器抽到 `layout.tsx`
#### BUG-D03`EmptyState` 的 `icon` 使用 `Inbox` 不合适
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:5, 22`
- **问题**:用户未找到时显示 `Inbox` 图标,语义不匹配
- **改进建议**:使用 `UserX` 或 `UserMinus` 图标
---
### 2.6 `schedule/page.tsx` — 严重度:低
#### BUG-S01嵌套三元表达式可读性差
- **位置**`src/app/(dashboard)/student/schedule/page.tsx:38`
- **问题**
```tsx
const classId = typeof classIdParam === "string" ? classIdParam : Array.isArray(classIdParam) ? classIdParam[0] : "all"
```
- **规范依据**:编码规范「避免嵌套三元」
- **改进建议**:抽为独立函数或使用 if 语句
#### BUG-S02`searchParams` 类型未共享
- **位置**`src/app/(dashboard)/student/schedule/page.tsx:11` 和 `learning/textbooks/page.tsx:11`
- **问题**:两处定义相同的 `type SearchParams = { [key: string]: string | string[] | undefined }`
- **改进建议**:抽取到 `shared/types` 或 `shared/lib/utils`
---
### 2.7 `elective/page.tsx` — 严重度:中
#### BUG-E01直接调用 `auth()` 绕过 auth-guard
- **位置**`src/app/(dashboard)/student/elective/page.tsx:1, 11`
- **问题**
```tsx
import { auth } from "@/auth"
...
const session = await auth()
const studentId = String(session?.user?.id ?? "")
```
- **规范依据**项目规则「app 层不应直接依赖 `@/auth`」+ 编码规范 8.3
- **改进建议**:改用 `getAuthContext()`
```tsx
const ctx = await getAuthContext()
const studentId = ctx.userId
```
#### BUG-E02`String(... ?? "")` 模式冗余
- **位置**`src/app/(dashboard)/student/elective/page.tsx:12`
- **问题**`String(session?.user?.id ?? "")` — `session.user.id` 已是 string 类型,`String()` 包裹多余
- **改进建议**:使用 `getAuthContext()` 后直接用 `ctx.userId`
---
### 2.8 `student-courses-view.tsx` — 严重度:中
#### BUG-C01catch 块吞掉错误
- **位置**`src/modules/student/components/student-courses-view.tsx:39-41`
- **问题**
```tsx
} catch {
toast.error("Failed to join class")
}
```
错误被吞掉,无 `console.error` 记录,调试困难
- **规范依据**:编码规范 9.2「错误必须可观测」
- **改进建议**
```tsx
} catch (err) {
console.error("[joinClass] failed:", err)
toast.error("Failed to join class")
}
```
#### BUG-C02未使用 `useFormStatus` / `useTransition`
- **位置**`src/modules/student/components/student-courses-view.tsx:26-44`
- **问题**:使用本地 `useState` + `setIsWorking` 管理 loading 状态,但 Next.js 16 推荐 `useFormStatus`(用于 `<form action>`)或 `useTransition`
- **违反规则**`rerender-transitions`、`rendering-usetransition-loading`
- **改进建议**:使用 `useTransition` 包裹 Server Action 调用
#### BUG-C03表单缺少客户端格式校验
- **位置**`src/modules/student/components/student-courses-view.tsx:136-148`
- **问题**`maxLength={6}` 和 `inputMode="numeric"` 不阻止字母输入,用户可提交 "abcdef"
- **改进建议**:添加 `pattern="\d{6}"` 或 `onChange` 过滤非数字字符
---
### 2.9 `student-schedule-view.tsx` — 严重度:低
#### BUG-SV01`for...of` 修改 Map 后再次 set
- **位置**`src/modules/student/components/student-schedule-view.tsx:36-39`
- **问题**
```tsx
for (const [day, list] of itemsByDay) {
list.sort((a, b) => a.startTime.localeCompare(b.startTime))
itemsByDay.set(day, list) // 多余sort 已 in-place
}
```
`list.sort()` 已原地排序,`itemsByDay.set(day, list)` 多余
- **改进建议**:删除 `itemsByDay.set(day, list)` 行
---
### 2.10 跨文件一致性 — 严重度:中
#### BUG-X01页面容器 className 不统一
- **位置**:多个页面
- **问题**:存在 4 种不同的容器写法:
| className | 使用页面 |
|-----------|---------|
| `h-full flex-1 flex-col space-y-8 p-8 md:flex` | attendance / diagnostic / grades / learning/textbooks / learning/textbooks/[id] |
| `flex h-full flex-col space-y-8 p-8` | elective / learning/courses / schedule |
| `flex h-full flex-col space-y-4 p-6` | learning/assignments/[assignmentId] |
| `h-full flex-1 flex-col space-y-8 p-8 md:flex` | learning/assignments |
| 无容器 | dashboard |
- **改进建议**:抽取到 `student/layout.tsx` 的 `<main>` 中,统一 padding 和布局
#### BUG-X02"No user" 处理方式不一致
- **位置**:多个页面
- **问题**
- `dashboard`、`learning/courses`、`learning/textbooks`、`learning/textbooks/[id]`、`schedule`、`learning/assignments`、`elective`:返回 `EmptyState`
- `learning/assignments/[assignmentId]`:调用 `notFound()`
- `attendance`、`grades`:返回 `EmptyState`(但检查的是 `summary` 而非 `student`
- **改进建议**:未认证场景应由 `proxy.ts` 拦截,页面内统一用 `notFound()` 或统一 `EmptyState`
#### BUG-X03图标选择不一致
- **位置**:多个页面
- **问题**:同样 "No user found" 场景使用不同图标:
- `dashboard`、`learning/courses`、`learning/textbooks`、`schedule`、`learning/assignments``Inbox`
- `attendance``CalendarCheck`
- `grades``GraduationCap`
- `elective``Inbox`
- **改进建议**:未认证场景统一使用 `UserX`;空数据场景使用业务相关图标
---
## 三、React 性能优化(应用 `vercel-react-best-practices` 技能)
### PERF-01`student-courses-view.tsx` 未使用 `useTransition`
- **位置**`src/modules/student/components/student-courses-view.tsx:28-44`
- **问题**Server Action `joinClassByInvitationCodeAction` 用 `useState` 管理 loading阻塞 UI 线程
- **违反规则**`rerender-transitions`、`rendering-usetransition-loading`
- **改进建议**
```tsx
const [isPending, startTransition] = useTransition()
const handleJoin = async (formData: FormData) => {
startTransition(async () => {
const res = await joinClassByInvitationCodeAction(null, formData)
...
})
}
```
### PERF-02`student-courses-view.tsx` 卡片列表未 memoize
- **位置**`src/modules/student/components/student-courses-view.tsx:50-108`
- **问题**`classes.map(...)` 每次渲染都重新创建 6+ 个 Card 元素,当 `code` 输入变化时所有卡片重渲染
- **违反规则**`rerender-memo`、`rerender-no-inline-components`
- **改进建议**:抽取 `<ClassCard class={c} />` 组件并用 `React.memo` 包裹
### PERF-03`student-schedule-filters.tsx` 的 `options` 依赖 `classes` 引用
- **位置**`src/modules/student/components/student-schedule-filters.tsx:12`
- **问题**`useMemo(() => [...classes], [classes])` — 父组件每次传入新 `classes` 数组引用时都重新计算
- **违反规则**`rerender-dependencies`
- **现状**:已用 `useMemo`,但依赖项是父组件传入的 prop若父组件不 memoize 则失效
- **改进建议**:可接受,但建议父组件 `schedule/page.tsx` 用 `React.cache` 或在 Server Component 层面稳定引用
### PERF-04`dashboard/page.tsx` 多次 `filter` 遍历同一数组
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:40-52`
- **问题**
```tsx
const dueSoonCount = assignments.filter(...).length
const overdueCount = assignments.filter(...).length
const gradedCount = assignments.filter(...).length
```
3 次遍历同一数组O(3n)
- **违反规则**`js-combine-iterations`
- **改进建议**:单次遍历统计:
```tsx
let dueSoonCount = 0, overdueCount = 0, gradedCount = 0
for (const a of assignments) {
if (a.progressStatus === "graded") { gradedCount++; continue }
if (!a.dueAt) continue
const due = new Date(a.dueAt)
if (due >= now && due <= in7Days) dueSoonCount++
else if (due < now) overdueCount++
}
```
### PERF-05`learning/assignments/page.tsx` 重复 `filter` 调用
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:73-74`
- **问题**
```tsx
const answeredItems = items.filter((a) => isAnswered(a.progressStatus))
const unansweredItems = items.filter((a) => !isAnswered(a.progressStatus))
```
2 次遍历同一数组
- **违反规则**`js-combine-iterations`
- **改进建议**:单次遍历分桶:
```tsx
const { answered, unanswered } = items.reduce(
(acc, a) => {
isAnswered(a.progressStatus) ? acc.answered.push(a) : acc.unanswered.push(a)
return acc
},
{ answered: [], unanswered: [] }
)
```
### PERF-06`student-schedule-view.tsx` 在渲染期构建 Map
- **位置**`src/modules/student/components/student-schedule-view.tsx:30-39`
- **问题**:每次渲染都重建 `itemsByDay` Map 并排序
- **现状**:作为 Server Component 每次请求只渲染一次,影响较小
- **违反规则**`js-cache-function-results`(轻度)
- **改进建议**:可接受,若未来改为 Client Component 则需 `useMemo`
### PERF-07`dashboard/page.tsx` 已正确使用 `Promise.all` 并行获取
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:29-34`
- **现状**:✅ 符合 `async-parallel` 规则
- **说明**4 个独立查询并行执行,无需优化
### PERF-08`diagnostic/page.tsx` 已正确使用 `Promise.all`
- **位置**`src/app/(dashboard)/student/diagnostic/page.tsx:12-15`
- **现状**:✅ 符合 `async-parallel` 规则
### PERF-09`schedule/page.tsx` 已正确使用 `Promise.all`
- **位置**`src/app/(dashboard)/student/schedule/page.tsx:31-35`
- **现状**:✅ 符合 `async-parallel` 规则
---
## 四、Web 界面规范审查(应用 `web-design-guidelines` 技能)
### UI-01缺少 `loading.tsx` 导致白屏
- **位置**attendance / diagnostic / elective / grades / learning/assignments / learning/assignments/[assignmentId] / learning/textbooks / learning/textbooks/[id]
- **问题**8 个路由无骨架屏,跳转时显示白屏,违反 "Perceived Performance" 原则
- **违反规则**Web Interface Guidelines — Perceived Performance「Always show loading state」
- **改进建议**:为每个缺失的路由添加 `loading.tsx`,参考已有的 `dashboard/loading.tsx` 模式
### UI-02缺少 `error.tsx` 错误边界
- **位置**:整个 student 路由组
- **问题**Server Component 抛错时显示 Next.js 默认错误页,无法"恢复"
- **违反规则**Web Interface Guidelines — Error Handling「Provide recoverable error states」
- **改进建议**:在 `student/error.tsx` 中提供"重试"按钮:
```tsx
"use client"
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<EmptyState
title="Something went wrong"
description={error.message}
action={{ label: "Try again", onClick: reset }}
/>
)
}
```
### UI-03装饰性元素缺少 `aria-hidden`
- **位置**
- `learning/textbooks/[id]/page.tsx:49` — 分隔线 `<span>`
- `learning/assignments/page.tsx:98, 139` — `<span className="px-2">•</span>` 分隔点
- `learning/assignments/[assignmentId]/page.tsx:45` — `<span className="mx-2">•</span>`
- `student-courses-view.tsx:63` — `<span>•</span>`
- **问题**:屏幕阅读器会朗读"•"或空内容
- **违反规则**Web Interface Guidelines — Accessibility「Decorative elements need `aria-hidden`」
- **改进建议**:所有装饰性 `<span>` 添加 `aria-hidden="true"`
### UI-04图标缺少 `aria-hidden` 或 `aria-label`
- **位置**:所有页面使用的 lucide-react 图标
- **问题**lucide 图标默认 `aria-hidden="true"`,但部分图标作为唯一内容(如按钮内仅图标)时需 `aria-label`
- **现状**:本目录内图标均伴随文字,符合规范 ✅
### UI-05表单缺少 `noValidate` 和客户端校验
- **位置**`student-courses-view.tsx:133`
- **问题**`<form action={handleJoin}>` 依赖 HTML5 原生校验(`required`),但未添加数字格式校验
- **违反规则**Web Interface Guidelines — Forms「Validate on client before submit」
- **改进建议**:添加 `pattern="\d{6}"` 或自定义校验
### UI-06链接缺少 `prefetch` 控制
- **位置**`learning/assignments/page.tsx:88, 110, 129, 151`、`student-courses-view.tsx:94, 100`
- **问题**`<Link href="/student/learning/assignments/...">` 默认 prefetch但作业详情页数据量大可能浪费带宽
- **违反规则**`bundle-preload`(轻度)
- **改进建议**:对详情页链接添加 `prefetch={false}` 或在 hover 时预加载
### UI-07`hover:shadow-md` 可能导致性能问题
- **位置**`learning/assignments/page.tsx:84, 125`、`student-courses-view.tsx:51`
- **问题**:大量卡片同时绑定 `hover:shadow-md`,低端设备滚动时可能卡顿
- **违反规则**`rendering-content-visibility`(轻度)
- **改进建议**:长列表添加 `content-visibility: auto` 或使用 `will-change: shadow`
### UI-08颜色对比度待验证
- **位置**`text-muted-foreground` 在多处用于次要信息
- **问题**`text-muted-foreground` 在浅色模式下对比度可能不足 WCAG AA 标准4.5:1
- **违反规则**Web Interface Guidelines — Accessibility「Color contrast WCAG AA」
- **改进建议**:使用工具验证 `--muted-foreground` 与 `--background` 的对比度
### UI-09`tabular-nums` 使用正确 ✅
- **位置**`learning/assignments/page.tsx:107, 148`、`student-schedule-view.tsx:64`
- **现状**:数字列使用 `tabular-nums`,对齐良好 ✅
### UI-10响应式断点一致 ✅
- **位置**:所有页面
- **现状**:统一使用 `md:`、`lg:`、`xl:` 断点,符合规范 ✅
---
## 五、架构文档同步问题
### DOC-01005 JSON 中 `/student/grades` 的 dataAccess 记录错误
- **位置**`docs/architecture/005_architecture_data.json:12047`
- **问题**:记录为 `"grades/actions.getStudentGradeSummaryAction"`,实际代码调用 `grades/data-access.getStudentGradeSummary`
- **改进建议**:修正为 `"grades/data-access.getStudentGradeSummary"`
### DOC-02005 JSON 中 `/student/learning/textbooks/[id]` 的 type 错误
- **位置**`docs/architecture/005_architecture_data.json:12024`
- **问题**:记录 `"type": "client"`,实际 `page.tsx` 是 Server Component无 `"use client"`
- **改进建议**:改为 `"type": "server"`,并注明内部 `TextbookReader` 组件为 client
### DOC-03005 JSON 中 `/student/diagnostic` 的 type 错误
- **位置**`docs/architecture/005_architecture_data.json:12063`
- **问题**:记录 `"type": "client"`,实际 `page.tsx` 是 Server Component
- **改进建议**:改为 `"type": "server"`,并注明内部 `StudentDiagnosticView` 组件为 client
### DOC-04005 JSON 中 `/student/dashboard` 缺少 `getStudentSchedule` 记录
- **位置**`docs/architecture/005_architecture_data.json:11978-11982`
- **问题**:实际代码调用了 `getStudentSchedule(student.id)`,但 dataAccess 数组未记录
- **改进建议**:补充 `"classes/data-access.getStudentSchedule"`
### DOC-05005 JSON 中 `/student/learning/assignments` 缺少 `getDemoStudentUser` 记录
- **位置**`docs/architecture/005_architecture_data.json:11989-11991`
- **问题**:实际代码调用了 `getDemoStudentUser()`,但 dataAccess 数组未记录
- **改进建议**:补充 `"homework/data-access.getDemoStudentUser"`(或迁移后更新为 `users/data-access.getCurrentStudentUser`
### DOC-06004 文档 2.26 节未记录 `loading.tsx` 文件
- **位置**`docs/architecture/004_architecture_impact_map.md:1109-1114`
- **问题**:文件清单仅列出 3 个组件,未记录 `app/(dashboard)/student/*/loading.tsx`
- **改进建议**:补充 loading.tsx 文件清单
### DOC-07004 文档未记录 student 路由的认证模式不一致问题
- **位置**`docs/architecture/004_architecture_impact_map.md:1095-1115`
- **问题**2.26 节"已知问题"未提及三种认证模式混用
- **改进建议**:在"已知问题"中补充 P1 项「认证模式不一致」
---
## 六、问题汇总统计
| 严重度 | 数量 | 问题编号 |
|--------|------|----------|
| 高 | 4 | BUG-A01, BUG-A02, BUG-A03, BUG-L01 |
| 中 | 10 | BUG-L02, BUG-L04, BUG-L05, BUG-T01, BUG-T02, BUG-T03, BUG-TD01, BUG-TD02, BUG-D01, BUG-D02, BUG-E01, BUG-C01, BUG-X01, BUG-X02, BUG-X03 |
| 低 | 6 | BUG-L03, BUG-L06, BUG-D03, BUG-S01, BUG-S02, BUG-E02, BUG-C02, BUG-C03, BUG-SV01 |
| 性能 | 6 | PERF-01, PERF-02, PERF-03, PERF-04, PERF-05, PERF-06 |
| 界面 | 8 | UI-01, UI-02, UI-03, UI-05, UI-06, UI-07, UI-08, UI-09 |
| 文档 | 7 | DOC-01 ~ DOC-07 |
| **合计** | **41** | |
---
## 七、修复优先级建议
### P0立即修复 — 影响正确性和架构合规性)
1. **BUG-A01 + BUG-A02 + BUG-A03**:将 `getDemoStudentUser` 迁移到 `users/data-access.ts` 并重命名为 `getCurrentStudentUser()`,所有 student 页面统一使用 `getAuthContext()` 或新函数
2. **BUG-E01**`elective/page.tsx` 改用 `getAuthContext()`,移除 `import { auth } from "@/auth"`
3. **BUG-L01**`learning/assignments/page.tsx` 中英文混排修正
4. **BUG-L03**`learning/assignments/page.tsx:162` JSX 语法格式错误修正
5. **BUG-T01**`learning/textbooks/page.tsx` 删除注释代码
### P1本迭代修复 — 影响可维护性和一致性)
6. **BUG-X01**:创建 `student/layout.tsx` 统一页面容器
7. **UI-01**:为 8 个缺失路由添加 `loading.tsx`
8. **UI-02**:创建 `student/error.tsx` 错误边界
9. **BUG-L02**:抽取 `AssignmentCard` 组件消除重复
10. **BUG-D01**:移除 `as` 类型断言
11. **BUG-L04**`getStatusVariant` 区分 submitted / in_progress
12. **BUG-X02 + BUG-X03**:统一 "No user" 处理和图标选择
13. **PERF-01 + PERF-02**`student-courses-view.tsx` 使用 `useTransition` + memoize 卡片
14. **PERF-04 + PERF-05**:合并重复 `filter` 遍历
### P2下迭代修复 — 增强健壮性)
15. **BUG-L05 + BUG-L06**:类型精确化
16. **BUG-T02 + BUG-D02**:补全页面标题
17. **BUG-TD01 + BUG-TD03**:清理未使用变量 + `aria-hidden`
18. **UI-03**:所有装饰性 `<span>` 添加 `aria-hidden`
19. **UI-05**:表单客户端校验
20. **BUG-C01 + BUG-C02 + BUG-C03**`student-courses-view.tsx` 错误处理 + 表单优化
21. **BUG-S01 + BUG-S02 + BUG-SV01**:小重构
### P3文档同步
22. **DOC-01 ~ DOC-07**:同步 004 和 005 架构文档
---
## 八、`student/layout.tsx` 推荐实现
```tsx
import type { ReactNode } from "react"
export default function StudentLayout({ children }: { children: ReactNode }) {
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
{children}
</div>
)
}
```
> 注:`dashboard/page.tsx` 和 `learning/assignments/[assignmentId]/page.tsx` 若需不同 padding可在页面内覆盖。
---
## 九、`AssignmentCard` 组件推荐实现(消除 BUG-L02 重复)
```tsx
import Link from "next/link"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { formatDate } from "@/shared/lib/utils"
import type { StudentHomeworkAssignment } from "@/modules/homework/types"
const getStatusVariant = (status: StudentHomeworkProgressStatus): "default" | "secondary" | "outline" => {
switch (status) {
case "graded": return "default"
case "submitted": return "secondary"
case "in_progress": return "outline" // BUG-L04 修复:区分 in_progress
default: return "outline"
}
}
export function AssignmentCard({ assignment: a }: { assignment: StudentHomeworkAssignment }) {
return (
<Card className="flex h-full flex-col overflow-hidden transition-all hover:shadow-md">
<CardHeader className="gap-2 pb-3">
<div className="flex items-start justify-between gap-3">
<CardTitle className="text-base">
<Link href={`/student/learning/assignments/${a.id}`} className="hover:underline">
{a.title}
</Link>
</CardTitle>
<Badge variant={getStatusVariant(a.progressStatus)} className="capitalize">
{getStatusLabel(a.progressStatus)}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
<span>Due {a.dueAt ? formatDate(a.dueAt) : "-"}</span>
<span className="px-2" aria-hidden="true">•</span>
<span>Attempts {a.attemptsUsed}/{a.maxAttempts}</span>
</div>
</CardHeader>
<CardContent className="mt-auto flex items-center justify-between">
<div className="text-sm">
<div className="text-muted-foreground">Score</div>
<div className="font-medium tabular-nums">{a.latestScore ?? "-"}</div>
</div>
<Button asChild size="sm" variant={getActionVariant(a.progressStatus)}>
<Link href={`/student/learning/assignments/${a.id}`}>
{getActionLabel(a.progressStatus)}
</Link>
</Button>
</CardContent>
</Card>
)
}
```
---
## 十、验证命令
修复完成后应运行以下命令确保零错误:
```bash
npm run lint
npx tsc --noEmit
npm run test:unit -- student
```
---
> 报告生成人AI AgentGLM-5.2
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配
> 应用技能:`vercel-react-best-practices`(性能优化)、`web-artifacts-builder`(界面构建参考)、`web-design-guidelines`(界面规范审查)