实现了包含试卷管理、考试答题、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>
258 lines
7.2 KiB
Go
258 lines
7.2 KiB
Go
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)
|
||
}
|