Files
ai_english/serve/internal/services/learning_session_service.go

511 lines
16 KiB
Go
Raw Normal View History

2025-11-17 13:39:05 +08:00
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
}