Initial commit: secondary school grade archive system.

Add FastAPI/React app with Docker deployment, Ubuntu one-click install, and docs for junior/senior high score tracking and mistake bank.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 11:18:58 +08:00
commit e329d3398a
76 changed files with 8506 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
import enum
import uuid
from datetime import date, datetime, timezone
from sqlalchemy import Date, DateTime, Enum, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class ExamType(str, enum.Enum):
weekly = "weekly"
monthly = "monthly"
final = "final"
class WrongQuestionStatus(str, enum.Enum):
pending = "pending"
ocr_done = "ocr_done"
solved = "solved"
failed = "failed"
class SchoolLevel(str, enum.Enum):
junior_high = "junior_high"
senior_high = "senior_high"
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
students: Mapped[list["Student"]] = relationship(back_populates="user", cascade="all, delete-orphan")
class Student(Base):
__tablename__ = "students"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
name: Mapped[str] = mapped_column(String(64))
school_level: Mapped[SchoolLevel] = mapped_column(
Enum(SchoolLevel), default=SchoolLevel.junior_high
)
grade: Mapped[str | None] = mapped_column(String(32), nullable=True)
class_name: Mapped[str | None] = mapped_column(String(32), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
user: Mapped["User"] = relationship(back_populates="students")
exam_records: Mapped[list["ExamRecord"]] = relationship(
back_populates="student", cascade="all, delete-orphan"
)
wrong_questions: Mapped[list["WrongQuestion"]] = relationship(
back_populates="student", cascade="all, delete-orphan"
)
class Subject(Base):
__tablename__ = "subjects"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(32), unique=True)
scores: Mapped[list["SubjectScore"]] = relationship(back_populates="subject")
wrong_questions: Mapped[list["WrongQuestion"]] = relationship(back_populates="subject")
class ExamRecord(Base):
__tablename__ = "exam_records"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
student_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("students.id"), index=True)
exam_type: Mapped[ExamType] = mapped_column(Enum(ExamType))
exam_date: Mapped[date] = mapped_column(Date)
title: Mapped[str | None] = mapped_column(String(128), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
student: Mapped["Student"] = relationship(back_populates="exam_records")
scores: Mapped[list["SubjectScore"]] = relationship(
back_populates="exam_record", cascade="all, delete-orphan"
)
class SubjectScore(Base):
__tablename__ = "subject_scores"
__table_args__ = (UniqueConstraint("exam_record_id", "subject_id", name="uq_exam_subject"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
exam_record_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("exam_records.id"), index=True
)
subject_id: Mapped[int] = mapped_column(Integer, ForeignKey("subjects.id"))
total_score: Mapped[float] = mapped_column(Numeric(8, 2))
obtained_score: Mapped[float] = mapped_column(Numeric(8, 2))
ratio: Mapped[float] = mapped_column(Numeric(8, 4))
exam_record: Mapped["ExamRecord"] = relationship(back_populates="scores")
subject: Mapped["Subject"] = relationship(back_populates="scores")
class WrongQuestion(Base):
__tablename__ = "wrong_questions"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
student_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("students.id"), index=True)
subject_id: Mapped[int] = mapped_column(Integer, ForeignKey("subjects.id"))
image_path: Mapped[str] = mapped_column(String(512))
ocr_raw_text: Mapped[str | None] = mapped_column(Text, nullable=True)
question_text: Mapped[str | None] = mapped_column(Text, nullable=True)
solution_text: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[WrongQuestionStatus] = mapped_column(
Enum(WrongQuestionStatus), default=WrongQuestionStatus.pending
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
student: Mapped["Student"] = relationship(back_populates="wrong_questions")
subject: Mapped["Subject"] = relationship(back_populates="wrong_questions")