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 } // 查询题目详情 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 } // 按原始顺序排序题目 questionMap := make(map[uint]models.PracticeQuestion) for _, q := range questions { questionMap[q.ID] = q } orderedQuestions := make([]models.PracticeQuestion, 0, len(questionIDs)) for _, id := range questionIDs { if q, ok := questionMap[id]; ok { orderedQuestions = append(orderedQuestions, q) } } // 是否显示答案 showAnswer := c.Query("show_answer") == "true" if !showAnswer { // 不显示答案时,隐藏答案字段 for i := range orderedQuestions { orderedQuestions[i].AnswerData = "" } } c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "exam": exam, "questions": orderedQuestions, }, }) } // 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, }, }) }