Files
ai_english/serve/internal/services/learning_session_service.go
2025-11-17 13:39:05 +08:00

511 lines
16 KiB
Go
Raw Permalink 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 (
"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
}