修复复盘保存失败:原地更新成绩并新增 review 专用接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 16:38:20 +08:00
parent ff4e0b1d37
commit bec9df5d6f
6 changed files with 128 additions and 37 deletions
+79 -11
View File
@@ -7,7 +7,15 @@ 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, ReviewStatusEnum, ScoreOut, TrendResponse
from app.schemas import (
ExamCreate,
ExamOut,
ExamReviewUpdate,
ExamUpdate,
ReviewStatusEnum,
ScoreOut,
TrendResponse,
)
from app.services.score_trend import build_trend
from app.services.student_access import get_student_for_user
@@ -35,7 +43,13 @@ def _parse_review_statuses(raw: str | None) -> list[ReviewStatusEnum]:
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)
values: list[str] = []
for item in statuses:
if isinstance(item, ReviewStatusEnum):
values.append(item.value)
else:
values.append(str(item))
return json.dumps(values, ensure_ascii=False)
def _score_to_out(score: SubjectScore) -> ScoreOut:
@@ -62,18 +76,34 @@ def _exam_to_out(exam: ExamRecord) -> ExamOut:
def _apply_scores(db: Session, exam: ExamRecord, scores_data):
exam.scores.clear()
existing_by_subject = {s.subject_id: s for s in list(exam.scores)}
keep_subject_ids: set[int] = set()
for item in scores_data:
keep_subject_ids.add(item.subject_id)
ratio = round(item.obtained_score / item.total_score, 4)
exam.scores.append(
SubjectScore(
subject_id=item.subject_id,
total_score=item.total_score,
obtained_score=item.obtained_score,
ratio=ratio,
review_statuses_json=_serialize_review_statuses(item.review_statuses),
review_json = _serialize_review_statuses(item.review_statuses)
existing = existing_by_subject.get(item.subject_id)
if existing is not None:
existing.total_score = item.total_score
existing.obtained_score = item.obtained_score
existing.ratio = ratio
existing.review_statuses_json = review_json
else:
exam.scores.append(
SubjectScore(
subject_id=item.subject_id,
total_score=item.total_score,
obtained_score=item.obtained_score,
ratio=ratio,
review_statuses_json=review_json,
)
)
)
for subject_id, score in existing_by_subject.items():
if subject_id not in keep_subject_ids:
exam.scores.remove(score)
db.delete(score)
@router.get("/students/{student_id}/exams", response_model=list[ExamOut])
@@ -177,6 +207,44 @@ def update_exam(
return _exam_to_out(exam)
@router.patch("/exams/{exam_id}/review", response_model=ExamOut)
def update_exam_review(
exam_id: uuid.UUID,
data: ExamReviewUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
exam = (
db.query(ExamRecord)
.options(joinedload(ExamRecord.scores).joinedload(SubjectScore.subject))
.join(ExamRecord.student)
.filter(ExamRecord.id == exam_id)
.first()
)
if exam is None or exam.student.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="考试记录不存在")
by_subject = {s.subject_id: s for s in exam.scores}
for item in data.reviews:
score = by_subject.get(item.subject_id)
if score is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"科目 {item.subject_id} 不在该次考试中",
)
score.review_statuses_json = _serialize_review_statuses(item.review_statuses)
db.commit()
db.refresh(exam)
exam = (
db.query(ExamRecord)
.options(joinedload(ExamRecord.scores).joinedload(SubjectScore.subject))
.filter(ExamRecord.id == exam.id)
.first()
)
return _exam_to_out(exam)
@router.delete("/exams/{exam_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_exam(
exam_id: uuid.UUID,
+9
View File
@@ -219,6 +219,15 @@ class ExamUpdate(BaseModel):
scores: list[ScoreInput] | None = None
class ReviewScoreInput(BaseModel):
subject_id: int
review_statuses: list[ReviewStatusEnum] = []
class ExamReviewUpdate(BaseModel):
reviews: list[ReviewScoreInput] = []
class ExamOut(BaseModel):
id: UUID
exam_type: ExamTypeEnum