152 lines
5.1 KiB
Python
152 lines
5.1 KiB
Python
"""推荐引擎:找出薄弱知识点并生成“下一步任务”。"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from models import (
|
|
AttemptItem,
|
|
Chapter,
|
|
DailyCompletion,
|
|
ErrorEntry,
|
|
Knowledge,
|
|
Question,
|
|
Textbook,
|
|
UserKnowledge,
|
|
)
|
|
from services.mastery_engine import snapshot
|
|
from services.knowledge_service import chapter_knowledge_names
|
|
|
|
|
|
def _error_counts(db: Session, user_id: int) -> dict[str, int]:
|
|
counts: dict[str, int] = {}
|
|
for primary, raw_names in (
|
|
db.query(ErrorEntry.knowledge_name, ErrorEntry.knowledge_names)
|
|
.filter(ErrorEntry.user_id == user_id)
|
|
.all()
|
|
):
|
|
try:
|
|
names = json.loads(raw_names or "[]")
|
|
if not isinstance(names, list) or not names:
|
|
names = [primary]
|
|
except (TypeError, ValueError):
|
|
names = [primary]
|
|
for item in names:
|
|
counts[str(item)] = counts.get(str(item), 0) + 1
|
|
return counts
|
|
|
|
|
|
def build_plan(db: Session, user_id: int) -> dict:
|
|
knowledge = snapshot(db, user_id)
|
|
counts = _error_counts(db, user_id)
|
|
weak = [k for k in knowledge if k.mastery < 75]
|
|
strong = [k for k in knowledge if k.mastery >= 75]
|
|
|
|
focus_names = [k.name for k in weak]
|
|
if not focus_names:
|
|
focus_names = [strong[0].name] if strong else []
|
|
|
|
focus_text = " / ".join(focus_names[:2]) or "当前知识点"
|
|
notice = f"系统根据已有答题记录,将“{focus_text}”设为当前强化重点。"
|
|
|
|
steps: list[str] = []
|
|
if weak:
|
|
group = weak[:2]
|
|
step1 = " / ".join(k.name for k in group)
|
|
steps.append(f"① 重点强化 {step1}:巩固定义 → 点的对应 → 图像性质判断。")
|
|
if len(weak) > 2:
|
|
steps.append(f"② {weak[2].name}:从表达式判断图像性质,补做针对性小题。")
|
|
else:
|
|
steps.append("② 再完成一次章节测试,检验本轮掌握情况。")
|
|
steps.append("③ 间隔复习:在后续测试中穿插旧知识,防止遗忘。")
|
|
else:
|
|
steps.append("① 保持当前节奏,进入下一章节的学习。")
|
|
steps.append("② 每周做一次章节测试,维持各知识点掌握度。")
|
|
steps.append("③ 错题清零后开始新主题,建立更完整的知识地图。")
|
|
|
|
top_error = sorted(knowledge, key=lambda k: (-counts.get(k.name, 0), k.mastery))
|
|
return {
|
|
"notice": notice,
|
|
"focus_names": focus_names,
|
|
"steps": steps,
|
|
"top_error": (
|
|
top_error[0].name
|
|
if top_error and counts.get(top_error[0].name, 0)
|
|
else None
|
|
),
|
|
}
|
|
|
|
|
|
def build_daily_recommendation(
|
|
db: Session, user_id: int
|
|
) -> dict | None:
|
|
"""推荐今日章节:无掌握度且未做过的章优先,然后按薄弱反馈/错误加权。"""
|
|
chapters = (
|
|
db.query(Chapter, Textbook)
|
|
.join(Textbook, Textbook.id == Chapter.textbook_id)
|
|
.order_by(Textbook.position.asc(), Chapter.position.asc())
|
|
.all()
|
|
)
|
|
candidates: list[dict] = []
|
|
for chapter, textbook in chapters:
|
|
question_count = (
|
|
db.query(Question.id).filter(Question.chapter_id == chapter.id).count()
|
|
)
|
|
if question_count == 0:
|
|
continue
|
|
tags = chapter_knowledge_names(db, chapter.id)
|
|
masteries = [
|
|
mastery
|
|
for mastery, in db.query(UserKnowledge.mastery)
|
|
.join(Knowledge, Knowledge.id == UserKnowledge.knowledge_id)
|
|
.filter(
|
|
UserKnowledge.user_id == user_id,
|
|
Knowledge.name.in_(tags),
|
|
)
|
|
.all()
|
|
]
|
|
answered = (
|
|
db.query(AttemptItem.id)
|
|
.join(Question, Question.id == AttemptItem.question_id)
|
|
.filter(
|
|
AttemptItem.user_id == user_id,
|
|
Question.chapter_id == chapter.id,
|
|
)
|
|
.count()
|
|
)
|
|
candidates.append(
|
|
{
|
|
"chapter": chapter,
|
|
"textbook": textbook,
|
|
"tags": tags,
|
|
"mastery_avg": sum(masteries) / len(masteries)
|
|
if masteries
|
|
else None,
|
|
"answered": answered,
|
|
"knowledge_count": len(tags),
|
|
}
|
|
)
|
|
if not candidates:
|
|
return None
|
|
# 未开始章节优先;同等情况按章节顺序
|
|
candidates.sort(
|
|
key=lambda item: (
|
|
0 if item["answered"] == 0 and item["mastery_avg"] is None else 1,
|
|
item["mastery_avg"] if item["mastery_avg"] is not None else 50,
|
|
)
|
|
)
|
|
choice = candidates[0]
|
|
reason = (
|
|
"你还没有练习过这一章,建议从本章开始建立基础"
|
|
if choice["answered"] == 0
|
|
else "该章知识点掌握度较低,需要优先强化"
|
|
)
|
|
return {
|
|
"chapter_id": choice["chapter"].id,
|
|
"chapter_name": choice["chapter"].name,
|
|
"book_name": choice["textbook"].name,
|
|
"knowledge_names": choice["tags"],
|
|
"reason": reason,
|
|
}
|