from datetime import date, datetime from enum import Enum from uuid import UUID from pydantic import BaseModel, Field, field_validator class ExamTypeEnum(str, Enum): weekly = "weekly" monthly = "monthly" final = "final" class WrongQuestionStatusEnum(str, Enum): pending = "pending" ocr_done = "ocr_done" solved = "solved" failed = "failed" class SchoolLevelEnum(str, Enum): junior_high = "junior_high" senior_high = "senior_high" class TokenResponse(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" class UserRegister(BaseModel): username: str = Field(min_length=3, max_length=64) password: str = Field(min_length=6, max_length=128) class UserLogin(BaseModel): username: str password: str class RefreshRequest(BaseModel): refresh_token: str class UserOut(BaseModel): id: UUID username: str created_at: datetime model_config = {"from_attributes": True} class StudentCreate(BaseModel): name: str = Field(min_length=1, max_length=64) school_level: SchoolLevelEnum = SchoolLevelEnum.junior_high grade: str | None = None class_name: str | None = None class StudentUpdate(BaseModel): name: str | None = None school_level: SchoolLevelEnum | None = None grade: str | None = None class_name: str | None = None class StudentOut(BaseModel): id: UUID name: str school_level: SchoolLevelEnum grade: str | None class_name: str | None created_at: datetime model_config = {"from_attributes": True} class SubjectOut(BaseModel): id: int name: str model_config = {"from_attributes": True} class ScoreInput(BaseModel): subject_id: int total_score: float obtained_score: float @field_validator("total_score") @classmethod def validate_total(cls, v: float) -> float: if v <= 0: raise ValueError("总分必须大于0") return v @field_validator("obtained_score") @classmethod def validate_obtained(cls, v: float, info) -> float: total = info.data.get("total_score") if total is not None and v > total: raise ValueError("得分不能大于总分") if v < 0: raise ValueError("得分不能为负") return v class ScoreOut(BaseModel): id: UUID subject_id: int subject_name: str | None = None total_score: float obtained_score: float ratio: float model_config = {"from_attributes": True} class ExamCreate(BaseModel): exam_type: ExamTypeEnum exam_date: date title: str | None = None scores: list[ScoreInput] = [] class ExamUpdate(BaseModel): exam_type: ExamTypeEnum | None = None exam_date: date | None = None title: str | None = None scores: list[ScoreInput] | None = None class ExamOut(BaseModel): id: UUID exam_type: ExamTypeEnum exam_date: date title: str | None created_at: datetime scores: list[ScoreOut] = [] model_config = {"from_attributes": True} class TrendPoint(BaseModel): exam_id: UUID exam_type: ExamTypeEnum exam_date: date title: str | None ratio: float ratio_percent: float delta: float | None = None delta_percent: float | None = None is_volatile: bool = False direction: str | None = None class TrendResponse(BaseModel): subject_id: int subject_name: str threshold: float points: list[TrendPoint] class WrongQuestionOut(BaseModel): id: UUID student_id: UUID subject_id: int subject_name: str | None = None image_path: str ocr_raw_text: str | None question_text: str | None solution_text: str | None status: WrongQuestionStatusEnum created_at: datetime model_config = {"from_attributes": True} class WrongQuestionUpdate(BaseModel): question_text: str | None = None solution_text: str | None = None subject_id: int | None = None