考试明细单行展示,选中科目后增加 AI 解读与建议。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user