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:
711
bugs/student_bug.md
Normal file
711
bugs/student_bug.md
Normal 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-L03:JSX 语法格式错误
|
||||
- **位置**:`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-C01:catch 块吞掉错误
|
||||
- **位置**:`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-01:005 JSON 中 `/student/grades` 的 dataAccess 记录错误
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:12047`
|
||||
- **问题**:记录为 `"grades/actions.getStudentGradeSummaryAction"`,实际代码调用 `grades/data-access.getStudentGradeSummary`
|
||||
- **改进建议**:修正为 `"grades/data-access.getStudentGradeSummary"`
|
||||
|
||||
### DOC-02:005 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-03:005 JSON 中 `/student/diagnostic` 的 type 错误
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:12063`
|
||||
- **问题**:记录 `"type": "client"`,实际 `page.tsx` 是 Server Component
|
||||
- **改进建议**:改为 `"type": "server"`,并注明内部 `StudentDiagnosticView` 组件为 client
|
||||
|
||||
### DOC-04:005 JSON 中 `/student/dashboard` 缺少 `getStudentSchedule` 记录
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:11978-11982`
|
||||
- **问题**:实际代码调用了 `getStudentSchedule(student.id)`,但 dataAccess 数组未记录
|
||||
- **改进建议**:补充 `"classes/data-access.getStudentSchedule"`
|
||||
|
||||
### DOC-05:005 JSON 中 `/student/learning/assignments` 缺少 `getDemoStudentUser` 记录
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:11989-11991`
|
||||
- **问题**:实际代码调用了 `getDemoStudentUser()`,但 dataAccess 数组未记录
|
||||
- **改进建议**:补充 `"homework/data-access.getDemoStudentUser"`(或迁移后更新为 `users/data-access.getCurrentStudentUser`)
|
||||
|
||||
### DOC-06:004 文档 2.26 节未记录 `loading.tsx` 文件
|
||||
- **位置**:`docs/architecture/004_architecture_impact_map.md:1109-1114`
|
||||
- **问题**:文件清单仅列出 3 个组件,未记录 `app/(dashboard)/student/*/loading.tsx`
|
||||
- **改进建议**:补充 loading.tsx 文件清单
|
||||
|
||||
### DOC-07:004 文档未记录 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 Agent(GLM-5.2)
|
||||
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配
|
||||
> 应用技能:`vercel-react-best-practices`(性能优化)、`web-artifacts-builder`(界面构建参考)、`web-design-guidelines`(界面规范审查)
|
||||
Reference in New Issue
Block a user