| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- # -*- coding: utf-8 -*-
- """
- 弱点建模与掌握度更新服务
- 测评 → 诊断(弱点更新) → 每日一练(推送) → 训练/辅导 → 再测评 闭环核心逻辑
- """
- from datetime import datetime, date
- from typing import Dict, List, Optional, Tuple
- from sqlalchemy.orm import Session
- from backend.models import (
- Question,
- AnswerRecord,
- KnowledgePoint,
- QuestionKnowledgePoint,
- StudentKnowledgeMastery,
- StudentProfile,
- DailyRecommendation,
- )
- # 弱点更新参数
- ALPHA = 1 # 答错时 weakness_weight 增加系数
- BETA = 5 # 答错时 mastery_score 降低系数
- GAMMA = 1 # 答对时 weakness_weight 衰减系数
- MASTERY_GAIN = 3 # 答对时 mastery_score 提升量
- MASTERY_THRESHOLD = 80 # 推荐时排除已掌握的知识点(mastery_score > 此值)
- def _get_knowledge_points_for_question(
- db: Session, question_id: int, question: Question = None
- ) -> List[Tuple[int, int]]:
- """
- 获取题目关联的知识点 (knowledge_point_id, weight) 列表。
- 优先从 question_knowledge_points 读取;若无则从 Question.knowledge_point 字符串解析并懒建映射。
- """
- qkps = (
- db.query(QuestionKnowledgePoint)
- .filter(QuestionKnowledgePoint.question_id == question_id)
- .all()
- )
- if qkps:
- return [(qkp.knowledge_point_id, qkp.weight or 1) for qkp in qkps]
- # 回退:从 Question.knowledge_point 字符串解析
- if question is None:
- question = db.query(Question).filter(Question.id == question_id).first()
- if not question or not (question.knowledge_point or "").strip():
- return []
- kp_name = (question.knowledge_point or "").strip()
- kp = db.query(KnowledgePoint).filter(KnowledgePoint.name == kp_name).first()
- if not kp:
- kp = KnowledgePoint(name=kp_name)
- db.add(kp)
- db.flush() # 获取 id
- # 懒建 question_knowledge_points
- qkp = QuestionKnowledgePoint(
- question_id=question_id,
- knowledge_point_id=kp.id,
- weight=1,
- )
- db.add(qkp)
- db.flush()
- return [(kp.id, 1)]
- def _get_or_create_profile(db: Session, user_id: int) -> StudentProfile:
- """获取或创建学生学习档案"""
- profile = db.query(StudentProfile).filter(StudentProfile.user_id == user_id).first()
- if not profile:
- profile = StudentProfile(user_id=user_id)
- db.add(profile)
- db.flush()
- return profile
- def _get_or_create_mastery(
- db: Session, user_id: int, knowledge_point_id: int
- ) -> StudentKnowledgeMastery:
- """获取或创建学生-知识点掌握度记录"""
- row = (
- db.query(StudentKnowledgeMastery)
- .filter(
- StudentKnowledgeMastery.user_id == user_id,
- StudentKnowledgeMastery.knowledge_point_id == knowledge_point_id,
- )
- .first()
- )
- if not row:
- row = StudentKnowledgeMastery(
- user_id=user_id,
- knowledge_point_id=knowledge_point_id,
- )
- db.add(row)
- db.flush()
- return row
- def update_mastery_from_answers(
- db: Session,
- user_id: int,
- answers: List[dict],
- session_kind: str = "practice",
- include_diagnostic_in_mastery: bool = False,
- ) -> None:
- """
- 根据一次测评的答题记录更新 student_knowledge_mastery 与 student_profiles。
- answers: [{"question_id": int, "selected_option": str}, ...]
- 内部会查询 Question 判断 is_correct。
- """
- # 诊断/再测评用于弱点建模与推荐题选择,不直接影响“学情分析展示口径”
- # (掌握度颜色/雷达图/章节均值/最近练习),避免用户感知到“被重置”。
- is_diagnostic = session_kind in {"diagnostic", "re_evaluate"}
- update_mastery_score = not is_diagnostic
- update_last_practiced_at = not is_diagnostic
- if is_diagnostic and include_diagnostic_in_mastery:
- # 兼容式开关:仅在显式开启时,诊断/再测评也计入掌握度分值
- update_mastery_score = True
- update_last_practiced_at = True
- profile = _get_or_create_profile(db, user_id)
- profile.total_questions_answered += len(answers)
- profile.last_active_at = datetime.utcnow()
- for item in answers:
- qid = item.get("question_id")
- selected = (item.get("selected_option") or "").strip()
- if not qid or not selected:
- continue
- question = db.query(Question).filter(Question.id == qid).first()
- if not question:
- continue
- is_correct = selected == question.correct_option
- kp_list = _get_knowledge_points_for_question(db, qid, question)
- for kp_id, weight in kp_list:
- mastery = _get_or_create_mastery(db, user_id, kp_id)
- if is_correct:
- mastery.correct_count = (mastery.correct_count or 0) + 1
- mastery.weakness_weight = max(
- 0, (mastery.weakness_weight or 0) - GAMMA * weight
- )
- if update_mastery_score:
- mastery.mastery_score = min(
- 100, (mastery.mastery_score or 0) + MASTERY_GAIN
- )
- else:
- mastery.wrong_count = (mastery.wrong_count or 0) + 1
- mastery.weakness_weight = (mastery.weakness_weight or 0) + ALPHA * weight
- if update_mastery_score:
- mastery.mastery_score = max(
- 0, (mastery.mastery_score or 0) - BETA * weight
- )
- if update_last_practiced_at:
- mastery.last_practiced_at = datetime.utcnow()
- def mark_daily_recommendations_done(
- db: Session,
- user_id: int,
- question_ids: List[int],
- today: date = None,
- answer_record_ids: Optional[Dict[int, int]] = None,
- ) -> None:
- """将今日推荐中已作答的题目标记为完成"""
- if today is None:
- today = date.today()
- if not question_ids:
- return
- if not answer_record_ids:
- db.query(DailyRecommendation).filter(
- DailyRecommendation.user_id == user_id,
- DailyRecommendation.recommend_date == today,
- DailyRecommendation.question_id.in_(question_ids),
- ).update({DailyRecommendation.is_done: 1}, synchronize_session=False)
- return
- # 精细化更新:为每道题写入对应的 answer_record_id(用于闭环追踪)
- for qid in set(question_ids):
- arid = answer_record_ids.get(qid)
- if not arid:
- # 没拿到 answer_record_id 就只标记完成,避免影响主流程
- continue
- db.query(DailyRecommendation).filter(
- DailyRecommendation.user_id == user_id,
- DailyRecommendation.recommend_date == today,
- DailyRecommendation.question_id == qid,
- ).update(
- {DailyRecommendation.is_done: 1, DailyRecommendation.answer_record_id: arid},
- synchronize_session=False,
- )
- # 兜底:对那些没命中 answer_record_ids 的题,至少标记完成
- missing_qids = [qid for qid in question_ids if qid not in answer_record_ids]
- if missing_qids:
- db.query(DailyRecommendation).filter(
- DailyRecommendation.user_id == user_id,
- DailyRecommendation.recommend_date == today,
- DailyRecommendation.question_id.in_(missing_qids),
- ).update({DailyRecommendation.is_done: 1}, synchronize_session=False)
|