AnCao/internal/handlers/exam_grading.go
yanlongqi 3704d52a26 调整模拟考试试卷结构和评分规则
修改试卷组成为:
- 填空题:20道(40分,每题2分)
- 判断题:10道(10分,每题1分)
- 单选题:10道(10分,每题1分)
- 多选题:10道(20分,每题2分)
- 简答题:1道(10分)
- 论述题:2道供选择,根据职位类型作答1道(10分)

总分:100分(包含两道论述题但用户只需选答其中一道)

主要变更:
- exam_handler.go: 更新试卷创建时的题型配置
- exam_grading.go: 更新阅卷时的分值映射表
- 确保创建和评分逻辑一致

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 21:18:18 +08:00

270 lines
7.7 KiB
Go
Raw Permalink 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[int64]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, // 填空题每题2分
"true-false": 1.0, // 判断题每题1分
"multiple-choice": 1.0, // 单选题每题1分
"multiple-selection": 2.0, // 多选题每题2分
"short-answer": 10.0, // 简答题10分
"ordinary-essay": 10.0, // 论述题10分
"management-essay": 10.0, // 论述题10分
}
// 评分
totalScore := 0.0
aiService, err := services.NewAIGradingService()
if err != nil {
log.Printf("AI服务初始化失败: %v将跳过AI评分", err)
// 不返回错误继续评分流程只是跳过AI评分
}
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)
// 检查AI服务是否可用
if aiService == nil {
log.Printf("AI服务不可用无法评分问题 %d", question.ID)
isCorrect = false
score = 0
} else {
aiResult, aiErr := aiService.GradeShortAnswer(question.Question, correctAnswer, userAnswerStr)
if aiErr != nil {
log.Printf("AI评分失败: %v", aiErr)
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)
}