docs(architecture): update impact map, data, audit reports, superpowers docs
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

- 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
This commit is contained in:
SpecialX
2026-07-07 16:23:35 +08:00
parent 7387d70289
commit 5d9981fd7d
15 changed files with 10598 additions and 78 deletions

View File

@@ -0,0 +1,806 @@
[
{
"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 分钟)"
}
]