"""初等 / 高等数学标准知识图谱(v1)。 知识图谱作为全局地基: - 每个知识点是唯一 Knowledge 节点; - 不同教材/章节通过 ChapterKnowledge 引用节点; - 题目通过 QuestionKnowledge 引用节点; - 用户掌握度 UserKnowledge 绑定 knowledge_id; - 节点间关系(包含 / 前置 / 相关)存于 knowledge_relations, 供后续图谱展示、推荐与路径规划使用。 """ from __future__ import annotations from sqlalchemy.orm import Session from models import Knowledge from services.knowledge_service import ( ensure_knowledge_relation, ) LEGACY_KNOWLEDGE_MAP = { "直角三角形": "三角比", "同角关系": "同角三角函数关系", "弧长与扇形": "弧度制", "和角公式": "和角与差角公式", "差角公式": "和角与差角公式", "周期函数": "三角函数图像", "斜率": "直线方程", "一次函数图像": "一次函数", "图像平移": "函数图像与变换", } # 常用知识点的定义;仅在节点描述为空时写入,不覆盖管理员维护的内容。 KNOWLEDGE_DEFINITIONS = { "函数与图像": "以图像直观呈现自变量与函数值对应关系的分支:先“看见”图像,再回到公式与性质。", "函数概念": "设 x 取数集 D 中的值,若按确定的对应法则,每个 x 都有唯一确定的 y 与之对应,则称 y 是 x 的函数,记作 y=f(x),x∈D。", "函数值": "当自变量取定 x=a 时,按对应法则算出的 f(a) 就是函数在 a 处的函数值,它是图像上点 (a, f(a)) 的纵坐标。", "定义域": "使函数表达式有意义的所有自变量取值组成的集合,常见限制有分母不为零、偶次根号下非负、对数的真数为正等。", "值域": "自变量取遍定义域时,所有函数值组成的集合,即 {f(x) | x∈D}。", "零点": "使 f(x)=0 的自变量 x 的值,几何上对应函数图像与 x 轴交点的横坐标。", "正负性": "函数在区间上取正值或负值的性质,由 f(x)>0 与 f(x)<0 的解集刻画,对应图像位于 x 轴上方或下方。", "单调性": "在区间 I 上,若 x₁f(x₂)),则称函数在 I 上单调递增(或递减)。", "最值": "函数在给定区间上取得的最大值与最小值,可结合单调性、图像或配方等方法求得。", "奇偶性": "若定义域关于原点对称,且对任意 x 有 f(-x)=f(x),则为偶函数;若恒有 f(-x)=-f(x),则为奇函数。", "偶函数": "满足 f(-x)=f(x) 的函数,其图像关于 y 轴对称。", "奇函数": "满足 f(-x)=-f(x) 的函数,其图像关于原点中心对称;若 0 在定义域内,则 f(0)=0。", "图像对称性": "函数图像的轴对称与中心对称性质,与奇偶性、平移伸缩等变换密切相关。", "一次函数": "形如 y=kx+b(k≠0)的函数,图像是一条直线,k 为斜率、b 为纵截距;k>0 时单调递增。", "二次函数": "形如 y=ax²+bx+c(a≠0)的函数,图像是抛物线,可通过配方求顶点、对称轴与最值。", "抛物线": "二次函数图像的几何形状,具有对称轴与顶点,开口方向由二次项系数的正负决定。", "幂函数": "形如 y=x^α 的函数,其定义域、图像与单调性随指数 α 的不同而明显变化。", "函数图像与变换": "由基本函数的图像出发,经平移(左加右减、上加下减)、伸缩与对称得到新图像的方法。", "直线方程": "用方程表示直线的形式,如斜截式 y=kx+b、点斜式 y-y₀=k(x-x₀);两直线平行则斜率相等。", "三角函数": "以角为自变量、以三角比为函数值的函数,包括正弦、余弦、正切等,具有周期性与有界性。", "任意角": "由射线绕顶点旋转生成的角,按旋转方向分为正角、负角与零角,并用终边位置统一刻画。", "弧度制": "用弧长与半径之比度量角的大小,π 弧度 = 180°,使弧长与扇形面积公式的形式更简洁。", "三角比": "直角三角形中边与边的比值(正弦、余弦、正切),并可借助单位圆推广到任意角。", "同角三角函数关系": "同一个角的正弦、余弦、正切之间的基本关系,如 sin²α+cos²α=1、tanα=sinα/cosα。", "和角与差角公式": "描述两角和或差的三角函数公式,如 sin(α±β)=sinαcosβ±cosαsinβ,是推导倍角公式的基础。", "三角恒等式": "对定义域内任意角都成立的三角关系式,包括平方关系、和差角、倍角与半角公式等。", "三角函数图像": "正弦、余弦、正切函数的图像及其周期、振幅、相位与图像变换规律。", "反三角函数": "三角函数在限定单调区间上的反函数,如反正弦 arcsin、反余弦 arccos、反正切 arctan。", "三角方程": "含有未知角的三角函数的方程,通常先化为基本三角方程,再结合周期性写出通解。", "正弦定理": "在 △ABC 中 a/sinA = b/sinB = c/sinC = 2R(R 为外接圆半径),用于已知边角组合解三角形。", "余弦定理": "在 △ABC 中 a²=b²+c²-2bc·cosA,用于已知两边及夹角或三边求解三角形。", "解三角形": "利用正弦定理、余弦定理与内角和关系,由已知元素求三角形其余边角的过程。", } GRAPH = [ { "name": "初等数学", "domain": "初等", "description": "中学与竞赛基础的数学知识总图", "children": [ { "name": "数与式", "children": [ "自然数与整数", "有理数", "实数", "数的整除", "代数式", "因式分解", "根式与无理式", "幂与指数", ], }, { "name": "方程与不等式", "children": [ "一元一次方程", "一元二次方程", "二元一次方程组", "分式方程", "一元一次不等式", "一元二次不等式", "不等式的性质与证明", ], }, { "name": "函数与图像", "children": [ "函数概念", "函数值", "定义域", "值域", "零点", "正负性", "单调性", "最值", "奇偶性", "偶函数", "奇函数", "图像对称性", "一次函数", "二次函数", "幂函数", "指数函数", "对数函数", "函数图像与变换", ], }, { "name": "三角函数", "children": [ "任意角", "弧度制", "三角比", "同角三角函数关系", "诱导公式", "和角与差角公式", "倍角公式", "三角恒等式", "三角函数图像", "反三角函数", "三角方程", "正弦定理", "余弦定理", "解三角形", ], }, { "name": "平面几何", "children": [ "点线面的基本关系", "平行与垂直", "三角形", "全等", "相似", "四边形", "圆", "面积与周长", "几何变换", ], }, { "name": "坐标与解析几何", "children": [ "数轴", "平面直角坐标系", "两点间距离", "直线方程", "圆的标准方程", "椭圆", "双曲线", "抛物线", "曲线与方程", ], }, { "name": "数列与证明", "children": [ "等差数列", "等比数列", "数列通项与求和", "递推数列", "数学归纳法", "逻辑与证明方法", ], }, { "name": "概率统计", "children": [ "计数原理", "排列组合", "古典概型", "条件概率", "随机变量", "统计图表", "均值与方差", ], }, ], }, { "name": "高等数学", "domain": "高等", "description": "大学基础数学知识总图", "children": [ { "name": "分析与微积分", "children": [ "集合与映射", "极限与连续", "导数与微分", "微分中值定理", "一元函数积分", "无穷级数", "多元函数微分", "重积分与曲线曲面积分", "微分方程", ], }, { "name": "线性代数", "children": [ "行列式", "矩阵", "线性方程组", "向量空间", "特征值与特征向量", "二次型", ], }, { "name": "概率论与数理统计", "children": [ "概率空间", "随机变量及其分布", "数字特征", "大数定律与中心极限定理", "参数估计", "假设检验", "回归分析", ], }, { "name": "离散与优化", "children": [ "图论基础", "组合优化", "最优化方法", ], }, ], }, ] PREREQUISITES: list[tuple[str, str]] = [ ("自然数与整数", "有理数"), ("有理数", "实数"), ("自然数与整数", "数的整除"), ("实数", "代数式"), ("代数式", "因式分解"), ("一元一次方程", "一元二次方程"), ("一元二次方程", "二次函数"), ("幂与指数", "指数函数"), ("幂与指数", "幂函数"), ("函数概念", "定义域"), ("函数概念", "函数值"), ("函数概念", "偶函数"), ("函数概念", "奇函数"), ("偶函数", "奇函数"), ("定义域", "零点"), ("一次函数", "二次函数"), ("二次函数", "函数图像与变换"), ("三角比", "任意角"), ("任意角", "弧度制"), ("弧度制", "三角函数图像"), ("和角与差角公式", "倍角公式"), ("倍角公式", "三角恒等式"), ("函数与图像", "三角函数"), ("平面直角坐标系", "直线方程"), ("直线方程", "圆的标准方程"), ("圆的标准方程", "抛物线"), ("极限与连续", "导数与微分"), ("导数与微分", "微分中值定理"), ("导数与微分", "一元函数积分"), ("一元函数积分", "多元函数微分"), ("矩阵", "行列式"), ("矩阵", "线性方程组"), ("线性方程组", "特征值与特征向量"), ("随机变量及其分布", "数字特征"), ("概率空间", "随机变量及其分布"), ] def _create( db: Session, name: str, domain: str, category: str = "", description: str = "", ) -> Knowledge: row = db.query(Knowledge).filter(Knowledge.name == name).first() if row is None: row = Knowledge( name=name, domain=domain, category=category, description=description, ) db.add(row) db.flush() else: if row.domain != domain: row.domain = domain if category and not row.category: row.category = category if description and not row.description: row.description = description return row def _insert_tree(db: Session, node: dict, parent: Knowledge | None = None) -> None: if isinstance(node, str): current = _create( db, str(node), parent.domain if parent else "初等", parent.category if parent else "", ) if parent is not None: ensure_knowledge_relation(db, parent, current, "包含") return name = node["name"] if parent is None: category = "总纲" elif parent.name in {"初等数学", "高等数学"}: category = name else: category = node.get("category") or (parent.category or "") current = _create( db, name, node.get("domain") or (parent.domain if parent else "初等"), category, node.get("description", ""), ) if parent is not None: ensure_knowledge_relation(db, parent, current, "包含") for child in node.get("children", []): _insert_tree(db, child, current) def ensure_knowledge_graph(db: Session) -> None: if db.query(Knowledge).count() == 0: for node in GRAPH: _insert_tree(db, node) else: _repair_existing_categories(db) _cleanup_legacy_knowledge(db) for name, text in KNOWLEDGE_DEFINITIONS.items(): row = db.query(Knowledge).filter(Knowledge.name == name).first() if row is not None and not (row.description or "").strip(): row.description = text for source_name, target_name in PREREQUISITES: source = db.query(Knowledge).filter(Knowledge.name == source_name).first() target = db.query(Knowledge).filter(Knowledge.name == target_name).first() if source is None or target is None: continue ensure_knowledge_relation(db, source, target, "前置") db.commit() def _cleanup_legacy_knowledge(db: Session) -> None: """把早期自由生成的节点合并到标准图谱,并重挂章节/题目/用户掌握度。""" from models import ( ChapterKnowledge, KnowledgeRelation, QuestionKnowledge, UserKnowledge, ) for legacy_name, canonical_name in LEGACY_KNOWLEDGE_MAP.items(): legacy = db.query(Knowledge).filter(Knowledge.name == legacy_name).first() if legacy is None: continue canonical = db.query(Knowledge).filter(Knowledge.name == canonical_name).first() if canonical is None: canonical = Knowledge( name=canonical_name, domain=legacy.domain, category=legacy.category or "三角函数", ) db.add(canonical) db.flush() old_chapter_links = ( db.query(ChapterKnowledge) .filter(ChapterKnowledge.knowledge_id == legacy.id) .all() ) old_question_links = ( db.query(QuestionKnowledge) .filter(QuestionKnowledge.knowledge_id == legacy.id) .all() ) old_user_links = ( db.query(UserKnowledge) .filter(UserKnowledge.knowledge_id == legacy.id) .all() ) db.query(ChapterKnowledge).filter( ChapterKnowledge.knowledge_id == legacy.id ).delete(synchronize_session=False) db.query(QuestionKnowledge).filter( QuestionKnowledge.knowledge_id == legacy.id ).delete(synchronize_session=False) db.query(UserKnowledge).filter( UserKnowledge.knowledge_id == legacy.id ).delete(synchronize_session=False) for link in old_chapter_links: exists = ( db.query(ChapterKnowledge) .filter( ChapterKnowledge.chapter_id == link.chapter_id, ChapterKnowledge.knowledge_id == canonical.id, ) .first() ) if exists is None: db.add( ChapterKnowledge( chapter_id=link.chapter_id, knowledge_id=canonical.id, ) ) for link in old_question_links: exists = ( db.query(QuestionKnowledge) .filter( QuestionKnowledge.question_id == link.question_id, QuestionKnowledge.knowledge_id == canonical.id, ) .first() ) if exists is None: db.add( QuestionKnowledge( question_id=link.question_id, knowledge_id=canonical.id, ) ) for link in old_user_links: exists = ( db.query(UserKnowledge) .filter( UserKnowledge.user_id == link.user_id, UserKnowledge.knowledge_id == canonical.id, ) .first() ) if exists is None: db.add( UserKnowledge( user_id=link.user_id, knowledge_id=canonical.id, mastery=link.mastery, position=link.position, ) ) db.query(KnowledgeRelation).filter( (KnowledgeRelation.source_id == legacy.id) | (KnowledgeRelation.target_id == legacy.id) ).delete(synchronize_session=False) db.delete(legacy) db.flush() def _repair_existing_categories(db: Session) -> None: """存量库按种子树回填 domain/category(不删除已有节点)。""" def visit(node: dict, parent: Knowledge | None = None) -> None: if isinstance(node, str): row = db.query(Knowledge).filter(Knowledge.name == node).first() if row is not None: row.domain = parent.domain if parent else row.domain if parent and parent.category: row.category = parent.category return domain = node.get("domain") or (parent.domain if parent else "初等") if parent is None: category = "总纲" elif parent.name in {"初等数学", "高等数学"}: category = node["name"] else: category = parent.category or "" row = db.query(Knowledge).filter(Knowledge.name == node["name"]).first() if row is None: row = Knowledge( name=node["name"], domain=domain, category=category, description=node.get("description", ""), ) db.add(row) db.flush() else: row.domain = domain row.category = category if node.get("description") and not row.description: row.description = node["description"] for child in node.get("children", []): visit(child, row) for top in GRAPH: visit(top) db.commit()