AnCao/internal/services/wrong_question_service.go

290 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 int64) (*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 int64, 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 int64, limit int, excludeQuestionID int64) ([]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
}