feat(textbooks): 知识图谱功能全面重构 — 前置依赖 + dagre 布局 + React Flow 可视化 + 师生双视角

将教材模块图谱从基本无用状态升级为完整知识图谱可视化系统。

数据层:新增 knowledgePointPrerequisites 表(复合主键+双外键 cascade);新增 data-access-graph.ts(server-only)知识点关联聚合、学生/班级掌握度查询;utils.ts 新增 hasCycleAfterAddingEdge(DFS 循环依赖检测)。

业务层:3 个新 Server Action(getKnowledgeGraphDataAction 三视图模式、createPrerequisiteAction 含循环检测、deletePrerequisiteAction);graph-layout.ts 重写为 dagre 分层有向图布局。

视图层:knowledge-graph.tsx 重写为 React Flow 主组件(全书视图+搜索高亮+关联节点高亮+章节着色);4 个新组件(graph-kp-node/graph-prerequisite-edge/graph-toolbar/graph-node-detail-panel);use-graph-data.ts 派生值模式避免 effect 中 setState。

架构:严格三层架构,客户端通过 Server Action 间接访问 server-only 数据层;权限校验+ i18n 全覆盖;架构文档 004/005 同步。

测试:utils.test.ts 新增 5 个循环检测测试,graph-layout.test.ts 重写 5 个 dagre 布局测试,全部 30 个教材模块单元测试通过。

附带提交 drizzle/0005 error-book 迁移文件以保持 journal 一致性。
This commit is contained in:
SpecialX
2026-06-23 00:13:03 +08:00
parent 15aa84b72c
commit 58656da983
28 changed files with 21377 additions and 575 deletions

View File

@@ -153,6 +153,27 @@ export const knowledgePoints = mysqlTable("knowledge_points", {
chapterIdIdx: index("kp_chapter_id_idx").on(table.chapterId),
}));
// --- 知识点前置依赖(知识图谱) ---
export const knowledgePointPrerequisites = mysqlTable("knowledge_point_prerequisites", {
knowledgePointId: varchar("knowledge_point_id", { length: 128 }).notNull(),
prerequisiteKpId: varchar("prerequisite_kp_id", { length: 128 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
kpPairPk: primaryKey({ columns: [table.knowledgePointId, table.prerequisiteKpId] }),
kpIdx: index("kp_prereq_kp_idx").on(table.knowledgePointId),
prereqIdx: index("kp_prereq_prereq_idx").on(table.prerequisiteKpId),
kpFk: foreignKey({
columns: [table.knowledgePointId],
foreignColumns: [knowledgePoints.id],
name: "kp_prereq_kp_fk",
}).onDelete("cascade"),
prereqFk: foreignKey({
columns: [table.prerequisiteKpId],
foreignColumns: [knowledgePoints.id],
name: "kp_prereq_prereq_fk",
}).onDelete("cascade"),
}));
// --- 3. Question Bank (Core) ---
export const questionTypeEnum = mysqlEnum("type", ["single_choice", "multiple_choice", "text", "judgment", "composite"]);
@@ -1337,3 +1358,85 @@ export const systemSettings = mysqlTable("system_settings", {
categoryKeyIdx: uniqueIndex("ss_category_key_idx").on(table.category, table.key),
categoryIdx: index("ss_category_idx").on(table.category),
}));
// --- 26. Error Book (错题本) ---
export const errorBookSourceTypeEnum = mysqlEnum("source_type", ["exam", "homework", "manual"]);
export const errorBookStatusEnum = mysqlEnum("error_status", ["new", "learning", "mastered", "archived"]);
export const errorBookReviewResultEnum = mysqlEnum("review_result", ["again", "hard", "good", "easy"]);
/**
* 错题本条目 - 存储学生的错题记录。
* 来源可以是考试提交、作业提交,或学生手动添加。
* 采用简化版 SM-2 间隔重复算法调度复习。
*/
export const errorBookItems = mysqlTable("error_book_items", {
id: id("id").primaryKey(),
studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
questionId: varchar("question_id", { length: 128 }).notNull().references(() => questions.id),
// 来源
sourceType: errorBookSourceTypeEnum.default("manual").notNull(),
/** exam_submission_id / homework_submission_id / null手动添加 */
sourceId: varchar("source_id", { length: 128 }),
// 作答快照(冗余存储,避免源记录被删除后丢失上下文)
studentAnswer: json("student_answer"),
correctAnswer: json("correct_answer"),
// 分类元数据(冗余,加速筛选)
subjectId: varchar("subject_id", { length: 128 }),
knowledgePointIds: json("knowledge_point_ids"),
// 状态与掌握度
status: errorBookStatusEnum.default("new").notNull(),
/** 0-5 掌握程度0=未学5=已掌握 */
masteryLevel: int("mastery_level").default(0).notNull(),
// SM-2 间隔重复参数
nextReviewAt: timestamp("next_review_at", { mode: "date" }),
/** 复习间隔(天) */
reviewInterval: int("review_interval").default(1).notNull(),
reviewCount: int("review_count").default(0).notNull(),
/** 连续答对次数(用于判断是否标记为已掌握) */
correctStreak: int("correct_streak").default(0).notNull(),
// 学生笔记与反思
note: text("note"),
/** 错误原因标签JSON 数组,如 ["粗心", "概念不清", "计算错误"] */
errorTags: json("error_tags"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
studentIdx: index("eb_item_student_idx").on(table.studentId),
studentStatusIdx: index("eb_item_student_status_idx").on(table.studentId, table.status),
studentReviewIdx: index("eb_item_student_review_idx").on(table.studentId, table.nextReviewAt),
questionIdx: index("eb_item_question_idx").on(table.questionId),
subjectIdx: index("eb_item_subject_idx").on(table.subjectId),
sourceIdx: index("eb_item_source_idx").on(table.sourceType, table.sourceId),
}));
/**
* 错题复习记录 - 每次复习的历史快照。
* 用于追踪复习进度和算法调参。
*/
export const errorBookReviews = mysqlTable("error_book_reviews", {
id: id("id").primaryKey(),
itemId: varchar("item_id", { length: 128 }).notNull().references(() => errorBookItems.id, { onDelete: "cascade" }),
studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
/** 复习自评结果again(重来) / hard(困难) / good(良好) / easy(简单) */
result: errorBookReviewResultEnum.notNull(),
reviewedAt: timestamp("reviewed_at").defaultNow().notNull(),
/** 本次复习后的新间隔(天) */
newInterval: int("new_interval"),
newMasteryLevel: int("new_mastery_level"),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
itemIdx: index("eb_review_item_idx").on(table.itemId),
studentIdx: index("eb_review_student_idx").on(table.studentId),
studentReviewedIdx: index("eb_review_student_reviewed_idx").on(table.studentId, table.reviewedAt),
}));