成绩复盘与 PC 端上传优化:各科考试状态多选及树状统计。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -115,6 +115,7 @@ class SubjectScore(Base):
|
||||
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))
|
||||
review_statuses_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
exam_record: Mapped["ExamRecord"] = relationship(back_populates="scores")
|
||||
subject: Mapped["Subject"] = relationship(back_populates="scores")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
@@ -6,13 +7,37 @@ from sqlalchemy.orm import Session, joinedload
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_user
|
||||
from app.models.user import ExamRecord, SubjectScore, User
|
||||
from app.schemas import ExamCreate, ExamOut, ExamUpdate, ScoreOut, TrendResponse
|
||||
from app.schemas import ExamCreate, ExamOut, ExamUpdate, ReviewStatusEnum, ScoreOut, TrendResponse
|
||||
from app.services.score_trend import build_trend
|
||||
from app.services.student_access import get_student_for_user
|
||||
|
||||
router = APIRouter(tags=["exams"])
|
||||
|
||||
|
||||
def _parse_review_statuses(raw: str | None) -> list[ReviewStatusEnum]:
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
result: list[ReviewStatusEnum] = []
|
||||
for item in data:
|
||||
try:
|
||||
result.append(ReviewStatusEnum(str(item)))
|
||||
except ValueError:
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def _serialize_review_statuses(statuses: list[ReviewStatusEnum] | None) -> str | None:
|
||||
if not statuses:
|
||||
return None
|
||||
return json.dumps([s.value for s in statuses], ensure_ascii=False)
|
||||
|
||||
|
||||
def _score_to_out(score: SubjectScore) -> ScoreOut:
|
||||
return ScoreOut(
|
||||
id=score.id,
|
||||
@@ -21,6 +46,7 @@ def _score_to_out(score: SubjectScore) -> ScoreOut:
|
||||
total_score=float(score.total_score),
|
||||
obtained_score=float(score.obtained_score),
|
||||
ratio=float(score.ratio),
|
||||
review_statuses=_parse_review_statuses(score.review_statuses_json),
|
||||
)
|
||||
|
||||
|
||||
@@ -45,6 +71,7 @@ def _apply_scores(db: Session, exam: ExamRecord, scores_data):
|
||||
total_score=item.total_score,
|
||||
obtained_score=item.obtained_score,
|
||||
ratio=ratio,
|
||||
review_statuses_json=_serialize_review_statuses(item.review_statuses),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ class ExamTypeEnum(str, Enum):
|
||||
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"
|
||||
@@ -151,6 +161,7 @@ class ScoreInput(BaseModel):
|
||||
subject_id: int
|
||||
total_score: float
|
||||
obtained_score: float
|
||||
review_statuses: list[ReviewStatusEnum] = []
|
||||
|
||||
@field_validator("total_score")
|
||||
@classmethod
|
||||
@@ -169,6 +180,18 @@ class ScoreInput(BaseModel):
|
||||
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
|
||||
@@ -177,6 +200,7 @@ class ScoreOut(BaseModel):
|
||||
total_score: float
|
||||
obtained_score: float
|
||||
ratio: float
|
||||
review_statuses: list[ReviewStatusEnum] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
@@ -85,3 +85,9 @@ def run_migrations() -> None:
|
||||
with engine.begin() as conn:
|
||||
for clause in wq_alters:
|
||||
conn.execute(text(f"ALTER TABLE wrong_questions {clause}"))
|
||||
|
||||
if "subject_scores" in tables:
|
||||
ss_columns = {col["name"] for col in inspector.get_columns("subject_scores")}
|
||||
if "review_statuses_json" not in ss_columns:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("ALTER TABLE subject_scores ADD COLUMN review_statuses_json TEXT"))
|
||||
|
||||
Reference in New Issue
Block a user