# -*- coding: utf-8 -*- """个性化推荐模块:习题测评、错题集、学情分析""" from datetime import datetime from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.orm import Session from backend.database import get_db from backend.models import Question, AnswerRecord from backend.services.auth import get_current_user_id from backend.services.recommendation import ( get_user_proficiency, get_recommended_questions, get_or_generate_daily_recommendations, get_mastery_matrix, get_motivation_summary, get_question_analysis, get_wrong_questions_for_user, get_diagnostic_questions, ) from backend.services.mastery_service import ( update_mastery_from_answers, mark_daily_recommendations_done, ) from backend.models import WrongQuestion from backend.models import DailyRecommendation router = APIRouter(prefix="/recommendation", tags=["个性化推荐"]) @router.get("/proficiency") def proficiency( db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """知识点熟练度(学情报表)""" return get_user_proficiency(db, user_id) @router.get("/wrong_questions") def wrong_questions( limit: int = 50, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """错题集""" questions = get_wrong_questions_for_user(db, user_id, limit=limit) return [ { "id": q.id, "content": q.content, "option_a": q.option_a, "option_b": q.option_b, "option_c": q.option_c, "option_d": q.option_d, "knowledge_point": q.knowledge_point, } for q in questions ] @router.get("/recommended_questions") def recommended_questions( count: int = 5, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """习题测评推荐算法(错题优先 + 薄弱知识点)""" qs = get_recommended_questions(db, user_id, count) return [{"id": q.id, "content": q.content, "option_a": q.option_a, "option_b": q.option_b, "option_c": q.option_c, "option_d": q.option_d, "knowledge_point": q.knowledge_point} for q in qs] @router.get("/daily_practice") def daily_practice( count: int = 10, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """每日一练:按弱点权重生成今日推荐题,若今日已有则直接返回""" qs = get_or_generate_daily_recommendations(db, user_id, count=count) return [{"id": q.id, "content": q.content, "option_a": q.option_a, "option_b": q.option_b, "option_c": q.option_c, "option_d": q.option_d, "knowledge_point": q.knowledge_point} for q in qs] @router.get("/mastery_matrix") def mastery_matrix( db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """知识点掌握度矩阵:用于前端颜色分级、雷达图等可视化""" return get_mastery_matrix(db, user_id) @router.get("/motivation_summary") def motivation_summary( db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """激励信息汇总:连续打卡天数、全绿模块、章节平均掌握度""" return get_motivation_summary(db, user_id) @router.get("/question_analysis") def question_analysis( question_id: int, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """题目解析:从 chemistry_question_bank.analysis 读取。""" _ = user_id analysis = get_question_analysis(db, question_id) return {"question_id": question_id, "analysis": analysis} class SubmitAnswerRequest(BaseModel): question_id: int selected_option: str @router.post("/submit_answer") def submit_answer( req: SubmitAnswerRequest, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """提交单题答案(答题记录)""" q = db.query(Question).get(req.question_id) if not q: return {"ok": False, "error": "题目不存在"} is_correct = req.selected_option == q.correct_option rec = AnswerRecord(user_id=user_id, question_id=q.id, selected_option=req.selected_option, is_correct=is_correct) db.add(rec) db.commit() return {"ok": True, "is_correct": is_correct} class SubmitBatchRequest(BaseModel): answers: list[dict] # [{"question_id": 1, "selected_option": "A"}, ...] class DiagnosticSubmitRequest(BaseModel): answers: list[dict] # [{"question_id": 1, "selected_option": "A"}, ...] train_count: int = 10 regenerate_daily: bool = False @router.post("/submit_batch") def submit_batch( req: SubmitBatchRequest, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """批量提交(每日训练),同时更新弱点权重与掌握度""" correct, total = 0, 0 processed = [] answer_record_ids = {} # {question_id: answer_records.id} for item in req.answers: q = db.query(Question).get(item.get("question_id")) if not q: continue sel = item.get("selected_option", "") is_correct = sel == q.correct_option total += 1 if is_correct: correct += 1 rec = AnswerRecord( user_id=user_id, question_id=q.id, selected_option=sel, is_correct=is_correct, ) db.add(rec) # 获取 rec.id,后续写回 daily_recommendations.answer_record_id db.flush() answer_record_ids[q.id] = rec.id # 第二阶段:记录错题集到 wrong_questions(用于错题→变式题闭环) if not is_correct: try: wq = ( db.query(WrongQuestion) .filter( WrongQuestion.user_id == user_id, WrongQuestion.question_id == q.id, ) .first() ) if not wq: wq = WrongQuestion( user_id=user_id, question_id=q.id, wrong_count=1, last_wrong_at=datetime.utcnow(), ) db.add(wq) else: wq.wrong_count = (wq.wrong_count or 0) + 1 wq.last_wrong_at = datetime.utcnow() except Exception: # 不影响主流程:错题集写入失败时仍允许学习闭环继续 pass processed.append({"question_id": q.id, "selected_option": sel}) if total > 0: try: update_mastery_from_answers(db, user_id, processed, session_kind="practice") from datetime import date mark_daily_recommendations_done( db, user_id, [a["question_id"] for a in processed], today=date.today(), answer_record_ids=answer_record_ids, ) except Exception: pass db.commit() acc = int(round(correct / total * 100)) if total else 0 return {"ok": True, "total": total, "correct": correct, "accuracy": acc} @router.get("/diagnostic_questions") def diagnostic_questions( count: int = 10, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """诊断测评:覆盖广、难度适中(用于弱点更新)""" qs = get_diagnostic_questions(db, user_id, count=count, session_kind="diagnostic") return [ { "id": q.id, "content": q.content, "option_a": q.option_a, "option_b": q.option_b, "option_c": q.option_c, "option_d": q.option_d, "knowledge_point": q.knowledge_point, } for q in qs ] @router.get("/re_evaluate_questions") def re_evaluate_questions( count: int = 10, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """再测评:在测评后/训练后重新抽取题目""" qs = get_diagnostic_questions(db, user_id, count=count, session_kind="re_evaluate") return [ { "id": q.id, "content": q.content, "option_a": q.option_a, "option_b": q.option_b, "option_c": q.option_c, "option_d": q.option_d, "knowledge_point": q.knowledge_point, } for q in qs ] @router.post("/diagnostic_submit_batch") def diagnostic_submit_batch( req: DiagnosticSubmitRequest, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """ 诊断测评提交: - 写入 answer_records - 按弱点更新规则更新 student_knowledge_mastery - 生成(或复用)每日一练 daily_recommendations(train_count) """ correct, total = 0, 0 processed: list[dict] = [] # 1) 写入答题记录(同时更新 wrong_questions 聚合) for item in req.answers: try: qid = int(item.get("question_id")) except (TypeError, ValueError): continue selected = (item.get("selected_option") or "").strip() if not selected: continue q = db.query(Question).get(qid) if not q: continue is_correct = selected == q.correct_option total += 1 if is_correct: correct += 1 db.add( AnswerRecord( user_id=user_id, question_id=q.id, selected_option=selected, is_correct=is_correct, ) ) if not is_correct: try: wq = ( db.query(WrongQuestion) .filter(WrongQuestion.user_id == user_id, WrongQuestion.question_id == q.id) .first() ) if not wq: db.add( WrongQuestion( user_id=user_id, question_id=q.id, wrong_count=1, last_wrong_at=datetime.utcnow(), ) ) else: wq.wrong_count = (wq.wrong_count or 0) + 1 wq.last_wrong_at = datetime.utcnow() except Exception: pass processed.append({"question_id": q.id, "selected_option": selected}) if total == 0: return {"ok": False, "detail": "未提交有效答题"} db.commit() # 2) 更新弱点/掌握度(批量测评结束后统一更新) update_mastery_from_answers(db, user_id, processed, session_kind="diagnostic") db.commit() # 3) 生成每日一练(闭环:测评→推荐) from datetime import date today = date.today() if req.regenerate_daily: db.query(DailyRecommendation).filter( DailyRecommendation.user_id == user_id, DailyRecommendation.recommend_date == today, DailyRecommendation.is_done == 0, ).delete(synchronize_session=False) db.commit() daily_qs = get_or_generate_daily_recommendations( db, user_id, count=req.train_count, today=today ) # 返回:测评结果 + 当天每日一练题目 acc = int(round(correct / total * 100)) return { "ok": True, "diagnostic": {"total": total, "correct": correct, "accuracy": acc}, "daily_practice": [ { "id": q.id, "content": q.content, "option_a": q.option_a, "option_b": q.option_b, "option_c": q.option_c, "option_d": q.option_d, "knowledge_point": q.knowledge_point, } for q in daily_qs ], } @router.post("/re_evaluate_submit_batch") def re_evaluate_submit_batch( req: DiagnosticSubmitRequest, db: Session = Depends(get_db), user_id: int = Depends(get_current_user_id), ): """再测评提交:同诊断提交逻辑,然后给出下一轮每日一练""" resp = diagnostic_submit_batch( req=req, db=db, user_id=user_id, ) return resp