package handlers import ( "ankao/internal/database" "ankao/internal/models" "ankao/internal/services" "encoding/json" "fmt" "log" "net/http" "sort" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) // checkEssayPermission 检查用户是否有权限访问论述题 // 返回 true 表示有权限,false 表示无权限 func checkEssayPermission(c *gin.Context, questionType string) bool { // 如果不是论述题,直接允许访问 if !strings.HasSuffix(questionType, "-essay") { log.Printf("[论述题权限检查] 非论述题类型,直接允许访问 (type: %s)", questionType) return true } log.Printf("[论述题权限检查] 开始检查 (question_type: %s)", questionType) // 从上下文获取用户信息(Auth中间件已设置) username, exists := c.Get("username") if !exists { log.Printf("[论述题权限检查] 失败: 未登录用户 (question_type: %s)", questionType) return false } log.Printf("[论述题权限检查] 已登录用户 (username: %v, question_type: %s)", username, questionType) // 管理员可以访问所有论述题 if username == "yanlongqi" { log.Printf("[论述题权限检查] 通过: 管理员用户,允许访问所有论述题 (username: %s)", username) return true } // 获取用户信息 db := database.GetDB() var user models.User if err := db.Where("username = ?", username).First(&user).Error; err != nil { log.Printf("[论述题权限检查] 失败: 查询用户失败 (username: %v, error: %v)", username, err) return false } log.Printf("[论述题权限检查] 用户信息 (username: %s, user_type: '%s', question_type: '%s')", user.Username, user.UserType, questionType) // 检查用户类型是否匹配 if questionType == "ordinary-essay" && user.UserType == "ordinary-person" { log.Printf("[论述题权限检查] 通过: 普通涉密人员访问普通论述题 (username: %s)", user.Username) return true } if questionType == "management-essay" && user.UserType == "management-person" { log.Printf("[论述题权限检查] 通过: 保密管理人员访问管理论述题 (username: %s)", user.Username) return true } log.Printf("[论述题权限检查] 失败: 类型不匹配 (username: %s, user_type: '%s', question_type: '%s')", user.Username, user.UserType, questionType) return false } // GetPracticeQuestions 获取练习题目列表 func GetPracticeQuestions(c *gin.Context) { typeParam := c.Query("type") searchQuery := c.Query("search") log.Printf("[GetPracticeQuestions] 收到请求 (type: '%s', search: '%s')", typeParam, searchQuery) db := database.GetDB() var questions []models.PracticeQuestion var total int64 query := db.Model(&models.PracticeQuestion{}) // 根据题型过滤 if typeParam != "" { // 如果请求论述题,检查权限 if strings.HasSuffix(typeParam, "-essay") && !checkEssayPermission(c, typeParam) { // 权限不足,返回空列表 log.Printf("[GetPracticeQuestions] 权限检查失败,返回空列表 (type: '%s')", typeParam) c.JSON(http.StatusOK, gin.H{ "success": true, "data": []models.PracticeQuestionDTO{}, "total": 0, }) return } query = query.Where("type = ?", typeParam) log.Printf("[GetPracticeQuestions] 添加题型过滤 (type: '%s')", typeParam) } // 根据搜索关键词过滤(搜索题目内容或题目编号) if searchQuery != "" { // 将 question_id 显式转换为文本类型,避免 PostgreSQL 将其作为数字类型处理 query = query.Where("question LIKE ? OR question_id::text LIKE ?", "%"+searchQuery+"%", "%"+searchQuery+"%") } // 获取总数 query.Count(&total) log.Printf("[GetPracticeQuestions] 查询结果统计 (total: %d)", total) // 查询所有题目 - 按题型和题目编号升序排序 // 先将 question_id 转为文本,提取数字部分,再转为整数排序 // 显式查询所有字段,包括 answer_data err := query.Select("*").Order("type ASC, CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) ASC").Find(&questions).Error if err != nil { log.Printf("[GetPracticeQuestions] 查询失败 (error: %v)", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "查询题目失败", }) return } log.Printf("[GetPracticeQuestions] 查询成功,返回 %d 条数据", len(questions)) // 转换为DTO dtos := make([]models.PracticeQuestionDTO, len(questions)) for i, q := range questions { dto := convertToDTO(q) dtos[i] = dto } c.JSON(http.StatusOK, gin.H{ "success": true, "data": dtos, "total": total, }) } // GetPracticeQuestionByID 获取单个练习题目 func GetPracticeQuestionByID(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "无效的题目ID", }) return } db := database.GetDB() var question models.PracticeQuestion if err := db.First(&question, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "题目不存在", }) return } // 转换为DTO dto := convertToDTO(question) c.JSON(http.StatusOK, gin.H{ "success": true, "data": dto, }) } // GetRandomPracticeQuestion 获取随机练习题目 func GetRandomPracticeQuestion(c *gin.Context) { typeParam := c.Query("type") log.Printf("[GetRandomPracticeQuestion] 收到请求 (type: '%s')", typeParam) db := database.GetDB() var question models.PracticeQuestion query := db.Model(&models.PracticeQuestion{}) if typeParam != "" { // 如果请求论述题,检查权限 if strings.HasSuffix(typeParam, "-essay") && !checkEssayPermission(c, typeParam) { // 权限不足,返回暂无题目 log.Printf("[GetRandomPracticeQuestion] 权限检查失败,返回暂无题目 (type: '%s')", typeParam) c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "暂无题目", }) return } query = query.Where("type = ?", typeParam) log.Printf("[GetRandomPracticeQuestion] 添加题型过滤 (type: '%s')", typeParam) } // 使用PostgreSQL的随机排序 if err := query.Order("RANDOM()").First(&question).Error; err != nil { log.Printf("[GetRandomPracticeQuestion] 未找到题目 (type: '%s', error: %v)", typeParam, err) c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "暂无题目", }) return } log.Printf("[GetRandomPracticeQuestion] 找到题目 (id: %d, type: '%s')", question.ID, question.Type) // 转换为DTO dto := convertToDTO(question) c.JSON(http.StatusOK, gin.H{ "success": true, "data": dto, }) } // SubmitPracticeAnswer 提交练习答案 func SubmitPracticeAnswer(c *gin.Context) { var submit models.PracticeAnswerSubmit if err := c.ShouldBindJSON(&submit); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "请求参数错误", }) return } // 获取用户ID(认证中间件已确保存在) userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": "未登录", }) return } db := database.GetDB() var question models.PracticeQuestion if err := db.First(&question, submit.QuestionID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "题目不存在", }) return } // 解析正确答案(论述题不需要标准答案) var correctAnswer interface{} // 论述题跳过答案解析 if strings.HasSuffix(question.Type, "-essay") { log.Printf("[论述题] 跳过答案解析 (题目ID: %d, 类型: %s)", question.ID, question.Type) correctAnswer = "" // 论述题没有标准答案 } else { // 其他题型需要解析标准答案 if question.AnswerData == "" { log.Printf("[错误] 题目缺少答案数据 (题目ID: %d, 类型: %s)", question.ID, question.Type) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "题目答案数据缺失", }) return } if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil { log.Printf("[错误] 答案数据解析失败 (题目ID: %d, 类型: %s, AnswerData: '%s', 错误: %v)", question.ID, question.Type, question.AnswerData, err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "答案数据错误", }) return } } // 验证答案(论述题不需要验证,因为没有标准答案) correct := false if strings.HasSuffix(question.Type, "-essay") { // 论述题默认为false,实际结果由AI评分决定 correct = false } else { // 其他题型使用标准答案验证 correct = checkPracticeAnswer(question.Type, submit.Answer, correctAnswer) } // AI评分结果(简答题和论述题) var aiGrading *models.AIGrading = nil // 对简答题和论述题使用AI评分(必须成功,失败重试最多5次) if question.Type == "short-answer" || strings.HasSuffix(question.Type, "-essay") { // 获取用户答案字符串 userAnswerStr, ok := submit.Answer.(string) if !ok { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "答案格式错误", }) return } // 调用AI评分服务(带重试机制) aiService := services.NewAIGradingService() var aiResult *services.AIGradingResult var err error maxRetries := 5 // 区分简答题和论述题的评分方式 if strings.HasSuffix(question.Type, "-essay") { // 论述题:不需要标准答案,直接评分 for attempt := 1; attempt <= maxRetries; attempt++ { log.Printf("论述题AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID) aiResult, err = aiService.GradeEssay(question.Question, userAnswerStr) if err == nil { log.Printf("论述题AI评分成功 (题目ID: %d, 得分: %.1f)", question.ID, aiResult.Score) break } log.Printf("论述题AI评分失败 (第 %d 次尝试): %v", attempt, err) if attempt < maxRetries { // 等待一小段时间后重试(指数退避) time.Sleep(time.Second * time.Duration(attempt)) } } } else { // 简答题:需要标准答案对比 // 获取标准答案字符串 standardAnswerStr, ok := correctAnswer.(string) if !ok { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "题目答案格式错误", }) return } for attempt := 1; attempt <= maxRetries; attempt++ { log.Printf("简答题AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID) aiResult, err = aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr) if err == nil { log.Printf("简答题AI评分成功 (题目ID: %d, 得分: %.1f)", question.ID, aiResult.Score) break } log.Printf("简答题AI评分失败 (第 %d 次尝试): %v", attempt, err) if attempt < maxRetries { // 等待一小段时间后重试(指数退避) time.Sleep(time.Second * time.Duration(attempt)) } } } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": fmt.Sprintf("AI评分服务暂时不可用,已重试%d次,请稍后再试", maxRetries), }) return } // 使用AI的评分结果 correct = aiResult.IsCorrect aiGrading = &models.AIGrading{ Score: aiResult.Score, Feedback: aiResult.Feedback, Suggestion: aiResult.Suggestion, ReferenceAnswer: aiResult.ReferenceAnswer, ScoringRationale: aiResult.ScoringRationale, } } // 记录用户答题历史 if uid, ok := userID.(uint); ok { record := models.UserAnswerRecord{ UserID: uid, QuestionID: question.ID, IsCorrect: correct, AnsweredAt: time.Now(), } // 记录到数据库(忽略错误,不影响主流程) if err := db.Create(&record).Error; err != nil { log.Printf("记录答题历史失败: %v", err) } } // 记录到错题本(新版)- 使用 V2 API if uid, ok := userID.(uint); ok { timeSpent := 0 // TODO: 从前端获取答题用时 if !correct { // 答错,记录到错题本 var wrongAnswer interface{} = submit.Answer var stdAnswer interface{} = correctAnswer if strings.HasSuffix(question.Type, "-essay") { stdAnswer = "" // 论述题没有标准答案 } if err := services.RecordWrongAnswer(uid, question.ID, wrongAnswer, stdAnswer, timeSpent); err != nil { log.Printf("记录错题失败: %v", err) } } else { // 答对,如果这道题在错题本中,更新连续答对次数 if err := services.RecordCorrectAnswer(uid, question.ID, submit.Answer, correctAnswer, timeSpent); err != nil { log.Printf("更新错题记录失败: %v", err) } } } // 构建返回结果 result := models.PracticeAnswerResult{ Correct: correct, UserAnswer: submit.Answer, AIGrading: aiGrading, // AI评分结果(简答题和论述题) } // 论述题不返回标准答案(因为没有固定答案) if !strings.HasSuffix(question.Type, "-essay") { result.CorrectAnswer = correctAnswer } else { result.CorrectAnswer = "" // 论述题返回空字符串 } c.JSON(http.StatusOK, gin.H{ "success": true, "data": result, }) } // GetPracticeQuestionTypes 获取题型列表 func GetPracticeQuestionTypes(c *gin.Context) { types := []gin.H{ { "type": "fill-in-blank", "type_name": "填空题", }, { "type": "true-false", "type_name": "判断题", }, { "type": "multiple-choice", "type_name": "选择题", }, { "type": "multiple-selection", "type_name": "多选题", }, { "type": "short-answer", "type_name": "简答题", }, { "type": "ordinary-essay", "type_name": "普通涉密人员论述题", }, { "type": "management-essay", "type_name": "保密管理人员论述题", }, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": types, }) } // checkPracticeAnswer 检查练习答案是否正确 func checkPracticeAnswer(questionType string, userAnswer, correctAnswer interface{}) bool { switch questionType { case "true-false": // 判断题: boolean 比较 userBool, ok1 := userAnswer.(bool) correctBool, ok2 := correctAnswer.(bool) return ok1 && ok2 && userBool == correctBool case "multiple-choice": // 单选题: 字符串比较 userStr, ok1 := userAnswer.(string) correctStr, ok2 := correctAnswer.(string) return ok1 && ok2 && userStr == correctStr case "multiple-selection": // 多选题: 数组比较 userArr, ok1 := toStringArray(userAnswer) correctArr, ok2 := toStringArray(correctAnswer) if !ok1 || !ok2 || len(userArr) != len(correctArr) { return false } // 转换为map进行比较 userMap := make(map[string]bool) for _, v := range userArr { userMap[v] = true } for _, v := range correctArr { if !userMap[v] { return false } } return true case "fill-in-blank": // 填空题: 数组比较 userArr, ok1 := toStringArray(userAnswer) correctArr, ok2 := toStringArray(correctAnswer) if !ok1 || !ok2 || len(userArr) != len(correctArr) { log.Printf("填空题验证失败 - 数组转换或长度不匹配: ok1=%v, ok2=%v, userLen=%d, correctLen=%d", ok1, ok2, len(userArr), len(correctArr)) return false } // 逐个比较填空答案(去除前后空格) for i := range correctArr { userTrimmed := strings.TrimSpace(userArr[i]) correctTrimmed := strings.TrimSpace(correctArr[i]) if userTrimmed != correctTrimmed { log.Printf("填空题验证失败 - 第%d个答案不匹配: user='%s', correct='%s'", i+1, userTrimmed, correctTrimmed) return false } } return true case "short-answer": // 简答题: 字符串比较(简单实现,实际可能需要更复杂的判断) userStr, ok1 := userAnswer.(string) correctStr, ok2 := correctAnswer.(string) return ok1 && ok2 && userStr == correctStr } return false } // toStringArray 将interface{}转换为字符串数组 func toStringArray(v interface{}) ([]string, bool) { switch arr := v.(type) { case []string: return arr, true case []interface{}: result := make([]string, len(arr)) for i, item := range arr { if str, ok := item.(string); ok { result[i] = str } else { return nil, false } } return result, true default: return nil, false } } // convertToDTO 将数据库模型转换为前端DTO func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO { dto := models.PracticeQuestionDTO{ ID: question.ID, QuestionID: question.QuestionID, Type: question.Type, // 直接使用数据库中的type,不做映射 Content: question.Question, Category: question.TypeName, // 使用typeName作为分类显示 Options: []models.Option{}, } // 解析答案数据 if question.AnswerData != "" { // 对于简答题和论述题,答案可能是纯字符串或JSON格式 if question.Type == "short-answer" || strings.HasSuffix(question.Type, "-essay") { // 尝试JSON解析 var answer interface{} if err := json.Unmarshal([]byte(question.AnswerData), &answer); err != nil { // JSON解析失败,直接使用原始字符串 dto.Answer = question.AnswerData } else { // JSON解析成功 dto.Answer = answer } } else { // 其他题型必须是JSON格式 var answer interface{} if err := json.Unmarshal([]byte(question.AnswerData), &answer); err != nil { log.Printf("[convertToDTO] 解析答案失败 (id: %d, type: %s, error: %v)", question.ID, question.Type, err) } else { dto.Answer = answer } } } // 判断题自动生成选项(正确在前,错误在后) if question.Type == "true-false" { dto.Options = []models.Option{ {Key: "true", Value: "正确"}, {Key: "false", Value: "错误"}, } return dto } // 解析选项数据(如果有) if question.OptionsData != "" { var optionsMap map[string]string if err := json.Unmarshal([]byte(question.OptionsData), &optionsMap); err == nil { // 将map转换为Option数组,并按key排序 keys := make([]string, 0, len(optionsMap)) for key := range optionsMap { keys = append(keys, key) } // 对keys进行排序 sort.Strings(keys) // 按排序后的key顺序添加选项 for _, key := range keys { dto.Options = append(dto.Options, models.Option{ Key: key, Value: optionsMap[key], }) } } } return dto } // GetStatistics 获取用户统计数据 func GetStatistics(c *gin.Context) { // 获取用户ID userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": "未登录", }) return } uid, ok := userID.(uint) if !ok { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "用户ID格式错误", }) return } db := database.GetDB() // 获取题库总数 var totalQuestions int64 db.Model(&models.PracticeQuestion{}).Count(&totalQuestions) // 获取用户已答题数(去重) var answeredQuestions int64 db.Model(&models.UserAnswerRecord{}). Where("user_id = ?", uid). Distinct("question_id"). Count(&answeredQuestions) // 获取用户答对题数 var correctAnswers int64 db.Model(&models.UserAnswerRecord{}). Where("user_id = ? AND is_correct = ?", uid, true). Count(&correctAnswers) // 获取用户错题数量(所有错题,包括已掌握和未掌握的) var wrongQuestions int64 db.Model(&models.WrongQuestion{}). Where("user_id = ?", uid). Count(&wrongQuestions) // 计算正确率和总答题数 var accuracy float64 var totalAnswers int64 db.Model(&models.UserAnswerRecord{}). Where("user_id = ?", uid). Count(&totalAnswers) if totalAnswers > 0 { // 正确率 = 答对题数 / 总答题数 accuracy = float64(correctAnswers) / float64(totalAnswers) * 100 } stats := gin.H{ "total_questions": int(totalQuestions), "answered_questions": int(answeredQuestions), "correct_answers": int(correctAnswers), "wrong_questions": int(wrongQuestions), "total_answers": int(totalAnswers), // 刷题次数 "accuracy": accuracy, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": stats, }) } // CreatePracticeQuestion 创建新的练习题目 func CreatePracticeQuestion(c *gin.Context) { var req struct { Type string `json:"type" binding:"required"` TypeName string `json:"type_name"` Question string `json:"question" binding:"required"` Answer interface{} `json:"answer" binding:"required"` Options map[string]string `json:"options"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "请求参数错误: " + err.Error(), }) return } db := database.GetDB() // 自动生成题目编号:找到该题型的最大编号并+1 var maxQuestionID string err := db.Model(&models.PracticeQuestion{}). Where("type = ?", req.Type). Select("question_id"). Order("CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) DESC"). Limit(1). Pluck("question_id", &maxQuestionID).Error // 生成新的题目编号 var newQuestionID string if err != nil || maxQuestionID == "" { // 没有找到该题型的题目,从1开始 newQuestionID = "1" } else { // 从最大编号中提取数字并+1 var maxNum int _, scanErr := strconv.Atoi(maxQuestionID) if scanErr == nil { maxNum, _ = strconv.Atoi(maxQuestionID) } newQuestionID = strconv.Itoa(maxNum + 1) } // 将答案序列化为JSON字符串 answerData, err := json.Marshal(req.Answer) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "答案格式错误", }) return } // 将选项序列化为JSON字符串 var optionsData string if req.Options != nil && len(req.Options) > 0 { optionsBytes, err := json.Marshal(req.Options) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "选项格式错误", }) return } optionsData = string(optionsBytes) } question := models.PracticeQuestion{ QuestionID: newQuestionID, Type: req.Type, TypeName: req.TypeName, Question: req.Question, AnswerData: string(answerData), OptionsData: optionsData, } if err := db.Create(&question).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "创建题目失败", }) return } // 返回创建的题目 dto := convertToDTO(question) c.JSON(http.StatusOK, gin.H{ "success": true, "data": dto, "message": "创建成功", }) } // UpdatePracticeQuestion 更新练习题目 func UpdatePracticeQuestion(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "无效的题目ID", }) return } var req struct { Type string `json:"type"` TypeName string `json:"type_name"` Question string `json:"question"` Answer interface{} `json:"answer"` Options map[string]string `json:"options"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "请求参数错误: " + err.Error(), }) return } db := database.GetDB() var question models.PracticeQuestion // 查找题目是否存在 if err := db.First(&question, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "题目不存在", }) return } // 更新字段(注意:不允许修改 QuestionID,由系统自动生成) if req.Type != "" { question.Type = req.Type } if req.TypeName != "" { question.TypeName = req.TypeName } if req.Question != "" { question.Question = req.Question } if req.Answer != nil { answerData, err := json.Marshal(req.Answer) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "答案格式错误", }) return } question.AnswerData = string(answerData) } if req.Options != nil { optionsBytes, err := json.Marshal(req.Options) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "选项格式错误", }) return } question.OptionsData = string(optionsBytes) } // 保存更新 if err := db.Save(&question).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "更新题目失败", }) return } // 返回更新后的题目 dto := convertToDTO(question) c.JSON(http.StatusOK, gin.H{ "success": true, "data": dto, "message": "更新成功", }) } // DeletePracticeQuestion 删除练习题目 func DeletePracticeQuestion(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "无效的题目ID", }) return } db := database.GetDB() // 检查题目是否存在 var question models.PracticeQuestion if err := db.First(&question, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "题目不存在", }) return } // 删除题目 if err := db.Delete(&question).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "删除题目失败", }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "删除成功", }) } // ExplainQuestion 生成题目解析 func ExplainQuestion(c *gin.Context) { var req struct { QuestionID uint `json:"question_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "请求参数错误", }) return } db := database.GetDB() var question models.PracticeQuestion // 查询题目 if err := db.First(&question, req.QuestionID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "题目不存在", }) return } // 解析标准答案 var standardAnswer interface{} if err := json.Unmarshal([]byte(question.AnswerData), &standardAnswer); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "解析题目答案失败", }) return } // 将标准答案转为字符串 var standardAnswerStr string switch v := standardAnswer.(type) { case string: standardAnswerStr = v case []interface{}: // 多选题或填空题,将数组转为逗号分隔的字符串 parts := make([]string, len(v)) for i, item := range v { parts[i] = fmt.Sprint(item) } standardAnswerStr = strings.Join(parts, ", ") case bool: // 判断题 if v { standardAnswerStr = "正确" } else { standardAnswerStr = "错误" } default: standardAnswerStr = fmt.Sprint(v) } // 设置SSE响应头 c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") // 调用AI服务生成流式解析 aiService := services.NewAIGradingService() err := aiService.ExplainQuestionStream(c.Writer, question.Question, standardAnswerStr, question.Type) if err != nil { log.Printf("AI流式解析失败: %v", err) // SSE格式的错误消息 fmt.Fprintf(c.Writer, "data: {\"error\": \"生成解析失败\"}\n\n") c.Writer.(http.Flusher).Flush() } }