511 lines
16 KiB
Go
511 lines
16 KiB
Go
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
|
||
}
|