Files
NextEdu/bugs/others_bug.md
SpecialX 49291fcc31 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.
2026-06-19 05:13:34 +08:00

49 KiB
Raw Permalink Blame History

src/app/(dashboard)/{announcements,dashboard,management,messages,profile,settings} 规范核查报告

核查日期2026-06-18 核查范围:src/app/(dashboard)/ 下的 announcements、dashboard、management、messages、profile、settings 子路由及其直接依赖的模块组件 依据文档:


一、核查文件清单

文件 行数 类型 用途
announcements/page.tsx 20 RSC 页面 公告列表(普通用户)
dashboard/page.tsx 18 RSC 页面 角色路由分发
management/grade/classes/page.tsx 31 RSC 页面 年级班级管理
management/grade/insights/page.tsx 243 RSC 页面 年级作业洞察
messages/page.tsx 31 RSC 页面 消息+通知列表
messages/[id]/page.tsx 30 RSC 页面 消息详情
messages/compose/page.tsx 34 RSC 页面 撰写消息
profile/page.tsx 305 RSC 页面 个人资料(学生/教师视图)
settings/page.tsx 32 RSC 页面 设置入口(按角色分发)
settings/security/page.tsx 50 RSC 页面 安全设置
layout.tsx 21 RSC 布局 Dashboard 通用布局
error.tsx 22 客户端组件 错误边界
not-found.tsx 23 RSC 组件 404 页面
modules/announcements/components/announcement-list.tsx 108 客户端组件 公告列表(含筛选)
modules/announcements/components/announcement-card.tsx 79 客户端组件 公告卡片
modules/announcements/components/announcement-detail.tsx 206 客户端组件 公告详情
modules/messaging/components/message-list.tsx 117 客户端组件 消息列表
modules/messaging/components/message-detail.tsx 153 客户端组件 消息详情
modules/messaging/components/message-compose.tsx 146 客户端组件 撰写消息表单
modules/messaging/components/notification-list.tsx 141 客户端组件 通知列表
modules/settings/components/admin-settings-view.tsx 129 客户端组件 管理员设置视图
modules/settings/components/teacher-settings-view.tsx 132 客户端组件 教师设置视图
modules/settings/components/student-settings-view.tsx 120 客户端组件 学生设置视图
modules/settings/components/password-change-form.tsx 180 客户端组件 修改密码表单
modules/settings/components/profile-settings-form.tsx 198 客户端组件 资料编辑表单
modules/settings/components/notification-preferences-form.tsx 260 客户端组件 通知偏好表单
modules/settings/components/theme-preferences-card.tsx 60 客户端组件 主题偏好
modules/settings/components/ai-provider-settings-card.tsx 405 客户端组件 AI Provider 配置
modules/classes/components/grade-classes-view.tsx 455 客户端组件 年级班级管理视图

二、违规问题清单

2.1 announcements/page.tsx — 严重度:高

BUG-A01缺少权限校验违反 Server Action 规范)

  • 位置src/app/(dashboard)/announcements/page.tsx:6-7
  • 问题:页面直接调用 getAnnouncements({ status: "published" }),未通过 requirePermission()requireAuth() 进行任何权限校验
  • 规范依据项目规则「Server Action 必须使用 requirePermission() 进行权限校验」;架构文档 004 已记录此问题P2-12
  • 影响:未登录用户可直接访问 /announcements 路由获取公告数据,存在信息泄露风险
  • 改进建议
    import { requirePermission } from "@/shared/lib/auth-guard"
    import { Permissions } from "@/shared/types/permissions"
    
    export default async function AnnouncementsPage() {
      await requirePermission(Permissions.ANNOUNCEMENT_READ)
      const announcements = await getAnnouncements({ status: "published" })
      // ...
    }
    

BUG-A02缺少 metadata 导出

  • 位置src/app/(dashboard)/announcements/page.tsx
  • 问题:未导出 metadata,浏览器标签页无标题
  • 规范依据Web Interface Guidelines — Metadata & SEO
  • 改进建议:补充 export const metadata = { title: "Announcements" }

2.2 dashboard/page.tsx — 严重度:中

BUG-D01使用权限反推角色硬编码反模式

  • 位置src/app/(dashboard)/dashboard/page.tsx:14-16
  • 问题:使用 permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE) 反推学生身份,应使用 hasRole("student")
  • 规范依据:项目规则「前端组件禁止使用 role === "xxx" 硬编码,统一使用 usePermission().hasPermission()」;架构文档 004 已标记此为 P2 问题
  • 影响:当学生被授予 EXAM_CREATE 权限(如助教)时会被错误路由到教师页面
  • 改进建议:服务端应使用 session.user.roles 判断
    const roles = session.user.roles ?? []
    if (roles.includes("admin")) redirect("/admin/dashboard")
    if (roles.includes("student")) redirect("/student/dashboard")
    if (roles.includes("parent")) redirect("/parent/dashboard")
    redirect("/teacher/dashboard")
    

BUG-D02多重 redirect 调用难以维护

  • 位置src/app/(dashboard)/dashboard/page.tsx:14-17
  • 问题4 个连续 if + redirect 缺乏优先级文档说明,新增角色时易遗漏
  • 改进建议:抽取为 resolveDefaultPath(roles) 单一函数(proxy.ts 已有类似实现),保持单一职责

2.3 management/grade/classes/page.tsx — 严重度:高

BUG-M01缺少权限校验

  • 位置src/app/(dashboard)/management/grade/classes/page.tsx:7-15
  • 问题:仅调用 auth() 获取 session未调用 requirePermission() 校验 CLASS_MANAGE 权限
  • 规范依据项目规则「Server Action 必须使用 requirePermission() 进行权限校验」
  • 影响:无 CLASS_MANAGE 权限的用户可访问页面并获取教师列表、年级数据
  • 改进建议
    import { requirePermission } from "@/shared/lib/auth-guard"
    import { Permissions } from "@/shared/types/permissions"
    
    export default async function GradeClassesPage() {
      const ctx = await requirePermission(Permissions.CLASS_MANAGE)
      const userId = ctx.userId
      // ...
    }
    

