Files
NextEdu/docs/architecture/audit/g1-audit-output.json
SpecialX 5d9981fd7d
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
docs(architecture): update impact map, data, audit reports, superpowers docs
- Update 004_architecture_impact_map.md and 005_architecture_data.json

- Add audit reports: data-access-audit-framework-v1, data-access-audit-v1-data.json,

  data-access-audit-v1, g1-g5 audit outputs

- Add superpowers plans and specs (logging-refactor, documentation-system-redesign)

- Update troubleshooting/known-issues.md
2026-07-07 16:23:35 +08:00

807 lines
44 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": "G1-001",
"file": "src/modules/textbooks/data-access-graph.ts",
"lines": "L7-L13, L46-L53, L107-L121",
"ruleId": "A-06",
"severity": "P0",
"dimension": "architecture",
"title": "textbooks 模块直接查询 questions/diagnostic 模块的表",
"description": "data-access-graph.ts 从 @/shared/db/schema 导入 questionsToKnowledgePoints属 questions 模块)和 knowledgePointMastery属 diagnostic 模块),并直接执行 SELECT FROM 查询L46-53 查 questionsToKnowledgePointsL107-121 查 knowledgePointMastery。这违反了三层架构'模块间通过对方 data-access 通信,不直接查询对方 DB 表'的规则。",
"recommendation": "1) questionsToKnowledgePoints 的关联题目数查询应改为调用 questions 模块 data-access 暴露的跨模块接口(如 getQuestionCountByKpIds2) knowledgePointMastery 查询应改为调用 diagnostic 模块 data-access 暴露的接口(如 getKpMasteryByTextbookId。",
"effort": "M (≤2h)"
},
{
"id": "G1-002",
"file": "src/modules/questions/data-access.ts",
"lines": "L294-L315",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "deleteQuestionRecursive 递归 N+1每个子题单独查询+删除",
"description": "deleteQuestionRecursive 在递归中对每个子题先 SELECT 子题列表L305-308再 for 循环递归调用自身L310-312最后 DELETE 当前题L314。对于有 N 层子题的复合题,会产生 2N 次数据库往返。",
"recommendation": "改为先递归收集所有后代 ID 到一个数组单次查询children即可然后用 inArray 批量 DELETE`await tx.delete(questions).where(inArray(questions.id, allDescendantIds))`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-003",
"file": "src/modules/questions/data-access.ts",
"lines": "L350-L378",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "deleteQuestionsBatch 循环调用 deleteQuestionRecursive 产生 N+1",
"description": "deleteQuestionsBatch 在 L372-374 对 targetIds 数组 for 循环,每个 id 单独调用 deleteQuestionRecursive每次调用内部又递归查询子题。批量删除 M 个题目时产生 M × (递归深度) 次查询。",
"recommendation": "先将所有 targetIds 的后代 ID 一次性收集(用 inArray 批量查询 parentId in targetIds递归用 Map 解析),再单次 inArray 批量删除所有后代+自身。",
"effort": "M (≤2h)"
},
{
"id": "G1-004",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L128-L140",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "deleteComment 递归 N+1每个子回复单独查询+删除",
"description": "deleteComment 先 SELECT 子回复列表L130-133再 for 循环递归调用 deleteCommentL134-136最后 DELETE 当前评论L137-139。嵌套回复深时产生大量 DB 往返。",
"recommendation": "改为先用单次查询获取该 plan 下所有评论,在内存中构建 parent→children Map收集所有后代 ID 后用 inArray 批量删除。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-005",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L426-L458",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "reorderChapters 循环内逐条 UPDATEN+1",
"description": "reorderChapters 在事务内 for 循环遍历所有兄弟章节L445-457每个章节单独执行 tx.updateL448-454。重排 N 个章节产生 N 次 UPDATE 语句。",
"recommendation": "使用 CASE WHEN 批量更新:`UPDATE chapters SET order = CASE id WHEN ... THEN ... END, parentId = CASE id WHEN ... THEN ... END WHERE id IN (...)`,或用 sql`VALUES(...)` 构造批量更新。",
"effort": "M (≤2h)"
},
{
"id": "G1-006",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L237",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LIKE '%query%' 全表扫描查询课案标题",
"description": "getLessonPlansRaw 在 L237 使用 `like(lessonPlans.title, \\`%${escapeLikePattern(params.query)}%\\`)`,前导通配符 % 导致无法使用索引,全表扫描。课案表数据量大时严重影响性能。",
"recommendation": "对 lessonPlans.title 建立全文索引MySQL FULLTEXT INDEX改用 `sql\\`MATCH(title) AGAINST(${query} IN BOOLEAN MODE)\\``;或至少对高频查询场景使用前缀匹配 `like(title, query + '%')`。",
"effort": "M (≤2h)"
},
{
"id": "G1-007",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L99, L129",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LIKE '%id%' 全表扫描 JSON content 字段",
"description": "getLessonPlansByKnowledgePointRawL99和 getLessonPlansByQuestionRawL129对 lessonPlans.contentJSON 列)使用 `like(content, \\`%${kpId}%\\`)` 做粗筛。JSON 列上的 LIKE 全表扫描代价极高,且无法走索引。",
"recommendation": "建立关联表 lesson_plan_knowledge_point_refs(plan_id, knowledge_point_id) 和 lesson_plan_question_refs(plan_id, question_id) 存储提取后的关联关系,改用 inArray 等值查询。短期可加 LIMIT 并在 actions 层缓存结果。",
"effort": "L (≤1d)"
},
{
"id": "G1-008",
"file": "src/modules/questions/data-access.ts",
"lines": "L60-L65",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LOWER(CAST(content AS CHAR)) LIKE '%q%' 全表扫描",
"description": "getQuestionsRaw 在 L61-64 使用 `sql\\`LOWER(CAST(${questions.content} AS CHAR)) LIKE ${needle}\\`` 对 JSON content 列做 LIKE 模糊搜索,包含 LOWER + CAST + 前导 % 三重性能杀手,无法走索引。",
"recommendation": "对 questions 表增加 searchable_text 列(存储从 content 提取的纯文本),建立 FULLTEXT 索引;或引入 Meilisearch/TypeSense 等外部搜索引擎处理题目全文检索。",
"effort": "L (≤1d)"
},
{
"id": "G1-009",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L48-L54, L545-L551",
"ruleId": "F-02",
"severity": "P1",
"dimension": "performance",
"title": "LIKE '%q%' 全表扫描 4 个字段",
"description": "getTextbooksRawL48-54和 getTextbooksWithScopeRawL545-551对 title/subject/grade/publisher 四个字段做 `like(field, \\`%${q}%\\`)` OR 查询4 个前导通配符 LIKE 全表扫描。",
"recommendation": "对 title 建立全文索引;或将 subject/grade/publisher 改为等值过滤(下拉选择),仅 title 做前缀匹配。",
"effort": "M (≤2h)"
},
{
"id": "G1-010",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L247-L277",
"ruleId": "F-04",
"severity": "P1",
"dimension": "performance",
"title": "getLessonPlansRaw 5 表 LEFT JOIN",
"description": "getLessonPlansRaw 在 L270-275 对 lessonPlans LEFT JOIN textbooks/chapters/subjects/grades/users 共 5 个表。JOIN 表数量 > 3查询计划复杂度高且无 LIMIT。",
"recommendation": "拆分为两步1) 先查 lessonPlans 主表(带 scope + 过滤条件 + LIMIT + ORDER BY2) 用 collect 的 textbookId/chapterId/subjectId/gradeId/creatorId 批量查 textbooks/chapters/subjects/grades/users 名称,在内存中 Map 关联。",
"effort": "L (≤1d)"
},
{
"id": "G1-011",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L247-L277",
"ruleId": "F-05",
"severity": "P1",
"dimension": "performance",
"title": "getLessonPlansRaw 列表查询无 LIMIT",
"description": "getLessonPlansRaw 查询课案列表时无 LIMIT当课案数量增长时会一次性拉取全表数据到内存做分组聚合L283-316可能导致 OOM。",
"recommendation": "添加默认分页 `.limit(pageSize).offset(offset)`,或至少 `.limit(500)` 保护;版本聚合逻辑应改为分页后处理。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-012",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L168-L218, L255-L294",
"ruleId": "F-05",
"severity": "P1",
"dimension": "performance",
"title": "getPendingReviewPlansRaw / getPlansByStatusesRaw 无 LIMIT",
"description": "getPendingReviewPlansRawL184-196和 getPlansByStatusesRawL275-285均无 LIMIT且后者还在内存中做 filterL199-208而非 SQL 过滤。待审核/按状态查询的课案可能很多。",
"recommendation": "添加分页参数 page/pageSizeSQL 层用 inArray 过滤 gradeId/subjectId 而非内存 filter加 `.limit(pageSize).offset(offset)`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-013",
"file": "src/modules/lesson-preparation/data-access-calendar.ts",
"lines": "L43-L60, L102-L120, L136-L153",
"ruleId": "F-05",
"severity": "P1",
"dimension": "performance",
"title": "getCalendarEventsRaw 三段查询均无 LIMIT",
"description": "getCalendarEventsRaw 对 lessonPlansL43、lessonPlanVersionsL102、lessonPlanReviewRecordsL136三段查询均无 LIMIT。日历范围跨度大时可能拉取大量记录。",
"recommendation": "每段查询添加 `.limit(500)` 上限保护,或在 actions 层强制限制日期范围跨度(如最多 90 天)。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-014",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L212-L239",
"ruleId": "F-10",
"severity": "P1",
"dimension": "performance",
"title": "getFormativeItemStatsRaw 全表拉取后内存聚合统计",
"description": "getFormativeItemStatsRaw 在 L215-218 SELECT 所有作答记录(无 LIMIT然后在 L220-232 内存循环统计 total/correct/incorrect/avgDuration。一个互动组件可能有上千条作答。",
"recommendation": "改用 SQL 聚合:`SELECT COUNT(*) as total, SUM(isCorrect=1) as correct, SUM(isCorrect=0) as incorrect, AVG(durationSec) as avgDuration FROM ... WHERE itemId=?`,单次查询完成。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-015",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L145-L157",
"ruleId": "F-10",
"severity": "P1",
"dimension": "performance",
"title": "countUnresolvedCommentsRaw SELECT 全部 ID 后取 length 计数",
"description": "countUnresolvedCommentsRaw 在 L146-155 SELECT 所有匹配的 id 字段,然后 L156 `return rows.length` 计数。应直接用 SQL COUNT 聚合,避免拉取全部行数据。",
"recommendation": "改为 `.select({ count: count() }).from(...).where(...)`,返回 `Number(rows[0]?.count ?? 0)`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-016",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L168-L184",
"ruleId": "F-06",
"severity": "P1",
"dimension": "performance",
"title": "getGlobalLessonPlanStatsRaw 5 次串行查询同表",
"description": "getGlobalLessonPlanStatsRaw 对 lessonPlans/lessonPlanStandards 表执行 5 次 SELECT COUNT 查询L168-184且是串行 await。仪表盘每次加载产生 5 次 DB 往返。",
"recommendation": "合并为单次 GROUP BY 查询:`SELECT status, COUNT(*) FROM lessonPlans GROUP BY status`,或用 Promise.all 并行执行lessonPlanStandards 计数可合并到同一查询。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-017",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L77-L123",
"ruleId": "F-01",
"severity": "P1",
"dimension": "performance",
"title": "getSchedulesByDateRangeRaw 拉全表后内存 filter",
"description": "getSchedulesByDateRangeRaw 仅按日期范围查询L99-104然后用 `rows.filter((r) => teacherPlanIds.includes(r.planId))`L108-109在内存过滤教师课案。注释 L101 自述'简化:仅按日期范围过滤'。当全校课案绑定量大时拉取大量无关数据。",
"recommendation": "将 planId 过滤下推到 SQL`inArray(lessonPlanSchedules.planId, teacherPlanIds)`,配合日期范围条件,避免拉取无关行。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-018",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L182-L205",
"ruleId": "F-01",
"severity": "P1",
"dimension": "performance",
"title": "getResponsesByStudentIdRaw 拉全量作答后内存 filter",
"description": "getResponsesByStudentIdRaw 当传入 planId 时L187-198先查该 plan 的 formative items IDL188-191再 SELECT 该学生的全部 responsesL194-197 无 itemId 过滤),最后内存 filter `itemIds.includes(r.itemId)`L198。应直接用 inArray 在 SQL 过滤。",
"recommendation": "在 L196 的 WHERE 中增加 `inArray(lessonPlanFormativeResponses.itemId, itemIds)` 条件,移除内存 filter。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-019",
"file": "src/modules/textbooks/actions.ts",
"lines": "L396-L398",
"ruleId": "F-08",
"severity": "P1",
"dimension": "performance",
"title": "getKnowledgeGraphDataAction 循环调用 getGradeNameByIdN+1",
"description": "getKnowledgeGraphDataAction 在 L396-398 用 `Promise.all(allowedGradeIds.map((gid) => getGradeNameById(gid)))` 逐个查询年级名称。虽然 Promise.all 并行了请求,但仍是 N 次 DB 查询。",
"recommendation": "school 模块应提供批量接口 `getGradeNamesByIds(gradeIds): Promise<Map<string,string>>`,单次 inArray 查询返回映射。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-020",
"file": "src/modules/questions/data-access.ts",
"lines": "L39-L49",
"ruleId": "P-04",
"severity": "P1",
"dimension": "pattern",
"title": "getQuestionsRaw 缺少显式返回类型标注",
"description": "getQuestionsRawL39使用 `=> {` 箭头函数,未显式标注返回类型 `Promise<T>`,依赖 TypeScript 推断。违反 P-04 规则'函数返回值必须显式标注,特别是 Promise<T>'。",
"recommendation": "定义返回类型并显式标注:`export const getQuestionsRaw = async (params: GetQuestionsParams = {}): Promise<QuestionsListResult> => { ... }`,将返回结构提取为命名类型。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-021",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L574-L590, L416-L426, L429-L444, L593-L617",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "4 个读函数未走 cacheFn 包装",
"description": "getLessonPlanStatsL574、getTextbooksForPickerL416、getChaptersForPickerL429、getTemplateByIdL593均为纯读函数但未用 cacheFn 包装。其中 getTemplateById 在 createLessonPlan 热路径中被调用L366缺少缓存影响创建性能。",
"recommendation": "为每个读函数添加 Raw + cacheFn 配对:`export const getTemplateById = cacheFn(getTemplateByIdRaw, { tags: [...], ttl: 300, keyParts: [...] })`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-022",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L125-L141",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "canTeacherAccessPlan 读函数未走 cacheFn",
"description": "canTeacherAccessPlanL125是读函数查询 plan + 查询 substitutes但未用 cacheFn 包装。该函数可能在权限校验热路径被频繁调用。",
"recommendation": "拆为 canTeacherAccessPlanRaw + cacheFn 包装,注意 TTL 应较短60s因权限相关。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-023",
"file": "src/modules/questions/data-access.ts",
"lines": "L380-L384, L391-L399, L406-L426, L433-L436, L577-L622",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "5 个读函数未走 cacheFn 包装",
"description": "getKnowledgePointOptionsL380、getTextbookOptionsL391、getChapterOptionsL406、getKnowledgePointOptionsByChapterL433、exportQuestionsL577均为读函数但未用 cacheFn。前四个是级联筛选下拉数据频繁调用。",
"recommendation": "为 getKnowledgePointOptions/getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter 添加 cacheFnttl 可较长 600s。exportQuestions 因可能导出大结果集,可不缓存或短 TTL。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-024",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L492-L504, L511-L524, L689-L703",
"ruleId": "P-03",
"severity": "P2",
"dimension": "pattern",
"title": "3 个读函数未走 cacheFn 包装",
"description": "verifyChapterBelongsToTextbookL492、verifyKnowledgePointBelongsToTextbookL511、getPrerequisiteEdgesForTextbookL689均为读函数但未用 cacheFn。verify* 函数在 actions 层归属校验热路径中被频繁调用actions.ts 中多处调用)。",
"recommendation": "添加 cacheFn 包装TTL 较短60-120s。getPrerequisiteEdgesForTextbook 用于循环检测,可缓存 300s。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-025",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L29-L34",
"ruleId": "P-07",
"severity": "P2",
"dimension": "pattern",
"title": "toDateStr 本地实现日期序列化,未用 shared helper",
"description": "toDateStrL29-34手动拼接 YYYY-MM-DD 字符串,未使用项目统一的 serializeDate/toISODateString helper。其他模块如 data-access.ts 的 mapRowToLessonPlan使用 `.toISOString()` 序列化。",
"recommendation": "统一使用 shared/lib 中的日期序列化 helper或将 toDateStr 提取到 shared/lib/date-utils.ts 供所有模块复用。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-026",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L16-L18",
"ruleId": "S-03",
"severity": "P2",
"dimension": "structure",
"title": "isStringArray 与 lib/type-guards 重复实现",
"description": "data-access-knowledge.ts 在 L16-18 本地定义 isStringArray而 data-access-ai-evaluation.ts L14 已从 './lib/type-guards' 导入同名函数。同一模块内重复实现 helper。",
"recommendation": "删除 data-access-knowledge.ts L16-18 的本地实现,改为 `import { isStringArray } from './lib/type-guards'`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-027",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L16-L23",
"ruleId": "S-03",
"severity": "P2",
"dimension": "structure",
"title": "toLessonPlanStatus/toReviewDecision 在多个文件重复定义",
"description": "data-access-review.tsL16-18和 data-access-calendar.tsL16-18各自定义了 toLessonPlanStatus 函数逻辑完全相同isLessonPlanStatus 守卫失败回退 'draft'。toReviewDecisionL21-23也仅在本文件定义但可共享。",
"recommendation": "将 toLessonPlanStatus 提取到 lib/type-guards.ts 或 lib/serialize.ts两个 data-access 文件统一导入。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-028",
"file": "src/modules/lesson-preparation/data-access-ai-evaluation.ts",
"lines": "L141-L192",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "evaluateDocument 业务逻辑(评分算法)放在 data-access 层",
"description": "evaluateDocumentL141-192是纯业务逻辑函数基于规则计算 5 维度评分 + 生成建议),不涉及任何 DB 操作,却导出在 data-access 文件中。违反 A-02'data-access 不含业务逻辑'规则。",
"recommendation": "将 evaluateDocument 移至 lib/ai-evaluation.ts纯函数模块data-access-ai-evaluation.ts 仅保留 DB CRUD。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-029",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L40-L45, L50-L76, L82-L137, L225-L250",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "状态机逻辑isValidTransition + 状态迁移)放在 data-access 层",
"description": "isValidTransitionL40-45是状态机校验纯函数submitForReviewL50-76、reviewPlanL82-137、withdrawSubmissionL225-250内部包含状态迁移判断逻辑L66-68、L100-107、L240-242属于业务编排而非纯数据访问。",
"recommendation": "将 isValidTransition 和状态迁移判断逻辑移至 actions-review.ts 或 lib/status-machine.tsdata-access 仅暴露 updateStatus(planId, newStatus) 和 insertReviewRecord() 等纯数据操作。",
"effort": "M (≤2h)"
},
{
"id": "G1-030",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L426-L458",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "reorderChapters 重排序业务逻辑放在 data-access 层",
"description": "reorderChaptersL426-458包含排序算法splice 插入 L442、parentId 变更判断L447等业务逻辑且在事务内循环更新。这些编排逻辑应属于 actions 层。",
"recommendation": "将排序算法和变更判断移至 actions.tsdata-access 仅暴露 updateChapterOrder(tx, id, order, parentId) 单条更新接口,由 actions 在事务内调用。",
"effort": "M (≤2h)"
},
{
"id": "G1-031",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L10-L15",
"ruleId": "A-06",
"severity": "P2",
"dimension": "architecture",
"title": "导入 textbooks/chapters 表(属 textbooks 模块)用于 JOIN",
"description": "data-access.ts L10-15 从 @/shared/db/schema 导入 textbooks、chapters 表(属 textbooks 模块)用于 L271-272 的 LEFT JOIN。虽然 L27 也通过 textbooks data-access 导入查询函数,但 JOIN 仍直接引用对方表。",
"recommendation": "短期:保留 JOIN 引用但添加注释说明;长期:重构为两步查询(先查 lessonPlans再用 ID 批量查 textbooks/chapters 名称),彻底消除跨模块 schema 引用。",
"effort": "L (≤1d)"
},
{
"id": "G1-032",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L9",
"ruleId": "A-06",
"severity": "P2",
"dimension": "architecture",
"title": "导入 classes 表(属 classes 模块)用于 JOIN",
"description": "data-access-schedules.ts L9 从 @/shared/db/schema 导入 classes 表(属 classes 模块),在 L55、L98、L164 的 LEFT JOIN 中获取 className。应通过 classes 模块 data-access 获取。",
"recommendation": "改为两步1) 查 lessonPlanSchedules不含 JOIN2) 收集 classId 后调用 classes 模块的 getClassNamesByIds(classIds) 批量获取名称,内存 Map 关联。",
"effort": "M (≤2h)"
},
{
"id": "G1-033",
"file": "src/modules/questions/data-access.ts",
"lines": "L4",
"ruleId": "A-06",
"severity": "P2",
"dimension": "architecture",
"title": "导入 knowledgePoints 表(属 textbooks 模块)用于 JOIN",
"description": "data-access.ts L4 从 @/shared/db/schema 导入 knowledgePoints 表(属 textbooks 模块),在 L463 的 INNER JOIN 中获取知识点名称。虽然 L8-14 已通过 textbooks data-access 导入查询函数,此处 JOIN 仍直接引用对方表。",
"recommendation": "getKnowledgePointsForQueries 改为两步1) 查 questionsToKnowledgePoints本模块表获取 questionId→knowledgePointId 映射2) 调用 textbooks data-access 批量获取知识点名称,内存关联。",
"effort": "M (≤2h)"
},
{
"id": "G1-034",
"file": "src/modules/lesson-preparation/data-access-versions.ts",
"lines": "L35-L55, L59-L95, L97-L126, L128-L165, L167-L205",
"ruleId": "S-06",
"severity": "P2",
"dimension": "structure",
"title": "5 个公共导出函数缺少 JSDoc 注释",
"description": "getLessonPlansRawL35、createLessonPlanVersionL59、getVersionContentRawL97、revertToVersionL128、pruneAutoVersionsL167均无 JSDoc。仅 L132/L140 有内联注释。公共导出函数应补齐 JSDoc 说明用途、参数、返回值。",
"recommendation": "为每个导出函数添加 JSDoc如 `/** 创建课案版本,在事务内 max(versionNo)+1 防止并发重复 */`。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-035",
"file": "src/modules/lesson-preparation/data-access-templates.ts",
"lines": "L45-L72, L76-L112, L114-L125",
"ruleId": "S-06",
"severity": "P2",
"dimension": "structure",
"title": "3 个公共导出函数缺少 JSDoc 注释",
"description": "getLessonPlansRawL45、saveAsTemplateL76、deletePersonalTemplateL114均无 JSDoc。saveAsTemplate 的 sourcePlanId→skeleton 提取逻辑L94-100需要文档说明。",
"recommendation": "添加 JSDoc 说明函数用途、参数含义、返回值。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-036",
"file": "src/modules/questions/data-access.ts",
"lines": "L39-L193, L214-L247, L258-L292",
"ruleId": "S-06",
"severity": "P2",
"dimension": "structure",
"title": "核心函数 getQuestionsRaw/insertQuestionWithRelations/updateQuestionById 缺少 JSDoc",
"description": "getQuestionsRawL39、insertQuestionWithRelationsL214、updateQuestionByIdL258等核心函数无 JSDoc。getQuestionsRaw 的级联筛选逻辑L75-122较复杂需要文档说明筛选优先级。",
"recommendation": "为这些函数添加 JSDoc特别是 getQuestionsRaw 的 knowledgePointId > chapterId > textbookId 级联筛选优先级。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-037",
"file": "src/modules/questions/data-access.ts",
"lines": "L1-L662",
"ruleId": "S-02",
"severity": "P2",
"dimension": "structure",
"title": "单文件导出函数数约 28 个,超过 20 警告阈值",
"description": "data-access.ts 导出约 28 个符号(含类型、函数、接口),包括 getQuestions/getQuestionsDashboardStats/createQuestionWithRelations/updateQuestionById/deleteQuestionByIdRecursive/deleteQuestionsBatch/getKnowledgePointOptions/getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter/getKnowledgePointsForQuestions/getQuestionsContentForErrorCollection/getQuestionTypeMapByIds/exportQuestions/importQuestions 等。职责混合了 CRUD + 跨模块接口 + 导入导出。",
"recommendation": "按职责拆分为 data-access.ts核心 CRUD、data-access-cross-module.ts跨模块只读接口、data-access-import-export.ts导入导出。",
"effort": "L (≤1d)"
},
{
"id": "G1-038",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L1-L703",
"ruleId": "S-02",
"severity": "P2",
"dimension": "structure",
"title": "单文件导出函数数约 35 个,超过 20 警告阈值",
"description": "data-access.ts 导出约 35 个符号,涵盖教材 CRUD、章节 CRUD、知识点 CRUD、排序、统计、归属校验、scope 查询、跨模块接口、前置依赖 CRUD。职责过重。",
"recommendation": "拆分为 data-access.ts教材+章节、data-access-knowledge-points.ts知识点+前置依赖、data-access-cross-module.ts跨模块只读接口。",
"effort": "L (≤1d)"
},
{
"id": "G1-039",
"file": "src/modules/lesson-preparation/data-access-ai-evaluation.ts",
"lines": "L58, L77",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getEvaluationsByPlanIdRaw / getLatestEvaluationRaw",
"description": "getEvaluationsByPlanIdRawL58和 getLatestEvaluationRawL77使用 `.select()` 无参数SELECT 所有列。表字段可能后续增加,且传输不需要的列浪费带宽。",
"recommendation": "改为显式列枚举 `.select({ id: ..., planId: ..., ... })`,仅查询 mapRowToEvaluation 实际使用的字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-040",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L63, L210",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getTeacherInvestmentRaw / upsertDailyAnalytics",
"description": "getTeacherInvestmentRawL63和 upsertDailyAnalytics 内的查询L210使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-041",
"file": "src/modules/lesson-preparation/data-access-review.ts",
"lines": "L146",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getReviewRecordsByPlanIdRaw",
"description": "getReviewRecordsByPlanIdRawL146使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-042",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L34, L52",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getSubstitutesByPlanIdRaw / getActiveSubstitutesByTeacherIdRaw",
"description": "两个读函数 L34、L52 均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-043",
"file": "src/modules/lesson-preparation/data-access-versions.ts",
"lines": "L50",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getLessonPlanVersionsRaw",
"description": "getLessonPlanVersionsRawL50使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-044",
"file": "src/modules/lesson-preparation/data-access-templates.ts",
"lines": "L61",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getLessonPlansRaw",
"description": "getLessonPlansRawL61使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-045",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L93, L123",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getLessonPlansByKnowledgePointRaw / getLessonPlansByQuestionRaw",
"description": "两个函数 L93、L123 均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段,仅查询 mapRowToListItemWithoutJoin 实际使用的列。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-046",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L51, L68, L169, L195, L201, L216",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "6 处 SELECT * 未指定列",
"description": "getFormativeItemsByPlanIdRawL51、getFormativeItemByIdRawL68、getResponsesByItemIdRawL169、getResponsesByStudentIdRawL195、L201、getFormativeItemStatsRawL216均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。getFormativeItemStatsRaw 尤其应仅查聚合字段。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-047",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L33, L51",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列getCommentsByPlanIdRaw / getCommentsByBlockIdRaw",
"description": "两个读函数 L33、L51 均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-048",
"file": "src/modules/lesson-preparation/data-access-attachments.ts",
"lines": "L31, L49, L123",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "3 处 SELECT * 未指定列",
"description": "getAttachmentsByPlanIdRawL31、getAttachmentsByBlockIdRawL49、getAttachmentByIdRawL123均使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-049",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L427, L431",
"ruleId": "F-03",
"severity": "P2",
"dimension": "performance",
"title": "SELECT * 未指定列reorderChapters 内查询)",
"description": "reorderChapters 中 L427 `db.select().from(chapters)` 和 L431 `db.select().from(chapters)` 使用 `.select()` 无参数。",
"recommendation": "显式枚举所需字段id, textbookId, parentId, order, title。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-050",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L43-L91, L627-L660",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getTextbooksRaw / getKnowledgePointOptionsRaw 无 LIMIT",
"description": "getTextbooksRawL64-79和 getKnowledgePointOptionsRawL628-648无 LIMIT。getKnowledgePointOptionsRaw 拉取全量知识点+章节+教材 JOIN数据量大时风险高。",
"recommendation": "getTextbooksRaw 添加分页或 `.limit(200)`getKnowledgePointOptionsRaw 应改为按 textbookId/subject 参数过滤,或前端懒加载。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-051",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L165-L175, L182-L205",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getResponsesByItemIdRaw / getResponsesByStudentIdRaw 无 LIMIT",
"description": "两个函数查询学生作答记录均无 LIMIT。一个互动组件可能有上千条作答一个学生可能有大量作答历史。",
"recommendation": "添加分页参数或 `.limit(500)` 上限保护。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-052",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L29-L39, L46-L62",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getCommentsByPlanIdRaw / getCommentsByBlockIdRaw 无 LIMIT",
"description": "两个函数查询评论均无 LIMIT。热门课案评论数可能很多。",
"recommendation": "添加分页参数或 `.limit(200)` 上限保护。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-053",
"file": "src/modules/lesson-preparation/data-access-knowledge.ts",
"lines": "L92-L101, L122-L131",
"ruleId": "F-05",
"severity": "P2",
"dimension": "performance",
"title": "getLessonPlansByKnowledgePointRaw / getLessonPlansByQuestionRaw 无 LIMIT",
"description": "两个函数对 lessonPlans 全表 LIKE 扫描后无 LIMIT且无分页。匹配数量不可控。",
"recommendation": "添加 `.limit(100)` 上限保护,或改为分页查询。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-054",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L465-L474",
"ruleId": "F-10",
"severity": "P2",
"dimension": "performance",
"title": "getTextbooksDashboardStatsRaw 全表 COUNT 无过滤",
"description": "getTextbooksDashboardStatsRawL465-474对 textbooks 和 chapters 表各执行 `count()` 无 WHERE 过滤,统计全量数据。仪表盘统计应至少按可见范围过滤。",
"recommendation": "如需按权限范围统计,传入 scope 参数添加 WHERE 条件;若确为管理员全局统计,可保留但加缓存(已有 cacheFn。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-055",
"file": "src/modules/questions/data-access.ts",
"lines": "L204-L207",
"ruleId": "F-10",
"severity": "P2",
"dimension": "performance",
"title": "getQuestionsDashboardStatsRaw 全表 COUNT 无过滤",
"description": "getQuestionsDashboardStatsRawL204-207对 questions 表执行 `count()` 无 WHERE 过滤。仪表盘应按用户可见范围统计。",
"recommendation": "传入 scope/authorId 参数添加 WHERE 条件,或确认是否为管理员全局统计。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-056",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L574-L590",
"ruleId": "F-10",
"severity": "P2",
"dimension": "performance",
"title": "getLessonPlanStats 全表 GROUP BY 无过滤",
"description": "getLessonPlanStatsL574-590对 lessonPlans 全表 GROUP BY status 统计,无 WHERE 过滤。管理员看板统计应限定范围(如本学期/本学年)。",
"recommendation": "添加时间范围 WHERE 条件(如 createdAt >= 学期开始日期),避免统计历史归档数据。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-057",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L125-L141",
"ruleId": "A-02",
"severity": "P3",
"dimension": "architecture",
"title": "canTeacherAccessPlan 含权限判断业务逻辑",
"description": "canTeacherAccessPlanL125-141包含'原教师→true / 代课教师→true'的权限判断逻辑,属于业务编排。虽然查询了 DB但'是否可访问'的判断应属于 actions 或权限层。",
"recommendation": "将 canTeacherAccessPlan 的判断逻辑移至 actions 层data-access 仅暴露 getPlanCreatorId 和 getActiveSubstitutesByTeacherId 两个纯读接口。",
"effort": "S (≤30 分钟)"
},
{
"id": "G1-058",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L201-L249",
"ruleId": "A-02",
"severity": "P3",
"dimension": "architecture",
"title": "upsertDailyAnalytics 含 read-then-write 业务逻辑",
"description": "upsertDailyAnalyticsL201-249先 SELECT 判断是否存在L209-218存在则 UPDATE 累加L222-234不存在则 INSERTL236-247。该 upsert 编排逻辑可下放到 actions 或用 SQL `INSERT ... ON DUPLICATE KEY UPDATE` 替代。",
"recommendation": "改用 MySQL `INSERT ... ON DUPLICATE KEY UPDATE` 单语句完成 upsert或在 actions 层编排 read-then-write。",
"effort": "M (≤2h)"
},
{
"id": "G1-059",
"file": "src/modules/lesson-preparation/data-access.ts",
"lines": "L336, L535, L612",
"ruleId": "F-03",
"severity": "P3",
"dimension": "performance",
"title": "3 处 SELECT * 未指定列",
"description": "getLessonPlanByIdRawL336、duplicateLessonPlanL535、getTemplateByIdL612使用 `.select()` 无参数。其中 getLessonPlanByIdRaw 查询后用 mapRowToLessonPlan 映射,所需字段已知。",
"recommendation": "显式枚举 mapRowToLessonPlan 所需的 14 个字段。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-060",
"file": "src/modules/lesson-preparation/data-access-substitutes.ts",
"lines": "L136",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "plan[0]!.creatorId 非空断言",
"description": "L136 `if (plan[0]!.creatorId === teacherId) return true;` 在已检查 `plan.length === 0`L135后使用 `!` 非空断言。虽逻辑正确,但可改为更安全的 `const row = plan[0]; if (row && row.creatorId === teacherId) ...`。",
"recommendation": "用 `const row = plan[0]; if (!row) return false; if (row.creatorId === teacherId) return true;` 替代非空断言。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-061",
"file": "src/modules/lesson-preparation/data-access-calendar.ts",
"lines": "L181",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "split('T')[0]! 非空断言",
"description": "L181 `e.occurredAt.toISOString().split('T')[0]!` 对数组取值使用 `!`。虽然 toISOString() 必定含 'T',但 `!` 属非空断言。",
"recommendation": "改为 `e.occurredAt.toISOString().split('T')[0] ?? ''` 或用专门的 toISODateString helper。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-062",
"file": "src/modules/lesson-preparation/data-access-formative.ts",
"lines": "L72",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "rows[0]! 非空断言",
"description": "L72 `return rows.length === 0 ? null : mapRowToItem(rows[0]!);` 使用 `!`。虽逻辑正确,但可避免。",
"recommendation": "改为 `const row = rows[0]; return row ? mapRowToItem(row) : null;`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-063",
"file": "src/modules/lesson-preparation/data-access-comments.ts",
"lines": "L118",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "rows[0]!.resolved 非空断言",
"description": "L118 `const newResolved = !rows[0]!.resolved;` 使用 `!`。已检查 `rows.length === 0`L117但风格上可改进。",
"recommendation": "改为 `const row = rows[0]; if (!row) return; const newResolved = !row.resolved;`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-064",
"file": "src/modules/lesson-preparation/data-access-schedules.ts",
"lines": "L167",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "created[0]! 非空断言",
"description": "L167 `const r = created[0]!;` 在 createSchedule 中查询刚插入的记录后使用 `!`。INSERT 后立即查询,理论上必定有值,但 `!` 不够安全。",
"recommendation": "改为 `const r = created[0]; if (!r) throw new Error('SCHEDULE_CREATE_FAILED');`。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-065",
"file": "src/modules/lesson-preparation/data-access-analytics.ts",
"lines": "L98",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "r.templateId! 非空断言",
"description": "L98 `templateId: r.templateId!,` 在 WHERE 已过滤 `templateId IS NOT NULL`L93后使用 `!`。",
"recommendation": "改为 `templateId: r.templateId ?? ''`,或用类型守卫收窄。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-066",
"file": "src/modules/questions/data-access.ts",
"lines": "L1-L662",
"ruleId": "S-06",
"severity": "P3",
"dimension": "structure",
"title": "deleteQuestionRecursive/insertQuestionWithRelations 缺少 JSDoc",
"description": "deleteQuestionRecursiveL294、insertQuestionWithRelationsL214等内部函数无 JSDoc。环检测逻辑L299-303需要文档说明。",
"recommendation": "补充 JSDoc 说明环检测目的和 visited Set 的作用。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G1-067",
"file": "src/modules/textbooks/data-access.ts",
"lines": "L170-L206, L208-L210, L212-L240, L242-L273, L275-L333",
"ruleId": "S-06",
"severity": "P3",
"dimension": "structure",
"title": "createTextbook/updateTextbook/deleteTextbook/createChapter 等多个函数缺少 JSDoc",
"description": "createTextbookL170、updateTextbookL192、deleteTextbookL208、createChapterL212、updateChapterContentL242、deleteChapterL275、createKnowledgePointL398、updateKnowledgePointL411、deleteKnowledgePointL422、reorderChaptersL426均无 JSDoc。deleteChapter 的级联删除逻辑L310-332较复杂需要文档。",
"recommendation": "为这些函数添加 JSDoc特别是 deleteChapter 需说明级联删除知识点+前置依赖的行为。",
"effort": "S (≤30 分钟)"
}
]