package handlers import ( "ankao/internal/database" "ankao/internal/models" "ankao/internal/services" "encoding/json" "net/http" "strconv" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // ==================== 错题管理 API ==================== // GetWrongQuestions 获取错题列表(新版) // GET /api/v2/wrong-questions?is_mastered=false&type=single-choice&tag=数学&sort=review_time func GetWrongQuestions(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) return } db := database.GetDB() query := db.Model(&models.WrongQuestion{}).Where("user_id = ?", userID) // 筛选条件 if isMastered := c.Query("is_mastered"); isMastered != "" { query = query.Where("is_mastered = ?", isMastered == "true") } if tag := c.Query("tag"); tag != "" { query = query.Where("tags LIKE ?", "%"+tag+"%") } // 排序 switch c.Query("sort") { case "wrong_count": // 按错误次数排序(错误最多的在前) query = query.Order("total_wrong_count DESC") case "mastery_level": // 按掌握度排序(掌握度最低的在前) query = query.Order("mastery_level ASC") default: // 默认按最后错误时间排序 query = query.Order("last_wrong_time DESC") } var wrongQuestions []models.WrongQuestion // 先查询错题记录 if err := query.Find(&wrongQuestions).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询错题失败"}) return } // 手动加载关联数据 for i := range wrongQuestions { // 加载题目信息(确保使用正确的关联) var practiceQuestion models.PracticeQuestion if err := db.Where("id = ?", wrongQuestions[i].QuestionID).First(&practiceQuestion).Error; err == nil { wrongQuestions[i].PracticeQuestion = &practiceQuestion } // 加载最近3次历史 var history []models.WrongQuestionHistory if err := db.Where("wrong_question_id = ?", wrongQuestions[i].ID). Order("answered_at DESC"). Limit(3). Find(&history).Error; err == nil { wrongQuestions[i].History = history } } // 转换为 DTO dtos := make([]models.WrongQuestionDTO, len(wrongQuestions)) for i, wq := range wrongQuestions { dtos[i] = convertWrongQuestionToDTO(&wq, true) // 包含最近历史 } c.JSON(http.StatusOK, gin.H{ "success": true, "data": dtos, }) } // GetWrongQuestionDetail 获取错题详情(包含完整历史) // GET /api/v2/wrong-questions/:id func GetWrongQuestionDetail(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) return } id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的错题ID"}) return } db := database.GetDB() var wrongQuestion models.WrongQuestion if err := db.Where("id = ? AND user_id = ?", id, userID). Preload("PracticeQuestion"). Preload("History", func(db *gorm.DB) *gorm.DB { return db.Order("answered_at DESC") }). First(&wrongQuestion).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "错题不存在"}) return } // 转换为 DTO(包含完整历史) dto := convertToDetailDTO(&wrongQuestion) c.JSON(http.StatusOK, gin.H{ "success": true, "data": dto, }) } // GetWrongQuestionStats 获取错题统计(新版) // GET /api/v2/wrong-questions/stats func GetWrongQuestionStats(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) return } stats, err := services.GetWrongQuestionStats(userID.(uint)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取统计失败"}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "data": stats, }) } // GetRecommendedWrongQuestions 获取推荐练习的错题(智能推荐) // GET /api/v2/wrong-questions/recommended?limit=10&exclude=123 func GetRecommendedWrongQuestions(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) return } limit := 10 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed } } // 获取要排除的题目ID(前端传递当前题目ID,避免重复推荐) excludeQuestionID := uint(0) if e := c.Query("exclude"); e != "" { if parsed, err := strconv.ParseUint(e, 10, 32); err == nil { excludeQuestionID = uint(parsed) } } questions, err := services.GetRecommendedWrongQuestions(userID.(uint), limit, excludeQuestionID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取推荐错题失败"}) return } // 转换为 DTO dtos := make([]models.WrongQuestionDTO, len(questions)) for i, wq := range questions { dtos[i] = convertWrongQuestionToDTO(&wq, false) } c.JSON(http.StatusOK, gin.H{ "success": true, "data": dtos, }) } // DeleteWrongQuestion 删除错题(新版) // DELETE /api/v2/wrong-questions/:id func DeleteWrongQuestion(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) return } id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的错题ID"}) return } db := database.GetDB() // 删除历史记录 db.Where("wrong_question_id = ?", id).Delete(&models.WrongQuestionHistory{}) // 删除错题记录 result := db.Where("id = ? AND user_id = ?", id, userID).Delete(&models.WrongQuestion{}) if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "删除错题失败"}) return } if result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "错题不存在"}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "删除成功", }) } // ClearWrongQuestions 清空错题本(新版) // DELETE /api/v2/wrong-questions func ClearWrongQuestions(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"}) return } db := database.GetDB() // 获取所有错题ID var wrongQuestionIDs []uint db.Model(&models.WrongQuestion{}).Where("user_id = ?", userID).Pluck("id", &wrongQuestionIDs) // 删除历史记录 if len(wrongQuestionIDs) > 0 { db.Where("wrong_question_id IN ?", wrongQuestionIDs).Delete(&models.WrongQuestionHistory{}) } // 删除错题记录 if err := db.Where("user_id = ?", userID).Delete(&models.WrongQuestion{}).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "清空错题本失败"}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "错题本已清空", }) } // ==================== 辅助函数 ==================== // convertWrongQuestionToDTO 转换为 DTO V2(可选是否包含最近历史) func convertWrongQuestionToDTO(wq *models.WrongQuestion, includeHistory bool) models.WrongQuestionDTO { dto := models.WrongQuestionDTO{ ID: wq.ID, QuestionID: wq.QuestionID, FirstWrongTime: wq.FirstWrongTime, LastWrongTime: wq.LastWrongTime, TotalWrongCount: wq.TotalWrongCount, MasteryLevel: wq.MasteryLevel, ConsecutiveCorrect: wq.ConsecutiveCorrect, IsMastered: wq.IsMastered, } // 转换题目信息 if wq.PracticeQuestion != nil { questionDTO := convertToDTO(*wq.PracticeQuestion) dto.Question = &questionDTO } // 包含最近3次历史 if includeHistory && len(wq.History) > 0 { count := 3 if len(wq.History) < count { count = len(wq.History) } dto.RecentHistory = make([]models.WrongQuestionHistoryDTO, count) for i := 0; i < count; i++ { dto.RecentHistory[i] = convertWrongHistoryToDTO(&wq.History[i]) } } return dto } // convertToDetailDTO 转换为详情 DTO(包含完整历史) func convertToDetailDTO(wq *models.WrongQuestion) models.WrongQuestionDTO { dto := convertWrongQuestionToDTO(wq, false) // 包含完整历史 if len(wq.History) > 0 { dto.RecentHistory = make([]models.WrongQuestionHistoryDTO, len(wq.History)) for i, h := range wq.History { dto.RecentHistory[i] = convertWrongHistoryToDTO(&h) } } return dto } // convertWrongHistoryToDTO 转换历史记录为 DTO func convertWrongHistoryToDTO(h *models.WrongQuestionHistory) models.WrongQuestionHistoryDTO { return models.WrongQuestionHistoryDTO{ ID: h.ID, UserAnswer: parseJSONAnswer(h.UserAnswer), CorrectAnswer: parseJSONAnswer(h.CorrectAnswer), AnsweredAt: h.AnsweredAt, TimeSpent: h.TimeSpent, IsCorrect: h.IsCorrect, } } // parseJSONAnswer 解析 JSON 答案 func parseJSONAnswer(answerStr string) interface{} { var answer interface{} if err := json.Unmarshal([]byte(answerStr), &answer); err != nil { return answerStr } return answer }