BUG-M02userId 兜底为空字符串存在隐患

  • 位置src/app/(dashboard)/management/grade/classes/page.tsx:9
  • 问题const userId = session?.user?.id ?? "" 在未登录时返回空字符串,下游 getGradeManagedClasses("") 会查询无意义数据
  • 改进建议:未登录应直接 redirect("/login"),不应继续执行

2.4 management/grade/insights/page.tsx — 严重度:高

BUG-MI01缺少权限校验

  • 位置src/app/(dashboard)/management/grade/insights/page.tsx:25-34
  • 问题:页面直接调用 getTeacherIdForMutations()getGradesForStaff(),未调用 requirePermission()
  • 规范依据项目规则「Server Action 必须使用 requirePermission() 进行权限校验」
  • 改进建议:增加 requirePermission(Permissions.HOMEWORK_READ) 或对应年级负责人权限校验

BUG-MI02使用原生 <select> 而非 shadcn Select 组件

  • 位置src/app/(dashboard)/management/grade/insights/page.tsx:70-81
  • 问题:使用原生 <select> 元素,与项目其他页面使用的 shadcn Select 组件风格不一致
  • 规范依据Web Interface Guidelines — Consistency项目组件规范
  • 影响:视觉风格不统一,无障碍特性差异,主题切换时原生 select 样式无法跟随
  • 改进建议:替换为 shadcn Select 组件

BUG-MI03<label> 缺少 htmlFor 关联

  • 位置src/app/(dashboard)/management/grade/insights/page.tsx:69
  • 问题<label className="text-sm font-medium">Grade</label> 未关联到 select 元素,点击 label 无法聚焦
  • 规范依据Web Interface Guidelines — Forms「Labels properly associated」
  • 改进建议<label htmlFor="gradeId" className="...">Grade</label>

BUG-MI04表单提交触发整页刷新

  • 位置src/app/(dashboard)/management/grade/insights/page.tsx:68
  • 问题<form action="/management/grade/insights" method="get"> 使用原生 GET 提交,导致整页刷新
  • 违反规则rerender-use-deferred-value、Next.js 客户端导航最佳实践
  • 改进建议:改为客户端组件 + useRouter().push() 或使用 useSearchParams 实现无刷新筛选

BUG-MI05fmt 工具函数命名过于简短

  • 位置src/app/(dashboard)/management/grade/insights/page.tsx:23
  • 问题const fmt = (v: number | null, digits = 1) => ... 命名过于简短,不符合可读性要求
  • 改进建议:重命名为 formatScoreformatNumber

2.5 messages/page.tsx — 严重度:低

BUG-MSG01缺少 metadata 导出

  • 位置src/app/(dashboard)/messages/page.tsx
  • 问题:未导出 metadata
  • 改进建议export const metadata = { title: "Messages" }

2.6 messages/[id]/page.tsx — 严重度:中

BUG-MSG02渲染期间执行写操作标记已读

  • 位置src/app/(dashboard)/messages/[id]/page.tsx:20-23
  • 问题:在 RSC 渲染期间调用 markMessageAsRead(id, ctx.userId) 执行写操作
  • 违反规则React Server Components 规范 — 渲染函数应为纯函数,不应有副作用
  • 影响
    1. React 18+ 严格模式下渲染函数可能被调用两次,导致重复写入
    2. 流式渲染时若渲染被中断,写操作可能已执行但 UI 未更新
    3. 错误边界捕获错误后重试渲染会再次执行写操作
  • 改进建议:使用 after() API 延迟执行非阻塞写操作
    import { after } from "next/server"
    
    if (!message.isRead && message.receiverId === ctx.userId) {
      after(() => markMessageAsRead(id, ctx.userId))
    }
    
  • 规范依据vercel-react-best-practicesserver-after-nonblocking

2.7 messages/compose/page.tsx — 严重度:低

BUG-MSG03缺少 metadata 导出

  • 改进建议export const metadata = { title: "Compose Message" }

2.8 profile/page.tsx — 严重度:高

BUG-P01使用权限反推角色硬编码反模式

  • 位置src/app/(dashboard)/profile/page.tsx:47-48
  • 问题isStudent = permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)isTeacher = permissions.includes(EXAM_CREATE)
  • 规范依据:项目规则禁止硬编码角色判断;架构文档 004 已标记
  • 改进建议:使用 session.user.roles 判断
    const roles = session.user.roles ?? []
    const isStudent = roles.includes("student")
    const isTeacher = roles.includes("teacher")
    

BUG-P02在 RSC 中使用 IIFE 异步块(可读性差)

  • 位置src/app/(dashboard)/profile/page.tsx:50-118
  • 问题:使用 await (async () => { ... })() 立即执行异步函数,将学生数据加载逻辑内联在组件中
  • 影响
    1. 函数体过长60+ 行),难以测试
    2. 无法被 React cache() 缓存
    3. 违反单一职责原则
  • 改进建议:抽取为 data-access.ts 中的 getStudentProfileData(userId) 函数
    // modules/users/data-access.ts
    export const getStudentProfileData = cache(async (userId: string) => {
      const [classes, schedule, assignmentsAll, grades] = await Promise.all([...])
      // ... 计算逻辑
      return { enrolledClassCount, dueSoonCount, ... }
    })
    

