考试明细单行展示,选中科目后增加 AI 解读与建议。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 17:32:28 +08:00
parent 5f00f07dbe
commit 02e7ba055a
10 changed files with 341 additions and 125 deletions
+76
View File
@@ -12,15 +12,53 @@ from app.schemas import (
ExamOut,
ExamReviewUpdate,
ExamUpdate,
ReviewInsightRequest,
ReviewInsightResponse,
ReviewStatusEnum,
ScoreOut,
TrendResponse,
)
from app.services import llm as llm_service
from app.services.score_trend import build_trend
from app.services.student_access import get_student_for_user
router = APIRouter(tags=["exams"])
_EXAM_TYPE_LABELS = {"weekly": "周考", "monthly": "月考", "final": "期末"}
_REVIEW_STATUS_LABELS = {
ReviewStatusEnum.careless: "粗心",
ReviewStatusEnum.unknown: "不会",
ReviewStatusEnum.nervous: "紧张",
ReviewStatusEnum.normal: "正常发挥",
}
def _subject_display_name(score: SubjectScore) -> str:
return score.subject.name if score.subject else f"科目{score.subject_id}"
def _build_review_records_text(exams: list[ExamRecord], subject_name: str) -> str:
lines: list[str] = []
for exam in sorted(exams, key=lambda item: item.exam_date, reverse=True):
for score in exam.scores:
if _subject_display_name(score) != subject_name:
continue
statuses = _parse_review_statuses(score.review_statuses_json)
if not statuses:
continue
type_label = _EXAM_TYPE_LABELS.get(exam.exam_type.value, exam.exam_type.value)
status_text = "".join(_REVIEW_STATUS_LABELS.get(s, s.value) for s in statuses)
ratio = float(score.ratio) * 100
line = (
f"- {exam.exam_date} {type_label} "
f"得分{float(score.obtained_score):g}/{float(score.total_score):g}({ratio:.1f}%) "
f"状态:{status_text}"
)
if exam.title:
line += f" 备注:{exam.title}"
lines.append(line)
return "\n".join(lines)
def _parse_review_statuses(raw: str | None) -> list[ReviewStatusEnum]:
if not raw:
@@ -245,6 +283,44 @@ def update_exam_review(
return _exam_to_out(exam)
@router.post("/students/{student_id}/review-insight", response_model=ReviewInsightResponse)
async def review_insight(
student_id: uuid.UUID,
data: ReviewInsightRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
student = get_student_for_user(db, student_id, current_user.id)
exams = (
db.query(ExamRecord)
.options(joinedload(ExamRecord.scores).joinedload(SubjectScore.subject))
.filter(ExamRecord.student_id == student_id)
.all()
)
subject_name = data.subject_name.strip()
records = _build_review_records_text(exams, subject_name)
if not records:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该科目暂无复盘数据",
)
ai_cfg = llm_service.load_ai_config(db)
try:
insight = await llm_service.generate_review_insight(
ai_cfg,
subject_name,
records,
student.school_level,
)
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"AI 调用失败: {exc}",
) from exc
return ReviewInsightResponse(insight=insight)
@router.delete("/exams/{exam_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_exam(
exam_id: uuid.UUID,