package services import ( "time" "github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models" "gorm.io/gorm" ) // LearningSessionService 学习会话服务 type LearningSessionService struct { db *gorm.DB } func NewLearningSessionService(db *gorm.DB) *LearningSessionService { return &LearningSessionService{db: db} } // StartLearningSession 开始学习会话 func (s *LearningSessionService) StartLearningSession(userID int64, bookID string, dailyGoal int) (*models.LearningSession, error) { now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // 检查今天是否已有学习会话 var existingSession models.LearningSession err := s.db.Where("user_id = ? AND book_id = ? AND DATE(created_at) = ?", userID, bookID, today.Format("2006-01-02")).First(&existingSession).Error if err == nil { // 已存在会话,返回现有会话 return &existingSession, nil } if err != gorm.ErrRecordNotFound { return nil, err } // 创建新的学习会话 session := &models.LearningSession{ UserID: userID, BookID: bookID, DailyGoal: dailyGoal, NewWordsCount: 0, ReviewCount: 0, MasteredCount: 0, StartedAt: now, } if err := s.db.Create(session).Error; err != nil { return nil, err } return session, nil } // GetTodayLearningTasks 获取今日学习任务(新词+复习词) // 学习逻辑:每天学习的单词 = 用户选择的新词数 + 当日所有需要复习的单词 func (s *LearningSessionService) GetTodayLearningTasks(userID int64, bookID string, newWordsLimit int) (map[string]interface{}, error) { now := time.Now() // 1. 获取所有需要复习的单词(到期的,不限数量) var reviewWords []models.UserWordProgress err := s.db.Raw(` SELECT uwp.* FROM ai_user_word_progress uwp INNER JOIN ai_vocabulary_book_words vbw ON CAST(vbw.vocabulary_id AS UNSIGNED) = uwp.vocabulary_id WHERE uwp.user_id = ? AND vbw.book_id = ? AND uwp.status IN ('learning', 'reviewing') AND (uwp.next_review_at IS NULL OR uwp.next_review_at <= ?) ORDER BY uwp.next_review_at ASC `, userID, bookID, now).Scan(&reviewWords).Error if err != nil { return nil, err } // 2. 获取新单词(从未学习的) var newWords []int64 err = s.db.Raw(` SELECT CAST(vbw.vocabulary_id AS UNSIGNED) as id FROM ai_vocabulary_book_words vbw LEFT JOIN ai_user_word_progress uwp ON uwp.vocabulary_id = CAST(vbw.vocabulary_id AS UNSIGNED) AND uwp.user_id = ? WHERE vbw.book_id = ? AND uwp.id IS NULL ORDER BY vbw.sort_order ASC LIMIT ? `, userID, bookID, newWordsLimit).Scan(&newWords).Error if err != nil { return nil, err } // 3. 获取已掌握的单词统计 var masteredCount int64 s.db.Model(&models.UserWordProgress{}). Joins("INNER JOIN ai_vocabulary_book_words vbw ON CAST(vbw.vocabulary_id AS UNSIGNED) = ai_user_word_progress.vocabulary_id"). Where("ai_user_word_progress.user_id = ? AND vbw.book_id = ? AND ai_user_word_progress.status = 'mastered'", userID, bookID). Count(&masteredCount) // 4. 获取词汇书总词数 var totalWords int64 s.db.Model(&models.VocabularyBookWord{}).Where("book_id = ?", bookID).Count(&totalWords) return map[string]interface{}{ "newWords": newWords, "reviewWords": reviewWords, "masteredCount": masteredCount, "totalWords": totalWords, "progress": float64(masteredCount) / float64(totalWords) * 100, }, nil } // RecordWordStudy 记录单词学习结果 func (s *LearningSessionService) RecordWordStudy(userID int64, wordID int64, difficulty string) (*models.UserWordProgress, error) { now := time.Now() var progress models.UserWordProgress err := s.db.Where("user_id = ? AND vocabulary_id = ?", userID, wordID).First(&progress).Error isNew := false if err == gorm.ErrRecordNotFound { isNew = true // 创建新记录 progress = models.UserWordProgress{ UserID: userID, VocabularyID: wordID, Status: "learning", StudyCount: 0, CorrectCount: 0, WrongCount: 0, Proficiency: 0, ReviewInterval: 1, FirstStudiedAt: now, LastStudiedAt: now, } } else if err != nil { return nil, err } // 更新学习统计 progress.StudyCount++ progress.LastStudiedAt = now // 根据难度更新进度和计算下次复习时间 nextInterval := s.calculateNextInterval(progress.ReviewInterval, difficulty, progress.StudyCount) switch difficulty { case "forgot": // 完全忘记:重置 progress.WrongCount++ progress.Proficiency = max(0, progress.Proficiency-30) progress.ReviewInterval = 1 progress.Status = "learning" progress.NextReviewAt = &[]time.Time{now.Add(24 * time.Hour)}[0] case "hard": // 困难:小幅增加间隔 progress.WrongCount++ progress.Proficiency = max(0, progress.Proficiency-10) progress.ReviewInterval = max(1, nextInterval/2) nextReview := now.Add(time.Duration(progress.ReviewInterval) * 24 * time.Hour) progress.NextReviewAt = &nextReview case "good": // 一般:正常增加 progress.CorrectCount++ progress.Proficiency = min(100, progress.Proficiency+15) progress.ReviewInterval = nextInterval nextReview := now.Add(time.Duration(progress.ReviewInterval) * 24 * time.Hour) progress.NextReviewAt = &nextReview // 更新状态 if progress.StudyCount >= 3 && progress.Proficiency >= 60 { progress.Status = "reviewing" } case "easy": // 容易:大幅增加间隔 progress.CorrectCount++ progress.Proficiency = min(100, progress.Proficiency+25) progress.ReviewInterval = int(float64(nextInterval) * 1.5) nextReview := now.Add(time.Duration(progress.ReviewInterval) * 24 * time.Hour) progress.NextReviewAt = &nextReview // 更新状态 if progress.StudyCount >= 2 && progress.Proficiency >= 70 { progress.Status = "reviewing" } case "perfect": // 完美:最大间隔 progress.CorrectCount++ progress.Proficiency = 100 progress.ReviewInterval = nextInterval * 2 nextReview := now.Add(time.Duration(progress.ReviewInterval) * 24 * time.Hour) progress.NextReviewAt = &nextReview // 达到掌握标准 if progress.StudyCount >= 5 && progress.Proficiency >= 90 { progress.Status = "mastered" progress.MasteredAt = &now } else { progress.Status = "reviewing" } } // 保存或更新 if isNew { if err := s.db.Create(&progress).Error; err != nil { return nil, err } } else { if err := s.db.Save(&progress).Error; err != nil { return nil, err } } // 更新今日学习会话的计数器 s.updateTodaySessionStats(userID) return &progress, nil } // updateTodaySessionStats 更新今日学习会话的统计数据 func (s *LearningSessionService) updateTodaySessionStats(userID int64) { now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // 查找今日会话 var session models.LearningSession err := s.db.Where("user_id = ? AND DATE(created_at) = ?", userID, today.Format("2006-01-02")).First(&session).Error if err != nil { return // 没有会话就不更新 } // 统计今日学习的单词 var stats struct { NewWords int64 ReviewWords int64 MasteredWords int64 } // 今日新学单词(首次学习时间是今天) s.db.Model(&models.UserWordProgress{}). Where("user_id = ? AND DATE(first_studied_at) = ?", userID, today.Format("2006-01-02")). Count(&stats.NewWords) // 今日复习单词(首次学习不是今天,但最后学习是今天) s.db.Model(&models.UserWordProgress{}). Where("user_id = ? AND DATE(first_studied_at) != ? AND DATE(last_studied_at) = ?", userID, today.Format("2006-01-02"), today.Format("2006-01-02")). Count(&stats.ReviewWords) // 今日掌握单词(掌握时间是今天) s.db.Model(&models.UserWordProgress{}). Where("user_id = ? AND DATE(mastered_at) = ?", userID, today.Format("2006-01-02")). Count(&stats.MasteredWords) // 更新会话 session.NewWordsCount = int(stats.NewWords) session.ReviewCount = int(stats.ReviewWords) session.MasteredCount = int(stats.MasteredWords) s.db.Save(&session) // 同时更新词汇书级别的进度 s.updateBookProgress(userID, session.BookID) } // updateBookProgress 更新词汇书级别的学习进度 func (s *LearningSessionService) updateBookProgress(userID int64, bookID string) { // 统计该词汇书的总体进度 var progress struct { TotalLearned int64 TotalMastered int64 } // 统计已学习的单词数(该词汇书中的所有已学习单词) s.db.Raw(` SELECT COUNT(DISTINCT uwp.vocabulary_id) as total_learned, SUM(CASE WHEN uwp.status = 'mastered' THEN 1 ELSE 0 END) as total_mastered FROM ai_user_word_progress uwp INNER JOIN ai_vocabulary_book_words vbw ON CAST(vbw.vocabulary_id AS UNSIGNED) = uwp.vocabulary_id WHERE uwp.user_id = ? AND vbw.book_id = ? `, userID, bookID).Scan(&progress) // 获取词汇书总单词数 var totalWords int64 s.db.Model(&models.VocabularyBookWord{}).Where("book_id = ?", bookID).Count(&totalWords) // 计算进度百分比 progressPercentage := 0.0 if totalWords > 0 { progressPercentage = float64(progress.TotalLearned) / float64(totalWords) * 100 } // 更新或创建词汇书进度记录 now := time.Now() var bookProgress models.UserVocabularyBookProgress err := s.db.Where("user_id = ? AND book_id = ?", userID, bookID).First(&bookProgress).Error if err == gorm.ErrRecordNotFound { // 创建新记录 bookProgress = models.UserVocabularyBookProgress{ UserID: userID, BookID: bookID, LearnedWords: int(progress.TotalLearned), MasteredWords: int(progress.TotalMastered), ProgressPercentage: progressPercentage, StartedAt: now, LastStudiedAt: now, } s.db.Create(&bookProgress) } else if err == nil { // 更新现有记录 bookProgress.LearnedWords = int(progress.TotalLearned) bookProgress.MasteredWords = int(progress.TotalMastered) bookProgress.ProgressPercentage = progressPercentage bookProgress.LastStudiedAt = now s.db.Save(&bookProgress) } } // calculateNextInterval 计算下次复习间隔(天数) func (s *LearningSessionService) calculateNextInterval(currentInterval int, difficulty string, studyCount int) int { // 基于SuperMemo SM-2算法的简化版本 baseIntervals := []int{1, 3, 7, 14, 30, 60, 120, 240} if studyCount <= len(baseIntervals) { return baseIntervals[min(studyCount-1, len(baseIntervals)-1)] } // 超过基础序列后,根据难度调整 switch difficulty { case "forgot": return 1 case "hard": return max(1, int(float64(currentInterval)*0.8)) case "good": return int(float64(currentInterval) * 1.5) case "easy": return int(float64(currentInterval) * 2.5) case "perfect": return currentInterval * 3 default: return currentInterval } } // UpdateSessionProgress 更新学习会话进度 func (s *LearningSessionService) UpdateSessionProgress(sessionID int64, newWords, reviewWords, masteredWords int) error { return s.db.Model(&models.LearningSession{}). Where("id = ?", sessionID). Updates(map[string]interface{}{ "new_words_count": newWords, "review_count": reviewWords, "mastered_count": masteredWords, "completed_at": time.Now(), }).Error } // GetLearningStatistics 获取学习统计 func (s *LearningSessionService) GetLearningStatistics(userID int64, bookID string) (map[string]interface{}, error) { // 今日学习统计 now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) var todaySession models.LearningSession s.db.Where("user_id = ? AND book_id = ? AND DATE(created_at) = ?", userID, bookID, today.Format("2006-01-02")).First(&todaySession) // 总体统计 var stats struct { TotalLearned int64 TotalMastered int64 AvgProficiency float64 } s.db.Model(&models.UserWordProgress{}). Select("COUNT(*) as total_learned, SUM(CASE WHEN status = 'mastered' THEN 1 ELSE 0 END) as total_mastered, AVG(proficiency) as avg_proficiency"). Joins("INNER JOIN ai_vocabulary_book_words vbw ON CAST(vbw.vocabulary_id AS UNSIGNED) = ai_user_word_progress.vocabulary_id"). Where("ai_user_word_progress.user_id = ? AND vbw.book_id = ?", userID, bookID). Scan(&stats) // 连续学习天数 var streakDays int s.db.Raw(` SELECT COUNT(DISTINCT DATE(created_at)) FROM ai_learning_sessions WHERE user_id = ? AND book_id = ? AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) `, userID, bookID).Scan(&streakDays) return map[string]interface{}{ "todayNewWords": todaySession.NewWordsCount, "todayReview": todaySession.ReviewCount, "todayMastered": todaySession.MasteredCount, "totalLearned": stats.TotalLearned, "totalMastered": stats.TotalMastered, "avgProficiency": stats.AvgProficiency, "streakDays": streakDays, }, nil } // GetTodayReviewWords 获取今日需要复习的所有单词(跨所有词汇书) func (s *LearningSessionService) GetTodayReviewWords(userID int64) ([]map[string]interface{}, error) { now := time.Now() // 获取所有到期需要复习的单词 var reviewWords []models.UserWordProgress err := s.db.Raw(` SELECT uwp.* FROM ai_user_word_progress uwp WHERE uwp.user_id = ? AND uwp.status IN ('learning', 'reviewing') AND (uwp.next_review_at IS NULL OR uwp.next_review_at <= ?) ORDER BY uwp.next_review_at ASC LIMIT 100 `, userID, now).Scan(&reviewWords).Error if err != nil { return nil, err } // 获取单词详情 result := make([]map[string]interface{}, 0) for _, progress := range reviewWords { result = append(result, map[string]interface{}{ "vocabulary_id": progress.VocabularyID, "status": progress.Status, "proficiency": progress.Proficiency, "study_count": progress.StudyCount, "next_review_at": progress.NextReviewAt, "last_studied_at": progress.LastStudiedAt, }) } return result, nil } // GetTodayOverallStatistics 获取今日总体学习统计(所有词汇书) func (s *LearningSessionService) GetTodayOverallStatistics(userID int64) (map[string]interface{}, error) { now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // 今日学习的单词总数 var todayStats struct { NewWords int64 ReviewWords int64 TotalStudied int64 } // 统计今日新学习的单词 s.db.Model(&models.UserWordProgress{}). Where("user_id = ? AND DATE(first_studied_at) = ?", userID, today.Format("2006-01-02")). Count(&todayStats.NewWords) // 统计今日复习的单词(今天学习但不是第一次) s.db.Model(&models.UserWordProgress{}). Where("user_id = ? AND DATE(first_studied_at) != ? AND DATE(last_studied_at) = ?", userID, today.Format("2006-01-02"), today.Format("2006-01-02")). Count(&todayStats.ReviewWords) todayStats.TotalStudied = todayStats.NewWords + todayStats.ReviewWords // 总体统计 var totalStats struct { TotalLearned int64 TotalMastered int64 } s.db.Model(&models.UserWordProgress{}). Select("COUNT(*) as total_learned, SUM(CASE WHEN status = 'mastered' THEN 1 ELSE 0 END) as total_mastered"). Where("user_id = ?", userID). Scan(&totalStats) // 连续学习天数(最近30天内有学习记录的天数) var streakDays int64 s.db.Raw(` SELECT COUNT(DISTINCT DATE(last_studied_at)) FROM ai_user_word_progress WHERE user_id = ? AND last_studied_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) `, userID).Scan(&streakDays) return map[string]interface{}{ "todayNewWords": todayStats.NewWords, "todayReviewWords": todayStats.ReviewWords, "todayTotalStudied": todayStats.TotalStudied, "totalLearned": totalStats.TotalLearned, "totalMastered": totalStats.TotalMastered, "streakDays": streakDays, }, nil } func min(a, b int) int { if a < b { return a } return b } func max(a, b int) int { if a > b { return a } return b }