BUG-P03本地 formatDate 函数与全局工具重复

  • 位置src/app/(dashboard)/profile/page.tsx:26-33
  • 问题:定义了本地 formatDate 函数,与 @/shared/lib/utils.formatDate 重复
  • 影响:日期格式不一致(本地使用 en-US,全局使用 zh-CN),维护成本增加
  • 改进建议:删除本地函数,使用全局 formatDate,或为全局函数增加 locale 参数

BUG-P04toWeekday 类型断言不必要

  • 位置src/app/(dashboard)/profile/page.tsx:21-24
  • 问题(day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7 使用 as 断言
  • 规范依据:编码规范 4.2.3「禁止 as 断言(除非从 unknown 转换)」
  • 改进建议:使用类型守卫
    const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
      const day = d.getDay()
      const result = day === 0 ? 7 : day
      if (result < 1 || result > 7) throw new Error("Invalid weekday")
      return result
    }
    

BUG-P05缩进不一致

  • 位置src/app/(dashboard)/profile/page.tsx:157,167,185,205-207
  • 问题:多处缩进不一致(如 157 行 <div 比 156 行多一个空格)
  • 规范依据.prettierrc 配置 tabWidth: 2
  • 改进建议:运行 npx prettier --write 统一格式

BUG-P06缺少 metadata 导出

  • 改进建议export const metadata = { title: "Profile" }

2.9 settings/page.tsx — 严重度:中

BUG-S01使用权限反推角色

  • 位置src/app/(dashboard)/settings/page.tsx:25-30
  • 问题:同 BUG-P01使用 permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE) 判断学生
  • 改进建议:使用 session.user.roles 判断

BUG-S02缺少 metadata 导出

  • 改进建议export const metadata = { title: "Settings" }

2.10 settings/security/page.tsx — 严重度:低

BUG-SS01缺少权限校验

  • 位置src/app/(dashboard)/settings/security/page.tsx:14-16
  • 问题:仅检查 session?.user,未调用 requirePermission()
  • 改进建议:至少调用 requireAuth() 确保登录状态

2.11 layout.tsx — 严重度:中

BUG-L01跳过链接样式使用任意值

  • 位置src/app/(dashboard)/layout.tsx:12
  • 问题focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground focus:border focus:border-border focus:rounded-md focus:m-2 类名过长且重复
  • 规范依据:项目规则「禁止使用任意值(w-[137px])」
  • 改进建议:抽取为 skip-link 类名或独立组件

BUG-L02<main> 元素缺少 role="main"(虽隐式但建议显式)

  • 位置src/app/(dashboard)/layout.tsx:16
  • 问题<main id="main-content"> 已有 id,但部分屏幕阅读器需要显式 role="main"
  • 改进建议:添加 role="main"(虽然 HTML5 规范中 <main> 隐式 role="main",但为兼容性建议显式)

2.12 error.tsx — 严重度:低

BUG-E01未使用 error.digest 信息

  • 位置src/app/(dashboard)/error.tsx:7
  • 问题error 参数包含 digest 字段(用于错误追踪),但未展示给用户或上报
  • 改进建议:在描述中包含 digest 或提供「复制错误码」按钮

2.13 not-found.tsx — 严重度:低

BUG-NF01使用原生 <a> 样式而非 Button 组件

  • 位置src/app/(dashboard)/not-found.tsx:15-20
  • 问题<Link className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-9 ..."> 手动拼接 Button 样式
  • 规范依据:项目组件规范「使用 cn() 工具函数管理条件类名」
  • 改进建议:使用 <Button asChild><Link href="/dashboard">...</Link></Button>

2.14 announcement-list.tsx — 严重度:中

BUG-AL01使用 <a href> 而非 <Link>(全页刷新)

  • 位置src/modules/announcements/components/announcement-list.tsx:76
  • 问题<a href={createHref}> 使用原生 <a> 标签,导致全页刷新
  • 违反规则vercel-react-best-practices — Next.js 客户端导航最佳实践
  • 改进建议:使用 next/link<Link> 组件
    import Link from "next/link"
    <Button asChild>
      <Link href={createHref ?? "#"}>
        <Plus className="mr-2 h-4 w-4" />
        New Announcement
      </Link>
    </Button>
    

BUG-AL02handleFilterChange 未使用 useCallback

  • 位置src/modules/announcements/components/announcement-list.tsx:51-57
  • 问题handleFilterChange 每次渲染创建新引用,传递给 SelectonValueChange 导致不必要重渲染
  • 违反规则rerender-functional-setstatererender-memo
  • 改进建议:使用 useCallback 包裹

2.15 announcement-card.tsx — 严重度:低

BUG-AC01useMemo 包裹整个 JSX过度优化

  • 位置src/modules/announcements/components/announcement-card.tsx:38-68
  • 问题:使用 useMemo 包裹整个卡片 JSX依赖项为 [announcement](对象)
  • 违反规则rerender-simple-expression-in-memo — 简单表达式不需要 memo
  • 影响announcement 是对象,每次父组件传入新引用时 memo 失效,无实际优化效果
  • 改进建议:移除 useMemo,直接渲染 JSX如需优化应使用 React.memo 包裹组件
    export const AnnouncementCard = React.memo(function AnnouncementCard({...}) {
      return <Card>...</Card>
    })
    

2.16 announcement-detail.tsx — 严重度:中

  • 位置src/modules/announcements/components/announcement-detail.tsx:123,146
  • 问题backHrefeditHref 使用原生 <a> 标签
  • 改进建议:替换为 next/link

BUG-AD02三个处理函数未 useCallback

  • 位置src/modules/announcements/components/announcement-detail.tsx:64-115
  • 问题handlePublishhandleArchivehandleDelete 每次渲染创建新引用
  • 违反规则rerender-functional-setstate
  • 改进建议:使用 useCallback 包裹

2.17 message-list.tsx — 严重度:中

