185 lines
6.5 KiB
Python
185 lines
6.5 KiB
Python
"""
|
||
历史知识问答引擎 — 七年级中国历史背诵题库
|
||
从 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
|