package handlers import ( "ankao/internal/database" "ankao/internal/models" "encoding/json" "log" "net/http" "strconv" "time" "github.com/gin-gonic/gin" ) // GetPracticeQuestions 获取练习题目列表 func GetPracticeQuestions(c *gin.Context) { typeParam := c.Query("type") category := c.Query("category") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 100 } db := database.GetDB() var questions []models.PracticeQuestion var total int64 query := db.Model(&models.PracticeQuestion{}) // 根据题型过滤 - 将前端类型映射到后端类型 if typeParam != "" { backendType := mapFrontendToBackendType(typeParam) query = query.Where("type = ?", backendType) } // 根据分类过滤 if category != "" { query = query.Where("type_name = ?", category) } // 获取总数 query.Count(&total) // 分页查询 offset := (page - 1) * pageSize err := query.Offset(offset).Limit(pageSize).Find(&questions).Error if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "查询题目失败", }) return } // 转换为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") db := database.GetDB() var question models.PracticeQuestion query := db.Model(&models.PracticeQuestion{}) if typeParam != "" { backendType := mapFrontendToBackendType(typeParam) query = query.Where("type = ?", backendType) } // 使用PostgreSQL的随机排序 if err := query.Order("RANDOM()").First(&question).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, }) } // 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 err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "答案数据错误", }) return } // 验证答案 correct := checkPracticeAnswer(question.Type, submit.Answer, correctAnswer) // 记录用户答题历史 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) } } // 如果答错,记录到错题本 if !correct { if uid, ok := userID.(uint); ok { // 记录错题 if err := recordWrongQuestion(uid, question.ID, submit.Answer, correctAnswer); err != nil { // 记录错题失败不影响主流程,只记录日志 log.Printf("记录错题失败: %v", err) } } } result := models.PracticeAnswerResult{ Correct: correct, UserAnswer: submit.Answer, CorrectAnswer: correctAnswer, // 始终返回正确答案 } c.JSON(http.StatusOK, gin.H{ "success": true, "data": result, }) } // GetPracticeQuestionTypes 获取题型列表 func GetPracticeQuestionTypes(c *gin.Context) { types := []gin.H{ { "type": models.FillInBlank, "type_name": "填空题", }, { "type": models.TrueFalseType, "type_name": "判断题", }, { "type": models.MultipleChoiceQ, "type_name": "选择题", }, { "type": models.MultipleSelection, "type_name": "多选题", }, { "type": models.ShortAnswer, "type_name": "简答题", }, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": types, }) } // checkPracticeAnswer 检查练习答案是否正确 func checkPracticeAnswer(questionType models.PracticeQuestionType, userAnswer, correctAnswer interface{}) bool { switch questionType { case models.TrueFalseType: // 判断题: boolean 比较 userBool, ok1 := userAnswer.(bool) correctBool, ok2 := correctAnswer.(bool) return ok1 && ok2 && userBool == correctBool case models.MultipleChoiceQ: // 单选题: 字符串比较 userStr, ok1 := userAnswer.(string) correctStr, ok2 := correctAnswer.(string) return ok1 && ok2 && userStr == correctStr case models.MultipleSelection: // 多选题: 数组比较 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 models.FillInBlank: // 填空题: 数组比较 userArr, ok1 := toStringArray(userAnswer) correctArr, ok2 := toStringArray(correctAnswer) if !ok1 || !ok2 || len(userArr) != len(correctArr) { return false } // 逐个比较填空答案 for i := range correctArr { if userArr[i] != correctArr[i] { return false } } return true case models.ShortAnswer: // 简答题: 字符串比较(简单实现,实际可能需要更复杂的判断) 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 } } // mapFrontendToBackendType 将前端类型映射到后端类型 func mapFrontendToBackendType(frontendType string) models.PracticeQuestionType { typeMap := map[string]models.PracticeQuestionType{ "single": models.MultipleChoiceQ, // 单选 "multiple": models.MultipleSelection, // 多选 "judge": models.TrueFalseType, // 判断 "fill": models.FillInBlank, // 填空 "short": models.ShortAnswer, // 简答 } if backendType, ok := typeMap[frontendType]; ok { return backendType } return models.MultipleChoiceQ // 默认返回单选 } // mapBackendToFrontendType 将后端类型映射到前端类型 func mapBackendToFrontendType(backendType models.PracticeQuestionType) string { typeMap := map[models.PracticeQuestionType]string{ models.MultipleChoiceQ: "single", // 单选 models.MultipleSelection: "multiple", // 多选 models.TrueFalseType: "judge", // 判断 models.FillInBlank: "fill", // 填空 models.ShortAnswer: "short", // 简答 } if frontendType, ok := typeMap[backendType]; ok { return frontendType } return "single" // 默认返回单选 } // convertToDTO 将数据库模型转换为前端DTO func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO { dto := models.PracticeQuestionDTO{ ID: question.ID, QuestionID: question.QuestionID, // 添加题目编号 Type: mapBackendToFrontendType(question.Type), Content: question.Question, Category: question.TypeName, Options: []models.Option{}, } // 判断题自动生成选项 if question.Type == models.TrueFalseType { 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数组 for key, value := range optionsMap { dto.Options = append(dto.Options, models.Option{ Key: key, Value: value, }) } } } 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 accuracy float64 if answeredQuestions > 0 { // 正确率 = 答对题数 / 总答题数 var totalAnswers int64 db.Model(&models.UserAnswerRecord{}). Where("user_id = ?", uid). Count(&totalAnswers) accuracy = float64(correctAnswers) / float64(totalAnswers) * 100 } stats := models.UserStatistics{ TotalQuestions: int(totalQuestions), AnsweredQuestions: int(answeredQuestions), CorrectAnswers: int(correctAnswers), Accuracy: accuracy, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": stats, }) }