🎉 Initial commit: QQuiz - 智能刷题与题库管理平台

## 功能特性

 **核心功能**
- 多文件上传与智能去重(基于 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>
This commit is contained in:
2025-12-01 12:39:46 +08:00
commit c5ecbeaec2
53 changed files with 6211 additions and 0 deletions

192
backend/routers/mistake.py Normal file
View File

@@ -0,0 +1,192 @@
"""
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()