135 lines
11 KiB
JSON
135 lines
11 KiB
JSON
[
|
||
{
|
||
"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.ts(L1)与 data-access-cross-module.ts(L1)均已正确声明,唯独主文件遗漏。",
|
||
"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 条 SQL(2N+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 的 getClassNamesByIds(1 条),总计 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": "跨模块在循环内多次调用 getActiveStudentIdsByClassId(classes 模块)",
|
||
"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.ts,data-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 分钟)"
|
||
}
|
||
]
|