成绩复盘与 PC 端上传优化:各科考试状态多选及树状统计。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 16:33:21 +08:00
parent acfe002fbf
commit ff4e0b1d37
14 changed files with 956 additions and 556 deletions
+1
View File
@@ -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")
+28 -1
View File
@@ -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),
)
)
+24
View File
@@ -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}
+6
View File
@@ -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"))