nex_math/backend/services/recommendation.py

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,
}