"""题目引擎:按章节动态组卷(掌握度缺口 + 错题 + 轮换扰动)。""" from __future__ import annotations import json from sqlalchemy.orm import Session from sqlalchemy import func from models import ( AttemptItem, Chapter, ErrorEntry, Knowledge, Question, UserKnowledge, ) from services.knowledge_service import question_knowledge_names def load_questions(db: Session, ids: list[int]) -> list[Question]: rows = db.query(Question).filter(Question.id.in_(ids)).all() by_id = {row.id: row for row in rows} return [by_id[qid] for qid in ids if qid in by_id] def chapter_question_ids( db: Session, user_id: int, chapter_id: int, limit: int = 5, variant: int = 0, ) -> list[int]: """按章节动态组卷:只取该章题库,不足则返回实际题量,不从其他章节补题。 权重 = 掌握度缺口 + 错题加成 + 上次已做惩罚 + 轮换扰动,避免每轮完全相同。 """ chapter = db.get(Chapter, chapter_id) if chapter is None: return [] knowledge_map = { knowledge.name: user_knowledge.mastery for user_knowledge, knowledge in ( db.query(UserKnowledge, Knowledge) .join(Knowledge, Knowledge.id == UserKnowledge.knowledge_id) .filter(UserKnowledge.user_id == user_id) .all() ) } error_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 name in {str(name) for name in names}: error_counts[name] = error_counts.get(name, 0) + 1 attempt_counts = dict( db.query(AttemptItem.question_id, func.count(AttemptItem.id)) .filter(AttemptItem.user_id == user_id, AttemptItem.question_id.isnot(None)) .group_by(AttemptItem.question_id) .all() ) chapter_questions = ( db.query(Question).filter(Question.chapter_id == chapter_id).all() ) ranked = [] for question in chapter_questions: tags = question_knowledge_names(db, question) knowledge_deficit = max( (110 - knowledge_map.get(tag, 60) for tag in tags), default=50, ) errors = sum(error_counts.get(tag, 0) for tag in tags) repeated = attempt_counts.get(question.id, 0) * 22 jitter = (question.id * 7 + variant * 13) % 19 weight = knowledge_deficit + errors * 12 - repeated + jitter ranked.append((weight, question)) ranked.sort(key=lambda pair: (-pair[0], pair[1].id)) return [q.id for _, q in ranked[:limit]]