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:
@@ -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),
|
||||
}));
|
||||
|
||||
@@ -34,10 +34,16 @@
|
||||
"graph": "Graph"
|
||||
},
|
||||
"selectChapter": "Please select a chapter to start reading.",
|
||||
"selectChapterDesc": "Choose a chapter from the sidebar to view its content.",
|
||||
"selectChapterKnowledge": "Please select a chapter to view knowledge points.",
|
||||
"selectChapterKnowledgeDesc": "Choose a chapter from the sidebar to see its knowledge points.",
|
||||
"selectChapterGraph": "Please select a chapter to view the knowledge graph.",
|
||||
"selectChapterGraphDesc": "Choose a chapter from the sidebar to see its knowledge graph.",
|
||||
"emptyKnowledge": "No knowledge points in this chapter yet.",
|
||||
"emptyKnowledgeDesc": "Select text while reading to create a knowledge point.",
|
||||
"emptyContent": "No content yet",
|
||||
"emptyContentDesc": "Click \"Edit Content\" to start writing this chapter.",
|
||||
"loadingKnowledge": "Loading knowledge points...",
|
||||
"editContent": "Edit Content",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
@@ -45,7 +51,9 @@
|
||||
"addKnowledgePoint": "Add Knowledge Point",
|
||||
"clickToViewKp": "Click to view knowledge point details",
|
||||
"noChapters": "No chapters",
|
||||
"noChaptersDesc": "This textbook has no chapters yet."
|
||||
"noChaptersDesc": "This textbook has no chapters yet.",
|
||||
"sidebar": "Chapters & Knowledge",
|
||||
"openSidebar": "Open Sidebar"
|
||||
},
|
||||
"dialog": {
|
||||
"create": {
|
||||
@@ -75,7 +83,11 @@
|
||||
"deleting": "Deleting...",
|
||||
"cannotDeleteWithSubchapters": "Cannot delete chapter with subchapters",
|
||||
"addSubchapter": "Add Subchapter",
|
||||
"titlePlaceholder": "e.g. Chapter 1: Introduction"
|
||||
"titlePlaceholder": "e.g. Chapter 1: Introduction",
|
||||
"toggle": "Toggle",
|
||||
"orderUpdated": "Order updated",
|
||||
"cancel": "Cancel",
|
||||
"dragHandle": "Drag to reorder"
|
||||
},
|
||||
"knowledge": {
|
||||
"createTitle": "Add Knowledge Point",
|
||||
@@ -133,6 +145,7 @@
|
||||
"level": "Lv."
|
||||
},
|
||||
"subject": {
|
||||
"chinese": "Chinese",
|
||||
"mathematics": "Mathematics",
|
||||
"physics": "Physics",
|
||||
"chemistry": "Chemistry",
|
||||
@@ -142,6 +155,8 @@
|
||||
"geography": "Geography"
|
||||
},
|
||||
"grade": {
|
||||
"grade1": "Grade 1",
|
||||
"grade2": "Grade 2",
|
||||
"grade7": "Grade 7",
|
||||
"grade8": "Grade 8",
|
||||
"grade9": "Grade 9",
|
||||
@@ -160,7 +175,7 @@
|
||||
"updateSuccess": "Textbook updated successfully.",
|
||||
"updateFailed": "Failed to update textbook.",
|
||||
"deleteSuccess": "Textbook deleted successfully.",
|
||||
"deleteFailed": "Failed to delete textbook.",
|
||||
"textbookDeleteFailed": "Failed to delete textbook.",
|
||||
"chapterCreateSuccess": "Chapter created successfully",
|
||||
"chapterCreateFailed": "Failed to create chapter",
|
||||
"chapterDeleteSuccess": "Chapter deleted successfully",
|
||||
@@ -181,6 +196,57 @@
|
||||
"invalidContent": "Invalid chapter content data",
|
||||
"errorOccurred": "An error occurred",
|
||||
"deleteFailed": "Deletion failed",
|
||||
"updateFailedGeneric": "Update failed"
|
||||
"updateFailedGeneric": "Update failed",
|
||||
"chapterNotBelong": "Chapter does not belong to this textbook",
|
||||
"kpNotBelong": "Knowledge point does not belong to this textbook",
|
||||
"chaptersReordered": "Chapters reordered successfully",
|
||||
"ok": "OK",
|
||||
"kpLoadFailed": "Failed to load knowledge points",
|
||||
"graphLoadFailed": "Graph failed to load",
|
||||
"invalidInput": "Invalid input",
|
||||
"cyclicDependency": "Cannot add cyclic dependency",
|
||||
"prerequisiteCreated": "Prerequisite added",
|
||||
"prerequisiteCreateFailed": "Failed to add prerequisite",
|
||||
"prerequisiteDeleted": "Prerequisite removed",
|
||||
"prerequisiteDeleteFailed": "Failed to remove prerequisite"
|
||||
},
|
||||
"graph": {
|
||||
"viewMode": {
|
||||
"structure": "Structure",
|
||||
"studentMastery": "My Mastery",
|
||||
"classMastery": "Class Mastery"
|
||||
},
|
||||
"node": {
|
||||
"questions": "Questions",
|
||||
"mastery": "Mastery",
|
||||
"prerequisite": "Prerequisite",
|
||||
"successor": "Successor"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Knowledge Point Details",
|
||||
"noDescription": "No description",
|
||||
"viewAllQuestions": "View all questions",
|
||||
"editPrerequisite": "Edit prerequisites",
|
||||
"addPrerequisite": "Add prerequisite",
|
||||
"removePrerequisite": "Remove",
|
||||
"noPrerequisites": "No prerequisite knowledge points",
|
||||
"noSuccessors": "No successor knowledge points",
|
||||
"masteryNotAssessed": "Not assessed",
|
||||
"correctRate": "Correct rate",
|
||||
"totalQuestions": "Total questions"
|
||||
},
|
||||
"toolbar": {
|
||||
"search": "Search knowledge points",
|
||||
"filterByChapter": "Filter by chapter",
|
||||
"resetView": "Reset view"
|
||||
},
|
||||
"empty": {
|
||||
"noPrerequisites": "No prerequisite relationships",
|
||||
"noData": "No graph data"
|
||||
},
|
||||
"error": {
|
||||
"cyclicDependency": "Cannot add cyclic dependency",
|
||||
"loadFailed": "Graph failed to load"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,16 @@
|
||||
"graph": "图谱"
|
||||
},
|
||||
"selectChapter": "请选择一个章节开始阅读。",
|
||||
"selectChapterDesc": "从左侧目录中选择一个章节以查看内容。",
|
||||
"selectChapterKnowledge": "请选择一个章节查看知识点。",
|
||||
"selectChapterKnowledgeDesc": "从左侧目录中选择一个章节以查看关联的知识点。",
|
||||
"selectChapterGraph": "请选择一个章节查看知识图谱。",
|
||||
"selectChapterGraphDesc": "从左侧目录中选择一个章节以查看知识图谱关系。",
|
||||
"emptyKnowledge": "该章节暂无知识点。",
|
||||
"emptyKnowledgeDesc": "在阅读内容时选中文本即可创建知识点。",
|
||||
"emptyContent": "暂无内容",
|
||||
"emptyContentDesc": "点击「编辑内容」开始编写本章正文。",
|
||||
"loadingKnowledge": "正在加载知识点...",
|
||||
"editContent": "编辑内容",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
@@ -45,7 +51,9 @@
|
||||
"addKnowledgePoint": "添加知识点",
|
||||
"clickToViewKp": "点击查看知识点详情",
|
||||
"noChapters": "暂无章节",
|
||||
"noChaptersDesc": "这本教材还没有章节。"
|
||||
"noChaptersDesc": "这本教材还没有章节。",
|
||||
"sidebar": "目录与知识点",
|
||||
"openSidebar": "打开目录"
|
||||
},
|
||||
"dialog": {
|
||||
"create": {
|
||||
@@ -75,7 +83,11 @@
|
||||
"deleting": "删除中...",
|
||||
"cannotDeleteWithSubchapters": "无法删除含有子章节的章节",
|
||||
"addSubchapter": "添加子章节",
|
||||
"titlePlaceholder": "例如:第一章:入门"
|
||||
"titlePlaceholder": "例如:第一章:入门",
|
||||
"toggle": "展开/折叠",
|
||||
"orderUpdated": "顺序已更新",
|
||||
"cancel": "取消",
|
||||
"dragHandle": "拖拽排序"
|
||||
},
|
||||
"knowledge": {
|
||||
"createTitle": "添加知识点",
|
||||
@@ -133,6 +145,7 @@
|
||||
"level": "等级"
|
||||
},
|
||||
"subject": {
|
||||
"chinese": "语文",
|
||||
"mathematics": "数学",
|
||||
"physics": "物理",
|
||||
"chemistry": "化学",
|
||||
@@ -142,6 +155,8 @@
|
||||
"geography": "地理"
|
||||
},
|
||||
"grade": {
|
||||
"grade1": "一年级",
|
||||
"grade2": "二年级",
|
||||
"grade7": "七年级",
|
||||
"grade8": "八年级",
|
||||
"grade9": "九年级",
|
||||
@@ -160,7 +175,7 @@
|
||||
"updateSuccess": "教材更新成功。",
|
||||
"updateFailed": "更新教材失败。",
|
||||
"deleteSuccess": "教材删除成功。",
|
||||
"deleteFailed": "删除教材失败。",
|
||||
"textbookDeleteFailed": "删除教材失败。",
|
||||
"chapterCreateSuccess": "章节创建成功",
|
||||
"chapterCreateFailed": "创建章节失败",
|
||||
"chapterDeleteSuccess": "章节删除成功",
|
||||
@@ -181,6 +196,57 @@
|
||||
"invalidContent": "章节内容数据无效",
|
||||
"errorOccurred": "发生错误",
|
||||
"deleteFailed": "删除失败",
|
||||
"updateFailedGeneric": "更新失败"
|
||||
"updateFailedGeneric": "更新失败",
|
||||
"chapterNotBelong": "章节不属于该教材",
|
||||
"kpNotBelong": "知识点不属于该教材",
|
||||
"chaptersReordered": "章节排序成功",
|
||||
"ok": "成功",
|
||||
"kpLoadFailed": "加载知识点失败",
|
||||
"graphLoadFailed": "图谱加载失败",
|
||||
"invalidInput": "输入无效",
|
||||
"cyclicDependency": "不能添加循环依赖",
|
||||
"prerequisiteCreated": "前置依赖已添加",
|
||||
"prerequisiteCreateFailed": "添加前置依赖失败",
|
||||
"prerequisiteDeleted": "前置依赖已删除",
|
||||
"prerequisiteDeleteFailed": "删除前置依赖失败"
|
||||
},
|
||||
"graph": {
|
||||
"viewMode": {
|
||||
"structure": "结构图",
|
||||
"studentMastery": "个人掌握度",
|
||||
"classMastery": "班级掌握度"
|
||||
},
|
||||
"node": {
|
||||
"questions": "题目",
|
||||
"mastery": "掌握度",
|
||||
"prerequisite": "前置",
|
||||
"successor": "后置"
|
||||
},
|
||||
"detail": {
|
||||
"title": "知识点详情",
|
||||
"noDescription": "暂无描述",
|
||||
"viewAllQuestions": "查看全部题目",
|
||||
"editPrerequisite": "编辑前置依赖",
|
||||
"addPrerequisite": "添加前置",
|
||||
"removePrerequisite": "移除",
|
||||
"noPrerequisites": "暂无前置知识点",
|
||||
"noSuccessors": "暂无后置知识点",
|
||||
"masteryNotAssessed": "未测评",
|
||||
"correctRate": "正确率",
|
||||
"totalQuestions": "总题数"
|
||||
},
|
||||
"toolbar": {
|
||||
"search": "搜索知识点",
|
||||
"filterByChapter": "按章节筛选",
|
||||
"resetView": "重置视图"
|
||||
},
|
||||
"empty": {
|
||||
"noPrerequisites": "暂无前置依赖关系",
|
||||
"noData": "暂无图谱数据"
|
||||
},
|
||||
"error": {
|
||||
"cyclicDependency": "不能添加循环依赖",
|
||||
"loadFailed": "图谱加载失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user