"""推荐引擎:找出薄弱知识点并生成“下一步任务”。""" 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, }