mirror of
https://github.com/handsomezhuzhu/QQuiz.git
synced 2026-02-20 20:10:14 +00:00
## 功能特性 ✅ **核心功能** - 多文件上传与智能去重(基于 content_hash) - 异步文档解析(支持 TXT/PDF/DOCX/XLSX) - AI 智能题目提取与评分(OpenAI/Anthropic/Qwen) - 断点续做与进度管理 - 自动错题本收集 ✅ **技术栈** - Backend: FastAPI + SQLAlchemy 2.0 + PostgreSQL - Frontend: React 18 + Vite + Tailwind CSS - Deployment: Docker Compose ✅ **项目结构** - 53 个文件 - 完整的前后端分离架构 - Docker/源码双模部署支持 🚀 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
5.5 KiB
Python
193 lines
5.5 KiB
Python
"""
|
|
Mistake Router - Handles user mistake book (错题本)
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, and_, func
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from database import get_db
|
|
from models import User, Question, UserMistake, Exam
|
|
from schemas import MistakeAdd, MistakeResponse, MistakeListResponse
|
|
from services.auth_service import get_current_user
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=MistakeListResponse)
|
|
async def get_user_mistakes(
|
|
skip: int = 0,
|
|
limit: int = 50,
|
|
exam_id: int = None, # Optional filter by exam
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get user's mistake book with optional exam filter"""
|
|
|
|
# Build query
|
|
query = (
|
|
select(UserMistake)
|
|
.options(selectinload(UserMistake.question))
|
|
.where(UserMistake.user_id == current_user.id)
|
|
.order_by(UserMistake.created_at.desc())
|
|
)
|
|
|
|
# Apply exam filter if provided
|
|
if exam_id is not None:
|
|
query = query.join(Question).where(Question.exam_id == exam_id)
|
|
|
|
# Get total count
|
|
count_query = select(func.count(UserMistake.id)).where(UserMistake.user_id == current_user.id)
|
|
if exam_id is not None:
|
|
count_query = count_query.join(Question).where(Question.exam_id == exam_id)
|
|
|
|
result = await db.execute(count_query)
|
|
total = result.scalar()
|
|
|
|
# Get mistakes
|
|
result = await db.execute(query.offset(skip).limit(limit))
|
|
mistakes = result.scalars().all()
|
|
|
|
# Format response
|
|
mistake_responses = []
|
|
for mistake in mistakes:
|
|
mistake_responses.append(
|
|
MistakeResponse(
|
|
id=mistake.id,
|
|
user_id=mistake.user_id,
|
|
question_id=mistake.question_id,
|
|
question=mistake.question,
|
|
created_at=mistake.created_at
|
|
)
|
|
)
|
|
|
|
return MistakeListResponse(mistakes=mistake_responses, total=total)
|
|
|
|
|
|
@router.post("/add", response_model=MistakeResponse, status_code=status.HTTP_201_CREATED)
|
|
async def add_to_mistakes(
|
|
mistake_data: MistakeAdd,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Manually add a question to mistake book"""
|
|
|
|
# Verify question exists and user has access to it
|
|
result = await db.execute(
|
|
select(Question)
|
|
.join(Exam)
|
|
.where(
|
|
and_(
|
|
Question.id == mistake_data.question_id,
|
|
Exam.user_id == current_user.id
|
|
)
|
|
)
|
|
)
|
|
question = result.scalar_one_or_none()
|
|
|
|
if not question:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Question not found or you don't have access"
|
|
)
|
|
|
|
# Check if already in mistake book
|
|
result = await db.execute(
|
|
select(UserMistake).where(
|
|
and_(
|
|
UserMistake.user_id == current_user.id,
|
|
UserMistake.question_id == mistake_data.question_id
|
|
)
|
|
)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="Question already in mistake book"
|
|
)
|
|
|
|
# Add to mistake book
|
|
new_mistake = UserMistake(
|
|
user_id=current_user.id,
|
|
question_id=mistake_data.question_id
|
|
)
|
|
db.add(new_mistake)
|
|
await db.commit()
|
|
await db.refresh(new_mistake)
|
|
|
|
# Load question relationship
|
|
result = await db.execute(
|
|
select(UserMistake)
|
|
.options(selectinload(UserMistake.question))
|
|
.where(UserMistake.id == new_mistake.id)
|
|
)
|
|
new_mistake = result.scalar_one()
|
|
|
|
return MistakeResponse(
|
|
id=new_mistake.id,
|
|
user_id=new_mistake.user_id,
|
|
question_id=new_mistake.question_id,
|
|
question=new_mistake.question,
|
|
created_at=new_mistake.created_at
|
|
)
|
|
|
|
|
|
@router.delete("/{mistake_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def remove_from_mistakes(
|
|
mistake_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Remove a question from mistake book"""
|
|
|
|
# Get mistake and verify ownership
|
|
result = await db.execute(
|
|
select(UserMistake).where(
|
|
and_(
|
|
UserMistake.id == mistake_id,
|
|
UserMistake.user_id == current_user.id
|
|
)
|
|
)
|
|
)
|
|
mistake = result.scalar_one_or_none()
|
|
|
|
if not mistake:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Mistake record not found"
|
|
)
|
|
|
|
await db.delete(mistake)
|
|
await db.commit()
|
|
|
|
|
|
@router.delete("/question/{question_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def remove_question_from_mistakes(
|
|
question_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Remove a question from mistake book by question ID"""
|
|
|
|
# Get mistake and verify ownership
|
|
result = await db.execute(
|
|
select(UserMistake).where(
|
|
and_(
|
|
UserMistake.question_id == question_id,
|
|
UserMistake.user_id == current_user.id
|
|
)
|
|
)
|
|
)
|
|
mistake = result.scalar_one_or_none()
|
|
|
|
if not mistake:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Question not found in mistake book"
|
|
)
|
|
|
|
await db.delete(mistake)
|
|
await db.commit()
|