Files
secondary-school-grade-archive/backend/app/schemas/__init__.py
T
dekun 1cb3c7fad5 新增作文区与 AI 解读开关,修复 CSV 导出。
系统设置可关闭成绩复盘 AI;学生详情增加作文区(OCR/手动题目、方案与范文、历史与 MD 下载);导出改用 UTF-8 文件名响应。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:42:17 +08:00

338 lines
8.2 KiB
Python

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 ReviewStatusEnum(str, Enum):
careless = "careless"
unknown = "unknown"
nervous = "nervous"
normal = "normal"
REVIEW_STATUS_VALUES = {s.value for s in ReviewStatusEnum}
class WrongQuestionStatusEnum(str, Enum):
pending = "pending"
ocr_done = "ocr_done"
solved = "solved"
failed = "failed"
class WrongQuestionCategoryEnum(str, Enum):
regular = "regular"
olympiad = "olympiad"
class AIProviderEnum(str, Enum):
ollama = "ollama"
openai = "openai"
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
is_superuser: bool = False
created_at: datetime
model_config = {"from_attributes": True}
class PublicSettingsOut(BaseModel):
registration_enabled: bool
class AppFeaturesOut(BaseModel):
ai_review_enabled: bool
class SystemSettingsOut(BaseModel):
registration_enabled: bool
ai_review_enabled: bool = True
ai_provider: AIProviderEnum
ollama_base_url: str | None = None
ollama_model: str | None = None
openai_base_url: str | None = None
openai_model: str | None = None
openai_api_key_set: bool = False
ocr_service_url: str | None = None
updated_at: datetime
model_config = {"from_attributes": True}
class SystemSettingsUpdate(BaseModel):
registration_enabled: bool | None = None
ai_review_enabled: bool | None = None
ai_provider: AIProviderEnum | None = None
ollama_base_url: str | None = None
ollama_model: str | None = None
openai_base_url: str | None = None
openai_model: str | None = None
openai_api_key: str | None = None
ocr_service_url: str | None = None
class AdminProfileUpdate(BaseModel):
username: str | None = Field(default=None, min_length=3, max_length=64)
current_password: str | None = None
password: str | None = Field(default=None, min_length=6, max_length=128)
class AdminUserCreate(BaseModel):
username: str = Field(min_length=3, max_length=64)
password: str = Field(min_length=6, max_length=128)
class AdminUserPasswordUpdate(BaseModel):
password: str = Field(min_length=6, max_length=128)
class AdminUserOut(BaseModel):
id: UUID
username: str
is_superuser: bool
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
review_statuses: list[ReviewStatusEnum] = []
@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
@field_validator("review_statuses")
@classmethod
def validate_review_statuses(cls, v: list[ReviewStatusEnum]) -> list[ReviewStatusEnum]:
seen: set[str] = set()
unique: list[ReviewStatusEnum] = []
for item in v:
key = item.value if isinstance(item, ReviewStatusEnum) else str(item)
if key not in seen:
seen.add(key)
unique.append(item if isinstance(item, ReviewStatusEnum) else ReviewStatusEnum(key))
return unique
class ScoreOut(BaseModel):
id: UUID
subject_id: int
subject_name: str | None = None
total_score: float
obtained_score: float
ratio: float
review_statuses: list[ReviewStatusEnum] = []
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 ReviewScoreInput(BaseModel):
subject_id: int
review_statuses: list[ReviewStatusEnum] = []
class ExamReviewUpdate(BaseModel):
reviews: list[ReviewScoreInput] = []
class ReviewInsightRequest(BaseModel):
subject_name: str = Field(..., min_length=1, max_length=32)
class ReviewInsightResponse(BaseModel):
insight: str
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
category: WrongQuestionCategoryEnum
image_path: str
ocr_raw_text: str | None
question_text: str | None
solution_approach: str | None = None
solution_text: str | None
mark_regions: list[dict] | None = None
has_annotated_image: bool = False
has_cropped_image: bool = False
error_message: str | None = None
status: WrongQuestionStatusEnum
created_at: datetime
model_config = {"from_attributes": True}
class WrongQuestionUpdate(BaseModel):
question_text: str | None = None
solution_approach: str | None = None
solution_text: str | None = None
subject_id: int | None = None
class CompositionStatusEnum(str, Enum):
pending = "pending"
generating = "generating"
done = "done"
failed = "failed"
class CompositionInputModeEnum(str, Enum):
manual = "manual"
ocr = "ocr"
class CompositionCreate(BaseModel):
topic: str = Field(..., min_length=1, max_length=4000)
input_mode: CompositionInputModeEnum = CompositionInputModeEnum.manual
class CompositionOcrOut(BaseModel):
text: str
class CompositionOut(BaseModel):
id: UUID
student_id: UUID
topic: str
input_mode: CompositionInputModeEnum
writing_plan: str | None = None
sample_essay: str | None = None
error_message: str | None = None
status: CompositionStatusEnum
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}