BUG-ML01使用字符串拼接动态类名

  • 位置src/modules/messaging/components/message-list.tsx:82,91
  • 问题className={`transition-colors hover:bg-accent/50 ${unread ? "border-primary/40" : ""}`} 使用模板字符串拼接类名
  • 规范依据:项目规则「使用 cn() 工具函数管理条件类名」
  • 改进建议
    className={cn(
      "transition-colors hover:bg-accent/50",
      unread && "border-primary/40"
    )}
    

BUG-ML02usePermission 在客户端组件中导致 hydration 风险

  • 位置src/modules/messaging/components/message-list.tsx:30-31
  • 问题usePermission() 依赖 useSession()服务端渲染时返回空权限客户端首次渲染后才有权限导致「Compose」按钮在 hydration 后闪烁
  • 违反规则Web Interface Guidelines — Hydration Safety
  • 改进建议:将 canSend 作为 prop 从 RSC 父组件传入

2.18 message-detail.tsx — 严重度:中

  • 位置src/modules/messaging/components/message-detail.tsx:79
  • 问题<a href={backHref}> 使用原生 <a>
  • 改进建议:替换为 next/link
  • 位置src/modules/messaging/components/message-detail.tsx:68,87-92
  • 问题:当 canSend 为 false 时 replyHrefundefined,但代码使用 <Link href={replyHref ?? "#"}> 仍渲染可点击链接,点击后跳转到 #
  • 影响:用户体验差,点击无效链接
  • 改进建议canSend 为 false 时不渲染 Reply 按钮(当前已有 {canSend ? ... : null} 包裹,但内部仍用 ?? "#" 兜底,应直接使用 replyHref! 或移除兜底)

BUG-MD03URL 参数未编码

  • 位置src/modules/messaging/components/message-detail.tsx:69-71
  • 问题subject=${encodeURIComponent(...)} 已编码 subjectparentIdreceiverId 未编码(虽然 UUID 不含特殊字符,但不严谨)
  • 改进建议:使用 URLSearchParams 构建查询字符串
    const params = new URLSearchParams({
      parentId: message.id,
      receiverId: isReceived ? message.senderId : message.receiverId,
      subject: message.subject?.startsWith("Re:") ? message.subject : `Re: ${message.subject ?? ""}`,
    })
    const replyHref = canSend ? `/messages/compose?${params.toString()}` : undefined
    

2.19 message-compose.tsx — 严重度:中

  • 位置src/modules/messaging/components/message-compose.tsx:73
  • 问题:返回按钮使用原生 <a>
  • 改进建议:替换为 next/link

BUG-MC02隐藏 input 与 formData.set 重复

  • 位置src/modules/messaging/components/message-compose.tsx:46,97
  • 问题handleSubmitformData.set("receiverId", receiverId),同时 JSX 中又有 <input type="hidden" name="receiverId" value={receiverId} />,两者重复
  • 改进建议:移除隐藏 input仅使用 formData.set

BUG-MC03handleSubmituseCallback

  • 位置src/modules/messaging/components/message-compose.tsx:41-66
  • 改进建议:使用 useCallback 包裹

2.20 notification-list.tsx — 严重度:中

BUG-NL01使用字符串拼接动态类名

  • 位置src/modules/messaging/components/notification-list.tsx:94,102
  • 问题className={`transition-colors ${!n.isRead ? "border-primary/40 bg-primary/5" : ""}`}
  • 规范依据:项目规则「使用 cn() 工具函数管理条件类名」
  • 改进建议:使用 cn()

BUG-NL02handleMarkReaduseCallback

  • 位置src/modules/messaging/components/notification-list.tsx:54-63
  • 改进建议:使用 useCallback

BUG-NL03<button> 元素缺少 type 属性

  • 位置src/modules/messaging/components/notification-list.tsx:118-124
  • 问题<button onClick={...}> 未指定 type="button",默认为 submit,若被表单包裹会触发提交
  • 规范依据Web Interface Guidelines — Forms
  • 改进建议:添加 type="button"

2.21 password-change-form.tsx — 严重度:高

BUG-PC01使用字符串拼接动态类名严重违规

  • 位置src/modules/settings/components/password-change-form.tsx:133
  • 问题className={`h-2 [&>div]:${meta.color}`} 动态拼接 Tailwind 类名
  • 规范依据:项目规则「禁止字符串拼接动态类名(bg-${color}-500)」
  • 影响Tailwind JIT 无法识别动态拼接的类名,bg-red-500bg-yellow-500bg-green-500 可能被 tree-shaking 移除,导致生产环境进度条无颜色
  • 改进建议:使用映射对象 + cn()
    const STRENGTH_BAR_CLASS: Record<PasswordStrength, string> = {
      weak: "h-2 [&>div]:bg-red-500",
      medium: "h-2 [&>div]:bg-yellow-500",
      strong: "h-2 [&>div]:bg-green-500",
    }
    
    <Progress value={meta.value} className={STRENGTH_BAR_CLASS[strength]} />
    

BUG-PC02使用 document.getElementById 操作 DOM反 React 模式)

  • 位置src/modules/settings/components/password-change-form.tsx:62-63
  • 问题const form = document.getElementById("password-change-form") as HTMLFormElement | null 直接操作 DOM
  • 规范依据React 最佳实践 — 避免直接 DOM 操作
  • 改进建议:使用 useRef<HTMLFormElement> 或受控组件重置表单
    const formRef = useRef<HTMLFormElement>(null)
    // ...
    formRef.current?.reset()
    

BUG-PC03as 断言使用

  • 位置src/modules/settings/components/password-change-form.tsx:62
  • 问题as HTMLFormElement | null 使用类型断言
  • 规范依据:编码规范 4.2.3「禁止 as 断言」
  • 改进建议:使用 useRef 后通过 ref.current 的类型推导

