package handlers import ( "ankao/internal/database" "ankao/internal/models" "encoding/json" "log" "net/http" "sort" "strconv" "time" "github.com/gin-gonic/gin" ) // GetPracticeQuestions 获取练习题目列表 func GetPracticeQuestions(c *gin.Context) { typeParam := c.Query("type") searchQuery := c.Query("search") db := database.GetDB() var questions []models.PracticeQuestion var total int64 query := db.Model(&models.PracticeQuestion{}) // 根据题型过滤 if typeParam != "" { query = query.Where("type = ?", typeParam) } // 根据搜索关键词过滤(搜索题目内容或题目编号) if searchQuery != "" { query = query.Where("question LIKE ? OR question_id LIKE ?", "%"+searchQuery+"%", "%"+searchQuery+"%") } // 获取总数 query.Count(&total) // 查询所有题目 - 按题型和题目编号升序排序 // 先将 question_id 转为文本,提取数字部分,再转为整数排序 err := query.Order("type ASC, CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) ASC").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 != "" { query = query.Where("type = ?", typeParam) } // 使用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": "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": "简答题", }, } 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) { return false } // 逐个比较填空答案 for i := range correctArr { if userArr[i] != correctArr[i] { 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{}, } // 解析答案数据 var answer interface{} if err := json.Unmarshal([]byte(question.AnswerData), &answer); err == nil { 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 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), WrongQuestions: int(wrongQuestions), 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": "删除成功", }) }