Files
NextEdu/docs/architecture/audit/archive/g2-data-access-audit.json

135 lines
11 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[
{
"id": "G2-001",
"file": "src/modules/exams/data-access.ts",
"lines": "L1",
"ruleId": "P-01",
"severity": "P0",
"dimension": "pattern",
"title": "文件首行缺少 import \"server-only\" 标记",
"description": "data-access.ts 首行为 `import { db } from \"@/shared/db\"`,未在文件头声明 `import \"server-only\"`。该文件包含直接 DB 访问exams/examQuestions 表的 CRUD若被客户端组件意外引入会将数据库连接与查询逻辑泄露到客户端 bundle造成安全漏洞。同模块的 data-access-error-collection.tsL1与 data-access-cross-module.tsL1均已正确声明唯独主文件遗漏。",
"recommendation": "在文件第一行(所有 import 之前)添加 `import \"server-only\"`。注意:必须位于首行,否则 next.js 的 server-only 边界检测可能不生效。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-002",
"file": "src/modules/grades/data-access-appeals.ts",
"lines": "L122-L151",
"ruleId": "F-01",
"severity": "P1",
"dimension": "performance",
"title": "getPendingAppealsForReviewRaw 在 JS 层过滤班级范围而非 SQL WHERE",
"description": "函数 WHERE 子句仅过滤 `gradeAppeals.status = 'pending'`L134未对 classIds 加任何过滤,导致 SQL 返回全库所有 pending 申诉(含 gradeRecord 全字段 innerJoin随后在 L141 用 `rows.filter((r) => classIds.includes(r.gradeRecord.classId))` 在 JS 层过滤。代码注释写明「在 JS 层过滤班级范围(避免复杂 SQL join但 innerJoin gradeRecords 已存在,加 `inArray(gradeRecords.classId, classIds)` 并不复杂。当 pending 申诉总量增长时(全校维度),单次查询会拉取大量无关行,造成内存与网络压力;同时若 JS filter 被误删将引发跨班级数据泄露。",
"recommendation": "在 L132-L137 的 `and()` 内追加 `inArray(gradeRecords.classId, classIds)` 条件classIds 为空时已在 L123 提前返回),删除 L140-L141 的 JS 层 filter直接返回 rows.map(...)。这样既收窄 SQL 结果集,又消除数据泄露风险。",
"effort": "S (≤30 分钟)"
},
{
"id": "G2-003",
"file": "src/modules/adaptive-practice/data-access-analytics.ts",
"lines": "L311-L384",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "getTeacherClassPracticeOverviewsRaw 在 Promise.all 内对每个班级循环发起 2 条 SQL2N+1 模式)",
"description": "函数对 classIds 数组执行两次 Promise.all 循环:(1) L320-L325 对每个 classId 调用 `getActiveStudentIdsByClassId(classId)`(每班 1 条 SQL共 N 条);(2) L328-L365 对每个班级再发起 1 条 `db.select().from(practiceSessions).where(inArray(studentId, ...))` 聚合查询(共 N 条)。加上 L317 的 getClassNamesByIds1 条),总计 2N+1 条 SQL。当教师所教班级数 N 较大(如年级主任辖 10+ 班级)时,单次请求产生 20+ 条 SQL且 Promise.all 仅并发 IO 不减少 DB 负载。",
"recommendation": "改为批量查询:(1) 一次性获取所有班级的学生 ID 映射(可用单条 SQL `SELECT classId, studentId FROM class_members WHERE classId IN (...) AND status='active'` 后在 JS 层 groupBy(2) 用单条聚合 SQL `SELECT classId, count(...), SUM(...), COUNT(DISTINCT studentId) FROM practiceSessions WHERE studentId IN (全部学生) GROUP BY studentId` 后在 JS 层按班级归并;或直接 JOIN class_members 按 classId 分组。目标:将 2N+1 降至 2-3 条 SQL。",
"effort": "M (≤2 小时)"
},
{
"id": "G2-004",
"file": "src/modules/adaptive-practice/data-access-analytics.ts",
"lines": "L320-L325",
"ruleId": "F-08",
"severity": "P1",
"dimension": "performance",
"title": "跨模块在循环内多次调用 getActiveStudentIdsByClassIdclasses 模块)",
"description": "在 Promise.all 内对每个 classId 单独调用 `@/modules/classes/data-access` 的 `getActiveStudentIdsByClassId`,属于 F-08 跨模块多次调用 getXxxByIds 模式。该函数内部本身可能已 cacheFn 包装,但首次填充缓存时仍会产生 N 条 SQL。应改用批量接口 `getActiveStudentIdsByClassIds(classIds)`(如不存在则需在 classes 模块新增)。",
"recommendation": "在 classes/data-access 新增 `getActiveStudentIdsByClassIds(classIds: string[]): Promise<Map<string, string[]>>` 批量接口(单条 SQL `WHERE classId IN (...)` 后 groupBy本函数改为一次调用获取全量映射。与 G2-003 的修复可合并执行。",
"effort": "M (≤2 小时)"
},
{
"id": "G2-005",
"file": "src/modules/grades/data-access-analytics.ts",
"lines": "L1-L831",
"ruleId": "S-01",
"severity": "P1",
"dimension": "structure",
"title": "文件 831 行超过 800 行警告阈值",
"description": "文件总计 831 行,超过 S-01 规则的 800 行警告线(虽未达 1000 行硬性上限。文件内含多个独立分析维度年级分布getGradeDistribution*、班级统计getClassGradeStats*、学生摘要getStudentGradeSummary*、排名getClassRanking*)等。职责虽同属 grades 分析,但可按分析维度进一步拆分以提升可维护性。",
"recommendation": "按分析维度拆分为 data-access-analytics-grade-distribution.ts / data-access-analytics-class-stats.ts / data-access-analytics-student-summary.ts 等,每个子文件 ≤ 300 行。或暂不拆分但监控增长,一旦逼近 1000 行必须拆分。",
"effort": "L (≤1 天)"
},
{
"id": "G2-006",
"file": "src/modules/homework/data-access.ts",
"lines": "L207-L212",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "data-access 内联 computeOverdueCount 业务计算闭包",
"description": "在 getHomeworkAssignmentsRaw 的数据组装段内定义了 `computeOverdueCount` 闭包,包含条件分支 `if (!dueAt || dueAt > now) return 0` 及逾期人数推导逻辑 `Math.max(0, targetCount - submittedCount)`。虽为纯计算非状态机但「逾期」的业务定义dueAt 已过且未提交)属于业务规则,下沉到 data-access 后未来若规则变更(如加宽限期、按作业类型区分)需改 data-access 而非 actions/lib。属 A-02 边界情形。",
"recommendation": "将 computeOverdueCount 提取到 homework/lib/overdue.ts 作为纯函数 `computeOverdueCount(dueAt, targetCount, submittedCount, now)`data-access 仅负责数据获取与组装,业务规则集中到 lib。优先级较低可在重构窗口处理。",
"effort": "S (≤30 分钟)"
},
{
"id": "G2-007",
"file": "src/modules/grades/data-access-drafts.ts",
"lines": "L367-L382",
"ruleId": "P-04",
"severity": "P2",
"dimension": "pattern",
"title": "releaseDraftLock 返回值依赖隐式类型推断的元组解构",
"description": "L367-L378 执行 `db.update(gradeDrafts).set(...).where(...)` 后L381 用 `const [header] = result` 解构L382 返回 `(header?.affectedRows ?? 0) > 0`。drizzle MySQL 的 update 返回类型为 `MySqlRawQueryResult`(即 `[ResultSetHeader, FieldPacket[]]`header 类型由推断得到。代码逻辑正确,但依赖 drizzle 内部类型推断而非显式标注,未来 drizzle 版本变更返回类型时可能静默失效。函数签名已显式标注 `Promise<boolean>`L364属轻微模式偏差。",
"recommendation": "可在解构处补充类型注释 `const [header] = result as [ResultSetHeader, unknown]`(此处 as 属从 unknown/drizzle 内部类型收窄,符合豁免);或保持现状但增加单元测试覆盖锁释放场景。优先级低。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-008",
"file": "src/modules/diagnostic/data-access.ts",
"lines": "L1-L553",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "诊断掌握度累积计算函数updateMasteryFrom*)含业务规则分支",
"description": "文件含 3 个掌握度累积函数updateMasteryFromSubmission / updateMasteryFromHomeworkSubmission / updateMasteryFromExamScore。这些函数内部包含掌握度合并算法加权平均/最大值取值等业务规则)与 DB 写入混合。掌握度计算属于诊断业务规则,理想分层应将算法提取到 diagnostic/lib/mastery-calculator.tsdata-access 仅负责读写 knowledgePointMastery 表。当前实现可行但职责混合,属 A-02 边界。",
"recommendation": "提取纯函数 `computeMasteryAfterSubmission(current: MasteryState, submission: SubmissionInput): MasteryState` 到 diagnostic/lib/data-access 函数改为:读取当前掌握度 → 调用纯函数计算新值 → 写回 DB。优先级中等可在掌握度算法需调整时一并重构。",
"effort": "M (≤2 小时)"
},
{
"id": "G2-009",
"file": "src/modules/adaptive-practice/data-access.ts",
"lines": "L307",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "sourceMeta as unknown 用于 JSON 序列化字段写入(属豁免范畴)",
"description": "L307 `sourceMeta: sourceMeta as unknown` 将类型化对象转为 unknown 以写入 JSON 列。此处的 as 属于「向 unknown 转换」的合规用法(框架 P-09 豁免:从 unknown 收窄或反向序列化)。仅作记录,非违规。",
"recommendation": "无需修改。若追求严谨,可改用 `JSON.parse(JSON.stringify(sourceMeta))` 显式序列化,但当前写法已合规。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-010",
"file": "src/modules/exams/data-access.ts",
"lines": "L316",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "new Map(generated.map((q) => [q.id, q] as const)) 使用 as const 构造 Map属豁免",
"description": "L316 `[q.id, q] as const` 用于向 Map 构造器提供 readonly tuple 类型。as const 属于 TypeScript 类型工具的合规用法P-09 豁免),非类型断言违规。仅作记录。",
"recommendation": "无需修改。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-011",
"file": "src/modules/homework/data-access-write.ts",
"lines": "L16,L20",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "import 语句中的 as 为模块别名(非类型断言)",
"description": "L16 `getClassTeacherById as getClassTeacherIdFromClass` 与 L20 `getExamWithQuestionsForHomework as getExamWithQuestionsFromExams` 为 ES module import 别名,用于避免跨模块同名函数冲突。非 P-09 规则所约束的类型断言。仅作记录,零违规。",
"recommendation": "无需修改。",
"effort": "XS (≤15 分钟)"
}
]