2.22 profile-settings-form.tsx — 严重度:高

BUG-PS01使用 as any 类型断言(严重违规)

  • 位置src/modules/settings/components/profile-settings-form.tsx:35
  • 问题resolver: zodResolver(profileFormSchema) as any 使用 as any
  • 规范依据:项目规则「禁止 any」「禁止 as 断言
  • 改进建议:修复 zodResolver 类型不匹配问题
    // 方案 1使用 react-hook-form 的 Resolver 类型
    import type { Resolver } from "react-hook-form"
    const resolver: Resolver<ProfileFormValues> = zodResolver(profileFormSchema)
    
    // 方案 2修正 schema 类型定义
    const profileFormSchema = z.object({...}) satisfies z.ZodType<ProfileFormValues>
    

BUG-PS02console.error 残留

  • 位置src/modules/settings/components/profile-settings-form.tsx:60
  • 问题console.error(error) 在生产代码中残留
  • 规范依据:编码规范 — 生产代码不应包含 console.*
  • 改进建议:移除或替换为日志服务

BUG-PS03onSubmituseCallback

  • 位置src/modules/settings/components/profile-settings-form.tsx:47-63
  • 改进建议:使用 useCallback

BUG-PS04age 字段使用 z.coerce.number() 但未处理 NaN

  • 位置src/modules/settings/components/profile-settings-form.tsx:25
  • 问题age: z.coerce.number().min(0).optional() 当输入为空字符串时会转换为 0,而非 undefined
  • 改进建议:使用 z.preprocess 处理空值
    age: z.preprocess(
      (v) => (v === "" || v === null || v === undefined ? undefined : Number(v)),
      z.number().min(0).optional()
    )
    

2.23 notification-preferences-form.tsx — 严重度:中

BUG-NPF01Switch 与隐藏 checkbox 状态同步问题

  • 位置src/modules/settings/components/notification-preferences-form.tsx:186-201,233-248
  • 问题:同时使用隐藏 <input type="checkbox"><Switch>,两者都调用 toggleChannel/toggleCategory,可能导致双重切换
  • 影响:用户点击 Switch 时,onCheckedChange 触发;同时隐藏 checkbox 的 onChange 也触发,导致状态切换两次回到原点
  • 改进建议:移除隐藏 checkbox仅使用 Switch + 隐藏 inputtype="hidden")提交表单
    <input type="hidden" name={item.key} value={checked ? "true" : "false"} />
    <Switch
      checked={checked}
      onCheckedChange={() => toggleChannel(item.key)}
      aria-label={item.label}
    />
    

BUG-NPF02本地状态与服务器状态可能不同步

  • 位置src/modules/settings/components/notification-preferences-form.tsx:122-133
  • 问题useState 初始化自 preferences prop但 prop 变化时状态不更新
  • 违反规则rerender-derived-state-no-effect — 不应使用 effect 同步派生状态
  • 改进建议:使用 key prop 重置组件,或使用受控组件

BUG-NPF03中文注释混合英文代码

  • 位置src/modules/settings/components/notification-preferences-form.tsx:121,161,209
  • 问题// 本地状态用于即时反馈 Switch 切换{/* 通知渠道 */}{/* 通知类别 */} 中文注释
  • 规范依据:项目代码一致性(其他文件使用英文注释)
  • 改进建议:统一为英文注释

2.24 theme-preferences-card.tsx — 严重度:低

BUG-TP01"use client" 后缺少空行

  • 位置src/modules/settings/components/theme-preferences-card.tsx:1-2
  • 问题"use client" 紧跟 import 无空行
  • 规范依据.prettierrc 格式规范
  • 改进建议:运行 npx prettier --write

BUG-TP02setTheme 参数类型不安全

  • 位置src/modules/settings/components/theme-preferences-card.tsx:31
  • 问题onValueChange={(v) => setTheme(v)}vstring,但 setTheme 期望特定类型
  • 改进建议onValueChange={(v) => setTheme(v as ThemeChoice)}(虽然违反 as 规范,但 next-themes 类型定义如此;或使用类型守卫)

2.25 ai-provider-settings-card.tsx — 严重度:高

BUG-AI01中英文混合 UI严重一致性违规

  • 位置src/modules/settings/components/ai-provider-settings-card.tsx:298,325,352,367
  • 问题FormLabel 使用中文「品牌方」「设为默认」FormDescription 使用中文「填写基础地址,不要包含 /chat/completions。」「不会回显历史 Key留空表示不更新。」
  • 规范依据Web Interface Guidelines — Consistency项目其他 UI 均为英文
  • 影响:用户在英文界面中突然看到中文,体验割裂
  • 改进建议:统一为英文
    <FormLabel>Provider</FormLabel>
    <FormDescription>Enter base URL without /chat/completions suffix.</FormDescription>
    <FormLabel>Set as default</FormLabel>
    <FormDescription>Existing key won't be displayed. Leave blank to keep current.</FormDescription>
    

BUG-AI02useEffect 依赖项过多导致重复执行

  • 位置src/modules/settings/components/ai-provider-settings-card.tsx:108-136
  • 问题useEffect 依赖 [form, selectedId, onProvidersChanged, initialMode, resetToNew],但使用 loadedRef 防止重复执行
  • 违反规则rerender-dependencies — 应使用原始依赖
  • 改进建议:将初始化逻辑移至 useEffect 内部,依赖项仅为 [](仅执行一次)
    useEffect(() => {
      let cancelled = false
      startTransition(async () => {
        const rows = await getAiProviderSummaries()
        if (cancelled) return
        // ...
      })
      return () => { cancelled = true }
    }, []) // 仅挂载时执行
    

BUG-AI03handleSelectChangeuseCallback

  • 位置src/modules/settings/components/ai-provider-settings-card.tsx:138-156
  • 改进建议:使用 useCallback

