AnCao/internal/handlers/exam_grading.go
yanlongqi 4c06a8acd5 feat: 实现完整的考试系统
实现了包含试卷管理、考试答题、AI智能阅卷的完整考试流程。

**后端新增功能**:
- 试卷管理: 创建试卷、获取试卷列表和详情
- 考试流程: 开始考试、提交答案、查询结果
- AI阅卷: 异步阅卷系统,支持简答题和论述题AI评分
- 实时答题: 题目级别的答案保存和加载
- 数据模型: ExamRecord(考试记录)、ExamUserAnswer(用户答案)

**前端新增页面**:
- 考试管理页面: 试卷列表展示,支持开始/继续考试
- 答题页面: 左侧题目列表、右侧答题区,支持实时保存
- 成绩查看页面: 展示详细评分结果和AI评语

**技术亮点**:
- 按题型固定分值配置(总分100分)
- 异步阅卷机制,提交后立即返回
- 答案实时保存,支持断点续答
- AI评分集成,智能评判主观题
- 响应式设计,适配移动端和PC端

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 03:55:24 +08:00

258 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"ankao/internal/database"
"ankao/internal/models"
"ankao/internal/services"
"encoding/json"
"fmt"
"log"
"gorm.io/datatypes"
)
// ReGradeExam 公开的重新阅卷函数,可被外部调用
func ReGradeExam(recordID uint, examID uint, userID uint) {
gradeExam(recordID, examID, userID)
}
// gradeExam 异步阅卷函数
func gradeExam(recordID uint, examID uint, userID uint) {
db := database.GetDB()
// 查询考试记录
var record models.ExamRecord
if err := db.Where("id = ?", recordID).First(&record).Error; err != nil {
log.Printf("查询考试记录失败: %v", err)
return
}
// 查询试卷
var exam models.Exam
if err := db.Where("id = ?", examID).First(&exam).Error; err != nil {
log.Printf("查询试卷失败: %v", err)
return
}
// 从 ExamUserAnswer 表读取所有答案
var userAnswers []models.ExamUserAnswer
if err := db.Where("exam_record_id = ?", recordID).Find(&userAnswers).Error; err != nil {
log.Printf("查询用户答案失败: %v", err)
return
}
// 转换为 map 格式方便查找
answersMap := make(map[uint]interface{})
for _, ua := range userAnswers {
var answer interface{}
if err := json.Unmarshal(ua.Answer, &answer); err != nil {
log.Printf("解析答案失败: %v", err)
continue
}
answersMap[ua.QuestionID] = answer
}
// 解析题目ID列表
var questionIDs []uint
if err := json.Unmarshal(exam.QuestionIDs, &questionIDs); err != nil {
log.Printf("解析题目ID失败: %v", err)
return
}
// 查询题目详情
var questions []models.PracticeQuestion
if err := db.Where("id IN ?", questionIDs).Find(&questions).Error; err != nil {
log.Printf("查询题目失败: %v", err)
return
}
// 使用固定的题型分值映射
scoreMap := map[string]float64{
"fill-in-blank": 2.0,
"true-false": 2.0,
"multiple-choice": 1.0,
"multiple-selection": 2.5,
"short-answer": 10.0,
"ordinary-essay": 5.0,
"management-essay": 5.0,
}
// 评分
totalScore := 0.0
aiService := services.NewAIGradingService()
for _, question := range questions {
userAnswerRaw, answered := answersMap[question.ID]
if !answered {
// 更新数据库中的 ExamUserAnswer 记录为未作答
var userAnswer models.ExamUserAnswer
result := db.Where("exam_record_id = ? AND question_id = ?", recordID, question.ID).First(&userAnswer)
if result.Error == nil {
updates := map[string]interface{}{
"is_correct": false,
"score": 0.0,
}
db.Model(&userAnswer).Updates(updates)
}
continue
}
// 根据题型判断答案
var isCorrect bool
var score float64
var aiGrading *models.AIGrading
switch question.Type {
case "fill-in-blank":
// 填空题:比较数组
userAnswerArr, ok := userAnswerRaw.([]interface{})
if !ok {
isCorrect = false
score = 0
// 更新数据库
var userAnswer models.ExamUserAnswer
if result := db.Where("exam_record_id = ? AND question_id = ?", recordID, question.ID).First(&userAnswer); result.Error == nil {
db.Model(&userAnswer).Updates(map[string]interface{}{
"is_correct": false,
"score": 0.0,
})
}
continue
}
var correctAnswers []string
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswers); err != nil {
log.Printf("解析填空题答案失败: %v", err)
continue
}
isCorrect = len(userAnswerArr) == len(correctAnswers)
if isCorrect {
for i, ua := range userAnswerArr {
if i >= len(correctAnswers) || fmt.Sprintf("%v", ua) != correctAnswers[i] {
isCorrect = false
break
}
}
}
if isCorrect {
score = scoreMap["fill-in-blank"]
}
case "true-false":
// 判断题 - AnswerData 直接存储 "true" 或 "false" 字符串
correctAnswer := question.AnswerData
isCorrect = fmt.Sprintf("%v", userAnswerRaw) == correctAnswer
if isCorrect {
score = scoreMap["true-false"]
}
case "multiple-choice":
correctAnswer := question.AnswerData
isCorrect = fmt.Sprintf("\"%v\"", userAnswerRaw) == correctAnswer
if isCorrect {
score = scoreMap["multiple-choice"]
}
case "multiple-selection":
// 多选题:比较数组(顺序无关)
userAnswerArr, ok := userAnswerRaw.([]interface{})
if !ok {
isCorrect = false
score = 0
// 更新数据库
var userAnswer models.ExamUserAnswer
if result := db.Where("exam_record_id = ? AND question_id = ?", recordID, question.ID).First(&userAnswer); result.Error == nil {
db.Model(&userAnswer).Updates(map[string]interface{}{
"is_correct": false,
"score": 0.0,
})
}
continue
}
var correctAnswers []string
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswers); err != nil {
log.Printf("解析多选题答案失败: %v", err)
continue
}
userAnswerSet := make(map[string]bool)
for _, ua := range userAnswerArr {
userAnswerSet[fmt.Sprintf("%v", ua)] = true
}
isCorrect = len(userAnswerSet) == len(correctAnswers)
if isCorrect {
for _, ca := range correctAnswers {
if !userAnswerSet[ca] {
isCorrect = false
break
}
}
}
if isCorrect {
score = scoreMap["multiple-selection"]
}
case "short-answer", "ordinary-essay", "management-essay":
// 简答题和论述题使用AI评分
// AnswerData 直接存储答案文本
correctAnswer := question.AnswerData
userAnswerStr := fmt.Sprintf("%v", userAnswerRaw)
aiResult, err := aiService.GradeShortAnswer(question.Question, correctAnswer, userAnswerStr)
if err != nil {
log.Printf("AI评分失败: %v", err)
isCorrect = false
score = 0
} else {
isCorrect = aiResult.IsCorrect
// 按AI评分比例计算
var questionScore float64
if question.Type == "short-answer" {
questionScore = scoreMap["short-answer"]
} else if question.Type == "ordinary-essay" {
questionScore = scoreMap["ordinary-essay"]
} else if question.Type == "management-essay" {
questionScore = scoreMap["management-essay"]
}
score = questionScore * (aiResult.Score / 100.0)
aiGrading = &models.AIGrading{
Score: aiResult.Score,
Feedback: aiResult.Feedback,
Suggestion: aiResult.Suggestion,
}
}
}
totalScore += score
// 更新数据库中的 ExamUserAnswer 记录
var userAnswer models.ExamUserAnswer
result := db.Where("exam_record_id = ? AND question_id = ?", recordID, question.ID).First(&userAnswer)
if result.Error == nil {
// 序列化 AI 评分数据
var aiGradingJSON datatypes.JSON
if aiGrading != nil {
aiGradingData, _ := json.Marshal(aiGrading)
aiGradingJSON = datatypes.JSON(aiGradingData)
}
// 更新评分结果
updates := map[string]interface{}{
"is_correct": isCorrect,
"score": score,
"ai_grading_data": aiGradingJSON,
}
db.Model(&userAnswer).Updates(updates)
}
}
// 保存分数和状态到考试记录
record.Score = totalScore
record.Status = "graded"
record.IsPassed = totalScore >= float64(exam.PassScore)
if err := db.Save(&record).Error; err != nil {
log.Printf("保存考试记录失败: %v", err)
return
}
log.Printf("阅卷完成: 考试记录ID=%d, 总分=%.2f, 是否通过=%v", recordID, totalScore, record.IsPassed)
}