package handlers import ( "ankao/internal/database" "ankao/internal/models" "encoding/json" "fmt" "log" "math/rand" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // CreateExamRequest 创建试卷请求 type CreateExamRequest struct { Title string `json:"title" binding:"required"` } // CreateExam 创建试卷 func CreateExam(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } var req CreateExamRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据: " + err.Error()}) return } db := database.GetDB() // 查询用户信息,获取用户类型 var user models.User if err := db.Where("id = ?", userID).First(&user).Error; err != nil { log.Printf("查询用户失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询用户信息失败"}) return } // 使用固定的题型配置(总分100分) questionTypes := []models.QuestionTypeConfig{ {Type: "fill-in-blank", Count: 10, Score: 2.0}, // 20分 {Type: "true-false", Count: 10, Score: 2.0}, // 20分 {Type: "multiple-choice", Count: 10, Score: 1.0}, // 10分 {Type: "multiple-selection", Count: 10, Score: 2.5}, // 25分 {Type: "short-answer", Count: 2, Score: 10.0}, // 20分 {Type: "ordinary-essay", Count: 1, Score: 4.5}, // 4.5分(普通涉密人员论述题) {Type: "management-essay", Count: 1, Score: 4.5}, // 4.5分(保密管理人员论述题) } // 按题型配置随机抽取题目 var allQuestionIDs []int64 totalScore := 0.0 for _, qtConfig := range questionTypes { var questions []models.PracticeQuestion query := db.Where("type = ?", qtConfig.Type) if err := query.Find(&questions).Error; err != nil { log.Printf("查询题目失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询题目失败"}) return } // 检查题目数量是否足够 if len(questions) < qtConfig.Count { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": fmt.Sprintf("题型 %s 题目数量不足,需要 %d 道,实际 %d 道", qtConfig.Type, qtConfig.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[:qtConfig.Count] // 收集题目ID for _, q := range selectedQuestions { allQuestionIDs = append(allQuestionIDs, q.ID) } // 计算总分 totalScore += float64(qtConfig.Count) * qtConfig.Score } // 随机打乱题目ID顺序 rand.Seed(time.Now().UnixNano()) for i := len(allQuestionIDs) - 1; i > 0; i-- { j := rand.Intn(i + 1) allQuestionIDs[i], allQuestionIDs[j] = allQuestionIDs[j], allQuestionIDs[i] } // 序列化题目ID 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: uint(userID.(int64)), Title: req.Title, TotalScore: int(totalScore), // 总分100分 Duration: 60, // 固定60分钟 PassScore: 80, // 固定80分及格 QuestionIDs: questionIDsJSON, Status: "active", } 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{ "id": exam.ID, "title": exam.Title, "total_score": exam.TotalScore, "duration": exam.Duration, "pass_score": exam.PassScore, "question_count": len(allQuestionIDs), "created_at": exam.CreatedAt, }, }) } // GetExamList 获取试卷列表 func GetExamList(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } db := database.GetDB() // 查询用户创建的试卷 var exams []models.Exam if err := db.Where("user_id = ? AND status = ?", userID, "active"). Order("created_at DESC"). Find(&exams).Error; err != nil { log.Printf("查询试卷列表失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询试卷列表失败"}) return } // 为每个试卷计算题目数量和获取考试记录统计 type ExamWithStats struct { models.Exam QuestionCount int `json:"question_count"` AttemptCount int `json:"attempt_count"` // 考试次数 BestScore float64 `json:"best_score"` // 最高分 HasInProgressExam bool `json:"has_in_progress_exam"` // 是否有进行中的考试 InProgressRecordID uint `json:"in_progress_record_id,omitempty"` // 进行中的考试记录ID } result := make([]ExamWithStats, 0, len(exams)) for _, exam := range exams { var questionIDs []uint if err := json.Unmarshal(exam.QuestionIDs, &questionIDs); err == nil { stats := ExamWithStats{ Exam: exam, QuestionCount: len(questionIDs), } // 查询该试卷的考试记录统计 var count int64 db.Model(&models.ExamRecord{}).Where("exam_id = ? AND user_id = ?", exam.ID, userID).Count(&count) stats.AttemptCount = int(count) // 查询最高分 var record models.ExamRecord if err := db.Where("exam_id = ? AND user_id = ?", exam.ID, userID). Order("score DESC"). First(&record).Error; err == nil { stats.BestScore = record.Score } // 查询是否有进行中的考试(status为in_progress) var inProgressRecord models.ExamRecord if err := db.Where("exam_id = ? AND user_id = ? AND status = ?", exam.ID, userID, "in_progress"). Order("created_at DESC"). First(&inProgressRecord).Error; err == nil { stats.HasInProgressExam = true stats.InProgressRecordID = inProgressRecord.ID } result = append(result, stats) } } c.JSON(http.StatusOK, gin.H{ "success": true, "data": result, }) } // GetExamDetail 获取试卷详情 func GetExamDetail(c *gin.Context) { _, 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 } // 检查是否需要显示答案 showAnswer := c.Query("show_answer") == "true" db := database.GetDB() // 查询试卷 var exam models.Exam if err := db.Where("id = ?", examID).First(&exam).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"}) return } // 解析题目ID列表 var questionIDs []int64 if err := json.Unmarshal(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 } // 按原始顺序排序题目并转换为DTO questionMap := make(map[int64]models.PracticeQuestion) for _, q := range questions { questionMap[q.ID] = q } // 检查是否包含论述题,如果没有则添加两种论述题 hasOrdinaryEssay := false hasManagementEssay := false for _, q := range questions { if q.Type == "ordinary-essay" { hasOrdinaryEssay = true } if q.Type == "management-essay" { hasManagementEssay = true } } // 如果缺少论述题,则补充 var additionalQuestions []models.PracticeQuestion if !hasOrdinaryEssay { var ordinaryEssay models.PracticeQuestion if err := db.Where("type = ?", "ordinary-essay").First(&ordinaryEssay).Error; err == nil { additionalQuestions = append(additionalQuestions, ordinaryEssay) } } if !hasManagementEssay { var managementEssay models.PracticeQuestion if err := db.Where("type = ?", "management-essay").First(&managementEssay).Error; err == nil { additionalQuestions = append(additionalQuestions, managementEssay) } } // 将补充的题目添加到题目映射中 for _, q := range additionalQuestions { questionMap[q.ID] = q } orderedDTOs := make([]models.PracticeQuestionDTO, 0, len(questionIDs)) for _, id := range questionIDs { if q, ok := questionMap[id]; ok { dto := convertToDTO(q) // 根据showAnswer参数决定是否显示答案 if !showAnswer { dto.Answer = nil // 不显示答案 } orderedDTOs = append(orderedDTOs, dto) } } // 添加补充的论述题到结果中 for _, q := range additionalQuestions { 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, }, }) } // StartExam 开始考试(创建考试记录) func StartExam(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 = ?", examID).First(&exam).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"}) return } // 创建考试记录 now := time.Now() record := models.ExamRecord{ ExamID: uint(examID), UserID: uint(userID.(int64)), StartTime: &now, TotalScore: exam.TotalScore, Status: "in_progress", } if err := db.Create(&record).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{ "record_id": record.ID, "start_time": record.StartTime, "duration": exam.Duration, }, }) } // SubmitExam 提交试卷答案 func SubmitExam(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } recordIDStr := c.Param("record_id") recordID, err := strconv.ParseUint(recordIDStr, 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 (可选) } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据"}) return } db := database.GetDB() // 查询考试记录 var record models.ExamRecord if err := db.Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"}) return } // 检查是否已提交 if record.Status == "submitted" || record.Status == "graded" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "考试已提交"}) return } // 查询试卷 var exam models.Exam if err := db.Where("id = ?", record.ExamID).First(&exam).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"}) return } // 更新考试记录状态为已提交 now := time.Now() record.Status = "submitted" record.SubmitTime = &now // 计算用时(秒) if record.StartTime != nil { duration := now.Sub(*record.StartTime) record.TimeSpent = int(duration.Seconds()) // 确保用时不为负数(容错处理) if record.TimeSpent < 0 { log.Printf("警告: 计算出负的用时,开始时间=%v, 结束时间=%v", *record.StartTime, now) record.TimeSpent = 0 } } if err := db.Save(&record).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, "message": "提交成功,正在阅卷中...", "data": gin.H{ "record_id": record.ID, "status": "submitted", "time_spent": record.TimeSpent, "total_score": exam.TotalScore, }, }) // 异步执行阅卷(从 exam_user_answers 表读取答案) go gradeExam(uint(recordID), exam.ID, uint(userID.(int64))) } // GetExamRecord 获取考试记录详情 func GetExamRecord(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } recordIDStr := c.Param("record_id") recordID, err := strconv.ParseUint(recordIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试记录ID"}) return } db := database.GetDB() // 查询考试记录 var record models.ExamRecord if err := db.Where("id = ? AND user_id = ?", recordID, userID). Preload("Exam"). First(&record).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"}) return } // 从 exam_user_answers 表读取所有答案 var userAnswers []models.ExamUserAnswer if err := db.Where("exam_record_id = ?", recordID).Find(&userAnswers).Error; err != nil { log.Printf("查询用户答案失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询答案失败"}) return } // 查询所有题目以获取正确答案 var questionIDs []uint if err := json.Unmarshal(record.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[int64]models.PracticeQuestion) for _, q := range questions { questionMap[q.ID] = q } // 构建 ExamAnswer 列表 examAnswers := make([]models.ExamAnswer, 0, len(userAnswers)) for _, ua := range userAnswers { // 解析用户答案 var userAnswer interface{} if err := json.Unmarshal(ua.Answer, &userAnswer); err != nil { log.Printf("解析用户答案失败: %v", err) continue } // 获取题目并解析正确答案 question, ok := questionMap[ua.QuestionID] if !ok { continue } var correctAnswerRaw interface{} switch question.Type { case "fill-in-blank", "multiple-selection", "multiple-choice": // 数组类型:需要 JSON 解析(单选题也是数组格式A) var arr []string if err := json.Unmarshal([]byte(question.AnswerData), &arr); err != nil { // 尝试解析为单个字符串(兼容旧数据格式) var singleStr string if err2 := json.Unmarshal([]byte(question.AnswerData), &singleStr); err2 == nil { // 成功解析为字符串,单选题直接使用,其他类型转为数组 if question.Type == "multiple-choice" { correctAnswerRaw = singleStr } else { correctAnswerRaw = []string{singleStr} } } else { correctAnswerRaw = "解析失败" } } else { // 单选题只取第一个元素 if question.Type == "multiple-choice" && len(arr) > 0 { correctAnswerRaw = arr[0] } else { correctAnswerRaw = arr } } default: // 字符串类型:直接使用 AnswerData(true-false, short-answer, essay) correctAnswerRaw = question.AnswerData } // 构建答案对象 examAnswer := models.ExamAnswer{ QuestionID: ua.QuestionID, Answer: userAnswer, CorrectAnswer: correctAnswerRaw, IsCorrect: ua.IsCorrect != nil && *ua.IsCorrect, Score: ua.Score, } // 添加 AI 评分信息 if len(ua.AIGradingData) > 0 { var aiGrading models.AIGrading if err := json.Unmarshal(ua.AIGradingData, &aiGrading); err == nil { examAnswer.AIGrading = &aiGrading } } examAnswers = append(examAnswers, examAnswer) } c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "record": record, "answers": examAnswers, }, }) } // GetExamRecordList 获取考试记录列表 func GetExamRecordList(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } examIDStr := c.Query("exam_id") db := database.GetDB() query := db.Where("user_id = ?", userID) // 如果指定了试卷ID,只查询该试卷的记录 if examIDStr != "" { examID, err := strconv.ParseUint(examIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的试卷ID"}) return } query = query.Where("exam_id = ?", examID) } var records []models.ExamRecord if err := query.Preload("Exam"). Order("created_at DESC"). Find(&records).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": records, }) } // DeleteExam 删除试卷 func DeleteExam(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 } // 软删除 if err := db.Delete(&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, "message": "删除成功", }) } // SaveExamProgressRequest 保存考试进度请求 type SaveExamProgressRequest struct { QuestionID int64 `json:"question_id"` // 题目ID Answer interface{} `json:"answer"` // 答案数据 } // SaveExamProgress 保存单题答案 func SaveExamProgress(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } recordIDStr := c.Param("record_id") recordID, err := strconv.ParseInt(recordIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试记录ID"}) return } var req SaveExamProgressRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据"}) return } db := database.GetDB() // 查询考试记录 var record models.ExamRecord if err := db.Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"}) return } // 检查考试状态 if record.Status != "in_progress" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "考试已结束,无法保存答案"}) return } // 序列化答案数据 answerJSON, err := json.Marshal(req.Answer) if err != nil { log.Printf("序列化答案失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存失败"}) return } now := time.Now() // 查找是否已存在该题的答案 var userAnswer models.ExamUserAnswer result := db.Where("exam_record_id = ? AND question_id = ?", recordID, req.QuestionID).First(&userAnswer) if result.Error == gorm.ErrRecordNotFound { // 不存在,创建新记录 userAnswer = models.ExamUserAnswer{ ExamRecordID: recordID, QuestionID: req.QuestionID, UserID: userID.(int64), Answer: answerJSON, AnsweredAt: &now, LastModifiedAt: now, } if err := db.Create(&userAnswer).Error; err != nil { log.Printf("创建答案记录失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存答案失败"}) return } } else if result.Error != nil { log.Printf("查询答案记录失败: %v", result.Error) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询答案失败"}) return } else { // 已存在,更新答案 updates := map[string]interface{}{ "answer": answerJSON, "last_modified_at": now, } if err := db.Model(&userAnswer).Updates(updates).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, "message": "保存成功", }) } // GetExamUserAnswers 获取用户在考试中的所有答案 func GetExamUserAnswers(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } recordIDStr := c.Param("record_id") recordID, err := strconv.ParseUint(recordIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试记录ID"}) return } db := database.GetDB() // 查询考试记录,确保用户有权限 var record models.ExamRecord if err := db.Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"}) return } // 查询所有已保存的答案 var userAnswers []models.ExamUserAnswer if err := db.Where("exam_record_id = ?", recordID).Find(&userAnswers).Error; err != nil { log.Printf("查询用户答案失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询答案失败"}) return } // 转换为前端需要的格式: { question_id: answer } answers := make(map[string]interface{}) for _, ua := range userAnswers { var answer interface{} if err := json.Unmarshal(ua.Answer, &answer); err != nil { log.Printf("解析答案失败: %v", err) continue } // 使用 q_ 格式作为key,与前端表单字段名保持一致 fieldName := fmt.Sprintf("q_%d", ua.QuestionID) answers[fieldName] = answer } c.JSON(http.StatusOK, gin.H{ "success": true, "data": answers, }) }