- 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
807 lines
44 KiB
JSON
807 lines
44 KiB
JSON
[
|
||
{
|
||
"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 查 questionsToKnowledgePoints,L107-121 查 knowledgePointMastery)。这违反了三层架构'模块间通过对方 data-access 通信,不直接查询对方 DB 表'的规则。",
|
||
"recommendation": "1) questionsToKnowledgePoints 的关联题目数查询应改为调用 questions 模块 data-access 暴露的跨模块接口(如 getQuestionCountByKpIds);2) 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 循环递归调用 deleteComment(L134-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 循环内逐条 UPDATE(N+1)",
|
||
"description": "reorderChapters 在事务内 for 循环遍历所有兄弟章节(L445-457),每个章节单独执行 tx.update(L448-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": "getLessonPlansByKnowledgePointRaw(L99)和 getLessonPlansByQuestionRaw(L129)对 lessonPlans.content(JSON 列)使用 `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": "getTextbooksRaw(L48-54)和 getTextbooksWithScopeRaw(L545-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 BY);2) 用 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": "getPendingReviewPlansRaw(L184-196)和 getPlansByStatusesRaw(L275-285)均无 LIMIT,且后者还在内存中做 filter(L199-208)而非 SQL 过滤。待审核/按状态查询的课案可能很多。",
|
||
"recommendation": "添加分页参数 page/pageSize,SQL 层用 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 对 lessonPlans(L43)、lessonPlanVersions(L102)、lessonPlanReviewRecords(L136)三段查询均无 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 ID(L188-191),再 SELECT 该学生的全部 responses(L194-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 循环调用 getGradeNameById(N+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": "getQuestionsRaw(L39)使用 `=> {` 箭头函数,未显式标注返回类型 `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": "getLessonPlanStats(L574)、getTextbooksForPicker(L416)、getChaptersForPicker(L429)、getTemplateById(L593)均为纯读函数但未用 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": "canTeacherAccessPlan(L125)是读函数(查询 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": "getKnowledgePointOptions(L380)、getTextbookOptions(L391)、getChapterOptions(L406)、getKnowledgePointOptionsByChapter(L433)、exportQuestions(L577)均为读函数但未用 cacheFn。前四个是级联筛选下拉数据,频繁调用。",
|
||
"recommendation": "为 getKnowledgePointOptions/getTextbookOptions/getChapterOptions/getKnowledgePointOptionsByChapter 添加 cacheFn(ttl 可较长 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": "verifyChapterBelongsToTextbook(L492)、verifyKnowledgePointBelongsToTextbook(L511)、getPrerequisiteEdgesForTextbook(L689)均为读函数但未用 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": "toDateStr(L29-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.ts(L16-18)和 data-access-calendar.ts(L16-18)各自定义了 toLessonPlanStatus 函数,逻辑完全相同(isLessonPlanStatus 守卫失败回退 'draft')。toReviewDecision(L21-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": "evaluateDocument(L141-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": "isValidTransition(L40-45)是状态机校验纯函数;submitForReview(L50-76)、reviewPlan(L82-137)、withdrawSubmission(L225-250)内部包含状态迁移判断逻辑(L66-68、L100-107、L240-242),属于业务编排而非纯数据访问。",
|
||
"recommendation": "将 isValidTransition 和状态迁移判断逻辑移至 actions-review.ts 或 lib/status-machine.ts;data-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": "reorderChapters(L426-458)包含排序算法(splice 插入 L442)、parentId 变更判断(L447)等业务逻辑,且在事务内循环更新。这些编排逻辑应属于 actions 层。",
|
||
"recommendation": "将排序算法和变更判断移至 actions.ts,data-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(不含 JOIN);2) 收集 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": "getLessonPlansRaw(L35)、createLessonPlanVersion(L59)、getVersionContentRaw(L97)、revertToVersion(L128)、pruneAutoVersions(L167)均无 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": "getLessonPlansRaw(L45)、saveAsTemplate(L76)、deletePersonalTemplate(L114)均无 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": "getQuestionsRaw(L39)、insertQuestionWithRelations(L214)、updateQuestionById(L258)等核心函数无 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": "getEvaluationsByPlanIdRaw(L58)和 getLatestEvaluationRaw(L77)使用 `.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": "getTeacherInvestmentRaw(L63)和 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": "getReviewRecordsByPlanIdRaw(L146)使用 `.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": "getLessonPlanVersionsRaw(L50)使用 `.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": "getLessonPlansRaw(L61)使用 `.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": "getFormativeItemsByPlanIdRaw(L51)、getFormativeItemByIdRaw(L68)、getResponsesByItemIdRaw(L169)、getResponsesByStudentIdRaw(L195、L201)、getFormativeItemStatsRaw(L216)均使用 `.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": "getAttachmentsByPlanIdRaw(L31)、getAttachmentsByBlockIdRaw(L49)、getAttachmentByIdRaw(L123)均使用 `.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": "getTextbooksRaw(L64-79)和 getKnowledgePointOptionsRaw(L628-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": "getTextbooksDashboardStatsRaw(L465-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": "getQuestionsDashboardStatsRaw(L204-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": "getLessonPlanStats(L574-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": "canTeacherAccessPlan(L125-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": "upsertDailyAnalytics(L201-249)先 SELECT 判断是否存在(L209-218),存在则 UPDATE 累加(L222-234),不存在则 INSERT(L236-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": "getLessonPlanByIdRaw(L336)、duplicateLessonPlan(L535)、getTemplateById(L612)使用 `.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": "deleteQuestionRecursive(L294)、insertQuestionWithRelations(L214)等内部函数无 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": "createTextbook(L170)、updateTextbook(L192)、deleteTextbook(L208)、createChapter(L212)、updateChapterContent(L242)、deleteChapter(L275)、createKnowledgePoint(L398)、updateKnowledgePoint(L411)、deleteKnowledgePoint(L422)、reorderChapters(L426)均无 JSDoc。deleteChapter 的级联删除逻辑(L310-332)较复杂,需要文档。",
|
||
"recommendation": "为这些函数添加 JSDoc,特别是 deleteChapter 需说明级联删除知识点+前置依赖的行为。",
|
||
"effort": "S (≤30 分钟)"
|
||
}
|
||
]
|