AnCao/internal/handlers/exam_handler.go
yanlongqi a62c5b3e62 fix: 修复考试题目选项数据缺失问题
- 在GetExam API中使用convertToDTO函数转换题目数据
- 确保选项数据(options)正确解析并返回给前端
- 修复前端ExamOnline渲染选择题时的undefined错误

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 21:20:34 +08:00

421 lines
12 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"
"math/rand"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
// GenerateExam 生成考试
func GenerateExam(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
return
}
db := database.GetDB()
// 使用默认配置
config := models.DefaultExamConfig
// 按题型随机抽取题目
questionsByType := make(map[string][]models.PracticeQuestion)
questionTypes := []struct {
Type string
Count int
}{
{"fill-in-blank", config.FillInBlank},
{"true-false", config.TrueFalse},
{"multiple-choice", config.MultipleChoice},
{"multiple-selection", config.MultipleSelection},
{"short-answer", config.ShortAnswer},
{"ordinary-essay", config.OrdinaryEssay},
{"management-essay", config.ManagementEssay},
}
var allQuestionIDs []uint
for _, qt := range questionTypes {
var questions []models.PracticeQuestion
if err := db.Where("type = ?", qt.Type).Find(&questions).Error; err != nil {
log.Printf("查询题目失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询题目失败"})
return
}
// 检查题目数量是否足够
if len(questions) < qt.Count {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": fmt.Sprintf("题型 %s 题目数量不足,需要 %d 道,实际 %d 道", qt.Type, qt.Count, len(questions)),
})
return
}
// 随机抽取 (Fisher-Yates 洗牌算法)
rand.Seed(time.Now().UnixNano())
for i := len(questions) - 1; i > 0; i-- {
j := rand.Intn(i + 1)
questions[i], questions[j] = questions[j], questions[i]
}
selectedQuestions := questions[:qt.Count]
questionsByType[qt.Type] = selectedQuestions
// 收集题目ID
for _, q := range selectedQuestions {
allQuestionIDs = append(allQuestionIDs, q.ID)
}
}
// 将题目ID列表转为JSON
questionIDsJSON, err := json.Marshal(allQuestionIDs)
if err != nil {
log.Printf("序列化题目ID失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "生成考试失败"})
return
}
// 创建考试记录
exam := models.Exam{
UserID: userID.(uint),
QuestionIDs: string(questionIDsJSON),
Status: "draft",
}
if err := db.Create(&exam).Error; err != nil {
log.Printf("创建考试记录失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "创建考试失败"})
return
}
// 返回考试信息
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"exam_id": exam.ID,
"question_ids": allQuestionIDs,
"created_at": exam.CreatedAt,
},
})
}
// GetExam 获取考试详情
func GetExam(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
return
}
examIDStr := c.Param("id")
examID, err := strconv.ParseUint(examIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试ID"})
return
}
db := database.GetDB()
// 查询考试记录
var exam models.Exam
if err := db.Where("id = ? AND user_id = ?", examID, userID).First(&exam).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试不存在"})
return
}
// 解析题目ID列表
var questionIDs []uint
if err := json.Unmarshal([]byte(exam.QuestionIDs), &questionIDs); err != nil {
log.Printf("解析题目ID失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "解析考试数据失败"})
return
}
// 是否显示答案
showAnswer := c.Query("show_answer") == "true"
// 查询题目详情
var questions []models.PracticeQuestion
if err := db.Where("id IN ?", questionIDs).Find(&questions).Error; err != nil {
log.Printf("查询题目失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询题目失败"})
return
}
// 按原始顺序排序题目并转换为DTO
questionMap := make(map[uint]models.PracticeQuestion)
for _, q := range questions {
questionMap[q.ID] = q
}
orderedDTOs := make([]models.PracticeQuestionDTO, 0, len(questionIDs))
for _, id := range questionIDs {
if q, ok := questionMap[id]; ok {
dto := convertToDTO(q)
// 是否显示答案
if !showAnswer {
dto.Answer = nil
}
orderedDTOs = append(orderedDTOs, dto)
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"exam": exam,
"questions": orderedDTOs,
},
})
}
// SubmitExam 提交考试
func SubmitExam(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
return
}
examIDStr := c.Param("id")
examID, err := strconv.ParseUint(examIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试ID"})
return
}
// 解析请求体
var req struct {
Answers map[string]interface{} `json:"answers"` // question_id -> answer
EssayChoice string `json:"essay_choice"` // 论述题选择: ordinary 或 management
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据"})
return
}
db := database.GetDB()
// 查询考试记录
var exam models.Exam
if err := db.Where("id = ? AND user_id = ?", examID, userID).First(&exam).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试不存在"})
return
}
// 检查是否已提交
if exam.Status == "submitted" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "考试已提交"})
return
}
// 解析题目ID列表
var questionIDs []uint
if err := json.Unmarshal([]byte(exam.QuestionIDs), &questionIDs); err != nil {
log.Printf("解析题目ID失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "解析考试数据失败"})
return
}
// 查询题目详情
var questions []models.PracticeQuestion
if err := db.Where("id IN ?", questionIDs).Find(&questions).Error; err != nil {
log.Printf("查询题目失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询题目失败"})
return
}
// 评分
totalScore := 0.0
scoreConfig := models.DefaultScoreConfig
aiService := services.NewAIGradingService()
detailedResults := make(map[string]interface{})
for _, question := range questions {
questionIDStr := fmt.Sprintf("%d", question.ID)
userAnswerRaw, answered := req.Answers[questionIDStr]
if !answered {
detailedResults[questionIDStr] = gin.H{
"correct": false,
"score": 0,
"message": "未作答",
}
continue
}
// 论述题特殊处理:根据用户选择判断是否计分
if question.Type == "ordinary-essay" && req.EssayChoice != "ordinary" {
detailedResults[questionIDStr] = gin.H{
"correct": false,
"score": 0,
"message": "未选择此题",
}
continue
}
if question.Type == "management-essay" && req.EssayChoice != "management" {
detailedResults[questionIDStr] = gin.H{
"correct": false,
"score": 0,
"message": "未选择此题",
}
continue
}
// 根据题型判断答案
var isCorrect bool
var score float64
var aiGrading *models.AIGrading
switch question.Type {
case "fill-in-blank":
// 填空题:比较数组
userAnswerArr, ok := userAnswerRaw.([]interface{})
if !ok {
detailedResults[questionIDStr] = gin.H{"correct": false, "score": 0, "message": "答案格式错误"}
continue
}
var correctAnswers []string
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswers); err != nil {
log.Printf("解析填空题答案失败: %v", err)
continue
}
isCorrect = true
for i, ua := range userAnswerArr {
if i >= len(correctAnswers) || fmt.Sprintf("%v", ua) != correctAnswers[i] {
isCorrect = false
break
}
}
if isCorrect {
score = scoreConfig.FillInBlank
}
case "true-false":
// 判断题
var correctAnswer string
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil {
log.Printf("解析判断题答案失败: %v", err)
continue
}
isCorrect = fmt.Sprintf("%v", userAnswerRaw) == correctAnswer
if isCorrect {
score = scoreConfig.TrueFalse
}
case "multiple-choice":
// 单选题
var correctAnswer string
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil {
log.Printf("解析单选题答案失败: %v", err)
continue
}
isCorrect = fmt.Sprintf("%v", userAnswerRaw) == correctAnswer
if isCorrect {
score = scoreConfig.MultipleChoice
}
case "multiple-selection":
// 多选题:比较数组(顺序无关)
userAnswerArr, ok := userAnswerRaw.([]interface{})
if !ok {
detailedResults[questionIDStr] = gin.H{"correct": false, "score": 0, "message": "答案格式错误"}
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 = scoreConfig.MultipleSelection
}
case "short-answer", "ordinary-essay", "management-essay":
// 简答题和论述题使用AI评分
var correctAnswer string
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil {
log.Printf("解析简答题答案失败: %v", err)
continue
}
userAnswerStr := fmt.Sprintf("%v", userAnswerRaw)
aiResult, err := aiService.GradeShortAnswer(question.Question, correctAnswer, userAnswerStr)
if err != nil {
log.Printf("AI评分失败: %v", err)
// AI评分失败时给一个保守的分数
isCorrect = false
score = 0
} else {
isCorrect = aiResult.IsCorrect
if question.Type == "short-answer" {
// 简答题不计分,仅供参考
score = 0
} else {
// 论述题按AI评分比例计算
score = scoreConfig.Essay * (aiResult.Score / 100.0)
}
aiGrading = &models.AIGrading{
Score: aiResult.Score,
Feedback: aiResult.Feedback,
Suggestion: aiResult.Suggestion,
}
}
}
totalScore += score
detailedResults[questionIDStr] = gin.H{
"correct": isCorrect,
"score": score,
"ai_grading": aiGrading,
}
}
// 保存答案和分数
answersJSON, err := json.Marshal(req.Answers)
if err != nil {
log.Printf("序列化答案失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存答案失败"})
return
}
now := time.Now()
exam.Answers = string(answersJSON)
exam.Score = totalScore
exam.Status = "submitted"
exam.SubmittedAt = &now
if err := db.Save(&exam).Error; err != nil {
log.Printf("保存考试记录失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存考试失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"score": totalScore,
"detailed_results": detailedResults,
},
})
}