feat: 历史背诵问答引擎 — 355题七年级中国历史题库

This commit is contained in:
taolm 2026-05-29 13:46:32 +00:00
parent 90ac682a06
commit 44cd6cc234

184
tutor/history_quiz.py Normal file
View File

@ -0,0 +1,184 @@
"""
历史知识问答引擎 七年级中国历史背诵题库
data/历史背诵题库.json 加载题目支持随机出题答题判断进度追踪
"""
import json
import random
import os
from pathlib import Path
from datetime import datetime
QUIZ_FILE = Path(os.environ.get(
"HISTORY_QUIZ_FILE",
os.path.join(os.path.dirname(__file__), "..", "data", "历史背诵题库.json")
))
# 单元别名映射(用于前端显示)
UNIT_ALIASES = {
"U1": "七上·第一单元 史前时期",
"U2": "七上·第二单元 夏商周时期",
"U3": "七上·第三单元 秦汉时期",
"U4": "七上·第四单元 三国两晋南北朝",
"U5": "七下·第一单元 隋唐时期",
"U6": "七下·第二单元 辽宋夏金元",
"U7": "七下·第三单元 明清时期",
}
class HistoryQuiz:
"""历史题库引擎"""
def __init__(self):
self.data = self._load_quiz()
self.units = {u["unit_id"]: u for u in self.data.get("units", [])}
self.all_questions = []
for unit in self.data.get("units", []):
for q in unit.get("questions", []):
q["unit_id"] = unit["unit_id"]
q["unit_title"] = unit.get("title", "")
self.all_questions.append(q)
def _load_quiz(self) -> dict:
if QUIZ_FILE.exists():
return json.loads(QUIZ_FILE.read_text(encoding="utf-8"))
# 降级:空题库
return {"metadata": {}, "units": []}
def get_units(self) -> list:
"""获取所有单元信息(含题目数量)"""
result = []
for unit in self.data.get("units", []):
uid = unit["unit_id"]
result.append({
"unit_id": uid,
"title": unit.get("title", ""),
"grade": unit.get("grade", ""),
"alias": UNIT_ALIASES.get(uid, uid),
"question_count": len(unit.get("questions", [])),
"lessons": unit.get("lessons", []),
})
return result
def get_random_question(self, unit_id: str = None, question_type: str = None) -> dict:
"""随机获取一道题,可选按单元/题型过滤"""
pool = self.all_questions
if unit_id:
pool = [q for q in pool if q.get("unit_id") == unit_id]
if question_type:
pool = [q for q in pool if q.get("type") == question_type]
if not pool:
return {"error": "该条件下没有题目"}
q = random.choice(pool)
return {
"id": q["id"],
"type": q["type"],
"question": q["question"],
"answer": q["answer"],
"unit_id": q["unit_id"],
"unit_title": q.get("unit_title", ""),
"unit_alias": UNIT_ALIASES.get(q["unit_id"], q["unit_id"]),
"source": q.get("source", ""),
}
def get_batch(self, count: int = 10, unit_id: str = None, question_type: str = None) -> list:
"""批量获取题目(不重复)"""
pool = list(self.all_questions)
if unit_id:
pool = [q for q in pool if q.get("unit_id") == unit_id]
if question_type:
pool = [q for q in pool if q.get("type") == question_type]
random.shuffle(pool)
selected = pool[:count]
return [{
"id": q["id"],
"type": q["type"],
"question": q["question"],
"answer": q["answer"],
"unit_id": q["unit_id"],
"unit_title": q.get("unit_title", ""),
"unit_alias": UNIT_ALIASES.get(q["unit_id"], q["unit_id"]),
} for q in selected]
def check_answer(self, question_id: str, student_answer: str) -> dict:
"""检查答案 — 模糊匹配(关键词匹配)"""
q = next((q for q in self.all_questions if q["id"] == question_id), None)
if not q:
return {"correct": False, "error": "题目不存在"}
correct = q["answer"].strip()
student = student_answer.strip()
# 精确匹配
if student == correct:
return {
"correct": True,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"match_type": "exact"
}
# 包含匹配(学生答案包含在标准答案中,或反之)
if len(student) >= 2 and (student in correct or correct in student):
return {
"correct": True,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"match_type": "partial"
}
# 关键词匹配(答案拆分为关键词,匹配度>60%算对)
keywords = set(correct.replace("", " ").replace("", " ").replace("", " ").split())
student_words = set(student.replace("", " ").replace("", " ").replace("", " ").split())
if keywords and student_words:
overlap = len(keywords & student_words) / len(keywords)
if overlap >= 0.6:
return {
"correct": True,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"match_type": "keyword",
"keyword_match": f"{overlap:.0%}"
}
return {
"correct": False,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"hint": self._generate_hint(correct)
}
def _generate_hint(self, answer: str) -> str:
"""生成提示(显示答案首字+长度)"""
if len(answer) <= 1:
return "答案很短,再想想?"
first_char = answer[0]
return f"提示:答案以「{first_char}」开头,共{len(answer)}个字"
def get_stats(self) -> dict:
"""题库统计"""
type_counts = {}
for q in self.all_questions:
t = q.get("type", "unknown")
type_counts[t] = type_counts.get(t, 0) + 1
return {
"total_questions": len(self.all_questions),
"total_units": len(self.units),
"by_type": type_counts,
"by_unit": {uid: len(u.get("questions", [])) for uid, u in self.units.items()},
}
# 全局单例
_quiz_instance = None
def get_quiz() -> HistoryQuiz:
global _quiz_instance
if _quiz_instance is None:
_quiz_instance = HistoryQuiz()
return _quiz_instance