BUG-AI04文件行数 405 行,接近上限

  • 位置src/modules/settings/components/ai-provider-settings-card.tsx
  • 问题:文件 405 行,项目规则建议 React 组件 ≤ 500 行,但复杂度较高
  • 改进建议:考虑拆分为 AiProviderSelectAiProviderFormAiProviderTestButton 子组件

2.26 admin-settings-view.tsx — 严重度:低

BUG-AS01Tab 图标语义错误

  • 位置src/modules/settings/components/admin-settings-view.tsx:50-53
  • 问题appearance Tab 使用 <Shield /> 图标(盾牌通常表示安全),应使用 <Palette /><Monitor />
  • 规范依据Web Interface Guidelines — Iconography
  • 改进建议<TabsTrigger value="appearance"><Palette /></TabsTrigger>

BUG-AS02signOut 直接调用未确认

  • 位置src/modules/settings/components/admin-settings-view.tsx:120
  • 问题onClick={() => signOut({ callbackUrl: "/login" })} 直接登出,无确认对话框
  • 规范依据Web Interface Guidelines — Destructive Actions
  • 改进建议:增加确认对话框(虽然登出非破坏性,但意外登出影响体验)

2.27 teacher-settings-view.tsx — 严重度:低

BUG-TS01与 admin-settings-view.tsx 大量重复代码

  • 位置src/modules/settings/components/teacher-settings-view.tsx
  • 问题:与 admin-settings-view.tsxstudent-settings-view.tsx 90% 代码重复仅「Back to dashboard」链接和「Quick links」不同
  • 规范依据DRY 原则
  • 改进建议:抽取为 SettingsLayout 共享组件,通过 props 传入 backHrefquickLinks
    export function SettingsLayout({ title, description, backHref, quickLinks, children }: {...}) {
      return <div>...</div>
    }
    

2.28 student-settings-view.tsx — 严重度:低

BUG-ST01同 BUG-TS01代码重复

  • 改进建议:同 BUG-TS01

2.29 grade-classes-view.tsx — 严重度:高

BUG-GC01文件 455 行,超过 500 行建议上限的 91%

  • 位置src/modules/classes/components/grade-classes-view.tsx
  • 问题:单文件 455 行,包含列表、创建对话框、编辑对话框、删除确认对话框
  • 规范依据项目规则「React 组件:建议 ≤ 500 行」
  • 改进建议:拆分为:
    • grade-classes-view.tsx(主视图,< 100 行)
    • grade-class-create-dialog.tsx
    • grade-class-edit-dialog.tsx
    • grade-class-delete-dialog.tsx

BUG-GC02useEffect 依赖项导致不必要重渲染

  • 位置src/modules/classes/components/grade-classes-view.tsx:62-78
  • 问题:两个 useEffect 依赖 managedGrades 数组引用,父组件每次传入新数组都会触发
  • 违反规则rerender-dependencies
  • 改进建议:依赖 managedGrades[0]?.id 而非整个数组

BUG-GC03中英文混合 UI

  • 位置src/modules/classes/components/grade-classes-view.tsx:183-184,283,370,389
  • 问题:表头「班主任」「任课老师」使用中文,其他列使用英文
  • 规范依据Web Interface Guidelines — Consistency
  • 改进建议:统一为英文 Homeroom TeacherSubject Teachers

BUG-GC04formatSubjectTeachers 在每次渲染时重新创建

  • 位置src/modules/classes/components/grade-classes-view.tsx:140-146
  • 问题:函数在组件内定义,每次渲染创建新引用
  • 改进建议:移至模块级别(不依赖组件状态)

三、React 性能优化(应用 vercel-react-best-practices 技能)

3.1 重渲染优化

PERF-01usePermission 返回的回调未 memoize

  • 位置src/shared/hooks/use-permission.ts:11-25
  • 问题hasPermissionhasAnyPermissionhasAllPermissionshasRole 每次渲染创建新函数引用
  • 违反规则rerender-functional-setstatererender-memo
  • 影响message-list.tsxmessage-detail.tsx 中使用 usePermission() 的组件每次渲染都创建新 canSend/canDelete
  • 改进建议:使用 useCallback 包裹所有回调(详见 student_bug.md PERF-01

PERF-02AnnouncementCarduseMemo 无效

  • 位置src/modules/announcements/components/announcement-card.tsx:38-68
  • 问题useMemo 依赖 [announcement]对象父组件每次渲染传入新引用memo 失效
  • 违反规则rerender-simple-expression-in-memo
  • 改进建议:移除 useMemo,使用 React.memo 包裹组件

PERF-03profile/page.tsx 串行 await 未并行化

  • 位置src/app/(dashboard)/profile/page.tsx:53-58
  • 问题:学生数据加载使用 Promise.all ,但 userProfilestudentData 是串行执行
  • 违反规则async-parallel
  • 改进建议userProfile 和角色判断后,并行加载学生/教师数据(当前已是此模式,但 userProfile 必须先获取才能判断角色,无法并行)

PERF-04messages/page.tsx 已正确使用 Promise.all

  • 位置src/app/(dashboard)/messages/page.tsx:12-15
  • 现状 已使用 Promise.all 并行加载 messages 和 notifications

PERF-05management/grade/classes/page.tsx 已正确使用 Promise.all

  • 位置src/app/(dashboard)/management/grade/classes/page.tsx:11-15
  • 现状 已并行加载 classes、teachers、managedGrades

PERF-06ai-provider-settings-card.tsx 使用 loadedRef 防止重复加载

  • 位置src/modules/settings/components/ai-provider-settings-card.tsx:66,109-110
  • 问题:使用 loadedRef 而非空依赖 useEffect
  • 违反规则rerender-dependencies
  • 改进建议:使用空依赖数组 [] + 清理函数

3.2 Bundle Size 优化

PERF-07lucide-react 导入方式

  • 位置:多处,如 src/app/(dashboard)/profile/page.tsx:17
  • 问题import { User, Mail, Phone, MapPin, Calendar, Clock, Shield } from "lucide-react" 从 barrel 文件导入
  • 违反规则bundle-barrel-imports
  • 现状Next.js 13+ 自动 tree-shaking lucide-react,影响较小
  • 改进建议:保持现状,但确保 next.config.js 启用了 optimizePackageImports

3.3 服务端性能

PERF-08profile/page.tsx 数据加载未使用 cache()

  • 位置src/app/(dashboard)/profile/page.tsx:50-118
  • 问题:学生数据加载逻辑内联在组件中,无法被 React cache() 去重
  • 违反规则server-cache-react
  • 改进建议:抽取为 data-access.ts 中的 cache() 包裹函数

PERF-09messages/[id]/page.tsx 渲染期间写操作

  • 位置src/app/(dashboard)/messages/[id]/page.tsx:20-23
  • 问题:渲染期间调用 markMessageAsRead 执行写操作
  • 违反规则server-after-nonblocking
  • 改进建议:使用 after() API

四、Web 界面规范审查(应用 web-design-guidelines 技能)

4.1 Hydration Safety

UI-01usePermission 导致 hydration 闪烁

  • 位置src/modules/messaging/components/message-list.tsx:30-31src/modules/messaging/components/message-detail.tsx:41-43
  • 问题usePermission() 依赖 useSession(),服务端渲染时无权限,客户端 hydration 后权限相关 UICompose、Reply、Delete 按钮)闪烁出现
  • 违反规则Web Interface Guidelines — Hydration Safety
  • 改进建议:将权限判断结果作为 prop 从 RSC 父组件传入
    // RSC 父组件
    const canSend = ctx.permissions.includes(Permissions.MESSAGE_SEND)
    <MessageList messages={...} canSend={canSend} />
    

