513 lines
20 KiB
Python
513 lines
20 KiB
Python
"""初等 / 高等数学标准知识图谱(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₁<x₂ 时恒有 f(x₁)<f(x₂)(或恒有 f(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()
|