后端模块规范核查报告
核查日期:2026-06-18
核查范围:src/modules/ 下所有后端 .ts 文件(actions / data-access / schema / types / services 等非 components、非 hooks 文件)
核查依据:
.trae/rules/project_rules.md 项目规则
docs/standards/coding-standards.md 编码规范
docs/architecture/004_architecture_impact_map.md 架构影响地图
- Vercel React Best Practices 性能优化规则(react-best-practices 技能)
严重程度定义:
- P0:严重违规,破坏架构分层 / 安全漏洞 / 数据正确性问题,必须立即修复
- P1:明确违规,影响可维护性 / 类型安全 / 性能,应尽快修复
- P2:轻微违规或代码异味,可在迭代中优化
目录
一、汇总统计
1.1 按严重程度
| 严重程度 |
数量 |
典型问题 |
| P0 |
14 |
跨模块直查/直写 DB 表、循环依赖、z.any()、N+1 查询、未返回 ActionState、无 Zod 验证、硬编码弱密码 |
| P1 |
60+ |
as 断言滥用、requireAuth 替代 requirePermission、缺少 import type、动态 import、无事务、除零 bug、导出数据截断 |
| P2 |
100+ |
非空断言 !、未用 React.cache()、串行查询未并行化、重复 filter、静默吞异常、缺返回类型标注、文档不同步 |
1.2 按问题类别
| 问题类别 |
涉及文件数 |
关键发现 |
| 架构违规 |
25+ |
跨模块直查 DB 是最普遍问题;messaging↔notifications 循环依赖;actions 层直接 DB 操作 |
| TS 规范 |
30+ |
as 断言大量存在;z.any();import type 未用;非空断言 !;隐式 any[] |
| Server Action 规范 |
12+ |
requireAuth 替代 requirePermission;未返回 ActionState;无 Zod 验证;缺 revalidatePath |
| 性能 |
20+ |
N+1 查询;动态 import;未用 cache();串行 await 循环;重复 filter 遍历 |
| 安全 |
2 |
硬编码弱密码 "123456";as 断言掩盖类型不兼容 |
| 行数 |
1 |
exams/ai-pipeline.ts 912 行(超 800 建议,未超 1000 硬上限) |
1.3 模块问题分布
| 模块 |
P0 |
P1 |
P2 |
总体评价 |
| exams |
3 |
5 |
6 |
跨模块直查/直写严重 |
| homework |
0 |
8 |
5 |
跨模块直查普遍 |
| questions |
2 |
2 |
3 |
z.any() + 未返回 ActionState |
| grades |
1 |
6 |
7 |
N+1 查询 + 跨模块直查 |
| textbooks |
2 |
2 |
4 |
无 Zod 验证 + as 断言 |
| classes |
3 |
8 |
6 |
耦合最严重,混入多模块逻辑 |
| school |
1 |
1 |
2 |
actions 层直接 DB 操作 |
| scheduling |
0 |
4 |
3 |
缺返回类型标注 |
| attendance |
0 |
1 |
0 |
整体规范 |
| course-plans |
0 |
3 |
2 |
缺 revalidatePath |
| users |
1 |
3 |
4 |
硬编码弱密码 + 无事务 |
| messaging |
1 |
6 |
6 |
循环依赖 + requireAuth |
| notifications |
3 |
3 |
8 |
循环依赖 + as 断言类型不兼容 |
| parent |
0 |
2 |
2 |
跨模块直查 |
| audit |
0 |
4 |
6 |
导出数据截断 |
| elective |
0 |
3 |
5 |
无事务 + 串行更新 |
| proctoring |
1 |
6 |
3 |
actions 层直接 DB 操作 |
| diagnostic |
0 |
2 |
5 |
跨模块直查 + 性能问题 |
| dashboard |
0 |
0 |
0 |
标杆模块 |
| files |
0 |
2 |
3 |
隐式 any[] + 非空断言 |
| announcements |
0 |
1 |
5 |
as 断言 + 错误吞没 |
| settings |
0 |
3 |
9 |
缺 data-access 层 + 无 Zod |
| layout |
0 |
0 |
2 |
类型松散 |
二、P0 严重问题清单(立即修复)
P0-1:exams 模块直写 questions 表
- 文件:
src/modules/exams/data-access.ts
- 行号:2, 318
- 问题:
persistAiGeneratedExamDraft 直接 db.insert(questions) 写入 questions 表(属 questions 模块),破坏模块封装
- 修复:改为调用
questions/data-access.createQuestionWithRelations() 或在 questions 模块暴露批量插入接口
P0-2:exams 模块直查 classes 表
- 文件:
src/modules/exams/data-access.ts
- 行号:2, 72-80, 156-163, 357-364
- 问题:
getExams/getExamById/getExamsDashboardStats 直接查询 classes 表获取 gradeId
- 修复:在 classes 模块暴露
getGradeIdsByClassIds(classIds) 接口
P0-3:questions/schema.ts 使用 z.any()
- 文件:
src/modules/questions/schema.ts
- 行号:6
- 问题:
content: z.any() 违反"禁止 any"规则
- 修复:改为
z.unknown()
P0-4:questions/actions.ts 未返回 ActionState
- 文件:
src/modules/questions/actions.ts
- 行号:154, 166-175
- 问题:
getQuestionsAction 和 getKnowledgePointOptionsAction 直接返回原始结果,未包装为 ActionState<T>,错误时 throw 而非返回失败状态
- 修复:改为
Promise<ActionState<...>> 返回 { success: true, data: result }
P0-5:textbooks 模块无 Zod 验证 + 大量 as 断言
- 文件:
src/modules/textbooks/actions.ts
- 行号:全文件,特别 54-58, 92-98, 154, 219-221, 259-261
- 问题:缺少
schema.ts 文件,所有 Action 均无 Zod 验证,使用手动 if (!rawData.title) 检查;14 处 formData.get("title") as string 断言
- 修复:新建
schema.ts,为每个 Action 定义 Zod schema;改用 typeof 守卫或 getStringValue() 辅助函数
P0-6:grades 模块 N+1 查询
- 文件:
src/modules/grades/data-access-analytics.ts
- 行号:142-185
- 问题:
getClassComparison 中 for (const cls of classRows) { ... await db.select(...) } 循环内串行查询,N+1 问题
- 修复:用
inArray 一次性查询所有班级的成绩,再在内存分组
P0-7:classes 模块跨模块直接查询 DB 表
- 文件:
src/modules/classes/data-access-stats.ts、src/modules/classes/data-access-students.ts
- 行号:data-access-stats.ts: 11-18;data-access-students.ts: 10-14
- 问题:直接导入并查询
homeworkAssignments、homeworkSubmissions、homeworkAssignmentTargets、homeworkAssignmentQuestions、exams 等其他模块的表
- 修复:通过 homework 模块的 data-access 暴露的函数获取数据
P0-8:school 模块 actions 层直接操作 DB
- 文件:
src/modules/school/actions.ts
- 行号:7-8, 26-30, 53-59, 73, 96-108, 133-147, 161, 182-186, 211-217, 233, 261-268, 294-303, 317
- 问题:actions 层直接导入
db 和所有 schema,所有 mutation 直接操作 DB,未下沉到 data-access 层
- 修复:在
data-access.ts 中新增 createDepartment、updateDepartment、deleteDepartment、createSchool 等函数
P0-9:proctoring 模块 actions 层直接查询 DB
- 文件:
src/modules/proctoring/actions.ts
- 行号:11-13, 79-84
- 问题:actions.ts(编排层)直接导入
db、examSubmissions 并执行 DB 查询,违反三层架构
- 修复:将 submission 归属校验逻辑移至
data-access.ts
P0-10:messaging↔notifications 循环依赖
- 文件:
src/modules/messaging/data-access.ts、src/modules/notifications/data-access.ts、src/modules/notifications/channels/in-app-channel.ts
- 行号:messaging/data-access.ts: 192-203;notifications/data-access.ts: 20-21;in-app-channel.ts: 50
- 问题:messaging 写
messageNotifications 表,notifications 反向依赖 messaging,形成循环依赖
- 修复:将
messageNotifications 和 notificationPreferences 表所有权移交 notifications 模块
P0-11:notifications/in-app-channel.ts 非法 as 断言
- 文件:
src/modules/notifications/channels/in-app-channel.ts
- 行号:54
- 问题:
payload.type as "message" | "announcement" | "homework" | "grade" 源类型 "info"|"warning"|"error"|"success" 与目标类型不兼容,as 断言非法
- 修复:重新设计类型映射函数,或统一 messaging/notifications 的 type 定义
P0-12:users 模块硬编码弱密码
- 文件:
src/modules/users/user-service.ts
- 行号:13
- 问题:
const DEFAULT_PASSWORD = "123456" 硬编码弱密码,批量导入用户使用极弱默认密码
- 修复:改为随机生成密码或要求首次登录强制修改密码,至少使用
crypto.randomBytes 生成
P0-13:users/actions.ts updateUserProfile 绕过权限校验
- 文件:
src/modules/users/actions.ts
- 行号:29-51
- 问题:
updateUserProfile 使用 requireAuth() 而非 requirePermission(),且在 actions 层直接执行 db.update(users),返回 Promise<void> 而非 ActionState<T>
- 修复:改为
requirePermission(Permissions.USER_MANAGE) 或新增 USER_PROFILE_UPDATE 权限点;DB 操作下沉到 data-access;返回 ActionState<void>
P0-14:scheduling/data-access.ts 缺返回类型标注
- 文件:
src/modules/scheduling/data-access.ts
- 行号:239, 246, 255, 262
- 问题:
getAdminClassesForScheduling()、getTeachersForScheduling()、getClassroomsForScheduling()、getClassSubjectsForScheduling() 缺少返回类型标注,违反"函数返回值必须显式标注,特别是 Promise"
- 修复:添加返回类型
: Promise<Array<{ id: string; name: string; grade: string }>> 等
三、按模块详细核查
3.1 exams(考试模块)
src/modules/exams/actions.ts(767 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 270 |
TS规范 |
P1 |
formData.get("questionsJson") as string | null — 使用 as 断言,非从 unknown 转换 |
改用 typeof 守卫:const raw = formData.get("questionsJson"); const rawQuestions = typeof raw === "string" ? raw : null |
| 349-351 |
TS规范 |
P1 |
三处 as string | null 断言(questionsJson/aiQuestionsJson/structureJson) |
统一使用 getStringValue() 辅助函数 |
| 4 |
TS规范 |
P2 |
import { ActionState } — 类型未使用 import type |
改为 import type { ActionState } |
| 564-565 |
Server Action规范 |
P2 |
JSON.parse(rawQuestions) 直接 parse 后传入 Zod,parse 异常未捕获 |
用 try-catch 包裹或先 as unknown 再 safeParse |
| 全文件 |
行数 |
P2 |
实际 767 行,架构文档记录为 691 行,文档与代码不同步 |
同步更新 004 和 005 架构文档 |
合规项:所有 10 个 Action 均调用 requirePermission() ✓;均使用 Zod 验证 ✓;均返回 ActionState<T> ✓;均使用 revalidatePath ✓。
src/modules/exams/ai-pipeline.ts(912 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 全文件 |
行数 |
P2 |
912 行,超出 800 行建议(架构文档记录 857 行,已不同步)。混合 4 类职责 |
按职责拆分为 ai-json-parser.ts、ai-prompts.ts、ai-requests.ts、ai-preview-builder.ts |
| 464 |
TS规范 |
P2 |
draft.sections!.forEach — 非空断言 |
改用 if (draft.sections) { draft.sections.forEach(...) } |
| 657, 665, 666, 671 |
TS规范 |
P2 |
多处 aiParsed.sections!.flatMap / aiParsed.sections!.map — 非空断言 |
同上,用条件守卫替代 |
| 505, 508, 545, 879 |
TS规范 |
P2 |
as Record<string, unknown> — 虽然从 unknown 转换允许,但缺少注释说明原因 |
添加注释 // safe: validated by isRecord |
| 557-558 |
代码质量 |
P2 |
catch {} 空捕获块,静默吞掉错误 |
至少记录 console.warn |
| 503-524 |
代码质量 |
P2 |
normalizeQuestionCandidate 嵌套定义在 parseQuestionDetail 内部,每次调用都重新创建闭包 |
提取为模块级函数 |
合规项:使用 mapWithConcurrency 实现并发控制 ✓;使用 Promise.all ✓;env.AI_MODEL 服务端环境变量无 NEXT_PUBLIC_ 前缀 ✓。
src/modules/exams/data-access.ts(524 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 2, 318 |
架构违规 |
P0 |
persistAiGeneratedExamDraft 直接 db.insert(questions) 写入 questions 表 |
改为调用 questions/data-access.createQuestionWithRelations() |
| 2, 72-80, 156-163, 357-364 |
架构违规 |
P0 |
getExams/getExamById/getExamsDashboardStats 直接查询 classes 表 |
在 classes 模块暴露 getGradeIdsByClassIds(classIds) 接口 |
| 2, 206-226, 509-524 |
架构违规 |
P1 |
resolveSubjectGradeNames/getExamSubjects/getExamGrades 直接查询 subjects/grades 表 |
在 school 模块暴露 getSubjectOptions()/getGradeOptions() 接口 |
| 76, 160, 361 |
TS规范 |
P1 |
teacherGradeIds.map(g => g.gradeId).filter(Boolean) as string[] — as 断言 |
使用类型守卫:.filter((id): id is string => Boolean(id)) |
| 108, 172 |
TS规范 |
P2 |
(exam.status as ExamStatus) — as 断言从 string 转换 |
使用 z.enum() 验证或类型守卫函数 |
| 182 |
TS规范 |
P2 |
exam.structure as unknown — 转换到 unknown 允许,但无注释 |
添加注释说明 |
合规项:使用 cache() 包装查询函数 ✓;使用 Promise.all 并行查询 ✓;使用 Map 做 O(1) 查找 ✓。
src/modules/exams/types.ts(31 行)
无违规问题。类型定义规范,接口命名 PascalCase 无 I 前缀 ✓。
3.2 homework(作业模块)
src/modules/homework/actions.ts(282 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 247 |
TS规范 |
P1 |
formData.get("answersJson") as string | null — as 断言 |
改用 typeof 守卫 |
| 54 |
TS规范 |
P2 |
JSON.parse(targetStudentIdsJson) as unknown — 转换到 unknown 允许,但无注释 |
添加注释 |
合规项:所有 5 个 Action 均调用 requirePermission() ✓;使用 Zod 验证 ✓;返回 ActionState ✓;使用 revalidatePath ✓;使用 Set 做查找 ✓。
src/modules/homework/data-access.ts(681 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 8-20, 101-105, 162-166, 283-287, 337-340, 518-519 |
架构违规 |
P1 |
直接查询 exams 表(5 处),属 exams 模块 |
在 exams 模块暴露 getExamIdsByGradeIds(gradeIds) 接口 |
| 8-20, 66-77, 86-95, 149-158, 242-254, 275-281, 347-351 |
架构违规 |
P1 |
直接查询 classEnrollments 表(多处),属 classes 模块 |
在 classes 模块暴露 getStudentIdsByClassId(classId) / getStudentIdsByClassIds(classIds) 接口 |
| 8-20, 515-519 |
架构违规 |
P1 |
直接查询 subjects 表,属 school 模块 |
通过 school data-access 获取 |
| 8-20, 480-486 |
架构违规 |
P1 |
直接查询 users/roles/usersToRoles 表,属 users 模块 |
在 users 模块暴露 getUserWithRole(userId, roleName) 接口 |
| 475-490 |
架构违规 |
P2 |
getDemoStudentUser 使用 auth() 而非 getAuthContext(),绕过 auth-guard 标准模式 |
改用 getAuthContext() 获取 userId |
| 39 |
TS规范 |
P1 |
return v as HomeworkQuestionContent — 从 Record<string, unknown> 断言 |
使用类型守卫或 z.safeParse() 验证 |
| 124, 226, 314, 392, 467, 645 |
TS规范 |
P2 |
多处 (status as HomeworkAssignmentStatus) — as 断言 |
使用类型守卫函数或 Zod 验证 |
| 451-455 |
性能 |
P2 |
getHomeworkSubmissionDetails 获取全部 submissions 仅为计算 prev/next 导航 |
改用 SQL LAG/LEAD 窗口函数或仅查询相邻 ID |
合规项:使用 cache() ✓;使用 Map/Set 聚合 ✓;"server-only" 导入 ✓。
src/modules/homework/data-access-write.ts(317 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 8-17, 70-76, 125-130 |
架构违规 |
P1 |
直接查询 classes 表(getClassTeacherById) |
在 classes 模块暴露 getClassTeacherById(classId) 接口 |
| 8-17, 132-143 |
架构违规 |
P1 |
直接查询 classEnrollments 表(getActiveClassStudentIdsForHomework) |
在 classes 模块暴露 getActiveStudentIdsByClassId(classId) 接口 |
| 8-17, 107-116 |
架构违规 |
P1 |
直接查询 classSubjectTeachers 表 |
在 classes 模块暴露 getTeacherSubjectIdsByClass(classId, teacherId) 接口 |
| 8-17, 81-88 |
架构违规 |
P1 |
直接查询 exams 表(getExamWithQuestionsForHomework) |
在 exams 模块暴露 getExamForHomeworkCreation(examId) 接口 |
| 305-311 |
性能 |
P2 |
gradeHomeworkAnswers 中 for (const ans of answers) { await db.update(...) } — 循环内串行 await,未使用事务 |
用 db.transaction 包裹,或用 Promise.all 并行化无依赖更新 |
合规项:"server-only" 导入 ✓;所有函数显式标注返回类型 ✓;无 as/any ✓。
src/modules/homework/schema.ts(29 行)
无违规问题。Zod 验证完整 ✓。
src/modules/homework/stats-service.ts(483 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 8-16, 313-323, 320-323 |
架构违规 |
P1 |
直接查询 classEnrollments 表 |
通过 classes data-access 获取 |
| 8-16, 437-441 |
架构违规 |
P1 |
直接查询 classes 表 |
通过 classes data-access 获取 |
| 8-16, 425-429, 443-447 |
架构违规 |
P1 |
直接查询 exams 表 |
通过 exams data-access 获取 |
| 8-16, 366-369 |
架构违规 |
P1 |
直接查询 users 表 |
在 users 模块暴露 getUserNamesByIds(ids) 接口 |
| 220 |
TS规范 |
P2 |
(assignment.status as HomeworkAssignmentStatus) — as 断言 |
使用类型守卫 |
| 346 |
性能 |
P2 |
limit: 5000 查询班级所有 graded submissions,大班级可能性能问题 |
考虑分批查询或 SQL 聚合 |
合规项:使用 cache() ✓;使用 Promise.all 并行查询 ✓;使用 Map 聚合 ✓。
src/modules/homework/types.ts(205 行)
无违规问题。类型定义规范 ✓。
3.3 questions(题库模块)
src/modules/questions/actions.ts(176 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 154 |
Server Action规范 |
P0 |
getQuestionsAction 未返回 ActionState<T>,直接返回 getQuestions(params) 的原始结果 { data, meta },错误时 throw |
改为 Promise<ActionState<...>> 返回 { success: true, data: result } |
| 166-175 |
Server Action规范 |
P0 |
getKnowledgePointOptionsAction 未返回 ActionState<T>,直接返回 KnowledgePointOption[] |
同上,包装为 ActionState |
| 154 |
TS规范 |
P1 |
getQuestionsAction 缺少显式返回类型标注 |
添加 : Promise<ActionState<...>> |
| 7 |
TS规范 |
P2 |
import { ActionState } — 类型导入未用 import type |
改为 import type { ActionState } |
| 158-163, 170-175 |
代码质量 |
P2 |
catch 块两个分支都 throw e,是死代码 |
简化为单个 throw e 或返回失败 ActionState |
| 20 |
命名规范 |
P2 |
函数名 createNestedQuestion 与其他模块的 createXxxAction 命名模式不一致 |
改为 createQuestionAction |
合规项:所有 Action 调用 requirePermission() ✓;create/update/delete 使用 Zod 验证 ✓;写操作使用 revalidatePath ✓。
src/modules/questions/data-access.ts(299 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 4, 266-298 |
架构违规 |
P1 |
getKnowledgePointOptions 直接查询 knowledgePoints/chapters/textbooks 表,属 textbooks 模块 |
在 textbooks 模块暴露 getKnowledgePointOptions() 接口 |
| 108 |
代码质量 |
P2 |
局部变量名 knowledgePoints 与 import 的 knowledgePoints 表名冲突 |
重命名为 kpRows 或 kps |
| 151-184, 231-242 |
性能 |
P2 |
insertQuestionWithRelations / deleteQuestionRecursive 递归内串行 await |
对于删除可改为先收集所有 ID 再批量删除 |
合规项:使用 cache() ✓;"server-only" 导入 ✓;无 as/any ✓;显式返回类型 ✓。
src/modules/questions/schema.ts(18 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 6 |
TS规范 |
P0 |
content: z.any() — 使用 any,违反"禁止 any"规则 |
改为 z.unknown() |
src/modules/questions/types.ts(34 行)
无违规问题。content: unknown 正确使用 unknown ✓。
3.4 grades(成绩模块)
src/modules/grades/actions.ts(312 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 93 |
代码质量 |
P2 |
JSON.parse(recordsJson) 未用 try-catch 包裹,parse 失败会 500 |
用 try-catch 或先 as unknown 再 safeParse |
合规项:所有 Action 调用 requirePermission() ✓;使用 Zod 验证 ✓;返回 ActionState<T> ✓;使用 revalidatePath ✓;scope 二次校验 ✓。
src/modules/grades/actions-analytics.ts(133 行)
合规项:所有 Action 调用 requirePermission() ✓;返回 ActionState<T> ✓;scope 二次校验 ✓。无违规问题。
src/modules/grades/data-access.ts(419 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 6-12, 100, 382-386 |
架构违规 |
P1 |
直接查询 classes 表 |
通过 classes data-access 获取 |
| 6-12, 365-368, 408-411 |
架构违规 |
P1 |
直接查询 classEnrollments 表 |
通过 classes data-access 获取 |
| 6-12, 102, 276, 280 |
架构违规 |
P1 |
直接查询 subjects 表 |
通过 school data-access 获取 |
| 6-12, 100, 109-112, 269, 342 |
架构违规 |
P1 |
直接查询 users 表 |
通过 users data-access 获取 |
| 148, 172 |
性能 |
P1 |
const { createId } = await import("@paralleldrive/cuid2") — 函数内动态 import,每次调用都有开销 |
改为文件顶部静态 import { createId } from "@paralleldrive/cuid2" |
| 249 |
代码质量/Bug |
P1 |
const ratio = scores[i] / fullScores[i] — fullScores[i] 可能为 0,导致 Infinity/NaN |
添加 if (fullScores[i] <= 0) continue 守卫 |
| 全文件 |
性能 |
P2 |
查询函数未使用 React.cache() 包装 |
用 cache() 包装查询函数 |
| 248-252 |
性能 |
P2 |
for 循环内两次独立 if 判断遍历同一数组 |
合并为单次循环同时计算 passCount 和 excellentCount |
合规项:"server-only" 导入 ✓;使用 Map/Set ✓;无 as/any ✓;显式返回类型 ✓。
src/modules/grades/data-access-analytics.ts(293 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 6-10, 79, 125-128 |
架构违规 |
P1 |
直接查询 classes 表 |
通过 classes data-access 获取 |
| 6-10, 80, 211 |
架构违规 |
P1 |
直接查询 subjects 表 |
通过 school data-access 获取 |
| 22, 27, 32 |
代码质量 |
P2 |
toNumber/normalize/buildScopeClassFilter 在 data-access.ts 和 data-access-ranking.ts 中重复定义 |
提取到 grades/utils.ts |
| 142-185 |
性能 |
P0 |
getClassComparison 中 for (const cls of classRows) { ... await db.select(...) } — 循环内串行查询,N+1 问题 |
用 inArray 一次性查询所有班级的成绩,再在内存分组 |
| 133-136 |
性能 |
P2 |
classRows.filter((c) => scope.classIds.includes(c.id)) — includes 是 O(n) 查找 |
将 scope.classIds 转为 Set 后用 .has() |
| 180-181, 238-239 |
性能 |
P2 |
normalized.filter((s) => s >= 60).length 和 normalized.filter((s) => s >= 85).length — 两次 filter 遍历同一数组 |
合并为单次 reduce 同时统计两个计数 |
| 全文件 |
性能 |
P2 |
查询函数未使用 React.cache() |
用 cache() 包装 |
合规项:"server-only" ✓;无 as/any ✓;显式返回类型 ✓。
src/modules/grades/data-access-ranking.ts(121 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 6-10, 44-53 |
架构违规 |
P1 |
直接查询 classEnrollments 表 |
通过 classes data-access 获取 |
| 6-10, 37-41 |
架构违规 |
P1 |
直接查询 users 表 |
通过 users data-access 获取 |
| 17, 22 |
代码质量 |
P2 |
toNumber/normalize 重复定义 |
提取到共享 utils |
| 100, 102 |
性能 |
P2 |
sorted.findIndex(...) 和 sorted.find(...) 两次 O(n) 查找同一元素 |
合并为一次遍历 |
| 全文件 |
性能 |
P2 |
查询函数未使用 React.cache() |
用 cache() 包装 |
src/modules/grades/export.ts(214 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 6-11, 116-120 |
架构违规 |
P1 |
直接查询 classes 表 |
通过 classes data-access 获取 |
| 6-11, 124-132 |
架构违规 |
P1 |
直接查询 subjects 表 |
通过 school data-access 获取 |
| 6-11, 135-144 |
架构违规 |
P1 |
直接查询 users 表 |
通过 users data-access 获取 |
| 154 |
TS规范 |
P2 |
subjMap.get(r.studentId)! — 非空断言 |
改用 const subjMap = scoreMap.get(r.studentId); if (!subjMap) continue; |
src/modules/grades/schema.ts(52 行)& types.ts(176 行)
无违规问题。Zod 验证完整,类型定义规范 ✓。
3.5 textbooks(教材模块)
src/modules/textbooks/actions.ts(276 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 全文件 |
Server Action规范 |
P0 |
缺少 schema.ts 文件,所有 Action 均无 Zod 验证,使用手动 if (!rawData.title) 检查 |
新建 schema.ts,为每个 Action 定义 Zod schema |
| 41-45 |
架构违规 |
P1 |
本地定义 export type ActionState,未使用 @/shared/types/action-state 的统一类型 |
删除本地定义,改用 import type { ActionState } from "@/shared/types/action-state" |
| 18 |
TS规范 |
P1 |
import { CreateTextbookInput, UpdateTextbookInput } — 类型导入未用 import type |
改为 import type { ... } |
| 54-58, 92-98, 154, 219-221, 259-261 |
TS规范 |
P0 |
多处 formData.get("title") as string 等 as 断言(共 14 处) |
改用 typeof 守卫或 getStringValue() 辅助函数 |
| 20, 47 |
代码质量 |
P2 |
// ... existing code ... 残留注释 |
删除 |
| 164 |
代码质量 |
P2 |
order: 0 // Default order 魔法数字 |
提取为常量 const DEFAULT_CHAPTER_ORDER = 0 |
合规项:所有 Action 调用 requirePermission() ✓;使用 revalidatePath ✓。
src/modules/textbooks/data-access.ts(444 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 42 |
TS规范 |
P2 |
byId.get(pid)!.children.push(ch) — 非空断言 |
改用 const parent = byId.get(pid); if (parent) parent.children.push(ch) |
| 51 |
TS规范 |
P1 |
sortRecursive(n.children as Array<Chapter & { children: Chapter[] }>) — as 断言 |
使用类型守卫或在 Chapter 类型中明确 children 字段 |
| 71 |
TS规范 |
P2 |
or(...)! — 对 or() 返回值使用非空断言 |
改用条件判断 |
| 110, 128 |
代码质量 |
P2 |
getTextbookById 返回 Textbook | undefined,与其他模块返回 null 的模式不一致 |
统一返回 Textbook | null |
| 368, 381 |
代码质量 |
P2 |
createKnowledgePoint/updateKnowledgePoint 参数使用内联类型 |
将输入类型移至 types.ts |
合规项:使用 cache() ✓;使用 Promise.all ✓;"server-only" ✓;无 any ✓;显式返回类型 ✓。这是架构文档中的"标杆模块",无跨模块 DB 访问 ✓。
src/modules/textbooks/types.ts(83 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 1-3 |
代码质量 |
P2 |
残留注释 // In a real app, we would infer these from the schema... |
删除过时注释 |
3.6 classes(班级模块)
src/modules/classes/actions.ts(765 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 8-9 |
架构违规 |
P0 |
actions 层直接导入 db 和 grades, classes schema 并执行 DB 查询 |
将权限校验查询下沉到 data-access-admin.ts |
| 48-53, 109-111, 140-142, 175-183, 369-377, 518-533, 636-644 |
Server Action规范 |
P1 |
输入验证使用手动 typeof 检查而非 Zod,classes 模块完全没有 schema.ts 文件 |
新建 schema.ts,定义 Zod schema |
| 538 |
TS规范 |
P1 |
weekdayNum as 1 | 2 | 3 | 4 | 5 | 6 | 7 — as 断言 |
用类型守卫 isValidWeekday(weekdayNum): weekdayNum is 1|2|3|4|5|6|7 替换 |
| 582 |
TS规范 |
P1 |
weekdayNum as 1 | 2 | 3 | 4 | 5 | 6 | 7 | undefined — 同上 |
同上 |
| 287, 706 |
TS规范 |
P2 |
JSON.parse(subjectTeachers) as unknown 后续用 as { subject?: unknown } |
用 Zod schema 解析 |
src/modules/classes/data-access.ts(656 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 26-27, 182-206 |
架构违规 |
P0 |
getTeacherClasses 中调用 getClassHomeworkInsights(homework 逻辑)和 getClassSchedule(scheduling 逻辑) |
将 homework insights 聚合逻辑移至 homework 模块 |
| 47 |
TS规范 |
P1 |
isDuplicateInvitationCodeError 箭头函数缺少返回类型标注 |
添加 : boolean |
| 54 |
TS规范 |
P1 |
generateInvitationCode 箭头函数缺少返回类型标注 |
添加 : string |
| 87 |
TS规范 |
P1 |
normalizeSortText 箭头函数缺少返回类型标注 |
添加 : string |
| 89 |
TS规范 |
P1 |
parseFirstInt 箭头函数缺少返回类型标注 |
添加 : number | null |
| 94 |
TS规范 |
P1 |
compareGradeLabel 箭头函数缺少返回类型标注 |
添加 : number |
| 101-104 |
TS规范 |
P1 |
compareClassLike 箭头函数缺少返回类型标注 |
添加 : number |
| 240 |
TS规范 |
P1 |
r.subject as ClassSubject — as 断言 |
用类型守卫 isClassSubject(r.subject) 过滤 |
| 266 |
TS规范 |
P1 |
r.name as ClassSubject — 同上 |
同上 |
| 287 |
TS规范 |
P2 |
idByName.get(name)! — 非空断言 |
用 const id = idByName.get(name); if (!id) return null 显式处理 |
| 297-299 |
代码质量 |
P1 |
throw new Error("Failed to create class") 后有 return id 不可达代码 |
删除 return id |
| 561 |
TS规范 |
P1 |
r.name as ClassSubject — as 断言 |
用类型守卫替换 |
| 567 |
TS规范 |
P2 |
idByName.get(name)! — 非空断言 |
显式判空处理 |
| 120-127 |
性能 |
P2 |
getAccessibleClassIdsForTeacher 中两个独立 DB 查询串行执行 |
用 Promise.all 并行化 |
src/modules/classes/data-access-admin.ts(441 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 82-117, 237-239 |
代码质量 |
P1 |
try-catch 吞掉错误返回空数组或 fallback 查询 |
移除 try-catch 或记录错误日志后 rethrow |
| 135 |
TS规范 |
P1 |
r.subject as ClassSubject — as 断言 |
用类型守卫替换 |
| 259 |
TS规范 |
P1 |
r.subject as ClassSubject — 同上 |
同上 |
| 303 |
TS规范 |
P1 |
getManagedGrades 缺少返回类型标注 |
添加 : Promise<GradeListItem[]> |
| 349 |
TS规范 |
P1 |
r.name as ClassSubject — as 断言 |
用类型守卫替换 |
| 369 |
TS规范 |
P2 |
idByName.get(name)! — 非空断言 |
显式判空 |
| 380-382 |
代码质量 |
P1 |
throw new Error("Failed to create class") 后有 return id 不可达代码 |
删除 return id |
src/modules/classes/data-access-schedule.ts(230 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 7-11, 33-48 |
架构违规 |
P1 |
直接导入并查询 classSchedule 表(该表归属 scheduling 模块) |
将读取也委托给 scheduling 模块的 data-access |
| 54 |
TS规范 |
P1 |
r.weekday as StudentScheduleItem["weekday"] — as 断言 |
用类型守卫或 Zod 校验 weekday 范围 |
| 93 |
TS规范 |
P1 |
r.weekday as ClassScheduleItem["weekday"] — 同上 |
同上 |
src/modules/classes/data-access-stats.ts(604 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 11-18 |
架构违规 |
P0 |
直接导入并查询 homeworkAssignmentQuestions、homeworkAssignmentTargets、homeworkAssignments、homeworkSubmissions、exams 等 homework/exams 模块的 DB 表 |
通过 homework 模块的 data-access 暴露的函数获取数据 |
| 250 |
TS规范 |
P1 |
(s.status ?? "started") as string — as 断言 |
s.status 已是 string 类型,无需断言 |
| 261 |
TS规范 |
P1 |
(a.status as string) ?? "draft" — as 断言 |
同上 |
| 512 |
TS规范 |
P1 |
(s.status ?? "started") as string — 同上 |
同上 |
| 523 |
TS规范 |
P1 |
(a.status as string) ?? "draft" — 同上 |
同上 |
| 130-151, 175-191 |
性能 |
P2 |
多个早期返回中重复构造相同的返回对象结构 |
提取 buildEmptyInsights(classRow, studentCounts) 工厂函数 |
src/modules/classes/data-access-students.ts(280 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 10-14 |
架构违规 |
P0 |
直接导入并查询 homeworkAssignmentTargets、homeworkAssignments、homeworkSubmissions、exams 等 homework/exams 模块的 DB 表 |
通过 homework 模块 data-access 获取数据 |
| 102 |
TS规范 |
P2 |
studentScores.get(s.studentId)! — 非空断言 |
显式判空处理 |
| 168-204 |
代码质量 |
P2 |
getStudentClasses 中 try-catch fallback 查询使用 sql\NULL`.as(...)` 模式,吞掉错误 |
移除 try-catch |
src/modules/classes/types.ts(201 行)
基本符合规范,无违规问题。
3.7 school(学校模块)
src/modules/school/actions.ts(325 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 7-8, 26-30, 53-59, 73, 96-108, 133-147, 161, 182-186, 211-217, 233, 261-268, 294-303, 317 |
架构违规 |
P0 |
actions 层直接导入 db 和所有 schema,所有 mutation 直接操作 DB,完全没有下沉到 data-access 层 |
在 data-access.ts 中新增 createDepartment、updateDepartment、deleteDepartment、createSchool 等函数 |
| 188, 219, 235 |
性能 |
P2 |
await logAudit(...) 阻塞响应,审计日志写入是非关键路径 |
使用 Next.js after() 函数将 logAudit 改为非阻塞执行 |
src/modules/school/data-access.ts(186 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 13, 27, 44, 59, 112, 133 |
代码质量 |
P2 |
所有函数都用 try-catch 包裹并返回空数组 [] 吞掉错误 |
移除 try-catch 或记录错误日志后 rethrow |
src/modules/school/schema.ts(51 行)& types.ts(42 行)
符合规范。
3.8 scheduling(排课模块)
src/modules/scheduling/actions.ts(300 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 10-11, 112-116 |
架构违规 |
P0 |
actions 层直接导入 db 和 users schema,在 autoScheduleAction 中直接查询 users 表获取教师信息 |
在 data-access.ts 中新增 getTeachersByIds(teacherIds: string[]) 函数 |
| 115 |
TS规范 |
P1 |
teacherIds[0]! — 非空断言 |
用条件表达式 eq(users.id, teacherIds[0] ?? "") |
src/modules/scheduling/auto-scheduler.ts(310 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 144-145 |
TS规范 |
P1 |
schedule[i]!、schedule[j]! — 非空断言 |
用局部变量 + 显式判空 |
| 全文件 |
行数 |
P2 |
工具函数规范要求 ≤ 40 行,此文件 310 行 |
拆分为 auto-scheduler/ 目录下的多个文件 |
src/modules/scheduling/data-access.ts(368 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 239 |
TS规范 |
P0 |
getAdminClassesForScheduling() 缺少返回类型标注 |
添加返回类型 |
| 246 |
TS规范 |
P0 |
getTeachersForScheduling() 缺少返回类型标注 |
添加返回类型 |
| 255 |
TS规范 |
P0 |
getClassroomsForScheduling() 缺少返回类型标注 |
添加返回类型 |
| 262 |
TS规范 |
P0 |
getClassSubjectsForScheduling(classId: string) 缺少返回类型标注 |
添加返回类型 |
| 113-114 |
代码质量 |
P2 |
requesterName: users.name 与 originalTeacherName: users.name 选中同一列,requesterName 在 map 中被 userMap.get() 覆盖,select 中的字段是死代码 |
删除 select 中的 requesterName: users.name |
| 140 |
TS规范 |
P1 |
userIds[0]! — 非空断言 |
显式判空 |
| 221-222 |
TS规范 |
P1 |
rows[i]!、rows[j]! — 非空断言 |
用局部变量 + 显式判空 |
src/modules/scheduling/schema.ts(81 行)& types.ts(124 行)
符合规范。
3.9 attendance(考勤模块)
src/modules/attendance/actions.ts(271 行)
基本符合规范。所有 action 调用了 requirePermission、使用 Zod 验证、返回 ActionState、使用 revalidatePath。
src/modules/attendance/data-access.ts(271 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 195 |
TS规范 |
P1 |
const update: Record<string, unknown> = { updatedAt: new Date() } — 使用宽泛的 Record<string, unknown> 丢失类型安全 |
使用 Partial<typeof attendanceRecords.$inferSelect> 类型 |
src/modules/attendance/data-access-stats.ts(145 行)& schema.ts(43 行)& types.ts(103 行)
符合规范。
3.10 course-plans(教学计划模块)
src/modules/course-plans/actions.ts(265 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 236-246 |
Server Action规范 |
P1 |
deleteCoursePlanItemAction 未调用 revalidatePath |
添加 revalidatePlanPaths() 调用 |
| 248-264 |
Server Action规范 |
P1 |
toggleCoursePlanItemCompletedAction 未调用 revalidatePath |
同上 |
src/modules/course-plans/data-access.ts(320 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 149 |
TS规范 |
P1 |
params.status as CoursePlanStatus — as 断言 |
用类型守卫或 Zod 校验 |
| 158, 183 |
代码质量 |
P2 |
getCoursePlans 和 getCoursePlanById 用 try-catch 返回空数组/null 吞掉错误 |
移除 try-catch 或记录日志后 rethrow |
| 300-305 |
性能 |
P2 |
reorderCoursePlanItems 中循环内串行 await db.update(),N 次 DB 往返 |
用 db.transaction 批量更新,或用 Promise.all 并行化 |
src/modules/course-plans/schema.ts(148 行)& types.ts(60 行)
符合规范。
3.11 users(用户模块)
src/modules/users/actions.ts(151 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 29-51 |
Server Action规范 |
P0 |
updateUserProfile 使用 requireAuth() 而非 requirePermission() |
改为 requirePermission(Permissions.USER_MANAGE) 或新增 USER_PROFILE_UPDATE 权限点 |
| 29-51 |
架构违规 |
P1 |
updateUserProfile 在 actions 层直接执行 db.update(users) |
将写操作下沉到 data-access.ts |
| 29-51 |
Server Action规范 |
P1 |
updateUserProfile 返回 Promise<void> 而非 ActionState<T> |
改为返回 Promise<ActionState<void>> |
| 29 |
TS规范 |
P2 |
updateUserProfile(data: UpdateUserProfileInput) 缺少显式返回类型标注 |
标注为 Promise<ActionState<void>> |
| 29-51 |
Server Action规范 |
P2 |
updateUserProfile 输入未使用 Zod 验证 |
新增 Zod schema |
| 76-125 |
Server Action规范 |
P2 |
importUsersAction 输入验证使用手动逻辑,未使用 Zod |
将 parseUserImportData 改用 Zod 实现 |
src/modules/users/class-registration.ts(27 行)
合规,无违规问题。正确通过 classes/data-access.enrollStudentByInvitationCode 跨模块通信。
src/modules/users/data-access.ts(152 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 24 |
命名规范 |
P2 |
const rolePriority 常量未使用 UPPER_SNAKE_CASE |
重命名为 ROLE_PRIORITY |
| 26-31 |
架构违规 |
P2 |
normalizeRoleName 与 shared/lib/role-utils.ts 的 normalizeRole 重复 |
改为 import { normalizeRole } from "@/shared/lib/role-utils" 复用 |
| 26 |
TS规范 |
P2 |
normalizeRoleName 缺少显式返回类型标注 |
标注为 (value: string) => string |
| 33 |
TS规范 |
P2 |
resolvePrimaryRole 缺少显式返回类型标注 |
标注为 (roleNames: string[]) => string |
src/modules/users/import-export.ts(176 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 98 |
TS规范 |
P2 |
const conditions = [] 隐式推断为 any[],违反"禁止 any"规则 |
标注类型 const conditions: ReturnType<typeof eq>[] = [] |
| 100-114 |
性能 |
P2 |
exportUsersToExcel 中 role 查询 → userIds 查询 → users 查询为串行 |
可接受(有依赖关系),但建议合并为子查询减少往返 |
src/modules/users/user-service.ts(99 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 13 |
安全规范 |
P0 |
const DEFAULT_PASSWORD = "123456" 硬编码弱密码 |
改为随机生成密码或要求首次登录强制修改密码 |
| 15-19 |
架构违规 |
P2 |
normalizeBcryptHash 与 shared/lib/bcrypt-utils.ts 重复 |
改为 import { normalizeBcryptHash } from "@/shared/lib/bcrypt-utils" 复用 |
| 15 |
TS规范 |
P2 |
normalizeBcryptHash 缺少显式返回类型标注 |
标注为 (value: string) => string |
| 30-92 |
架构违规 |
P1 |
batchImportUsers 注释标注"批量导入用户(事务)",但实际未使用 db.transaction 包裹 |
用 await db.transaction(async (tx) => { ... }) 包裹 |
| 50-92 |
性能 |
P2 |
循环内串行 await db.insert,N 条记录需 N 次 DB 往返 |
可批量插入用户和角色关联 |
3.12 messaging(消息模块)
src/modules/messaging/actions.ts(247 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 163 |
Server Action规范 |
P1 |
getNotificationsAction 使用 requireAuth() 而非 requirePermission() |
改为 requirePermission(Permissions.MESSAGE_READ) |
| 177 |
Server Action规范 |
P1 |
markNotificationAsReadAction 使用 requireAuth() 而非 requirePermission() |
同上 |
| 190 |
Server Action规范 |
P1 |
markAllNotificationsAsReadAction 使用 requireAuth() 而非 requirePermission() |
同上 |
| 203 |
Server Action规范 |
P1 |
getNotificationPreferencesAction 使用 requireAuth() 而非 requirePermission() |
同上 |
| 218 |
Server Action规范 |
P1 |
updateNotificationPreferencesAction 使用 requireAuth() 而非 requirePermission() |
同上 |
| 213-247 |
Server Action规范 |
P2 |
updateNotificationPreferencesAction 未使用 Zod 验证 |
新增 Zod schema 校验 8 个布尔字段 |
| 87, 101, 129 |
Server Action规范 |
P2 |
markMessageAsReadAction/deleteMessageAction/getMessageDetailAction 参数 messageId 未使用 Zod 验证 |
新增 z.string().min(1) 校验 messageId |
src/modules/messaging/data-access.ts(252 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 12-14 |
架构违规 |
P1 |
导入 classEnrollments, classes,getRecipients(227-251 行)直接 JOIN 跨模块表 |
通过 classes/data-access 暴露 getStudentsByClassIds(classIds) 接口 |
| 94 |
TS规范 |
P2 |
const conds = [] 隐式 any[] |
标注 drizzle 条件类型 |
| 97 |
TS规范 |
P2 |
or(...)! 使用非空断言 |
改为显式处理 null |
| 117 |
TS规范 |
P2 |
or(...)! 使用非空断言 |
同上 |
| 163 |
TS规范 |
P2 |
or(...)! 使用非空断言 |
同上 |
| 63 |
TS规范 |
P2 |
mapMessage 缺少显式返回类型标注 |
标注返回类型 |
| 77 |
TS规范 |
P2 |
mapNotification 缺少显式返回类型标注 |
标注返回类型 |
| 80 |
TS规范 |
P2 |
r.type as NotificationType 使用 as 断言 |
使用类型守卫 |
| 74, 85 |
TS规范 |
P2 |
toIso(r.createdAt) as string 使用 as 断言 |
处理 null 情况或修改 toIso 返回类型 |
| 192-203 |
架构违规 |
P0 |
createNotification 写 messageNotifications 表,但 notifications 模块反向调用此函数,形成循环依赖 |
将 messageNotifications 表所有权移交 notifications 模块 |
src/modules/messaging/notification-preferences.ts(166 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 14 |
TS规范 |
P2 |
toIso 缺少显式返回类型标注 |
标注为 (d: Date) => string |
| 16 |
TS规范 |
P2 |
mapRow 缺少显式返回类型标注 |
标注返回类型 |
src/modules/messaging/schema.ts(18 行)& types.ts(108 行)
合规,无违规问题。
3.13 notifications(通知模块)
src/modules/notifications/actions.ts(119 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 14-17 |
架构违规 |
P1 |
actions 层直接导入 db、classEnrollments、classes |
将 DB 查询下沉到 data-access.ts |
| 83-96 |
架构违规 |
P1 |
sendClassNotificationAction 直接查询 classes 和 classEnrollments 表 |
通过 classes/data-access 暴露查询接口 |
| 30-52 |
Server Action规范 |
P2 |
sendNotificationAction 参数 payload 未使用 Zod 验证 |
新增 NotificationPayloadSchema |
| 62-119 |
Server Action规范 |
P2 |
sendClassNotificationAction 参数 classId/payload 未使用 Zod 验证 |
新增 Zod schema 校验 |
src/modules/notifications/data-access.ts(86 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 20 |
架构违规 |
P0 |
import { getNotificationPreferences } from "@/modules/messaging/notification-preferences" 反向依赖 messaging 模块 |
将 notificationPreferences 表所有权移交 notifications 模块 |
| 21 |
架构违规 |
P0 |
import type { NotificationPreferences } from "@/modules/messaging/types" 反向依赖 messaging 模块 |
类型定义应迁移到 notifications 模块 |
| 71-77 |
架构违规 |
P2 |
logNotificationSend 仅 console.info,无 DB 持久化 |
新增 notification_logs 表并写入发送结果 |
src/modules/notifications/dispatcher.ts(152 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 44 |
TS规范 |
P2 |
getSenders 缺少显式返回类型标注 |
标注为 () => SenderRegistry |
| 59 |
TS规范 |
P2 |
selectChannels 缺少显式返回类型标注 |
标注返回类型 |
src/modules/notifications/external-sdk.d.ts(47 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 12, 20, 25, 33, 42, 43 |
TS规范 |
P2 |
多处使用 any(const _default: any、config: any、options: any) |
安装实际 SDK 后用其自带类型覆盖;或使用 unknown |
src/modules/notifications/index.ts(38 行)& types.ts(70 行)& channels/types.ts(38 行)
合规,无违规问题。
src/modules/notifications/channels/email-channel.ts(183 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 29 |
TS规范 |
P2 |
getEmailConfig 缺少显式返回类型标注 |
标注返回类型 |
| 45 |
TS规范 |
P2 |
getTypeColor 缺少显式返回类型标注 |
标注为 (type: NotificationPayload["type"]) => string |
| 60 |
TS规范 |
P2 |
buildHtmlContent 缺少显式返回类型标注 |
标注为 (payload: NotificationPayload) => string |
| 79 |
TS规范 |
P2 |
escapeHtml 缺少显式返回类型标注 |
标注为 (text: string) => string |
src/modules/notifications/channels/in-app-channel.ts(88 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 50 |
架构违规 |
P0 |
await import("@/modules/messaging/data-access") 动态 import messaging 模块,运行时形成循环 |
将 messageNotifications 表所有权移交 notifications |
| 54 |
TS规范 |
P1 |
payload.type as "message" | "announcement" | "homework" | "grade" 使用 as 断言,且源类型与目标类型不兼容,as 断言非法 |
重新设计类型映射函数 |
src/modules/notifications/channels/sms-channel.ts(236 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 30 |
TS规范 |
P2 |
getSmsConfig 缺少显式返回类型标注 |
标注返回类型 |
| 32 |
TS规范 |
P2 |
(process.env.SMS_PROVIDER ?? "mock") as "aliyun" | "tencent" | "mock" 使用 as 断言 |
使用类型守卫或 Zod 校验环境变量 |
| 44 |
TS规范 |
P2 |
buildTemplateParams 缺少显式返回类型标注 |
标注返回类型 |
src/modules/notifications/channels/wechat-channel.ts(208 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 40 |
TS规范 |
P2 |
getWechatConfig 缺少显式返回类型标注 |
标注返回类型 |
| 99 |
TS规范 |
P2 |
buildTemplateData 缺少显式返回类型标注 |
标注返回类型 |
3.14 parent(家长模块)
src/modules/parent/data-access.ts(234 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 59-105 |
架构违规 |
P1 |
getChildBasicInfo 直接查询 users、grades、classEnrollments、classes 表 |
通过 users/data-access.getUserProfile、classes/data-access 等接口查询 |
| 58 |
TS规范 |
P2 |
getChildBasicInfo 缺少显式返回类型标注 |
标注返回类型 |
| 30 |
TS规范 |
P2 |
(day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7 使用 as 断言 |
使用类型守卫 function isWeekday(n: number): n is 1|2|3|4|5|6|7 |
| 58-117 |
性能 |
P2 |
getChildBasicInfo 串行查询 student → grade → enrollment → class |
使用 JOIN 合并为单次查询,或用 Promise.all 并行化 |
src/modules/parent/types.ts(57 行)
合规,无违规问题。
3.15 audit(审计模块)
src/modules/audit/actions.ts(212 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 6, 70-100, 120-146, 166-192 |
架构违规 |
P2 |
Excel 导出逻辑内联在 actions 层 |
新建 audit/export.ts,将 Excel 构建逻辑迁移 |
| 63-205 |
架构违规 |
P2 |
三个导出 Action 结构高度重复 |
抽取通用 buildExcelSheet(name, columns, rows) 辅助函数 |
| 207-212 |
架构违规 |
P2 |
formatDateForFile 工具函数定义在 actions.ts 中 |
迁移到 shared/lib/utils.ts 或 audit/utils.ts |
| 24-61 |
Server Action规范 |
P2 |
getDataChangeLogsAction 参数 params 未使用 Zod 验证 |
新增 DataChangeLogQueryParamsSchema |
| 63-205 |
Server Action规范 |
P2 |
三个导出 Action 参数未使用 Zod 验证 |
新增对应 Zod schema |
src/modules/audit/data-access.ts(260 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 18 |
TS规范 |
P2 |
toIso 缺少显式返回类型标注 |
标注为 (d: Date) => string |
| 23 |
TS规范 |
P2 |
clampPageSize 缺少显式返回类型标注 |
标注为 (size?: number) => number |
| 28 |
TS规范 |
P2 |
clampPage 缺少显式返回类型标注 |
标注为 (page?: number) => number |
| 40, 95, 158 |
TS规范 |
P2 |
const conditions = [] 隐式 any[] |
标注 drizzle 条件类型 |
| 75 |
TS规范 |
P2 |
r.status as "success" | "failure" 使用 as 断言 |
使用类型守卫 |
| 122 |
TS规范 |
P2 |
r.action as "signin" | "signout" | "signup" 使用 as 断言 |
使用类型守卫 |
| 123 |
TS规范 |
P2 |
r.status as "success" | "failure" 使用 as 断言 |
使用类型守卫 |
| 186 |
TS规范 |
P2 |
r.action as "create" | "update" | "delete" 使用 as 断言 |
使用类型守卫 |
| 50-86, 104-137, 168-202 |
安全/质量 |
P2 |
getAuditLogs/getLoginLogs/getDataChangeLogs 使用 try-catch 吞错误返回空数组 |
至少 console.error 记录错误 |
| 235-240 |
逻辑错误 |
P1 |
getAuditLogsForExport 注释标注 "no pagination cap",但实际调用 getAuditLogs({ page: 1, pageSize: 100 }),最多只返回 100 条,导出数据不完整 |
实现真正的无分页查询,或循环分页拉取全部数据 |
| 245-250 |
逻辑错误 |
P1 |
getLoginLogsForExport 同上问题,最多 100 条 |
同上 |
| 255-260 |
逻辑错误 |
P1 |
getDataChangeLogsForExport 同上问题,最多 100 条 |
同上 |
src/modules/audit/types.ts(91 行)
合规,无违规问题。
3.16 elective(选修课模块)
src/modules/elective/data-access.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 117 |
TS规范 |
P1 |
params.status as ElectiveCourseStatus 使用 as 断言 |
直接使用 params.status,无需断言 |
| 78 |
TS规范 |
P2 |
buildCourseSelect = () => 箭头函数缺少显式返回类型标注 |
添加返回类型标注 |
| 136-138, 150-152 |
代码质量 |
P2 |
catch { return [] / null } 静默吞掉异常 |
至少 console.error 记录错误 |
src/modules/elective/data-access-selections.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 102 |
TS规范 |
P1 |
r.status as CourseSelectionStatus 使用 as 断言 |
应通过类型守卫或直接赋值 |
| 114 |
TS规范 |
P1 |
r.courseStatus as ElectiveCourseStatus | null 同上 |
同上 |
| 59, 117 |
TS规范 |
P2 |
buildCourseSelect 和 selectionDetailSelect 箭头函数缺少显式返回类型 |
添加返回类型标注 |
| 7-14 |
架构违规 |
P1 |
直接导入并查询 classes、classEnrollments 表 |
通过 @/modules/classes/data-access 暴露的函数获取 |
| 全文件 |
性能 |
P2 |
均未用 React.cache() 包裹 |
对读函数用 cache() 包裹 |
src/modules/elective/data-access-operations.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 18-34 |
性能 |
P2 |
runLottery 中先查 course 再查 selections,两次独立查询串行 await |
用 Promise.all 并行 |
| 46-71 |
性能 |
P1 |
for 循环内逐条 await db.update(),N 名学生产生 N 次 DB 往返 |
拆分为两组 ID,用 inArray 执行 2 次批量 UPDATE |
| 46-76 |
安全/数据完整性 |
P1 |
runLottery 多步更新未包裹事务 |
使用 db.transaction() 包裹整个摇号流程 |
| 86-112 |
性能 |
P2 |
selectCourse 中查 course 和查 existing selection 两次独立查询串行 |
用 Promise.all 并行 |
| 158-182 |
性能 |
P2 |
dropCourse 中查 existing 和查 course 串行 |
并行化独立查询 |
src/modules/elective/schema.ts(20-24 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 20-24 |
TS规范 |
P2 |
emptyToNull 和 optionalStringToNull 箭头函数缺少显式返回类型 |
添加返回类型 |
src/modules/elective/types.ts
未发现违规问题。
src/modules/elective/actions.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 278-294 |
Server Action规范 |
P2 |
getStudentSelectionsAction(studentId: string) 直接接收参数,未经 Zod 校验 |
对 studentId 用 Zod 校验 |
3.17 proctoring(监考模块)
src/modules/proctoring/actions.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 11-13, 79-84 |
架构违规 |
P0 |
actions.ts(编排层)直接导入 db、examSubmissions 并执行 DB 查询 |
将 submission 归属校验逻辑移至 data-access.ts |
| 3 |
TS规范 |
P1 |
import { ActionState } 应为 import type { ActionState } |
改为 import type { ActionState } |
| 39 |
TS规范 |
P1 |
as z.ZodType<ProctoringEventType> 使用 as 断言 |
直接使用 z.enum([...]) 推导 |
| 63 |
Server Action规范 |
P1 |
recordProctoringEventAction 使用 requireAuth() 而非 requirePermission() |
使用 requirePermission(Permissions.EXAM_SUBMIT) |
| 全文件 |
Server Action规范 |
P2 |
recordProctoringEventAction 未调用 revalidatePath |
添加 revalidatePath 刷新监考面板相关路径 |
src/modules/proctoring/data-access.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 127 |
TS规范 |
P1 |
row.event.eventType as ProctoringEventType 使用 as 断言 |
drizzle enum 列类型已是字面量联合,应直接匹配 |
| 152 |
TS规范 |
P1 |
row.eventType as ProctoringEventType 同上 |
同上 |
| 208 |
TS规范 |
P1 |
stat.eventType as ProctoringEventType 同上 |
同上 |
| 290 |
TS规范 |
P1 |
row.eventType as ProctoringEventType 同上 |
同上 |
| 313 |
TS规范 |
P1 |
(row.submission.status ?? null) as StudentProctoringStatus["submissionStatus"] 使用 as 断言 |
用类型守卫函数校验 status 值 |
| 378 |
TS规范 |
P1 |
row.event.eventType as ProctoringEventType 同上 |
同上 |
| 5-9 |
架构违规 |
P1 |
直接导入 exams、examSubmissions 表 |
通过 @/modules/exams/data-access 暴露的函数获取 |
| 188-193 |
性能 |
P2 |
getExamProctoringSummary 中对 submissions 数组调用两次 .filter() |
用单次 for 循环同时统计两个计数 |
| 178-223 |
性能 |
P2 |
getExamProctoringSummary 中 submissions 查询、eventStats 查询、studentEventCounts 查询三者相互独立,却串行 await |
用 Promise.all 并行执行 |
src/modules/proctoring/types.ts
未发现违规问题。
3.18 diagnostic(诊断模块)
src/modules/diagnostic/actions.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 26-33, 54-61, 82-85, 105-108 |
Server Action规范 |
P1 |
generateStudentReportAction、generateClassReportAction、publishReportAction、deleteReportAction 均使用手动 typeof + length 校验,未使用 Zod schema |
新建 schema.ts,定义 Zod schema |
| 121-133, 136-148 |
Server Action规范 |
P2 |
getDiagnosticReportsAction(params) 和 getDiagnosticReportByIdAction(id) 直接接收参数,未经 Zod 校验 |
对入参用 Zod 校验 |
| 123, 138 |
TS规范 |
P2 |
Promise<ActionState<Awaited<ReturnType<typeof getDiagnosticReports>>>> 类型表达式过于复杂 |
在 types.ts 中定义具名返回类型并导入 |
src/modules/diagnostic/data-access.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 9, 13 |
架构违规 |
P1 |
直接导入 examSubmissions、submissionAnswers、questionsToKnowledgePoints 表 |
通过对应模块的 data-access 函数获取数据 |
| 116 |
性能 |
P2 |
answers.find((a) => a.questionId === link.questionId) 在 for 循环内调用,O(n×m) 复杂度 |
先用 new Map(answers.map(a => [a.questionId, a])) 构建 Map |
| 125-146 |
性能 |
P2 |
for 循环内逐条 await db.insert().onDuplicateKeyUpdate(),N 个知识点产生 N 次 DB 往返 |
用 Promise.all 并行化,或构建批量 upsert |
| 80-81 |
性能 |
P2 |
allMastery.filter(m => m.masteryLevel >= 80) 和 allMastery.filter(m => m.masteryLevel < 60) 两次遍历同一数组 |
合并为单次循环 |
| 全文件 |
性能 |
P2 |
所有读函数均未用 React.cache() 包裹 |
用 cache() 包裹读函数 |
src/modules/diagnostic/data-access-reports.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 29-31 |
TS规范 |
P1 |
(r.strengths as string[] | null)、(r.weaknesses as string[] | null)、(r.recommendations as string[] | null) 三处 as 断言 |
编写类型守卫 isStringArray(v: unknown): v is string[] |
| 59, 103 |
性能/规范 |
P2 |
const { createId } = await import("@paralleldrive/cuid2") 在函数内部动态导入 |
改为顶层静态导入 |
| 201-202 |
代码质量 |
P2 |
void round2 — 为抑制未使用警告而保留无用函数 |
直接删除 round2 函数定义及 void round2 |
| 172-180 |
性能 |
P2 |
getDiagnosticReportById 中先查 report,再单独查 generator 名称,两次串行查询 |
将 generator 查询合并到主查询的 JOIN 中 |
| 全文件 |
性能 |
P2 |
getDiagnosticReports、getDiagnosticReportById 未用 React.cache() 包裹 |
用 cache() 包裹 |
| 107 |
代码质量 |
P2 |
studentId: generatedBy 班级报告将生成者 ID 存入 studentId 字段 |
考虑将 studentId 改为可空,或使用单独的 classId 字段 |
src/modules/diagnostic/types.ts
未发现违规问题。
3.19 dashboard(仪表盘模块)
src/modules/dashboard/data-access.ts & types.ts
无违规问题。正确使用 Promise.all 并行获取多模块数据,正确使用 cache(),正确通过各模块 data-access 通信。是除 textbooks 外的另一个标杆模块。
3.20 files(文件模块)
src/modules/files/data-access.ts
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 190 |
TS规范 |
P1 |
const conditions = [] 缺少类型标注,TypeScript 推断为 never[] 或 any[] |
改为 const conditions: SQL[] = [] |
| 204 |
TS规范 |
P1 |
or(...)! 使用非空断言 |
显式处理 undefined |
| 160-170 |
性能 |
P2 |
deleteFileAttachments 回退逻辑中 for 循环内逐条 await db.delete() |
用 Promise.allSettled 并行删除 |
| 53-55, 70-72, 95-97, 114-116, 131-133, 143-145, 219-221, 248-250, 264-266 |
代码质量 |
P2 |
大量 catch { return null / [] / false } 静默吞掉异常,共 9 处 |
至少 console.error 记录错误信息 |
| 全文件 |
性能 |
P2 |
所有读函数均未用 React.cache() 包裹 |
用 cache() 包裹读函数 |
src/modules/files/types.ts
未发现违规问题。
3.21 announcements(公告模块)
src/modules/announcements/data-access.ts(186 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 46-47 |
TS规范 |
P1 |
toIso(row.createdAt) as string — as 断言掩盖潜在 null |
让 toIso 对非空入参返回 string,拆分为两个函数 |
| 59, 62 |
TS规范 |
P2 |
params.status as AnnouncementStatus — 冗余 as 断言 |
直接使用收窄后的变量 |
| 88-90, 118-120 |
性能/可靠性 |
P2 |
catch {} 静默吞掉所有异常 |
至少记录错误日志 |
| 25-26 |
命名/DRY |
P2 |
mapRow 参数中内联定义类型,与 types.ts 重复 |
导入并复用 types.ts 中的类型 |
src/modules/announcements/actions.ts(231 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 217-231 |
Server Action规范 |
P2 |
getAnnouncementsAction 使用 requireAuth() 而非 requirePermission(Permissions.ANNOUNCEMENT_READ) |
改为 requirePermission(Permissions.ANNOUNCEMENT_READ) |
| 73-79, 137-143, 159-165, 185-191, 208-214, 224-230 |
性能/可维护性 |
P2 |
6 个 Action 的 try/catch 错误处理块完全相同 |
提取共享错误处理函数 handleActionError(e) |
src/modules/announcements/schema.ts(45 行)& types.ts(50 行)
符合规范。
3.22 settings(设置模块)
src/modules/settings/actions.ts(205 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 8, 64-76, 95-100, 104-113, 123-139, 154-169 |
架构违规 |
P1 |
模块无 data-access.ts 文件,actions.ts 直接 import { db } 并执行 DB 查询 |
新建 data-access.ts,将所有 DB 操作下沉 |
| 62-77 |
Server Action规范 |
P2 |
getAiProviderSummaries 返回 Promise<AiProviderSummary[]> 而非 ActionState<T> |
移至 data-access.ts 或包装为 ActionState |
| 38-46 |
架构违规 |
P2 |
AiProviderSummary 类型定义在 actions.ts 中 |
新建 types.ts,将类型移入 |
| 48-51 |
TS规范 |
P2 |
ensureUser 是 async 箭头函数,未显式标注返回类型 |
添加 : Promise<{ id: string }> |
| 53-60 |
TS规范 |
P2 |
normalizeBaseUrl 未显式标注返回类型 |
添加 : string | null |
| 95-113 |
性能 |
P2 |
upsertAiProviderAction 的更新分支中,count() 查询与 select 查询是两个独立查询,串行执行 |
用 Promise.all 并行化 |
| 120, 152 |
命名规范 |
P2 |
nextIsDefault、makeDefault 布尔变量前缀不规范 |
改为 isNextDefault、shouldMakeDefault |
src/modules/settings/actions-password.ts(113 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 7, 58-62, 78-81, 83-87, 89-105 |
架构违规 |
P1 |
actions-password.ts 直接 import { db } 并执行 DB 操作,无 data-access.ts |
将 DB 操作下沉至 data-access.ts |
| 39-51 |
Server Action规范 |
P1 |
输入通过 String(formData.get(...)) 手动读取,未使用 Zod schema 验证 |
定义 Zod schema 并 safeParse |
| 30 |
Server Action规范 |
P2 |
changePasswordAction 使用 requireAuth() 而非 requirePermission() |
新增 PASSWORD_SELF_CHANGE 权限点并赋给所有角色 |
| 58-87 |
性能 |
P2 |
查询 user 和查询 passwordSecurity 是两个独立表的查询,当前串行执行 |
重构为 Promise.all 并行获取 |
| 14-18 |
TS规范 |
P2 |
normalizeBcryptHash 未显式标注返回类型 |
添加 : string |
3.23 layout(布局模块)
src/modules/layout/config/navigation.ts(317 行)
| 行号范围 |
问题类别 |
严重程度 |
问题描述 |
改进建议 |
| 30-31 |
TS规范 |
P2 |
NavItem.permission 和子项的 permission 均为 string 类型,但项目已有 Permission 类型 |
导入 Permission 类型并使用 |
| 34 |
架构违规 |
P2 |
export type Role = "admin" | "teacher" | "student" | "parent" 定义在 config 文件中 |
考虑将 Role 类型提升至 @/shared/types |
四、跨模块共性问题
4.1 架构违规(P0/P1,最高优先级)
4.1.1 跨模块直接查询 DB 表(最普遍问题)
| 调用方模块 |
被查模块表 |
严重程度 |
| exams |
questions(直写)、classes、subjects、grades |
P0/P1 |
| homework |
exams(5 处)、classes、classEnrollments、subjects、users |
P1 |
| questions |
knowledgePoints、chapters、textbooks |
P1 |
| grades |
classes、classEnrollments、subjects、users |
P1 |
| classes |
homeworkAssignments、homeworkSubmissions、exams、classSchedule |
P0 |
| school |
(actions 层直接 DB 操作) |
P0 |
| scheduling |
users(actions 层) |
P0 |
| messaging |
classEnrollments、classes |
P1 |
| notifications |
classes、classEnrollments、messaging(循环) |
P0/P1 |
| parent |
users、grades、classEnrollments、classes |
P1 |
| proctoring |
exams、examSubmissions |
P0/P1 |
| diagnostic |
examSubmissions、submissionAnswers、questionsToKnowledgePoints |
P1 |
| elective |
classes、classEnrollments |
P1 |
统一修复方案:在各模块 data-access 暴露跨模块查询接口,消除直查。
4.1.2 messaging↔notifications 循环依赖
messaging/data-access.ts 写 messageNotifications 表
notifications/data-access.ts 反向 import messaging 的 notification-preferences 和 types
notifications/channels/in-app-channel.ts 动态 import messaging 的 data-access
修复方案:将 messageNotifications 和 notificationPreferences 表所有权移交 notifications 模块。
4.1.3 actions 层直接操作 DB
| 模块 |
文件 |
问题 |
| school |
actions.ts |
所有 mutation 直接操作 DB |
| scheduling |
actions.ts |
直接查询 users 表 |
| proctoring |
actions.ts |
直接查询 examSubmissions 表 |
| classes |
actions.ts |
直接查询 grades、classes 表做权限校验 |
| settings |
actions.ts、actions-password.ts |
无 data-access.ts,直接 DB 操作 |
| users |
actions.ts |
updateUserProfile 直接 db.update |
统一修复方案:DB 操作必须下沉到 data-access.ts。
4.2 TypeScript 规范违规(P1)
4.2.1 as 断言滥用(非从 unknown 转换)
| 模块 |
文件 |
行号 |
| exams |
data-access.ts |
76, 160, 361, 108, 172 |
| exams |
actions.ts |
270, 349-351 |
| homework |
data-access.ts |
39, 124, 226, 314, 392, 467, 645 |
| homework |
stats-service.ts |
220 |
| textbooks |
actions.ts |
54-58, 92-98, 154, 219-221, 259-261(14 处) |
| textbooks |
data-access.ts |
51 |
| classes |
data-access.ts |
240, 266, 561 |
| classes |
data-access-admin.ts |
135, 259, 349 |
| classes |
data-access-schedule.ts |
54, 93 |
| classes |
data-access-stats.ts |
250, 261, 512, 523 |
| classes |
actions.ts |
538, 582 |
| course-plans |
data-access.ts |
149 |
| messaging |
data-access.ts |
80, 74, 85 |
| notifications |
channels/in-app-channel.ts |
54(非法) |
| notifications |
channels/sms-channel.ts |
32 |
| audit |
data-access.ts |
75, 122, 123, 186 |
| elective |
data-access.ts |
117 |
| elective |
data-access-selections.ts |
102, 114 |
| proctoring |
data-access.ts |
127, 152, 208, 290, 313, 378 |
| proctoring |
actions.ts |
39 |
| diagnostic |
data-access-reports.ts |
29-31 |
| parent |
data-access.ts |
30 |
| announcements |
data-access.ts |
46-47, 59, 62 |
| attendance |
data-access.ts |
195(Record<string, unknown>) |
统一修复方案:用类型守卫函数或 Zod 校验替换 as 断言。
4.2.2 非空断言 !
| 模块 |
文件 |
行号 |
| exams |
ai-pipeline.ts |
464, 657, 665, 666, 671 |
| homework |
export.ts |
154 |
| textbooks |
data-access.ts |
42, 71 |
| classes |
data-access.ts |
287, 567 |
| classes |
data-access-admin.ts |
369 |
| classes |
data-access-students.ts |
102 |
| scheduling |
data-access.ts |
140, 221, 222 |
| scheduling |
actions.ts |
115 |
| scheduling |
auto-scheduler.ts |
144, 145 |
| messaging |
data-access.ts |
97, 117, 163 |
| files |
data-access.ts |
204 |
统一修复方案:用显式判空处理替换非空断言。
4.2.3 函数返回值未显式标注
| 模块 |
文件 |
函数 |
| classes |
data-access.ts |
isDuplicateInvitationCodeError, generateInvitationCode, normalizeSortText, parseFirstInt, compareGradeLabel, compareClassLike |
| classes |
data-access-admin.ts |
getManagedGrades |
| scheduling |
data-access.ts |
getAdminClassesForScheduling, getTeachersForScheduling, getClassroomsForScheduling, getClassSubjectsForScheduling |
| users |
data-access.ts |
normalizeRoleName, resolvePrimaryRole |
| users |
user-service.ts |
normalizeBcryptHash |
| messaging |
data-access.ts |
mapMessage, mapNotification |
| messaging |
notification-preferences.ts |
toIso, mapRow |
| notifications |
dispatcher.ts |
getSenders, selectChannels |
| notifications |
channels/email-channel.ts |
getEmailConfig, getTypeColor, buildHtmlContent, escapeHtml |
| notifications |
channels/sms-channel.ts |
getSmsConfig, buildTemplateParams |
| notifications |
channels/wechat-channel.ts |
getWechatConfig, buildTemplateData |
| audit |
data-access.ts |
toIso, clampPageSize, clampPage |
| parent |
data-access.ts |
getChildBasicInfo |
| announcements |
— |
— |
| settings |
actions.ts |
ensureUser, normalizeBaseUrl |
| settings |
actions-password.ts |
normalizeBcryptHash |
| elective |
data-access.ts |
buildCourseSelect |
| elective |
data-access-selections.ts |
buildCourseSelect, selectionDetailSelect |
| elective |
schema.ts |
emptyToNull, optionalStringToNull |
| questions |
actions.ts |
getQuestionsAction |
统一修复方案:所有函数(特别是箭头函数)必须显式标注返回类型。
4.2.4 import type 未使用
| 模块 |
文件 |
行号 |
| exams |
actions.ts |
4 |
| questions |
actions.ts |
7 |
| textbooks |
actions.ts |
18 |
| proctoring |
actions.ts |
3 |
统一修复方案:仅用于类型的导入必须使用 import type。
4.3 Server Action 规范违规
4.3.1 requireAuth() 替代 requirePermission()
| 模块 |
文件 |
Action |
| users |
actions.ts |
updateUserProfile |
| messaging |
actions.ts |
getNotificationsAction, markNotificationAsReadAction, markAllNotificationsAsReadAction, getNotificationPreferencesAction, updateNotificationPreferencesAction |
| proctoring |
actions.ts |
recordProctoringEventAction |
| announcements |
actions.ts |
getAnnouncementsAction |
| settings |
actions-password.ts |
changePasswordAction |
统一修复方案:改为 requirePermission(Permissions.XXX)。
4.3.2 缺少 Zod 验证
| 模块 |
文件 |
问题 |
| textbooks |
actions.ts |
完全无 schema.ts,所有 Action 无 Zod 验证 |
| classes |
actions.ts |
完全无 schema.ts,使用手动 typeof 检查 |
| diagnostic |
actions.ts |
4 个 Action 使用手动 typeof 校验 |
| settings |
actions-password.ts |
输入手动读取,无 Zod |
| messaging |
actions.ts |
多个 Action 参数未 Zod 校验 |
| notifications |
actions.ts |
多个 Action 参数未 Zod 校验 |
| audit |
actions.ts |
多个 Action 参数未 Zod 校验 |
| users |
actions.ts |
updateUserProfile 无 Zod |
| elective |
actions.ts |
getStudentSelectionsAction 无 Zod |
统一修复方案:新建/补充 schema.ts,所有 Action 输入用 Zod 验证。
4.3.3 缺少 revalidatePath
| 模块 |
文件 |
Action |
| course-plans |
actions.ts |
deleteCoursePlanItemAction, toggleCoursePlanItemCompletedAction |
| proctoring |
actions.ts |
recordProctoringEventAction |
4.3.4 未返回 ActionState<T>
| 模块 |
文件 |
Action |
| questions |
actions.ts |
getQuestionsAction, getKnowledgePointOptionsAction |
| users |
actions.ts |
updateUserProfile(返回 Promise) |
| settings |
actions.ts |
getAiProviderSummaries(返回原始数组) |
4.4 重复代码
| 重复内容 |
出现位置 |
修复方案 |
normalizeRoleName |
users/data-access.ts、shared/lib/role-utils.ts |
复用 shared |
normalizeBcryptHash |
users/user-service.ts、settings/actions-password.ts、shared/lib/bcrypt-utils.ts |
复用 shared |
toNumber/normalize/buildScopeClassFilter |
grades/data-access.ts、data-access-analytics.ts、data-access-ranking.ts |
提取到 grades/utils.ts |
serializeDate |
scheduling/data-access.ts、attendance/data-access.ts、attendance/data-access-stats.ts |
提取到 shared |
toIso |
announcements/data-access.ts、messaging/data-access.ts、messaging/notification-preferences.ts、audit/data-access.ts |
提取到 shared |
| try/catch 错误处理块 |
announcements/actions.ts(6 处) |
提取 handleActionError(e) |
| Excel 导出逻辑 |
audit/actions.ts(3 处) |
抽取 buildExcelSheet |
4.5 错误吞没(静默 catch)
| 模块 |
文件 |
处数 |
| school |
data-access.ts |
6 |
| classes |
data-access-admin.ts、data-access-students.ts |
多处 |
| course-plans |
data-access.ts |
2 |
| announcements |
data-access.ts |
2 |
| files |
data-access.ts |
9 |
| audit |
data-access.ts |
3 |
| elective |
data-access.ts |
2 |
统一修复方案:至少 console.error 记录错误,或向上抛出由调用方处理。
4.6 不可达代码
| 模块 |
文件 |
行号 |
| classes |
data-access.ts |
297-299(throw 后 return id) |
| classes |
data-access-admin.ts |
380-382(throw 后 return id) |
| questions |
actions.ts |
158-163, 170-175(catch 块两分支都 throw e) |
五、性能优化专项(react-best-practices)
依据 Vercel React Best Practices 规则集,识别以下性能优化机会:
5.1 CRITICAL:消除瀑布流(async-*)
5.1.1 async-parallel:独立操作用 Promise.all()
| 模块 |
文件 |
行号 |
问题 |
修复 |
| classes |
data-access.ts |
120-127 |
getAccessibleClassIdsForTeacher 两个独立 DB 查询串行 |
Promise.all |
| settings |
actions.ts |
95-113 |
upsertAiProviderAction count 与 select 串行 |
Promise.all |
| settings |
actions-password.ts |
58-87 |
user 与 passwordSecurity 查询串行 |
Promise.all |
| elective |
data-access-operations.ts |
18-34 |
runLottery course 与 selections 串行 |
Promise.all |
| elective |
data-access-operations.ts |
86-112 |
selectCourse course 与 existing 串行 |
Promise.all |
| elective |
data-access-operations.ts |
158-182 |
dropCourse existing 与 course 串行 |
Promise.all |
| proctoring |
data-access.ts |
178-223 |
getExamProctoringSummary 三个独立查询串行 |
Promise.all |
| diagnostic |
data-access-reports.ts |
172-180 |
getDiagnosticReportById report 与 generator 串行 |
合并 JOIN 或 Promise.all |
| parent |
data-access.ts |
58-117 |
getChildBasicInfo 4 个串行查询 |
JOIN 或 Promise.all |
5.1.2 async-defer-await:将 await 移到实际使用的分支
无明显违规,多数文件已正确处理。
5.2 HIGH:服务端性能(server-*)
5.2.1 server-cache-react:用 React.cache() 做请求内去重
| 模块 |
文件 |
缺失 cache() 的函数 |
| grades |
data-access.ts |
getGradeRecords, getGradeRecordById, getClassGradeStats |
| grades |
data-access-analytics.ts |
getClassComparison, getGradeTrends |
| grades |
data-access-ranking.ts |
getClassRanking, getStudentRanking |
| elective |
data-access-selections.ts |
getCourseSelections, getStudentSelections, getStudentGradeId, getAvailableCoursesForStudent |
| diagnostic |
data-access.ts |
getStudentMastery, getStudentMasterySummary, getClassMasterySummary, getKnowledgePointStats |
| diagnostic |
data-access-reports.ts |
getDiagnosticReports, getDiagnosticReportById |
| files |
data-access.ts |
getFileAttachment, getFileAttachmentsByTarget, getFileAttachmentsByUploader, getAllFileAttachments, getFileAttachmentsWithFilters, getFileStats, getFileAttachmentsByIds |
| proctoring |
data-access.ts |
多个查询函数 |
标杆:exams、homework、questions、textbooks、dashboard、announcements 已正确使用 cache() ✓。
5.2.2 server-after-nonblocking:用 after() 做非阻塞操作
| 模块 |
文件 |
行号 |
问题 |
| school |
actions.ts |
188, 219, 235 |
await logAudit(...) 阻塞响应 |
修复:使用 Next.js after() 将审计日志改为非阻塞。
5.2.3 server-serialization:最小化传给客户端组件的数据
需结合前端组件审查,本次后端核查未深入。
5.3 MEDIUM:JavaScript 性能(js-*)
5.3.1 js-set-map-lookups:用 Set/Map 做 O(1) 查找
| 模块 |
文件 |
行号 |
问题 |
| grades |
data-access-analytics.ts |
133-136 |
scope.classIds.includes(c.id) — O(n) 查找 |
| diagnostic |
data-access.ts |
116 |
answers.find(...) 在 for 循环内 — O(n×m) |
修复:将数组转为 Set/Map 后用 .has() / .get()。
5.3.2 js-combine-iterations:合并多次 filter/map 为一次循环
| 模块 |
文件 |
行号 |
问题 |
| grades |
data-access.ts |
248-252 |
两次 if 判断遍历同一数组 |
| grades |
data-access-analytics.ts |
180-181, 238-239 |
两次 filter 遍历同一数组 |
| grades |
data-access-ranking.ts |
100, 102 |
findIndex 和 find 查找同一元素 |
| diagnostic |
data-access.ts |
80-81 |
两次 filter 遍历同一数组 |
| proctoring |
data-access.ts |
188-193 |
两次 filter 统计 submissions |
5.3.3 js-early-exit:函数早返回
多数文件已正确处理。
5.3.4 动态 import 改静态 import
| 模块 |
文件 |
行号 |
问题 |
| grades |
data-access.ts |
148, 172 |
await import("@paralleldrive/cuid2") 函数内动态导入 |
| diagnostic |
data-access-reports.ts |
59, 103 |
同上 |
修复:改为文件顶部静态 import { createId } from "@paralleldrive/cuid2"。
5.4 性能问题汇总(按影响排序)
| 优先级 |
问题 |
影响范围 |
| CRITICAL |
grades N+1 查询(getClassComparison) |
大班级成绩对比性能差 |
| HIGH |
循环内串行 await db.update/insert(homework/data-access-write.ts、elective/data-access-operations.ts、diagnostic/data-access.ts、course-plans/data-access.ts) |
批量操作慢 |
| HIGH |
大量读函数未用 React.cache()(grades、elective、diagnostic、files、proctoring) |
请求内重复查询 |
| MEDIUM |
独立查询未并行化(9 处) |
响应时间累加 |
| MEDIUM |
动态 import(3 处) |
每次调用有开销 |
| LOW |
重复 filter 遍历(5 处) |
微小性能损失 |
| LOW |
O(n) 查找代替 O(1)(2 处) |
微小性能损失 |
六、优先修复路线图
第一阶段:P0 严重问题(立即修复)
- exams/data-access.ts:
persistAiGeneratedExamDraft 改为调用 questions data-access
- exams/data-access.ts:
getExams/getExamById 改为通过 classes data-access 获取 gradeId
- questions/schema.ts:
z.any() → z.unknown()
- questions/actions.ts:
getQuestionsAction 和 getKnowledgePointOptionsAction 包装为 ActionState
- textbooks/actions.ts:新建 schema.ts,添加 Zod 验证;清理 14 处
as 断言
- grades/data-access-analytics.ts:
getClassComparison N+1 查询重构为 inArray 批量查询
- classes/data-access-stats.ts、data-access-students.ts:移除对 homework/exams 表的直接查询
- school/actions.ts:所有 DB 操作下沉到 data-access.ts
- proctoring/actions.ts:DB 查询下沉到 data-access.ts
- messaging↔notifications:将
messageNotifications 和 notificationPreferences 表所有权移交 notifications 模块
- notifications/channels/in-app-channel.ts:54:修复非法 as 断言
- users/user-service.ts:13:移除硬编码弱密码 "123456"
- users/actions.ts:
updateUserProfile 整改(权限+下沉+返回类型)
- scheduling/data-access.ts:4 个函数补充返回类型标注
第二阶段:P1 重要问题(尽快修复)
- 跨模块直查 DB:在各模块 data-access 暴露跨模块查询接口,消除直查(涉及 exams、homework、questions、grades、classes、messaging、notifications、parent、proctoring、diagnostic、elective)
as 断言清理:全局替换为类型守卫(涉及 20+ 文件,60+ 处)
requireAuth → requirePermission:users、messaging、proctoring、announcements、settings 共 8 处
import type 修正:exams、questions、textbooks、proctoring 共 4 处
- 动态 import 改静态:grades、diagnostic 共 3 处
- grades/data-access.ts:249:除零 bug 守卫
- audit/data-access.ts:导出函数数据截断问题(3 处)
- elective/data-access-operations.ts:
runLottery 加事务包裹 + 批量更新
- diagnostic/actions.ts:新建 schema.ts,用 Zod 替代手动校验
- classes 模块:新建 schema.ts,用 Zod 替换 actions.ts 中的手动校验
- course-plans/actions.ts:补齐 revalidatePath 调用(2 处)
- homework/data-access-write.ts:305-311:
gradeHomeworkAnswers 加事务包裹
- users/user-service.ts:
batchImportUsers 加事务包裹
- 不可达代码清理:classes/data-access.ts:297-299、classes/data-access-admin.ts:380-382
第三阶段:P2 优化(迭代修复)
- React.cache() 包装:grades、elective、diagnostic、files、proctoring 读函数添加 cache()
- Promise.all 并行化:9 处独立查询串行执行
after() 非阻塞审计日志:school/actions.ts(3 处)
- 错误吞没清理:school、classes、course-plans、announcements、files、audit、elective 共 30+ 处静默 catch
- 非空断言
! 清理:20+ 处替换为显式判空
- 函数返回类型补齐:30+ 个箭头函数
- 重复代码提取:normalizeRoleName、normalizeBcryptHash、toIso、serializeDate、toNumber/normalize、handleActionError、buildExcelSheet
- 合并 filter 遍历:5 处
- Set/Map 查找优化:2 处
- 架构文档同步:exams/actions.ts 行数、settings 导出函数列表、P2-11 状态、announcements 依赖关系
- exams/ai-pipeline.ts 拆分:912 行按职责拆分为 4 个文件
- layout/navigation.ts:permission 字段改用 Permission 类型
- notifications/external-sdk.d.ts:安装实际 SDK 后用其自带类型覆盖 any
附录:核查方法说明
核查流程
- 架构图优先:先阅读
docs/architecture/004_architecture_impact_map.md,理解模块依赖关系和已知问题
- 并行核查:启动 5 个并行搜索代理,分别核查核心教学、教学管理、用户沟通、扩展功能、其他模块
- 逐文件审查:每个代理读取所有后端
.ts 文件,对照项目规范逐条核查
- 性能规则应用:应用 Vercel React Best Practices 的 65 条规则,识别性能优化机会
- 结果汇总:合并 5 个代理的核查结果,按严重程度和模块分类整理
核查依据
.trae/rules/project_rules.md:项目规则(架构分层、TS规范、命名、组件、Server Action、Tailwind、安全、提交)
docs/standards/coding-standards.md:编码规范(详细规范)
docs/architecture/004_architecture_impact_map.md:架构影响地图(模块清单、依赖关系、已知问题)
docs/architecture/005_architecture_data.json:AI 友好格式的结构化数据
- Vercel React Best Practices:65 条性能优化规则(8 个类别)
未核查范围
src/modules/*/components/ 下的前端组件文件
src/modules/*/hooks/ 下的 Hook 文件
src/shared/ 下的基础设施文件
src/app/ 下的路由层文件
- 测试文件(
*.test.ts)
如需核查上述范围,请另行指派任务。