UI-02theme-preferences-card.tsx 已使用 suppressHydrationWarning

  • 位置src/modules/settings/components/theme-preferences-card.tsx:32
  • 现状 已正确处理主题切换的 hydration 问题

4.2 Navigation & State

UI-03使用 <a href> 导致全页刷新

  • 位置多处BUG-AL01、BUG-AD01、BUG-MD01、BUG-MC01
  • 问题:使用原生 <a> 而非 <Link>,破坏 SPA 导航
  • 违反规则Web Interface Guidelines — Navigation
  • 改进建议:全部替换为 next/link

UI-04announcement-list.tsx 筛选状态未反映在 URL

  • 位置src/modules/announcements/components/announcement-list.tsx:51-57
  • 问题handleFilterChange 使用 router.replace(qs ? ?${qs} : ?) 更新 URL ,但初始 filter 状态来自 initialStatus prop 而非 URL
  • 改进建议:使用 useSearchParams 读取 URL 状态

UI-05message-detail.tsx 回复链接 URL 参数构建不严谨

  • 位置src/modules/messaging/components/message-detail.tsx:69-71
  • 问题:手动拼接 URL 参数,未使用 URLSearchParams
  • 改进建议:见 BUG-MD03

4.3 Forms

UI-06management/grade/insights/page.tsx label 未关联 select

  • 位置src/app/(dashboard)/management/grade/insights/page.tsx:69
  • 问题<label> 缺少 htmlFor
  • 违反规则Web Interface Guidelines — Forms
  • 改进建议:见 BUG-MI03

UI-07notification-list.tsx button 缺少 type 属性

  • 位置src/modules/messaging/components/notification-list.tsx:118
  • 问题<button> 未指定 type="button"
  • 违反规则Web Interface Guidelines — Forms
  • 改进建议:见 BUG-NL03

