package services import ( "ankao/internal/database" "ankao/internal/models" "encoding/json" "fmt" "log" "time" "gorm.io/gorm" ) // ==================== 错题服务 ==================== // RecordWrongAnswer 记录错误答案 func RecordWrongAnswer(userID, questionID int64, userAnswer, correctAnswer interface{}, timeSpent int) error { db := database.GetDB() log.Printf("[错题记录] 开始记录错题 (userID: %d, questionID: %d)", userID, questionID) // 序列化答案 userAnswerJSON, _ := json.Marshal(userAnswer) correctAnswerJSON, _ := json.Marshal(correctAnswer) // 查找或创建错题记录 var wrongQuestion models.WrongQuestion err := db.Where("user_id = ? AND question_id = ?", userID, questionID).First(&wrongQuestion).Error if err != nil { // 不存在,创建新记录 log.Printf("[错题记录] 创建新错题记录 (userID: %d, questionID: %d)", userID, questionID) wrongQuestion = models.WrongQuestion{ UserID: userID, QuestionID: questionID, } wrongQuestion.RecordWrongAnswer() if err := db.Create(&wrongQuestion).Error; err != nil { log.Printf("[错题记录] 创建错题记录失败: %v", err) return fmt.Errorf("创建错题记录失败: %v", err) } log.Printf("[错题记录] 成功创建错题记录 (ID: %d)", wrongQuestion.ID) } else { // 已存在,更新记录 log.Printf("[错题记录] 更新已存在的错题记录 (ID: %d)", wrongQuestion.ID) wrongQuestion.RecordWrongAnswer() if err := db.Save(&wrongQuestion).Error; err != nil { log.Printf("[错题记录] 更新错题记录失败: %v", err) return fmt.Errorf("更新错题记录失败: %v", err) } log.Printf("[错题记录] 成功更新错题记录 (ID: %d, 错误次数: %d)", wrongQuestion.ID, wrongQuestion.TotalWrongCount) } // 创建历史记录 history := models.WrongQuestionHistory{ WrongQuestionID: wrongQuestion.ID, UserAnswer: string(userAnswerJSON), CorrectAnswer: string(correctAnswerJSON), AnsweredAt: time.Now(), TimeSpent: timeSpent, IsCorrect: false, } if err := db.Create(&history).Error; err != nil { log.Printf("[错题记录] 创建错题历史失败: %v", err) } else { log.Printf("[错题记录] 成功创建历史记录 (ID: %d, WrongQuestionID: %d)", history.ID, history.WrongQuestionID) } return nil } // RecordCorrectAnswer 记录正确答案(用于错题练习) func RecordCorrectAnswer(userID, questionID int64, userAnswer, correctAnswer interface{}, timeSpent int) error { db := database.GetDB() // 查找错题记录 var wrongQuestion models.WrongQuestion err := db.Where("user_id = ? AND question_id = ?", userID, questionID).First(&wrongQuestion).Error if err != nil { // 不存在错题记录,无需处理 return nil } // 序列化答案 userAnswerJSON, _ := json.Marshal(userAnswer) correctAnswerJSON, _ := json.Marshal(correctAnswer) // 更新连续答对次数 wrongQuestion.RecordCorrectAnswer() if err := db.Save(&wrongQuestion).Error; err != nil { return fmt.Errorf("更新错题记录失败: %v", err) } // 创建历史记录 history := models.WrongQuestionHistory{ WrongQuestionID: wrongQuestion.ID, UserAnswer: string(userAnswerJSON), CorrectAnswer: string(correctAnswerJSON), AnsweredAt: time.Now(), TimeSpent: timeSpent, IsCorrect: true, } if err := db.Create(&history).Error; err != nil { log.Printf("创建错题历史失败: %v", err) } return nil } // GetWrongQuestionStats 获取错题统计 func GetWrongQuestionStats(userID uint) (*models.WrongQuestionStats, error) { db := database.GetDB() stats := &models.WrongQuestionStats{ TypeStats: make(map[string]int), CategoryStats: make(map[string]int), MasteryLevelDist: make(map[string]int), } // 基础统计 var totalWrong, mastered int64 db.Model(&models.WrongQuestion{}).Where("user_id = ?", userID).Count(&totalWrong) db.Model(&models.WrongQuestion{}).Where("user_id = ? AND is_mastered = ?", userID, true).Count(&mastered) stats.TotalWrong = int(totalWrong) stats.Mastered = int(mastered) stats.NotMastered = int(totalWrong) - int(mastered) stats.NeedReview = 0 // 不再使用复习时间,设置为0 // 按题型统计 var typeStats []struct { Type string Count int } db.Model(&models.WrongQuestion{}). Select("practice_questions.type, COUNT(*) as count"). Joins("LEFT JOIN practice_questions ON practice_questions.id = wrong_questions.question_id"). Where("wrong_questions.user_id = ?", userID). Group("practice_questions.type"). Scan(&typeStats) for _, ts := range typeStats { stats.TypeStats[ts.Type] = ts.Count } // 按分类统计 var categoryStats []struct { Category string Count int } db.Model(&models.WrongQuestion{}). Select("practice_questions.category, COUNT(*) as count"). Joins("LEFT JOIN practice_questions ON practice_questions.id = wrong_questions.question_id"). Where("wrong_questions.user_id = ?", userID). Group("practice_questions.category"). Scan(&categoryStats) for _, cs := range categoryStats { stats.CategoryStats[cs.Category] = cs.Count } // 掌握度分布 var masteryDist []struct { Level string Count int } db.Model(&models.WrongQuestion{}). Select(` CASE WHEN mastery_level >= 80 THEN '优秀' WHEN mastery_level >= 60 THEN '良好' WHEN mastery_level >= 40 THEN '一般' WHEN mastery_level >= 20 THEN '较差' ELSE '很差' END as level, COUNT(*) as count `). Where("user_id = ?", userID). Group("level"). Scan(&masteryDist) for _, md := range masteryDist { stats.MasteryLevelDist[md.Level] = md.Count } // 错题趋势(最近7天) stats.TrendData = calculateTrendData(db, userID, 7) return stats, nil } // calculateTrendData 计算错题趋势数据 func calculateTrendData(db *gorm.DB, userID uint, days int) []models.TrendPoint { trendData := make([]models.TrendPoint, days) now := time.Now() for i := days - 1; i >= 0; i-- { date := now.AddDate(0, 0, -i) dateStr := date.Format("01-02") var count int64 startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) endOfDay := startOfDay.Add(24 * time.Hour) db.Model(&models.WrongQuestion{}). Where("user_id = ? AND last_wrong_time >= ? AND last_wrong_time < ?", userID, startOfDay, endOfDay). Count(&count) trendData[days-1-i] = models.TrendPoint{ Date: dateStr, Count: int(count), } } return trendData } // GetRecommendedWrongQuestions 获取推荐练习的错题(智能推荐) // 推荐策略(按优先级): // 1. 最优先推荐掌握度为0的题目(从未答对过) // 2. 其次推荐掌握度低的题目(mastery_level 从低到高) // 3. 最后推荐最近答错的题目 func GetRecommendedWrongQuestions(userID uint, limit int, excludeQuestionID uint) ([]models.WrongQuestion, error) { db := database.GetDB() var questions []models.WrongQuestion // 策略1: 最优先推荐掌握度为0的题目(从未答对过) var zeroMastery []models.WrongQuestion query1 := db.Where("user_id = ? AND is_mastered = ? AND mastery_level = 0", userID, false) if excludeQuestionID > 0 { query1 = query1.Where("question_id != ?", excludeQuestionID) } query1.Order("total_wrong_count DESC, last_wrong_time DESC"). Limit(limit). Preload("PracticeQuestion"). Find(&zeroMastery) questions = append(questions, zeroMastery...) // 如果已经够了,直接返回 if len(questions) >= limit { return questions[:limit], nil } // 策略2: 推荐掌握度低的题目(mastery_level 从低到高) var lowMastery []models.WrongQuestion query2 := db.Where("user_id = ? AND is_mastered = ? AND mastery_level > 0 AND id NOT IN ?", userID, false, getIDs(questions)) if excludeQuestionID > 0 { query2 = query2.Where("question_id != ?", excludeQuestionID) } query2.Order("mastery_level ASC, total_wrong_count DESC"). Limit(limit - len(questions)). Preload("PracticeQuestion"). Find(&lowMastery) questions = append(questions, lowMastery...) if len(questions) >= limit { return questions[:limit], nil } // 策略3: 最近答错的题目(填充剩余,以防万一) var recent []models.WrongQuestion query3 := db.Where("user_id = ? AND is_mastered = ? AND id NOT IN ?", userID, false, getIDs(questions)) if excludeQuestionID > 0 { query3 = query3.Where("question_id != ?", excludeQuestionID) } query3.Order("last_wrong_time DESC"). Limit(limit - len(questions)). Preload("PracticeQuestion"). Find(&recent) questions = append(questions, recent...) return questions, nil } // getIDs 获取错题记录的ID列表 func getIDs(questions []models.WrongQuestion) []int64 { if len(questions) == 0 { return []int64{0} // 避免 SQL 错误 } ids := make([]int64, len(questions)) for i, q := range questions { ids[i] = q.ID } return ids }