| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- # -*- 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
|