UI-08message-compose.tsx 表单提交使用 formData.set 而非受控组件

  • 位置src/modules/messaging/components/message-compose.tsx:46-49
  • 问题:混合使用受控(receiverId state和非受控FormData模式
  • 改进建议:统一使用受控组件或完全使用 FormData

4.4 Content & Copy

UI-09中英文混合 UI

  • 位置
    • ai-provider-settings-card.tsxBUG-AI01
    • grade-classes-view.tsxBUG-GC03
    • notification-preferences-form.tsxBUG-NPF03注释
  • 违反规则Web Interface Guidelines — Consistency
  • 改进建议:统一为英文

UI-10错误消息缺少修复步骤

  • 位置src/app/(dashboard)/error.tsx:13
  • 问题"We apologize for the inconvenience. An unexpected error occurred." 未提供下一步操作
  • 违反规则Web Interface Guidelines — Content & Copy
  • 改进建议:增加「联系管理员」链接或错误码展示

UI-11admin-settings-view.tsx Tab 图标语义错误

  • 位置src/modules/settings/components/admin-settings-view.tsx:50-53
  • 问题Appearance Tab 使用 Shield 图标
  • 违反规则Web Interface Guidelines — Iconography
  • 改进建议:见 BUG-AS01

4.5 Accessibility

UI-12notification-list.tsx icon 按钮缺少 aria-label

  • 位置src/modules/messaging/components/notification-list.tsx:118-124
  • 问题「Mark as read」按钮文本存在但图标按钮模式未统一
  • 改进建议:确保所有图标按钮有 aria-label

UI-13layout.tsx 跳过链接样式冗长

  • 位置src/app/(dashboard)/layout.tsx:12
  • 问题:跳过链接使用大量 focus: 前缀类名,难以维护
  • 改进建议:抽取为独立样式或组件

4.6 Performance

UI-14management/grade/insights/page.tsx 表单提交整页刷新

  • 位置src/app/(dashboard)/management/grade/insights/page.tsx:68
  • 问题:原生 form GET 提交导致整页刷新
  • 违反规则Web Interface Guidelines — Performance
  • 改进建议:见 BUG-MI04

UI-15profile/page.tsx 内联数据处理逻辑

  • 位置src/app/(dashboard)/profile/page.tsx:60-108
  • 问题:在组件内执行数组排序、过滤等耗时操作
  • 改进建议:移至 data-access 层

五、架构文档同步问题

5.1 004_architecture_impact_map.md

DOC-01announcements 模块未记录页面缺少权限校验

  • 位置004 文档 2.16 节
  • 问题:已记录 getAnnouncementsAction 使用 requireAuth() 而非 requirePermission(),但未记录 app/(dashboard)/announcements/page.tsx 完全缺少权限校验
  • 改进建议:补充已知问题「⚠️ P2app/(dashboard)/announcements/page.tsx 完全缺少权限校验」

DOC-02management 模块未在架构文档中独立记录

  • 位置004 文档
  • 问题app/(dashboard)/management/grade/ 路由未在架构文档中记录其依赖关系
  • 改进建议:补充 management 路由的模块依赖classes、school

DOC-03settings 模块文件清单过期

  • 位置004 文档 2.23 节
  • 问题:记录 components/* | 8 文件,但实际有 8 个文件 ,需核对行数
  • 改进建议:核对并更新各文件行数

5.2 005_architecture_data.json

DOC-04缺少 management 路由记录

  • 改进建议:在 routes 数组中补充 management 路由

六、问题汇总统计

严重度 数量 问题编号
9 BUG-A01, BUG-M01, BUG-MI01, BUG-P01, BUG-P02, BUG-PC01, BUG-PS01, BUG-AI01, BUG-GC01
14 BUG-D01, BUG-MI02, BUG-MI03, BUG-MI04, BUG-MSG02, BUG-S01, BUG-L01, BUG-AL01, BUG-AD01, BUG-ML01, BUG-ML02, BUG-MD01, BUG-MD02, BUG-MC01, BUG-NL01, BUG-NPF01, BUG-AI02
13 BUG-A02, BUG-D02, BUG-MI05, BUG-MSG01, BUG-MSG03, BUG-P03, BUG-P04, BUG-P05, BUG-P06, BUG-S02, BUG-SS01, BUG-L02, BUG-E01, BUG-NF01, BUG-AC01, BUG-AD02, BUG-MC02, BUG-MC03, BUG-NL02, BUG-NL03, BUG-PC02, BUG-PC03, BUG-PS02, BUG-PS03, BUG-PS04, BUG-NPF02, BUG-NPF03, BUG-TP01, BUG-TP02, BUG-AI03, BUG-AI04, BUG-AS01, BUG-AS02, BUG-TS01, BUG-ST01, BUG-GC02, BUG-GC03, BUG-GC04
性能 9 PERF-01, PERF-02, PERF-03, PERF-04, PERF-05, PERF-06, PERF-07, PERF-08, PERF-09
界面 15 UI-01 ~ UI-15
文档 4 DOC-01, DOC-02, DOC-03, DOC-04
合计 64

七、修复优先级建议

P0立即修复 — 影响安全与正确性)

  1. BUG-A01announcements/page.tsx 增加权限校验
  2. BUG-M01management/grade/classes/page.tsx 增加权限校验
  3. BUG-MI01management/grade/insights/page.tsx 增加权限校验
  4. BUG-PC01password-change-form.tsx 修复动态类名拼接(生产环境进度条无颜色)
  5. BUG-PS01profile-settings-form.tsx 移除 as any
  6. BUG-AI01ai-provider-settings-card.tsx 统一 UI 语言为英文

P1本迭代修复 — 影响可维护性与性能)

  1. BUG-P01、BUG-S01、BUG-D01:使用 roles 判断角色,移除权限反推
  2. BUG-P02profile/page.tsx 抽取数据加载逻辑到 data-access
  3. BUG-MSG02messages/[id]/page.tsx 使用 after() 延迟写操作
  4. BUG-AL01、BUG-AD01、BUG-MD01、BUG-MC01:替换 <a><Link>
  5. BUG-ML01、BUG-NL01:使用 cn() 替换字符串拼接
  6. PERF-01usePermission 回调 memoize
  7. UI-01:权限相关 UI 改为 RSC prop 传入

P2下迭代修复 — 增强健壮性)

  1. BUG-GC01grade-classes-view.tsx 拆分组件
  2. BUG-NPF01notification-preferences-form.tsx 修复 Switch/checkbox 双重切换
  3. BUG-MI02、BUG-MI03、BUG-MI04management/grade/insights 改用 shadcn Select
  4. BUG-PC02password-change-form.tsx 使用 useRef 替代 document.getElementById
  5. BUG-TS01、BUG-ST01:抽取 SettingsLayout 共享组件
  6. BUG-AS01:修复 Tab 图标语义
  7. UI-10:错误页增加修复步骤

P3文档同步

  1. DOC-01 ~ DOC-04:同步架构文档

八、验证命令

修复完成后应运行以下命令确保零错误:

npm run lint
npx tsc --noEmit
npm run test:unit

针对特定模块的端到端验证:

# 验证权限校验
curl -I http://localhost:3000/announcements  # 应返回 302 重定向到 /login
curl -I http://localhost:3000/management/grade/classes  # 应返回 302
curl -I http://localhost:3000/management/grade/insights  # 应返回 302

# 验证 hydration
# 在浏览器控制台检查无 hydration warning

报告生成人AI AgentGLM-5.2 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配 应用技能:vercel-react-best-practices65 条规则)、web-design-guidelinesWeb Interface Guidelines 注:web-artifacts-builder 技能加载失败,界面优化建议已合并至第四章