init
This commit is contained in:
510
serve/internal/services/learning_session_service.go
Normal file
510
serve/internal/services/learning_session_service.go
Normal file
@@ -0,0 +1,510 @@
|
||||
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
|
||||
}
|
||||
347
serve/internal/services/listening_service.go
Normal file
347
serve/internal/services/listening_service.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListeningService 听力训练服务
|
||||
type ListeningService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewListeningService 创建听力训练服务实例
|
||||
func NewListeningService(db *gorm.DB) *ListeningService {
|
||||
return &ListeningService{db: db}
|
||||
}
|
||||
|
||||
// GetListeningMaterials 获取听力材料列表
|
||||
func (s *ListeningService) GetListeningMaterials(level, category string, page, pageSize int) ([]models.ListeningMaterial, int64, error) {
|
||||
var materials []models.ListeningMaterial
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.ListeningMaterial{}).Where("is_active = ?", true)
|
||||
|
||||
if level != "" {
|
||||
query = query.Where("level = ?", level)
|
||||
}
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&materials).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return materials, total, nil
|
||||
}
|
||||
|
||||
// GetListeningMaterial 获取单个听力材料
|
||||
func (s *ListeningService) GetListeningMaterial(id string) (*models.ListeningMaterial, error) {
|
||||
var material models.ListeningMaterial
|
||||
if err := s.db.Where("id = ? AND is_active = ?", id, true).First(&material).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("听力材料不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &material, nil
|
||||
}
|
||||
|
||||
// CreateListeningMaterial 创建听力材料
|
||||
func (s *ListeningService) CreateListeningMaterial(material *models.ListeningMaterial) error {
|
||||
material.ID = uuid.New().String()
|
||||
material.CreatedAt = time.Now()
|
||||
material.UpdatedAt = time.Now()
|
||||
material.IsActive = true
|
||||
|
||||
return s.db.Create(material).Error
|
||||
}
|
||||
|
||||
// UpdateListeningMaterial 更新听力材料
|
||||
func (s *ListeningService) UpdateListeningMaterial(id string, updates map[string]interface{}) error {
|
||||
updates["updated_at"] = time.Now()
|
||||
result := s.db.Model(&models.ListeningMaterial{}).Where("id = ?", id).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("听力材料不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteListeningMaterial 删除听力材料(软删除)
|
||||
func (s *ListeningService) DeleteListeningMaterial(id string) error {
|
||||
result := s.db.Model(&models.ListeningMaterial{}).Where("id = ?", id).Update("is_active", false)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("听力材料不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchListeningMaterials 搜索听力材料
|
||||
func (s *ListeningService) SearchListeningMaterials(keyword, level, category string, page, pageSize int) ([]models.ListeningMaterial, int64, error) {
|
||||
var materials []models.ListeningMaterial
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.ListeningMaterial{}).Where("is_active = ?", true)
|
||||
|
||||
if keyword != "" {
|
||||
query = query.Where("title LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
if level != "" {
|
||||
query = query.Where("level = ?", level)
|
||||
}
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&materials).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return materials, total, nil
|
||||
}
|
||||
|
||||
// CreateListeningRecord 创建听力练习记录
|
||||
func (s *ListeningService) CreateListeningRecord(record *models.ListeningRecord) error {
|
||||
record.ID = uuid.New().String()
|
||||
record.StartedAt = time.Now()
|
||||
record.CreatedAt = time.Now()
|
||||
record.UpdatedAt = time.Now()
|
||||
|
||||
return s.db.Create(record).Error
|
||||
}
|
||||
|
||||
// UpdateListeningRecord 更新听力练习记录
|
||||
func (s *ListeningService) UpdateListeningRecord(id string, updates map[string]interface{}) error {
|
||||
updates["updated_at"] = time.Now()
|
||||
if _, exists := updates["completed_at"]; exists {
|
||||
now := time.Now()
|
||||
updates["completed_at"] = &now
|
||||
}
|
||||
|
||||
result := s.db.Model(&models.ListeningRecord{}).Where("id = ?", id).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("听力练习记录不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserListeningRecords 获取用户听力练习记录
|
||||
func (s *ListeningService) GetUserListeningRecords(userID string, page, pageSize int) ([]models.ListeningRecord, int64, error) {
|
||||
var records []models.ListeningRecord
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.ListeningRecord{}).Where("user_id = ?", userID)
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询,包含关联的材料信息
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Preload("Material").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
// GetListeningRecord 获取单个听力练习记录
|
||||
func (s *ListeningService) GetListeningRecord(id string) (*models.ListeningRecord, error) {
|
||||
var record models.ListeningRecord
|
||||
if err := s.db.Preload("Material").Where("id = ?", id).First(&record).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("听力练习记录不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// GetUserListeningStats 获取用户听力学习统计
|
||||
func (s *ListeningService) GetUserListeningStats(userID string) (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
// 总练习次数
|
||||
var totalRecords int64
|
||||
if err := s.db.Model(&models.ListeningRecord{}).Where("user_id = ?", userID).Count(&totalRecords).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["total_records"] = totalRecords
|
||||
|
||||
// 已完成练习次数
|
||||
var completedRecords int64
|
||||
if err := s.db.Model(&models.ListeningRecord{}).Where("user_id = ? AND completed_at IS NOT NULL", userID).Count(&completedRecords).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["completed_records"] = completedRecords
|
||||
|
||||
// 平均得分(NULL 安全)
|
||||
var avgScore sql.NullFloat64
|
||||
if err := s.db.Model(&models.ListeningRecord{}).
|
||||
Where("user_id = ? AND score IS NOT NULL", userID).
|
||||
Select("AVG(score)").
|
||||
Scan(&avgScore).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if avgScore.Valid {
|
||||
stats["average_score"] = avgScore.Float64
|
||||
} else {
|
||||
stats["average_score"] = 0.0
|
||||
}
|
||||
|
||||
// 平均准确率(NULL 安全)
|
||||
var avgAccuracy sql.NullFloat64
|
||||
if err := s.db.Model(&models.ListeningRecord{}).
|
||||
Where("user_id = ? AND accuracy IS NOT NULL", userID).
|
||||
Select("AVG(accuracy)").
|
||||
Scan(&avgAccuracy).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if avgAccuracy.Valid {
|
||||
stats["average_accuracy"] = avgAccuracy.Float64
|
||||
} else {
|
||||
stats["average_accuracy"] = 0.0
|
||||
}
|
||||
|
||||
// 总学习时间(分钟,NULL 安全)
|
||||
var totalTimeSpent sql.NullInt64
|
||||
if err := s.db.Model(&models.ListeningRecord{}).
|
||||
Where("user_id = ?", userID).
|
||||
Select("SUM(time_spent)").
|
||||
Scan(&totalTimeSpent).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if totalTimeSpent.Valid {
|
||||
stats["total_time_spent"] = totalTimeSpent.Int64 / 60
|
||||
} else {
|
||||
stats["total_time_spent"] = 0
|
||||
}
|
||||
|
||||
// 连续学习天数
|
||||
continuousDays, err := s.calculateContinuousLearningDays(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["continuous_days"] = continuousDays
|
||||
|
||||
// 按难度级别统计
|
||||
levelStats := make(map[string]int64)
|
||||
rows, err := s.db.Raw(`
|
||||
SELECT lm.level, COUNT(*) as count
|
||||
FROM ai_listening_records lr
|
||||
JOIN ai_listening_materials lm ON lr.material_id = lm.id
|
||||
WHERE lr.user_id = ? AND lr.completed_at IS NOT NULL
|
||||
GROUP BY lm.level
|
||||
`, userID).Rows()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var level string
|
||||
var count int64
|
||||
if err := rows.Scan(&level, &count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
levelStats[level] = count
|
||||
}
|
||||
stats["level_stats"] = levelStats
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// calculateContinuousLearningDays 计算连续学习天数
|
||||
func (s *ListeningService) calculateContinuousLearningDays(userID string) (int, error) {
|
||||
// 获取最近的学习记录日期
|
||||
rows, err := s.db.Raw(`
|
||||
SELECT DISTINCT DATE(created_at) as learning_date
|
||||
FROM ai_listening_records
|
||||
WHERE user_id = ? AND completed_at IS NOT NULL
|
||||
ORDER BY learning_date DESC
|
||||
LIMIT 30
|
||||
`, userID).Rows()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var dates []time.Time
|
||||
for rows.Next() {
|
||||
var date time.Time
|
||||
if err := rows.Scan(&date); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
dates = append(dates, date)
|
||||
}
|
||||
|
||||
if len(dates) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 计算连续天数
|
||||
continuousDays := 1
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
lastDate := dates[0].Truncate(24 * time.Hour)
|
||||
|
||||
// 如果最后一次学习不是今天或昨天,连续天数为0
|
||||
if lastDate.Before(today.AddDate(0, 0, -1)) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
for i := 1; i < len(dates); i++ {
|
||||
currentDate := dates[i].Truncate(24 * time.Hour)
|
||||
expectedDate := lastDate.AddDate(0, 0, -1)
|
||||
|
||||
if currentDate.Equal(expectedDate) {
|
||||
continuousDays++
|
||||
lastDate = currentDate
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return continuousDays, nil
|
||||
}
|
||||
|
||||
// GetListeningProgress 获取用户在特定材料上的学习进度
|
||||
func (s *ListeningService) GetListeningProgress(userID, materialID string) (*models.ListeningRecord, error) {
|
||||
var record models.ListeningRecord
|
||||
if err := s.db.Where("user_id = ? AND material_id = ?", userID, materialID).Order("created_at DESC").First(&record).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // 没有学习记录
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
163
serve/internal/services/notification_service.go
Normal file
163
serve/internal/services/notification_service.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// NotificationService 通知服务
|
||||
type NotificationService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewNotificationService 创建通知服务实例
|
||||
func NewNotificationService(db *gorm.DB) *NotificationService {
|
||||
return &NotificationService{db: db}
|
||||
}
|
||||
|
||||
// GetUserNotifications 获取用户通知列表
|
||||
func (s *NotificationService) GetUserNotifications(userID int64, page, limit int, onlyUnread bool) ([]models.Notification, int64, error) {
|
||||
var notifications []models.Notification
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.Notification{}).Where("user_id = ?", userID)
|
||||
|
||||
// 只查询未读通知
|
||||
if onlyUnread {
|
||||
query = query.Where("is_read = ?", false)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("统计通知数量失败: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Order("priority DESC, created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(limit).
|
||||
Find(¬ifications).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("查询通知列表失败: %w", err)
|
||||
}
|
||||
|
||||
return notifications, total, nil
|
||||
}
|
||||
|
||||
// GetUnreadCount 获取未读通知数量
|
||||
func (s *NotificationService) GetUnreadCount(userID int64) (int64, error) {
|
||||
var count int64
|
||||
if err := s.db.Model(&models.Notification{}).
|
||||
Where("user_id = ? AND is_read = ?", userID, false).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, fmt.Errorf("统计未读通知失败: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// MarkAsRead 标记通知为已读
|
||||
func (s *NotificationService) MarkAsRead(userID, notificationID int64) error {
|
||||
now := time.Now()
|
||||
result := s.db.Model(&models.Notification{}).
|
||||
Where("id = ? AND user_id = ?", notificationID, userID).
|
||||
Updates(map[string]interface{}{
|
||||
"is_read": true,
|
||||
"read_at": now,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("标记通知已读失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("通知不存在或无权限")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkAllAsRead 标记所有通知为已读
|
||||
func (s *NotificationService) MarkAllAsRead(userID int64) error {
|
||||
now := time.Now()
|
||||
result := s.db.Model(&models.Notification{}).
|
||||
Where("user_id = ? AND is_read = ?", userID, false).
|
||||
Updates(map[string]interface{}{
|
||||
"is_read": true,
|
||||
"read_at": now,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("标记所有通知已读失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteNotification 删除通知
|
||||
func (s *NotificationService) DeleteNotification(userID, notificationID int64) error {
|
||||
result := s.db.Where("id = ? AND user_id = ?", notificationID, userID).
|
||||
Delete(&models.Notification{})
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("删除通知失败: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("通知不存在或无权限")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateNotification 创建通知(内部使用)
|
||||
func (s *NotificationService) CreateNotification(notification *models.Notification) error {
|
||||
if err := s.db.Create(notification).Error; err != nil {
|
||||
return fmt.Errorf("创建通知失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendSystemNotification 发送系统通知
|
||||
func (s *NotificationService) SendSystemNotification(userID int64, title, content string, link *string, priority int) error {
|
||||
notification := &models.Notification{
|
||||
UserID: userID,
|
||||
Type: models.NotificationTypeSystem,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Link: link,
|
||||
Priority: priority,
|
||||
IsRead: false,
|
||||
}
|
||||
return s.CreateNotification(notification)
|
||||
}
|
||||
|
||||
// SendLearningReminder 发送学习提醒
|
||||
func (s *NotificationService) SendLearningReminder(userID int64, title, content string, link *string) error {
|
||||
notification := &models.Notification{
|
||||
UserID: userID,
|
||||
Type: models.NotificationTypeLearning,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Link: link,
|
||||
Priority: models.NotificationPriorityNormal,
|
||||
IsRead: false,
|
||||
}
|
||||
return s.CreateNotification(notification)
|
||||
}
|
||||
|
||||
// SendAchievementNotification 发送成就通知
|
||||
func (s *NotificationService) SendAchievementNotification(userID int64, title, content string, link *string) error {
|
||||
notification := &models.Notification{
|
||||
UserID: userID,
|
||||
Type: models.NotificationTypeAchievement,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Link: link,
|
||||
Priority: models.NotificationPriorityImportant,
|
||||
IsRead: false,
|
||||
}
|
||||
return s.CreateNotification(notification)
|
||||
}
|
||||
432
serve/internal/services/reading_service.go
Normal file
432
serve/internal/services/reading_service.go
Normal file
@@ -0,0 +1,432 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ReadingService 阅读理解服务
|
||||
type ReadingService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewReadingService 创建阅读理解服务实例
|
||||
func NewReadingService(db *gorm.DB) *ReadingService {
|
||||
return &ReadingService{db: db}
|
||||
}
|
||||
|
||||
// ===== 阅读材料管理 =====
|
||||
|
||||
// GetReadingMaterials 获取阅读材料列表
|
||||
func (s *ReadingService) GetReadingMaterials(level, category string, page, pageSize int) ([]models.ReadingMaterial, int64, error) {
|
||||
var materials []models.ReadingMaterial
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.ReadingMaterial{}).Where("is_active = ?", true)
|
||||
|
||||
// 按难度级别筛选
|
||||
if level != "" {
|
||||
query = query.Where("level = ?", level)
|
||||
}
|
||||
|
||||
// 按分类筛选
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&materials).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return materials, total, nil
|
||||
}
|
||||
|
||||
// GetReadingMaterial 获取单个阅读材料
|
||||
func (s *ReadingService) GetReadingMaterial(id string) (*models.ReadingMaterial, error) {
|
||||
var material models.ReadingMaterial
|
||||
if err := s.db.Where("id = ? AND is_active = ?", id, true).First(&material).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("阅读材料不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &material, nil
|
||||
}
|
||||
|
||||
// CreateReadingMaterial 创建阅读材料
|
||||
func (s *ReadingService) CreateReadingMaterial(material *models.ReadingMaterial) error {
|
||||
material.ID = uuid.New().String()
|
||||
material.CreatedAt = time.Now()
|
||||
material.UpdatedAt = time.Now()
|
||||
material.IsActive = true
|
||||
|
||||
return s.db.Create(material).Error
|
||||
}
|
||||
|
||||
// UpdateReadingMaterial 更新阅读材料
|
||||
func (s *ReadingService) UpdateReadingMaterial(id string, updates map[string]interface{}) error {
|
||||
updates["updated_at"] = time.Now()
|
||||
result := s.db.Model(&models.ReadingMaterial{}).Where("id = ?", id).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("阅读材料不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteReadingMaterial 删除阅读材料(软删除)
|
||||
func (s *ReadingService) DeleteReadingMaterial(id string) error {
|
||||
result := s.db.Model(&models.ReadingMaterial{}).Where("id = ?", id).Update("is_active", false)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("阅读材料不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchReadingMaterials 搜索阅读材料
|
||||
func (s *ReadingService) SearchReadingMaterials(keyword string, level, category string, page, pageSize int) ([]models.ReadingMaterial, int64, error) {
|
||||
var materials []models.ReadingMaterial
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.ReadingMaterial{}).Where("is_active = ?", true)
|
||||
|
||||
// 关键词搜索
|
||||
if keyword != "" {
|
||||
query = query.Where("title LIKE ? OR content LIKE ? OR summary LIKE ?",
|
||||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
// 按难度级别筛选
|
||||
if level != "" {
|
||||
query = query.Where("level = ?", level)
|
||||
}
|
||||
|
||||
// 按分类筛选
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&materials).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return materials, total, nil
|
||||
}
|
||||
|
||||
// ===== 阅读记录管理 =====
|
||||
|
||||
// CreateReadingRecord 创建阅读记录
|
||||
func (s *ReadingService) CreateReadingRecord(record *models.ReadingRecord) error {
|
||||
record.ID = uuid.New().String()
|
||||
record.StartedAt = time.Now()
|
||||
record.CreatedAt = time.Now()
|
||||
record.UpdatedAt = time.Now()
|
||||
|
||||
return s.db.Create(record).Error
|
||||
}
|
||||
|
||||
// UpdateReadingRecord 更新阅读记录
|
||||
func (s *ReadingService) UpdateReadingRecord(id string, updates map[string]interface{}) error {
|
||||
updates["updated_at"] = time.Now()
|
||||
result := s.db.Model(&models.ReadingRecord{}).Where("id = ?", id).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("阅读记录不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserReadingRecords 获取用户阅读记录
|
||||
func (s *ReadingService) GetUserReadingRecords(userID string, page, pageSize int) ([]models.ReadingRecord, int64, error) {
|
||||
var records []models.ReadingRecord
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.ReadingRecord{}).Where("user_id = ?", userID)
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询,预加载材料信息
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Preload("Material").Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
// GetReadingRecord 获取单个阅读记录
|
||||
func (s *ReadingService) GetReadingRecord(id string) (*models.ReadingRecord, error) {
|
||||
var record models.ReadingRecord
|
||||
if err := s.db.Preload("Material").Where("id = ?", id).First(&record).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("阅读记录不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// GetReadingProgress 获取用户对特定材料的阅读进度
|
||||
func (s *ReadingService) GetReadingProgress(userID, materialID string) (*models.ReadingRecord, error) {
|
||||
var record models.ReadingRecord
|
||||
if err := s.db.Where("user_id = ? AND material_id = ?", userID, materialID).First(&record).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // 没有阅读记录
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// ===== 阅读统计 =====
|
||||
|
||||
// ReadingStats 阅读统计结构
|
||||
type ReadingStats struct {
|
||||
TotalMaterials int64 `json:"total_materials"` // 总阅读材料数
|
||||
CompletedMaterials int64 `json:"completed_materials"` // 已完成材料数
|
||||
TotalReadingTime int64 `json:"total_reading_time"` // 总阅读时间(秒)
|
||||
AverageScore float64 `json:"average_score"` // 平均理解得分
|
||||
AverageSpeed float64 `json:"average_speed"` // 平均阅读速度(词/分钟)
|
||||
ContinuousDays int `json:"continuous_days"` // 连续阅读天数
|
||||
LevelStats []LevelStat `json:"level_stats"` // 各难度级别统计
|
||||
}
|
||||
|
||||
// LevelStat 难度级别统计
|
||||
type LevelStat struct {
|
||||
Level string `json:"level"`
|
||||
CompletedCount int64 `json:"completed_count"`
|
||||
AverageScore float64 `json:"average_score"`
|
||||
AverageSpeed float64 `json:"average_speed"`
|
||||
}
|
||||
|
||||
// GetUserReadingStats 获取用户阅读统计
|
||||
func (s *ReadingService) GetUserReadingStats(userID string) (*ReadingStats, error) {
|
||||
stats := &ReadingStats{}
|
||||
|
||||
// 获取总阅读材料数
|
||||
if err := s.db.Model(&models.ReadingMaterial{}).Where("is_active = ?", true).Count(&stats.TotalMaterials).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取已完成材料数
|
||||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ? AND completed_at IS NOT NULL", userID).Count(&stats.CompletedMaterials).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取总阅读时间
|
||||
var totalTime sql.NullInt64
|
||||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ?", userID).Select("SUM(reading_time)").Scan(&totalTime).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if totalTime.Valid {
|
||||
stats.TotalReadingTime = totalTime.Int64
|
||||
}
|
||||
|
||||
// 获取平均理解得分
|
||||
var avgScore sql.NullFloat64
|
||||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ? AND comprehension_score IS NOT NULL", userID).Select("AVG(comprehension_score)").Scan(&avgScore).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if avgScore.Valid {
|
||||
stats.AverageScore = avgScore.Float64
|
||||
}
|
||||
|
||||
// 获取平均阅读速度
|
||||
var avgSpeed sql.NullFloat64
|
||||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ? AND reading_speed IS NOT NULL", userID).Select("AVG(reading_speed)").Scan(&avgSpeed).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if avgSpeed.Valid {
|
||||
stats.AverageSpeed = avgSpeed.Float64
|
||||
}
|
||||
|
||||
// 计算连续阅读天数
|
||||
continuousDays, err := s.calculateContinuousReadingDays(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.ContinuousDays = continuousDays
|
||||
|
||||
// 获取各难度级别统计
|
||||
levelStats, err := s.getLevelStats(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.LevelStats = levelStats
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// calculateContinuousReadingDays 计算连续阅读天数
|
||||
func (s *ReadingService) calculateContinuousReadingDays(userID string) (int, error) {
|
||||
// 获取最近的阅读记录日期
|
||||
var dates []time.Time
|
||||
if err := s.db.Model(&models.ReadingRecord{}).Where("user_id = ?", userID).Select("DATE(created_at) as date").Group("DATE(created_at)").Order("date DESC").Limit(365).Scan(&dates).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(dates) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 计算连续天数
|
||||
continuousDays := 1
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
lastDate := dates[0].Truncate(24 * time.Hour)
|
||||
|
||||
// 如果最后一次阅读不是今天或昨天,连续天数为0
|
||||
if lastDate.Before(today.AddDate(0, 0, -1)) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
for i := 1; i < len(dates); i++ {
|
||||
currentDate := dates[i].Truncate(24 * time.Hour)
|
||||
expectedDate := lastDate.AddDate(0, 0, -1)
|
||||
|
||||
if currentDate.Equal(expectedDate) {
|
||||
continuousDays++
|
||||
lastDate = currentDate
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return continuousDays, nil
|
||||
}
|
||||
|
||||
// getLevelStats 获取各难度级别统计
|
||||
func (s *ReadingService) getLevelStats(userID string) ([]LevelStat, error) {
|
||||
var levelStats []LevelStat
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
m.level,
|
||||
COUNT(r.id) as completed_count,
|
||||
AVG(r.comprehension_score) as average_score,
|
||||
AVG(r.reading_speed) as average_speed
|
||||
FROM ai_reading_records r
|
||||
JOIN ai_reading_materials m ON r.material_id = m.id
|
||||
WHERE r.user_id = ? AND r.completed_at IS NOT NULL
|
||||
GROUP BY m.level
|
||||
`
|
||||
|
||||
if err := s.db.Raw(query, userID).Scan(&levelStats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return levelStats, nil
|
||||
}
|
||||
|
||||
// GetRecommendedMaterials 获取推荐阅读材料
|
||||
func (s *ReadingService) GetRecommendedMaterials(userID string, limit int) ([]models.ReadingMaterial, error) {
|
||||
// 获取用户最近的阅读记录,分析偏好
|
||||
var userLevel string
|
||||
var userCategory string
|
||||
|
||||
// 获取用户最常阅读的难度级别
|
||||
if err := s.db.Raw(`
|
||||
SELECT m.level
|
||||
FROM ai_reading_records r
|
||||
JOIN ai_reading_materials m ON r.material_id = m.id
|
||||
WHERE r.user_id = ?
|
||||
GROUP BY m.level
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 1
|
||||
`, userID).Scan(&userLevel).Error; err != nil {
|
||||
userLevel = "intermediate" // 默认中级
|
||||
}
|
||||
|
||||
// 获取用户最常阅读的分类
|
||||
if err := s.db.Raw(`
|
||||
SELECT m.category
|
||||
FROM ai_reading_records r
|
||||
JOIN ai_reading_materials m ON r.material_id = m.id
|
||||
WHERE r.user_id = ?
|
||||
GROUP BY m.category
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 1
|
||||
`, userID).Scan(&userCategory).Error; err != nil {
|
||||
userCategory = "" // 不限制分类
|
||||
}
|
||||
|
||||
// 获取用户未读过的材料
|
||||
var materials []models.ReadingMaterial
|
||||
query := s.db.Model(&models.ReadingMaterial{}).Where(`
|
||||
is_active = ? AND id NOT IN (
|
||||
SELECT material_id FROM ai_reading_records WHERE user_id = ?
|
||||
)
|
||||
`, true, userID)
|
||||
|
||||
// 优先推荐相同难度级别的材料
|
||||
if userLevel != "" {
|
||||
query = query.Where("level = ?", userLevel)
|
||||
}
|
||||
|
||||
// 如果有偏好分类,优先推荐
|
||||
if userCategory != "" {
|
||||
query = query.Where("category = ?", userCategory)
|
||||
}
|
||||
|
||||
if err := query.Order("created_at DESC").Limit(limit).Find(&materials).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果推荐材料不足,补充其他材料
|
||||
if len(materials) < limit {
|
||||
var additionalMaterials []models.ReadingMaterial
|
||||
remaining := limit - len(materials)
|
||||
|
||||
// 获取已推荐材料的ID列表
|
||||
excludeIDs := make([]string, len(materials))
|
||||
for i, m := range materials {
|
||||
excludeIDs[i] = m.ID
|
||||
}
|
||||
|
||||
additionalQuery := s.db.Model(&models.ReadingMaterial{}).Where(`
|
||||
is_active = ? AND id NOT IN (
|
||||
SELECT material_id FROM ai_reading_records WHERE user_id = ?
|
||||
)
|
||||
`, true, userID)
|
||||
|
||||
if len(excludeIDs) > 0 {
|
||||
additionalQuery = additionalQuery.Where("id NOT IN ?", excludeIDs)
|
||||
}
|
||||
|
||||
if err := additionalQuery.Order("created_at DESC").Limit(remaining).Find(&additionalMaterials).Error; err != nil {
|
||||
return materials, nil // 返回已有的推荐
|
||||
}
|
||||
|
||||
materials = append(materials, additionalMaterials...)
|
||||
}
|
||||
|
||||
return materials, nil
|
||||
}
|
||||
436
serve/internal/services/speaking_service.go
Normal file
436
serve/internal/services/speaking_service.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SpeakingService 口语练习服务
|
||||
type SpeakingService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSpeakingService 创建口语练习服务实例
|
||||
func NewSpeakingService(db *gorm.DB) *SpeakingService {
|
||||
return &SpeakingService{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 口语场景管理 ====================
|
||||
|
||||
// GetSpeakingScenarios 获取口语场景列表
|
||||
func (s *SpeakingService) GetSpeakingScenarios(level, category string, page, pageSize int) ([]models.SpeakingScenario, int64, error) {
|
||||
var scenarios []models.SpeakingScenario
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.SpeakingScenario{}).Where("is_active = ?", true)
|
||||
|
||||
// 添加过滤条件
|
||||
if level != "" {
|
||||
query = query.Where("level = ?", level)
|
||||
}
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&scenarios).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return scenarios, total, nil
|
||||
}
|
||||
|
||||
// GetSpeakingScenario 根据ID获取口语场景
|
||||
func (s *SpeakingService) GetSpeakingScenario(id string) (*models.SpeakingScenario, error) {
|
||||
var scenario models.SpeakingScenario
|
||||
if err := s.db.Where("id = ? AND is_active = ?", id, true).First(&scenario).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("口语场景不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &scenario, nil
|
||||
}
|
||||
|
||||
// CreateSpeakingScenario 创建口语场景
|
||||
func (s *SpeakingService) CreateSpeakingScenario(scenario *models.SpeakingScenario) error {
|
||||
scenario.CreatedAt = time.Now()
|
||||
scenario.UpdatedAt = time.Now()
|
||||
return s.db.Create(scenario).Error
|
||||
}
|
||||
|
||||
// UpdateSpeakingScenario 更新口语场景
|
||||
func (s *SpeakingService) UpdateSpeakingScenario(id string, updateData *models.SpeakingScenario) error {
|
||||
updateData.UpdatedAt = time.Now()
|
||||
result := s.db.Model(&models.SpeakingScenario{}).Where("id = ? AND is_active = ?", id, true).Updates(updateData)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("口语场景不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSpeakingScenario 删除口语场景(软删除)
|
||||
func (s *SpeakingService) DeleteSpeakingScenario(id string) error {
|
||||
result := s.db.Model(&models.SpeakingScenario{}).Where("id = ?", id).Update("is_active", false)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("口语场景不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchSpeakingScenarios 搜索口语场景
|
||||
func (s *SpeakingService) SearchSpeakingScenarios(keyword string, level, category string, page, pageSize int) ([]models.SpeakingScenario, int64, error) {
|
||||
var scenarios []models.SpeakingScenario
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.SpeakingScenario{}).Where("is_active = ?", true)
|
||||
|
||||
// 关键词搜索
|
||||
if keyword != "" {
|
||||
query = query.Where("title LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
// 添加过滤条件
|
||||
if level != "" {
|
||||
query = query.Where("level = ?", level)
|
||||
}
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&scenarios).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return scenarios, total, nil
|
||||
}
|
||||
|
||||
// GetRecommendedScenarios 获取推荐的口语场景
|
||||
func (s *SpeakingService) GetRecommendedScenarios(userID string, limit int) ([]models.SpeakingScenario, error) {
|
||||
var scenarios []models.SpeakingScenario
|
||||
|
||||
// 简单的推荐逻辑:基于用户水平和最近练习情况
|
||||
// 这里可以根据实际需求实现更复杂的推荐算法
|
||||
query := `
|
||||
SELECT
|
||||
s.*,
|
||||
CASE WHEN ar.cnt IS NULL OR ar.cnt = 0 THEN 0 ELSE 1 END AS has_record
|
||||
FROM ai_speaking_scenarios s
|
||||
LEFT JOIN (
|
||||
SELECT scenario_id, COUNT(*) AS cnt
|
||||
FROM ai_speaking_records
|
||||
WHERE user_id = ?
|
||||
GROUP BY scenario_id
|
||||
) ar ON ar.scenario_id = s.id
|
||||
WHERE s.is_active = true
|
||||
ORDER BY has_record ASC, s.created_at DESC
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
if err := s.db.Raw(query, userID, limit).Scan(&scenarios).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return scenarios, nil
|
||||
}
|
||||
|
||||
// ==================== 口语练习记录管理 ====================
|
||||
|
||||
// CreateSpeakingRecord 创建口语练习记录
|
||||
func (s *SpeakingService) CreateSpeakingRecord(record *models.SpeakingRecord) error {
|
||||
record.CreatedAt = time.Now()
|
||||
record.UpdatedAt = time.Now()
|
||||
return s.db.Create(record).Error
|
||||
}
|
||||
|
||||
// UpdateSpeakingRecord 更新口语练习记录
|
||||
func (s *SpeakingService) UpdateSpeakingRecord(id string, updateData *models.SpeakingRecord) error {
|
||||
updateData.UpdatedAt = time.Now()
|
||||
result := s.db.Model(&models.SpeakingRecord{}).Where("id = ?", id).Updates(updateData)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("口语练习记录不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSpeakingRecord 根据ID获取口语练习记录
|
||||
func (s *SpeakingService) GetSpeakingRecord(id string) (*models.SpeakingRecord, error) {
|
||||
var record models.SpeakingRecord
|
||||
if err := s.db.Preload("SpeakingScenario").Where("id = ?", id).First(&record).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("口语练习记录不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// GetUserSpeakingRecords 获取用户的口语练习记录
|
||||
func (s *SpeakingService) GetUserSpeakingRecords(userID string, page, pageSize int) ([]models.SpeakingRecord, int64, error) {
|
||||
var records []models.SpeakingRecord
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.SpeakingRecord{}).Where("user_id = ?", userID)
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Preload("SpeakingScenario").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
// SubmitSpeaking 提交口语练习
|
||||
func (s *SpeakingService) SubmitSpeaking(recordID string, audioURL, transcript string) error {
|
||||
updateData := map[string]interface{}{
|
||||
"audio_url": audioURL,
|
||||
"transcript": transcript,
|
||||
"completed_at": time.Now(),
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
result := s.db.Model(&models.SpeakingRecord{}).Where("id = ?", recordID).Updates(updateData)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("口语练习记录不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GradeSpeaking 评分口语练习
|
||||
func (s *SpeakingService) GradeSpeaking(recordID string, pronunciationScore, fluencyScore, accuracyScore, overallScore float64, feedback, suggestions string) error {
|
||||
updateData := map[string]interface{}{
|
||||
"pronunciation_score": pronunciationScore,
|
||||
"fluency_score": fluencyScore,
|
||||
"accuracy_score": accuracyScore,
|
||||
"overall_score": overallScore,
|
||||
"feedback": feedback,
|
||||
"suggestions": suggestions,
|
||||
"graded_at": time.Now(),
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
result := s.db.Model(&models.SpeakingRecord{}).Where("id = ?", recordID).Updates(updateData)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("口语练习记录不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== 口语学习统计 ====================
|
||||
|
||||
// GetUserSpeakingStats 获取用户口语学习统计
|
||||
func (s *SpeakingService) GetUserSpeakingStats(userID string) (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
// 总练习次数
|
||||
var totalRecords int64
|
||||
if err := s.db.Model(&models.SpeakingRecord{}).Where("user_id = ?", userID).Count(&totalRecords).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["total_records"] = totalRecords
|
||||
|
||||
// 已完成练习次数
|
||||
var completedRecords int64
|
||||
if err := s.db.Model(&models.SpeakingRecord{}).Where("user_id = ? AND completed_at IS NOT NULL", userID).Count(&completedRecords).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["completed_records"] = completedRecords
|
||||
|
||||
// 已评分练习次数
|
||||
var gradedRecords int64
|
||||
if err := s.db.Model(&models.SpeakingRecord{}).Where("user_id = ? AND overall_score IS NOT NULL", userID).Count(&gradedRecords).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["graded_records"] = gradedRecords
|
||||
|
||||
// 平均分数
|
||||
var avgScores struct {
|
||||
Pronunciation float64 `json:"pronunciation"`
|
||||
Fluency float64 `json:"fluency"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
Overall float64 `json:"overall"`
|
||||
}
|
||||
if err := s.db.Model(&models.SpeakingRecord{}).Where("user_id = ? AND overall_score IS NOT NULL", userID).Select(
|
||||
"AVG(pronunciation_score) as pronunciation, AVG(fluency_score) as fluency, AVG(accuracy_score) as accuracy, AVG(overall_score) as overall",
|
||||
).Scan(&avgScores).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["average_scores"] = avgScores
|
||||
|
||||
// 总练习时长
|
||||
var totalDuration int64
|
||||
if err := s.db.Model(&models.SpeakingRecord{}).Where("user_id = ? AND duration IS NOT NULL", userID).Select("COALESCE(SUM(duration), 0)").Scan(&totalDuration).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["total_duration"] = totalDuration
|
||||
|
||||
// 平均练习时长
|
||||
var avgDuration float64
|
||||
if completedRecords > 0 {
|
||||
avgDuration = float64(totalDuration) / float64(completedRecords)
|
||||
}
|
||||
stats["average_duration"] = avgDuration
|
||||
|
||||
// 连续练习天数
|
||||
continuousDays, err := s.calculateContinuousSpeakingDays(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["continuous_days"] = continuousDays
|
||||
|
||||
// 按难度级别统计
|
||||
levelStats, err := s.getSpeakingStatsByLevel(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["stats_by_level"] = levelStats
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// calculateContinuousSpeakingDays 计算连续练习天数
|
||||
func (s *SpeakingService) calculateContinuousSpeakingDays(userID string) (int, error) {
|
||||
var dates []time.Time
|
||||
if err := s.db.Model(&models.SpeakingRecord{}).Where("user_id = ? AND completed_at IS NOT NULL", userID).Select("DATE(created_at) as date").Group("DATE(created_at)").Order("date DESC").Pluck("date", &dates).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(dates) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
continuousDays := 1
|
||||
for i := 1; i < len(dates); i++ {
|
||||
diff := dates[i-1].Sub(dates[i]).Hours() / 24
|
||||
if diff == 1 {
|
||||
continuousDays++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return continuousDays, nil
|
||||
}
|
||||
|
||||
// getSpeakingStatsByLevel 获取按难度级别的统计
|
||||
func (s *SpeakingService) getSpeakingStatsByLevel(userID string) (map[string]interface{}, error) {
|
||||
var results []struct {
|
||||
Level string `json:"level"`
|
||||
Count int64 `json:"count"`
|
||||
Score float64 `json:"avg_score"`
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
ss.level,
|
||||
COUNT(sr.id) as count,
|
||||
AVG(sr.overall_score) as avg_score
|
||||
FROM ai_speaking_records sr
|
||||
JOIN ai_speaking_scenarios ss ON sr.scenario_id = ss.id
|
||||
WHERE sr.user_id = ? AND sr.overall_score IS NOT NULL
|
||||
GROUP BY ss.level
|
||||
`
|
||||
|
||||
if err := s.db.Raw(query, userID).Scan(&results).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
for _, result := range results {
|
||||
stats[result.Level] = map[string]interface{}{
|
||||
"count": result.Count,
|
||||
"avg_score": result.Score,
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetSpeakingProgress 获取口语学习进度
|
||||
func (s *SpeakingService) GetSpeakingProgress(userID, scenarioID string) (map[string]interface{}, error) {
|
||||
progress := make(map[string]interface{})
|
||||
|
||||
// 该场景的练习记录
|
||||
var records []models.SpeakingRecord
|
||||
if err := s.db.Where("user_id = ? AND scenario_id = ?", userID, scenarioID).Order("created_at ASC").Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
progress["total_attempts"] = len(records)
|
||||
|
||||
if len(records) == 0 {
|
||||
progress["completed"] = false
|
||||
progress["best_score"] = 0
|
||||
progress["latest_score"] = 0
|
||||
progress["improvement"] = 0
|
||||
return progress, nil
|
||||
}
|
||||
|
||||
// 最佳分数
|
||||
var bestScore float64
|
||||
for _, record := range records {
|
||||
if record.OverallScore != nil && *record.OverallScore > bestScore {
|
||||
bestScore = *record.OverallScore
|
||||
}
|
||||
}
|
||||
progress["best_score"] = bestScore
|
||||
|
||||
// 最新分数
|
||||
latestRecord := records[len(records)-1]
|
||||
latestScore := 0.0
|
||||
if latestRecord.OverallScore != nil {
|
||||
latestScore = *latestRecord.OverallScore
|
||||
}
|
||||
progress["latest_score"] = latestScore
|
||||
|
||||
// 是否完成(有评分记录)
|
||||
progress["completed"] = latestRecord.OverallScore != nil
|
||||
|
||||
// 进步情况(最新分数与第一次分数的差值)
|
||||
improvement := 0.0
|
||||
if len(records) > 1 && records[0].OverallScore != nil && latestRecord.OverallScore != nil {
|
||||
improvement = *latestRecord.OverallScore - *records[0].OverallScore
|
||||
}
|
||||
progress["improvement"] = improvement
|
||||
|
||||
return progress, nil
|
||||
}
|
||||
317
serve/internal/services/study_plan_service.go
Normal file
317
serve/internal/services/study_plan_service.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StudyPlanService 学习计划服务
|
||||
type StudyPlanService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewStudyPlanService(db *gorm.DB) *StudyPlanService {
|
||||
return &StudyPlanService{db: db}
|
||||
}
|
||||
|
||||
// CreateStudyPlan 创建学习计划
|
||||
func (s *StudyPlanService) CreateStudyPlan(userID int64, planName string, description *string, dailyGoal int,
|
||||
bookID *string, startDate time.Time, endDate *time.Time, remindTime *string, remindDays *string) (*models.StudyPlan, error) {
|
||||
|
||||
// 验证参数
|
||||
if dailyGoal < 1 || dailyGoal > 200 {
|
||||
return nil, errors.New("每日目标必须在1-200之间")
|
||||
}
|
||||
|
||||
// 如果指定了词汇书,获取总单词数
|
||||
totalWords := 0
|
||||
if bookID != nil && *bookID != "" {
|
||||
var count int64
|
||||
s.db.Model(&models.VocabularyBookWord{}).Where("book_id = ?", *bookID).Count(&count)
|
||||
totalWords = int(count)
|
||||
}
|
||||
|
||||
plan := &models.StudyPlan{
|
||||
UserID: userID,
|
||||
BookID: bookID,
|
||||
PlanName: planName,
|
||||
Description: description,
|
||||
DailyGoal: dailyGoal,
|
||||
TotalWords: totalWords,
|
||||
LearnedWords: 0,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Status: "active",
|
||||
RemindTime: remindTime,
|
||||
RemindDays: remindDays,
|
||||
IsRemindEnabled: remindTime != nil && remindDays != nil,
|
||||
StreakDays: 0,
|
||||
}
|
||||
|
||||
if err := s.db.Create(plan).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// GetUserStudyPlans 获取用户的学习计划列表
|
||||
func (s *StudyPlanService) GetUserStudyPlans(userID int64, status string) ([]models.StudyPlan, error) {
|
||||
var plans []models.StudyPlan
|
||||
query := s.db.Where("user_id = ?", userID)
|
||||
|
||||
if status != "" && status != "all" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Find(&plans).Error
|
||||
return plans, err
|
||||
}
|
||||
|
||||
// GetStudyPlanByID 获取学习计划详情
|
||||
func (s *StudyPlanService) GetStudyPlanByID(planID, userID int64) (*models.StudyPlan, error) {
|
||||
var plan models.StudyPlan
|
||||
err := s.db.Where("id = ? AND user_id = ?", planID, userID).First(&plan).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
// UpdateStudyPlan 更新学习计划
|
||||
func (s *StudyPlanService) UpdateStudyPlan(planID, userID int64, updates map[string]interface{}) (*models.StudyPlan, error) {
|
||||
var plan models.StudyPlan
|
||||
if err := s.db.Where("id = ? AND user_id = ?", planID, userID).First(&plan).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证dailyGoal
|
||||
if dailyGoal, ok := updates["daily_goal"].(int); ok {
|
||||
if dailyGoal < 1 || dailyGoal > 200 {
|
||||
return nil, errors.New("每日目标必须在1-200之间")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.Model(&plan).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
// DeleteStudyPlan 删除学习计划(软删除)
|
||||
func (s *StudyPlanService) DeleteStudyPlan(planID, userID int64) error {
|
||||
result := s.db.Where("id = ? AND user_id = ?", planID, userID).Delete(&models.StudyPlan{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("计划不存在或无权删除")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePlanStatus 更新计划状态
|
||||
func (s *StudyPlanService) UpdatePlanStatus(planID, userID int64, status string) error {
|
||||
validStatuses := map[string]bool{
|
||||
"active": true,
|
||||
"paused": true,
|
||||
"completed": true,
|
||||
"cancelled": true,
|
||||
}
|
||||
|
||||
if !validStatuses[status] {
|
||||
return errors.New("无效的状态")
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
}
|
||||
|
||||
if status == "completed" {
|
||||
now := time.Now()
|
||||
updates["completed_at"] = now
|
||||
}
|
||||
|
||||
result := s.db.Model(&models.StudyPlan{}).
|
||||
Where("id = ? AND user_id = ?", planID, userID).
|
||||
Updates(updates)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("计划不存在或无权修改")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordStudyProgress 记录学习进度
|
||||
func (s *StudyPlanService) RecordStudyProgress(planID, userID int64, wordsStudied, studyDuration int) error {
|
||||
var plan models.StudyPlan
|
||||
if err := s.db.Where("id = ? AND user_id = ?", planID, userID).First(&plan).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
|
||||
// 检查今天是否已有记录
|
||||
var record models.StudyPlanRecord
|
||||
err := s.db.Where("plan_id = ? AND user_id = ? AND study_date = ?", planID, userID, today).First(&record).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 创建新记录
|
||||
record = models.StudyPlanRecord{
|
||||
PlanID: planID,
|
||||
UserID: userID,
|
||||
StudyDate: today,
|
||||
WordsStudied: wordsStudied,
|
||||
GoalCompleted: wordsStudied >= plan.DailyGoal,
|
||||
StudyDuration: studyDuration,
|
||||
}
|
||||
if err := s.db.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err == nil {
|
||||
// 更新现有记录
|
||||
record.WordsStudied += wordsStudied
|
||||
record.StudyDuration += studyDuration
|
||||
record.GoalCompleted = record.WordsStudied >= plan.DailyGoal
|
||||
if err := s.db.Save(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新计划的已学单词数
|
||||
plan.LearnedWords += wordsStudied
|
||||
|
||||
// 更新连续学习天数
|
||||
if plan.LastStudyDate != nil {
|
||||
lastDate := plan.LastStudyDate.Truncate(24 * time.Hour)
|
||||
yesterday := today.AddDate(0, 0, -1)
|
||||
|
||||
if lastDate.Equal(yesterday) {
|
||||
plan.StreakDays++
|
||||
} else if !lastDate.Equal(today) {
|
||||
plan.StreakDays = 1
|
||||
}
|
||||
} else {
|
||||
plan.StreakDays = 1
|
||||
}
|
||||
|
||||
plan.LastStudyDate = &today
|
||||
|
||||
// 检查是否完成计划
|
||||
if plan.TotalWords > 0 && plan.LearnedWords >= plan.TotalWords {
|
||||
plan.Status = "completed"
|
||||
now := time.Now()
|
||||
plan.CompletedAt = &now
|
||||
}
|
||||
|
||||
return s.db.Save(&plan).Error
|
||||
}
|
||||
|
||||
// GetStudyPlanStatistics 获取学习计划统计
|
||||
func (s *StudyPlanService) GetStudyPlanStatistics(planID, userID int64) (map[string]interface{}, error) {
|
||||
var plan models.StudyPlan
|
||||
if err := s.db.Where("id = ? AND user_id = ?", planID, userID).First(&plan).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 统计总学习天数
|
||||
var totalStudyDays int64
|
||||
s.db.Model(&models.StudyPlanRecord{}).
|
||||
Where("plan_id = ? AND user_id = ?", planID, userID).
|
||||
Count(&totalStudyDays)
|
||||
|
||||
// 统计完成目标的天数
|
||||
var completedDays int64
|
||||
s.db.Model(&models.StudyPlanRecord{}).
|
||||
Where("plan_id = ? AND user_id = ? AND goal_completed = ?", planID, userID, true).
|
||||
Count(&completedDays)
|
||||
|
||||
// 计算平均每日学习单词数
|
||||
var avgWords float64
|
||||
s.db.Model(&models.StudyPlanRecord{}).
|
||||
Select("AVG(words_studied) as avg_words").
|
||||
Where("plan_id = ? AND user_id = ?", planID, userID).
|
||||
Scan(&avgWords)
|
||||
|
||||
// 计算完成率
|
||||
completionRate := 0.0
|
||||
if plan.TotalWords > 0 {
|
||||
completionRate = float64(plan.LearnedWords) / float64(plan.TotalWords) * 100
|
||||
}
|
||||
|
||||
// 获取最近7天的学习记录
|
||||
var recentRecords []models.StudyPlanRecord
|
||||
s.db.Where("plan_id = ? AND user_id = ?", planID, userID).
|
||||
Order("study_date DESC").
|
||||
Limit(7).
|
||||
Find(&recentRecords)
|
||||
|
||||
return map[string]interface{}{
|
||||
"plan": plan,
|
||||
"total_study_days": totalStudyDays,
|
||||
"completed_days": completedDays,
|
||||
"avg_words": avgWords,
|
||||
"completion_rate": completionRate,
|
||||
"recent_records": recentRecords,
|
||||
"streak_days": plan.StreakDays,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTodayStudyPlans 获取今日需要执行的学习计划
|
||||
func (s *StudyPlanService) GetTodayStudyPlans(userID int64) ([]map[string]interface{}, error) {
|
||||
var plans []models.StudyPlan
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
weekday := int(time.Now().Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7 // 将周日从0改为7
|
||||
}
|
||||
|
||||
// 查找活跃的计划
|
||||
err := s.db.Where("user_id = ? AND status = ? AND start_date <= ?", userID, "active", today).
|
||||
Find(&plans).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]map[string]interface{}, 0)
|
||||
|
||||
for _, plan := range plans {
|
||||
// 检查今天是否已完成
|
||||
var record models.StudyPlanRecord
|
||||
err := s.db.Where("plan_id = ? AND user_id = ? AND study_date = ?", plan.ID, userID, today).
|
||||
First(&record).Error
|
||||
|
||||
todayCompleted := err == nil && record.GoalCompleted
|
||||
todayProgress := 0
|
||||
if err == nil {
|
||||
todayProgress = record.WordsStudied
|
||||
}
|
||||
|
||||
// 检查是否需要提醒
|
||||
needRemind := false
|
||||
if plan.IsRemindEnabled && plan.RemindDays != nil {
|
||||
// 简单检查:这里可以根据RemindDays判断今天是否需要提醒
|
||||
needRemind = true
|
||||
}
|
||||
|
||||
result = append(result, map[string]interface{}{
|
||||
"plan": plan,
|
||||
"today_completed": todayCompleted,
|
||||
"today_progress": todayProgress,
|
||||
"need_remind": needRemind,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
497
serve/internal/services/test_service.go
Normal file
497
serve/internal/services/test_service.go
Normal file
@@ -0,0 +1,497 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
)
|
||||
|
||||
// TestService 测试服务
|
||||
type TestService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTestService 创建测试服务实例
|
||||
func NewTestService(db *gorm.DB) *TestService {
|
||||
return &TestService{db: db}
|
||||
}
|
||||
|
||||
// GetTestTemplates 获取测试模板列表
|
||||
func (s *TestService) GetTestTemplates(testType *models.TestType, difficulty *models.TestDifficulty, page, pageSize int) ([]models.TestTemplate, int64, error) {
|
||||
var templates []models.TestTemplate
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.TestTemplate{}).Where("is_active = ?", true)
|
||||
|
||||
if testType != nil {
|
||||
query = query.Where("type = ?", *testType)
|
||||
}
|
||||
if difficulty != nil {
|
||||
query = query.Where("difficulty = ?", *difficulty)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&templates).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return templates, total, nil
|
||||
}
|
||||
|
||||
// GetTestTemplateByID 根据ID获取测试模板
|
||||
func (s *TestService) GetTestTemplateByID(id string) (*models.TestTemplate, error) {
|
||||
var template models.TestTemplate
|
||||
if err := s.db.Where("id = ? AND is_active = ?", id, true).First(&template).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
// CreateTestSession 创建测试会话
|
||||
func (s *TestService) CreateTestSession(templateID, userID string) (*models.TestSession, error) {
|
||||
// 获取模板
|
||||
template, err := s.GetTestTemplateByID(templateID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("模板不存在: %w", err)
|
||||
}
|
||||
|
||||
// 获取模板对应的题目
|
||||
var questions []models.TestQuestion
|
||||
if err := s.db.Where("template_id = ?", templateID).Order("order_index ASC").Find(&questions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(questions) == 0 {
|
||||
return nil, errors.New("模板没有配置题目")
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
session := &models.TestSession{
|
||||
ID: uuid.New().String(),
|
||||
TemplateID: templateID,
|
||||
UserID: userID,
|
||||
Status: models.TestStatusPending,
|
||||
TimeRemaining: template.Duration,
|
||||
CurrentQuestionIndex: 0,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 保存会话
|
||||
if err := tx.Create(session).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 关联题目
|
||||
for i, question := range questions {
|
||||
sessionQuestion := models.TestSessionQuestion{
|
||||
SessionID: session.ID,
|
||||
QuestionID: question.ID,
|
||||
OrderIndex: i,
|
||||
}
|
||||
if err := tx.Create(&sessionQuestion).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加载关联数据
|
||||
session.Template = template
|
||||
session.Questions = questions
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// GetTestSession 获取测试会话
|
||||
func (s *TestService) GetTestSession(sessionID string) (*models.TestSession, error) {
|
||||
var session models.TestSession
|
||||
if err := s.db.Preload("Template").Preload("Answers.Question").First(&session, "id = ?", sessionID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加载题目
|
||||
var questions []models.TestQuestion
|
||||
if err := s.db.Raw(`
|
||||
SELECT q.* FROM test_questions q
|
||||
INNER JOIN test_session_questions sq ON q.id = sq.question_id
|
||||
WHERE sq.session_id = ?
|
||||
ORDER BY sq.order_index ASC
|
||||
`, sessionID).Scan(&questions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session.Questions = questions
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
// StartTest 开始测试
|
||||
func (s *TestService) StartTest(sessionID string) (*models.TestSession, error) {
|
||||
session, err := s.GetTestSession(sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if session.Status != models.TestStatusPending && session.Status != models.TestStatusPaused {
|
||||
return nil, errors.New("测试状态不允许开始")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
session.Status = models.TestStatusInProgress
|
||||
session.StartTime = &now
|
||||
session.UpdatedAt = now
|
||||
|
||||
if err := s.db.Save(session).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// SubmitAnswer 提交答案
|
||||
func (s *TestService) SubmitAnswer(sessionID, questionID, answer string) (*models.TestSession, error) {
|
||||
session, err := s.GetTestSession(sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if session.Status != models.TestStatusInProgress {
|
||||
return nil, errors.New("测试未在进行中")
|
||||
}
|
||||
|
||||
// 查找题目
|
||||
var question models.TestQuestion
|
||||
if err := s.db.First(&question, "id = ?", questionID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查是否已经回答过
|
||||
var existingAnswer models.TestAnswer
|
||||
err = s.db.Where("session_id = ? AND question_id = ?", sessionID, questionID).First(&existingAnswer).Error
|
||||
if err == nil {
|
||||
// 更新已有答案
|
||||
existingAnswer.Answer = answer
|
||||
existingAnswer.UpdatedAt = time.Now()
|
||||
|
||||
// 判断答案是否正确
|
||||
isCorrect := s.checkAnswer(question, answer)
|
||||
existingAnswer.IsCorrect = &isCorrect
|
||||
if isCorrect {
|
||||
existingAnswer.Score = question.Points
|
||||
} else {
|
||||
existingAnswer.Score = 0
|
||||
}
|
||||
|
||||
if err := s.db.Save(&existingAnswer).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 创建新答案
|
||||
isCorrect := s.checkAnswer(question, answer)
|
||||
score := 0
|
||||
if isCorrect {
|
||||
score = question.Points
|
||||
}
|
||||
|
||||
testAnswer := &models.TestAnswer{
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
QuestionID: questionID,
|
||||
Answer: answer,
|
||||
IsCorrect: &isCorrect,
|
||||
Score: score,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.db.Create(testAnswer).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载会话
|
||||
return s.GetTestSession(sessionID)
|
||||
}
|
||||
|
||||
// checkAnswer 检查答案是否正确
|
||||
func (s *TestService) checkAnswer(question models.TestQuestion, answer string) bool {
|
||||
switch question.QuestionType {
|
||||
case models.QuestionTypeSingleChoice, models.QuestionTypeTrueFalse:
|
||||
return answer == question.CorrectAnswer
|
||||
case models.QuestionTypeMultipleChoice:
|
||||
// 多选题需要比较JSON数组
|
||||
var userAnswers, correctAnswers []string
|
||||
json.Unmarshal([]byte(answer), &userAnswers)
|
||||
json.Unmarshal([]byte(question.CorrectAnswer), &correctAnswers)
|
||||
|
||||
if len(userAnswers) != len(correctAnswers) {
|
||||
return false
|
||||
}
|
||||
|
||||
answerMap := make(map[string]bool)
|
||||
for _, a := range correctAnswers {
|
||||
answerMap[a] = true
|
||||
}
|
||||
|
||||
for _, a := range userAnswers {
|
||||
if !answerMap[a] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case models.QuestionTypeFillBlank, models.QuestionTypeShortAnswer:
|
||||
// 简单的字符串比较,实际应用中可能需要更复杂的匹配逻辑
|
||||
return answer == question.CorrectAnswer
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// PauseTest 暂停测试
|
||||
func (s *TestService) PauseTest(sessionID string) (*models.TestSession, error) {
|
||||
session, err := s.GetTestSession(sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if session.Status != models.TestStatusInProgress {
|
||||
return nil, errors.New("测试未在进行中")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
session.Status = models.TestStatusPaused
|
||||
session.PausedAt = &now
|
||||
session.UpdatedAt = now
|
||||
|
||||
if err := s.db.Save(session).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// ResumeTest 恢复测试
|
||||
func (s *TestService) ResumeTest(sessionID string) (*models.TestSession, error) {
|
||||
session, err := s.GetTestSession(sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if session.Status != models.TestStatusPaused {
|
||||
return nil, errors.New("测试未暂停")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
session.Status = models.TestStatusInProgress
|
||||
session.PausedAt = nil
|
||||
session.UpdatedAt = now
|
||||
|
||||
if err := s.db.Save(session).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// CompleteTest 完成测试
|
||||
func (s *TestService) CompleteTest(sessionID string) (*models.TestResult, error) {
|
||||
session, err := s.GetTestSession(sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if session.Status != models.TestStatusInProgress {
|
||||
return nil, errors.New("测试未在进行中")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
session.Status = models.TestStatusCompleted
|
||||
session.EndTime = &now
|
||||
session.UpdatedAt = now
|
||||
|
||||
// 计算结果
|
||||
var answers []models.TestAnswer
|
||||
if err := s.db.Preload("Question").Where("session_id = ?", sessionID).Find(&answers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalScore := 0
|
||||
maxScore := 0
|
||||
correctCount := 0
|
||||
wrongCount := 0
|
||||
skippedCount := len(session.Questions) - len(answers)
|
||||
timeSpent := 0
|
||||
|
||||
skillScores := make(map[models.SkillType]int)
|
||||
skillMaxScores := make(map[models.SkillType]int)
|
||||
|
||||
for _, answer := range answers {
|
||||
if answer.Question != nil {
|
||||
maxScore += answer.Question.Points
|
||||
skillMaxScores[answer.Question.SkillType] += answer.Question.Points
|
||||
|
||||
if answer.IsCorrect != nil && *answer.IsCorrect {
|
||||
totalScore += answer.Score
|
||||
correctCount++
|
||||
skillScores[answer.Question.SkillType] += answer.Score
|
||||
} else {
|
||||
wrongCount++
|
||||
}
|
||||
|
||||
timeSpent += answer.TimeSpent
|
||||
}
|
||||
}
|
||||
|
||||
percentage := 0.0
|
||||
if maxScore > 0 {
|
||||
percentage = float64(totalScore) / float64(maxScore) * 100
|
||||
}
|
||||
|
||||
// 构建技能得分JSON
|
||||
skillScoresJSON, _ := json.Marshal(skillScores)
|
||||
|
||||
// 创建测试结果
|
||||
result := &models.TestResult{
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
UserID: session.UserID,
|
||||
TemplateID: session.TemplateID,
|
||||
TotalScore: totalScore,
|
||||
MaxScore: maxScore,
|
||||
Percentage: percentage,
|
||||
CorrectCount: correctCount,
|
||||
WrongCount: wrongCount,
|
||||
SkippedCount: skippedCount,
|
||||
TimeSpent: timeSpent,
|
||||
SkillScores: string(skillScoresJSON),
|
||||
Passed: totalScore >= session.Template.PassingScore,
|
||||
CompletedAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 保存会话状态
|
||||
if err := tx.Save(session).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存结果
|
||||
if err := tx.Create(result).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加载关联数据
|
||||
result.Session = session
|
||||
result.Template = session.Template
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserTestHistory 获取用户测试历史
|
||||
func (s *TestService) GetUserTestHistory(userID string, page, pageSize int) ([]models.TestResult, int64, error) {
|
||||
var results []models.TestResult
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.TestResult{}).Where("user_id = ?", userID)
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Preload("Template").Order("completed_at DESC").Offset(offset).Limit(pageSize).Find(&results).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return results, total, nil
|
||||
}
|
||||
|
||||
// GetTestResultByID 获取测试结果详情
|
||||
func (s *TestService) GetTestResultByID(resultID string) (*models.TestResult, error) {
|
||||
var result models.TestResult
|
||||
if err := s.db.Preload("Template").Preload("Session.Answers.Question").First(&result, "id = ?", resultID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetUserTestStats 获取用户测试统计
|
||||
func (s *TestService) GetUserTestStats(userID string) (map[string]interface{}, error) {
|
||||
var stats struct {
|
||||
TotalTests int64
|
||||
CompletedTests int64
|
||||
AverageScore float64
|
||||
PassRate float64
|
||||
}
|
||||
|
||||
// 总测试数
|
||||
s.db.Model(&models.TestSession{}).Where("user_id = ?", userID).Count(&stats.TotalTests)
|
||||
|
||||
// 完成的测试数
|
||||
s.db.Model(&models.TestSession{}).Where("user_id = ? AND status = ?", userID, models.TestStatusCompleted).Count(&stats.CompletedTests)
|
||||
|
||||
// 平均分和通过率
|
||||
var results []models.TestResult
|
||||
s.db.Where("user_id = ?", userID).Find(&results)
|
||||
|
||||
if len(results) > 0 {
|
||||
totalPercentage := 0.0
|
||||
passedCount := 0
|
||||
for _, result := range results {
|
||||
totalPercentage += result.Percentage
|
||||
if result.Passed {
|
||||
passedCount++
|
||||
}
|
||||
}
|
||||
stats.AverageScore = totalPercentage / float64(len(results))
|
||||
stats.PassRate = float64(passedCount) / float64(len(results)) * 100
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_tests": stats.TotalTests,
|
||||
"completed_tests": stats.CompletedTests,
|
||||
"average_score": stats.AverageScore,
|
||||
"pass_rate": stats.PassRate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteTestResult 删除测试结果
|
||||
func (s *TestService) DeleteTestResult(resultID string) error {
|
||||
return s.db.Delete(&models.TestResult{}, "id = ?", resultID).Error
|
||||
}
|
||||
349
serve/internal/services/user_service.go
Normal file
349
serve/internal/services/user_service.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/common"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/utils"
|
||||
)
|
||||
|
||||
// UserService 用户服务
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务实例
|
||||
func NewUserService(db *gorm.DB) *UserService {
|
||||
return &UserService{db: db}
|
||||
}
|
||||
|
||||
// CreateUser 创建用户
|
||||
func (s *UserService) CreateUser(username, email, password string) (*models.User, error) {
|
||||
// 检查用户名是否已存在
|
||||
var existingUser models.User
|
||||
if err := s.db.Where("username = ? OR email = ?", username, email).First(&existingUser).Error; err == nil {
|
||||
if existingUser.Username == username {
|
||||
return nil, common.ErrUsernameExists
|
||||
}
|
||||
if existingUser.Email == email {
|
||||
return nil, common.ErrEmailExists
|
||||
}
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成密码哈希
|
||||
passwordHash, err := utils.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := &models.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
PasswordHash: passwordHash,
|
||||
Status: "active",
|
||||
Timezone: "Asia/Shanghai",
|
||||
Language: "zh-CN",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.db.Create(user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建用户偏好设置
|
||||
preference := &models.UserPreference{
|
||||
UserID: user.ID,
|
||||
DailyGoal: 50,
|
||||
WeeklyGoal: 350,
|
||||
ReminderEnabled: true,
|
||||
DifficultyLevel: "beginner",
|
||||
LearningMode: "casual",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.db.Create(preference).Error; err != nil {
|
||||
// 如果创建偏好设置失败,记录日志但不影响用户创建
|
||||
// 可以在这里添加日志记录
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户
|
||||
func (s *UserService) GetUserByID(userID int64) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.Preload("Preferences").Preload("SocialLinks").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail 根据邮箱获取用户
|
||||
func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByUsername 根据用户名获取用户
|
||||
func (s *UserService) GetUserByUsername(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息
|
||||
func (s *UserService) UpdateUser(userID int64, updates map[string]interface{}) (*models.User, error) {
|
||||
// 检查用户是否存在
|
||||
var user models.User
|
||||
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果更新邮箱,检查邮箱是否已被其他用户使用
|
||||
if email, ok := updates["email"]; ok {
|
||||
var existingUser models.User
|
||||
if err := s.db.Where("email = ? AND id != ?", email, userID).First(&existingUser).Error; err == nil {
|
||||
return nil, common.ErrEmailExists
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果更新用户名,检查用户名是否已被其他用户使用
|
||||
if username, ok := updates["username"]; ok {
|
||||
var existingUser models.User
|
||||
if err := s.db.Where("username = ? AND id != ?", username, userID).First(&existingUser).Error; err == nil {
|
||||
return nil, common.ErrUsernameExists
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 更新时间戳
|
||||
updates["updated_at"] = time.Now()
|
||||
|
||||
// 执行更新
|
||||
if err := s.db.Model(&user).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 重新获取更新后的用户信息
|
||||
return s.GetUserByID(userID)
|
||||
}
|
||||
|
||||
// UpdatePassword 更新用户密码
|
||||
func (s *UserService) UpdatePassword(userID int64, oldPassword, newPassword string) error {
|
||||
// 获取用户
|
||||
var user models.User
|
||||
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return common.ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if !utils.CheckPasswordHash(oldPassword, user.PasswordHash) {
|
||||
return common.ErrInvalidPassword
|
||||
}
|
||||
|
||||
// 生成新密码哈希
|
||||
newPasswordHash, err := utils.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
return s.db.Model(&user).Updates(map[string]interface{}{
|
||||
"password_hash": newPasswordHash,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateLoginInfo 更新登录信息
|
||||
func (s *UserService) UpdateLoginInfo(userID int64, loginIP string) error {
|
||||
now := time.Now()
|
||||
return s.db.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
|
||||
"last_login_at": &now,
|
||||
"last_login_ip": loginIP,
|
||||
"login_count": gorm.Expr("login_count + 1"),
|
||||
"updated_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// VerifyPassword 验证密码
|
||||
func (s *UserService) VerifyPassword(userID int64, password string) error {
|
||||
var user models.User
|
||||
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return common.ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if !utils.CheckPasswordHash(password, user.PasswordHash) {
|
||||
return common.ErrInvalidPassword
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (s *UserService) DeleteUser(userID int64) error {
|
||||
// 检查用户是否存在
|
||||
var user models.User
|
||||
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return common.ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 软删除用户
|
||||
return s.db.Delete(&user).Error
|
||||
}
|
||||
|
||||
// GetUserPreferences 获取用户偏好设置
|
||||
func (s *UserService) GetUserPreferences(userID int64) (*models.UserPreference, error) {
|
||||
var preference models.UserPreference
|
||||
if err := s.db.Where("user_id = ?", userID).First(&preference).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &preference, nil
|
||||
}
|
||||
|
||||
// UpdateUserPreferences 更新用户偏好设置
|
||||
func (s *UserService) UpdateUserPreferences(userID int64, updates map[string]interface{}) (*models.UserPreference, error) {
|
||||
// 检查偏好设置是否存在
|
||||
var preference models.UserPreference
|
||||
if err := s.db.Where("user_id = ?", userID).First(&preference).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新时间戳
|
||||
updates["updated_at"] = time.Now()
|
||||
|
||||
// 执行更新
|
||||
if err := s.db.Model(&preference).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 重新获取更新后的偏好设置
|
||||
return s.GetUserPreferences(userID)
|
||||
}
|
||||
|
||||
// GetUserLearningProgress 获取用户学习进度
|
||||
func (s *UserService) GetUserLearningProgress(userID string, page, limit int) ([]map[string]interface{}, int64, error) {
|
||||
// 初始化为空切片而不是nil,避免JSON序列化为null
|
||||
progressList := []map[string]interface{}{}
|
||||
var total int64
|
||||
|
||||
// 查询用户在各个词汇书的学习进度
|
||||
query := `
|
||||
SELECT
|
||||
vb.id,
|
||||
vb.name as title,
|
||||
vb.description as category,
|
||||
vb.level,
|
||||
COUNT(DISTINCT vbw.vocabulary_id) as total_words,
|
||||
COUNT(DISTINCT CASE WHEN uwp.status IN ('learning', 'reviewing', 'mastered') THEN uwp.vocabulary_id END) as learned_words,
|
||||
MAX(uwp.last_studied_at) as last_study_date
|
||||
FROM ai_vocabulary_books vb
|
||||
LEFT JOIN ai_vocabulary_book_words vbw ON vbw.book_id = vb.id
|
||||
LEFT JOIN ai_user_word_progress uwp ON CAST(uwp.vocabulary_id AS UNSIGNED) = CAST(vbw.vocabulary_id AS UNSIGNED) AND uwp.user_id = ?
|
||||
WHERE vb.is_system = 1
|
||||
GROUP BY vb.id, vb.name, vb.description, vb.level
|
||||
HAVING total_words > 0
|
||||
ORDER BY last_study_date DESC
|
||||
`
|
||||
|
||||
// 获取总数
|
||||
countQuery := `
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT vb.id
|
||||
FROM ai_vocabulary_books vb
|
||||
LEFT JOIN ai_vocabulary_book_words vbw ON vbw.book_id = vb.id
|
||||
WHERE vb.is_system = 1
|
||||
GROUP BY vb.id
|
||||
HAVING COUNT(DISTINCT vbw.vocabulary_id) > 0
|
||||
) as subquery
|
||||
`
|
||||
if err := s.db.Raw(countQuery).Scan(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
rows, err := s.db.Raw(query+" LIMIT ? OFFSET ?", userID, limit, offset).Rows()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int64
|
||||
title string
|
||||
category string
|
||||
level string
|
||||
totalWords int
|
||||
learnedWords int
|
||||
lastStudyDate *time.Time
|
||||
)
|
||||
|
||||
if err := rows.Scan(&id, &title, &category, &level, &totalWords, &learnedWords, &lastStudyDate); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
progress := float64(0)
|
||||
if totalWords > 0 {
|
||||
progress = float64(learnedWords) / float64(totalWords)
|
||||
}
|
||||
|
||||
progressList = append(progressList, map[string]interface{}{
|
||||
"id": fmt.Sprintf("%d", id),
|
||||
"title": title,
|
||||
"category": category,
|
||||
"level": level,
|
||||
"total_words": totalWords,
|
||||
"learned_words": learnedWords,
|
||||
"progress": progress,
|
||||
"last_study_date": lastStudyDate,
|
||||
})
|
||||
}
|
||||
|
||||
return progressList, total, nil
|
||||
}
|
||||
1317
serve/internal/services/vocabulary_service.go
Normal file
1317
serve/internal/services/vocabulary_service.go
Normal file
File diff suppressed because it is too large
Load Diff
237
serve/internal/services/word_book_service.go
Normal file
237
serve/internal/services/word_book_service.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// WordBookService 生词本服务
|
||||
type WordBookService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewWordBookService(db *gorm.DB) *WordBookService {
|
||||
return &WordBookService{db: db}
|
||||
}
|
||||
|
||||
// ToggleFavorite 切换单词收藏状态
|
||||
func (s *WordBookService) ToggleFavorite(userID, wordID int64) (bool, error) {
|
||||
var progress models.UserWordProgress
|
||||
err := s.db.Where("user_id = ? AND vocabulary_id = ?", userID, wordID).First(&progress).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 如果没有进度记录,创建一个并设置为收藏
|
||||
now := time.Now()
|
||||
progress = models.UserWordProgress{
|
||||
UserID: userID,
|
||||
VocabularyID: wordID,
|
||||
Status: "not_started",
|
||||
StudyCount: 0,
|
||||
CorrectCount: 0,
|
||||
WrongCount: 0,
|
||||
Proficiency: 0,
|
||||
ReviewInterval: 1,
|
||||
FirstStudiedAt: now,
|
||||
LastStudiedAt: now,
|
||||
}
|
||||
|
||||
// 通过GORM钩子设置IsFavorite(因为是bool类型的零值问题)
|
||||
if err := s.db.Create(&progress).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 更新IsFavorite为true
|
||||
if err := s.db.Model(&progress).Update("is_favorite", true).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 切换收藏状态
|
||||
newStatus := !progress.IsFavorite
|
||||
if err := s.db.Model(&progress).Update("is_favorite", newStatus).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return newStatus, nil
|
||||
}
|
||||
|
||||
// GetFavoriteWords 获取生词本列表(带分页)
|
||||
func (s *WordBookService) GetFavoriteWords(userID int64, page, pageSize int, sortBy, order string) ([]map[string]interface{}, int64, error) {
|
||||
var total int64
|
||||
|
||||
// 统计总数
|
||||
s.db.Model(&models.UserWordProgress{}).
|
||||
Where("user_id = ? AND is_favorite = ?", userID, true).
|
||||
Count(&total)
|
||||
|
||||
if total == 0 {
|
||||
return []map[string]interface{}{}, 0, nil
|
||||
}
|
||||
|
||||
// 构建排序字段
|
||||
orderClause := "uwp.created_at DESC"
|
||||
switch sortBy {
|
||||
case "proficiency":
|
||||
orderClause = "uwp.proficiency " + order
|
||||
case "word":
|
||||
orderClause = "v.word " + order
|
||||
case "created_at":
|
||||
orderClause = "uwp.created_at " + order
|
||||
}
|
||||
|
||||
// 查询收藏的单词及其详情
|
||||
var results []map[string]interface{}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
err := s.db.Raw(`
|
||||
SELECT
|
||||
v.id,
|
||||
v.word,
|
||||
v.phonetic,
|
||||
v.audio_url,
|
||||
v.level,
|
||||
uwp.proficiency,
|
||||
uwp.study_count,
|
||||
uwp.status,
|
||||
uwp.next_review_at,
|
||||
uwp.created_at as favorited_at,
|
||||
GROUP_CONCAT(DISTINCT vd.definition_cn SEPARATOR '; ') as definitions,
|
||||
GROUP_CONCAT(DISTINCT vd.part_of_speech SEPARATOR ', ') as parts_of_speech
|
||||
FROM ai_user_word_progress uwp
|
||||
INNER JOIN ai_vocabulary v ON v.id = uwp.vocabulary_id
|
||||
LEFT JOIN ai_vocabulary_definitions vd ON vd.vocabulary_id = v.id
|
||||
WHERE uwp.user_id = ? AND uwp.is_favorite = true
|
||||
GROUP BY v.id, v.word, v.phonetic, v.audio_url, v.level,
|
||||
uwp.proficiency, uwp.study_count, uwp.status, uwp.next_review_at, uwp.created_at
|
||||
ORDER BY `+orderClause+`
|
||||
LIMIT ? OFFSET ?
|
||||
`, userID, pageSize, offset).Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return results, total, nil
|
||||
}
|
||||
|
||||
// GetFavoriteWordsByBook 获取指定词汇书的生词本
|
||||
func (s *WordBookService) GetFavoriteWordsByBook(userID int64, bookID string) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
err := s.db.Raw(`
|
||||
SELECT
|
||||
v.id,
|
||||
v.word,
|
||||
v.phonetic,
|
||||
v.audio_url,
|
||||
v.level,
|
||||
uwp.proficiency,
|
||||
uwp.study_count,
|
||||
uwp.status,
|
||||
GROUP_CONCAT(DISTINCT vd.definition_cn SEPARATOR '; ') as definitions,
|
||||
GROUP_CONCAT(DISTINCT vd.part_of_speech SEPARATOR ', ') as parts_of_speech
|
||||
FROM ai_user_word_progress uwp
|
||||
INNER JOIN ai_vocabulary v ON v.id = uwp.vocabulary_id
|
||||
INNER JOIN ai_vocabulary_book_words vbw ON CAST(vbw.vocabulary_id AS UNSIGNED) = v.id
|
||||
LEFT JOIN ai_vocabulary_definitions vd ON vd.vocabulary_id = v.id
|
||||
WHERE uwp.user_id = ? AND uwp.is_favorite = true AND vbw.book_id = ?
|
||||
GROUP BY v.id, v.word, v.phonetic, v.audio_url, v.level,
|
||||
uwp.proficiency, uwp.study_count, uwp.status
|
||||
ORDER BY uwp.created_at DESC
|
||||
`, userID, bookID).Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetFavoriteStats 获取生词本统计信息
|
||||
func (s *WordBookService) GetFavoriteStats(userID int64) (map[string]interface{}, error) {
|
||||
var stats struct {
|
||||
TotalWords int64
|
||||
MasteredWords int64
|
||||
ReviewingWords int64
|
||||
LearningWords int64
|
||||
AvgProficiency float64
|
||||
NeedReviewToday int64
|
||||
}
|
||||
|
||||
// 统计总数和各状态数量
|
||||
s.db.Raw(`
|
||||
SELECT
|
||||
COUNT(*) as total_words,
|
||||
SUM(CASE WHEN status = 'mastered' THEN 1 ELSE 0 END) as mastered_words,
|
||||
SUM(CASE WHEN status = 'reviewing' THEN 1 ELSE 0 END) as reviewing_words,
|
||||
SUM(CASE WHEN status = 'learning' THEN 1 ELSE 0 END) as learning_words,
|
||||
AVG(proficiency) as avg_proficiency,
|
||||
SUM(CASE WHEN next_review_at IS NOT NULL AND next_review_at <= NOW() THEN 1 ELSE 0 END) as need_review_today
|
||||
FROM ai_user_word_progress
|
||||
WHERE user_id = ? AND is_favorite = true
|
||||
`, userID).Scan(&stats)
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_words": stats.TotalWords,
|
||||
"mastered_words": stats.MasteredWords,
|
||||
"reviewing_words": stats.ReviewingWords,
|
||||
"learning_words": stats.LearningWords,
|
||||
"avg_proficiency": stats.AvgProficiency,
|
||||
"need_review_today": stats.NeedReviewToday,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BatchAddToFavorite 批量添加到生词本
|
||||
func (s *WordBookService) BatchAddToFavorite(userID int64, wordIDs []int64) (int, error) {
|
||||
count := 0
|
||||
now := time.Now()
|
||||
|
||||
for _, wordID := range wordIDs {
|
||||
var progress models.UserWordProgress
|
||||
err := s.db.Where("user_id = ? AND vocabulary_id = ?", userID, wordID).First(&progress).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 创建新记录
|
||||
progress = models.UserWordProgress{
|
||||
UserID: userID,
|
||||
VocabularyID: wordID,
|
||||
Status: "not_started",
|
||||
StudyCount: 0,
|
||||
CorrectCount: 0,
|
||||
WrongCount: 0,
|
||||
Proficiency: 0,
|
||||
ReviewInterval: 1,
|
||||
FirstStudiedAt: now,
|
||||
LastStudiedAt: now,
|
||||
}
|
||||
if err := s.db.Create(&progress).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
if err := s.db.Model(&progress).Update("is_favorite", true).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
} else if err == nil && !progress.IsFavorite {
|
||||
// 更新现有记录
|
||||
if err := s.db.Model(&progress).Update("is_favorite", true).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// RemoveFromFavorite 从生词本移除
|
||||
func (s *WordBookService) RemoveFromFavorite(userID, wordID int64) error {
|
||||
return s.db.Model(&models.UserWordProgress{}).
|
||||
Where("user_id = ? AND vocabulary_id = ?", userID, wordID).
|
||||
Update("is_favorite", false).Error
|
||||
}
|
||||
337
serve/internal/services/writing_service.go
Normal file
337
serve/internal/services/writing_service.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// WritingService 写作练习服务
|
||||
type WritingService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewWritingService 创建写作练习服务实例
|
||||
func NewWritingService(db *gorm.DB) *WritingService {
|
||||
return &WritingService{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 写作题目管理 =====
|
||||
|
||||
// GetWritingPrompts 获取写作题目列表
|
||||
func (s *WritingService) GetWritingPrompts(difficulty string, category string, limit, offset int) ([]*models.WritingPrompt, error) {
|
||||
var prompts []*models.WritingPrompt
|
||||
query := s.db.Where("is_active = ?", true)
|
||||
|
||||
if difficulty != "" {
|
||||
query = query.Where("level = ?", difficulty)
|
||||
}
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&prompts).Error
|
||||
return prompts, err
|
||||
}
|
||||
|
||||
// GetWritingPrompt 获取单个写作题目
|
||||
func (s *WritingService) GetWritingPrompt(id string) (*models.WritingPrompt, error) {
|
||||
var prompt models.WritingPrompt
|
||||
err := s.db.Where("id = ? AND is_active = ?", id, true).First(&prompt).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &prompt, nil
|
||||
}
|
||||
|
||||
// CreateWritingPrompt 创建写作题目
|
||||
func (s *WritingService) CreateWritingPrompt(prompt *models.WritingPrompt) error {
|
||||
return s.db.Create(prompt).Error
|
||||
}
|
||||
|
||||
// UpdateWritingPrompt 更新写作题目
|
||||
func (s *WritingService) UpdateWritingPrompt(id string, prompt *models.WritingPrompt) error {
|
||||
return s.db.Where("id = ?", id).Updates(prompt).Error
|
||||
}
|
||||
|
||||
// DeleteWritingPrompt 删除写作题目(软删除)
|
||||
func (s *WritingService) DeleteWritingPrompt(id string) error {
|
||||
return s.db.Model(&models.WritingPrompt{}).Where("id = ?", id).Update("is_active", false).Error
|
||||
}
|
||||
|
||||
// SearchWritingPrompts 搜索写作题目
|
||||
func (s *WritingService) SearchWritingPrompts(keyword string, difficulty string, category string, limit, offset int) ([]*models.WritingPrompt, error) {
|
||||
var prompts []*models.WritingPrompt
|
||||
query := s.db.Where("is_active = ?", true)
|
||||
|
||||
if keyword != "" {
|
||||
query = query.Where("title LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
if difficulty != "" {
|
||||
query = query.Where("level = ?", difficulty)
|
||||
}
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&prompts).Error
|
||||
return prompts, err
|
||||
}
|
||||
|
||||
// GetRecommendedPrompts 获取推荐写作题目
|
||||
func (s *WritingService) GetRecommendedPrompts(userID string, limit int) ([]*models.WritingPrompt, error) {
|
||||
// 基于用户历史表现推荐合适难度的题目
|
||||
var prompts []*models.WritingPrompt
|
||||
|
||||
// 获取用户最近的写作记录,分析难度偏好
|
||||
var avgScore sql.NullFloat64
|
||||
s.db.Model(&models.WritingSubmission{}).
|
||||
Where("user_id = ? AND score IS NOT NULL", userID).
|
||||
Select("AVG(score)").Scan(&avgScore)
|
||||
|
||||
// 根据平均分推荐合适难度
|
||||
var difficulty string
|
||||
if !avgScore.Valid || avgScore.Float64 < 60 {
|
||||
difficulty = "beginner"
|
||||
} else if avgScore.Float64 < 80 {
|
||||
difficulty = "intermediate"
|
||||
} else {
|
||||
difficulty = "advanced"
|
||||
}
|
||||
|
||||
err := s.db.Where("level = ? AND is_active = ?", difficulty, true).
|
||||
Order("RAND()").Limit(limit).Find(&prompts).Error
|
||||
return prompts, err
|
||||
}
|
||||
|
||||
// ===== 写作提交管理 =====
|
||||
|
||||
// CreateWritingSubmission 创建写作提交
|
||||
func (s *WritingService) CreateWritingSubmission(submission *models.WritingSubmission) error {
|
||||
return s.db.Create(submission).Error
|
||||
}
|
||||
|
||||
// UpdateWritingSubmission 更新写作提交
|
||||
func (s *WritingService) UpdateWritingSubmission(id string, submission *models.WritingSubmission) error {
|
||||
return s.db.Where("id = ?", id).Updates(submission).Error
|
||||
}
|
||||
|
||||
// GetWritingSubmission 获取写作提交详情
|
||||
func (s *WritingService) GetWritingSubmission(id string) (*models.WritingSubmission, error) {
|
||||
var submission models.WritingSubmission
|
||||
err := s.db.Preload("Prompt").Where("id = ?", id).First(&submission).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &submission, nil
|
||||
}
|
||||
|
||||
// GetUserWritingSubmissions 获取用户写作提交列表
|
||||
func (s *WritingService) GetUserWritingSubmissions(userID string, limit, offset int) ([]*models.WritingSubmission, error) {
|
||||
var submissions []*models.WritingSubmission
|
||||
err := s.db.Preload("Prompt").Where("user_id = ?", userID).
|
||||
Order("created_at DESC").Limit(limit).Offset(offset).Find(&submissions).Error
|
||||
return submissions, err
|
||||
}
|
||||
|
||||
// SubmitWriting 提交写作作业
|
||||
func (s *WritingService) SubmitWriting(submissionID string, content string, timeSpent int) error {
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"content": content,
|
||||
"word_count": len(content), // 简单字数统计,实际可能需要更复杂的逻辑
|
||||
"time_spent": timeSpent,
|
||||
"submitted_at": &now,
|
||||
}
|
||||
|
||||
return s.db.Model(&models.WritingSubmission{}).Where("id = ?", submissionID).Updates(updates).Error
|
||||
}
|
||||
|
||||
// GradeWriting AI批改写作
|
||||
func (s *WritingService) GradeWriting(submissionID string, score, grammarScore, vocabScore, coherenceScore float64, feedback, suggestions string) error {
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"score": score,
|
||||
"grammar_score": grammarScore,
|
||||
"vocab_score": vocabScore,
|
||||
"coherence_score": coherenceScore,
|
||||
"feedback": feedback,
|
||||
"suggestions": suggestions,
|
||||
"graded_at": &now,
|
||||
}
|
||||
|
||||
return s.db.Model(&models.WritingSubmission{}).Where("id = ?", submissionID).Updates(updates).Error
|
||||
}
|
||||
|
||||
// ===== 写作统计分析 =====
|
||||
|
||||
// GetUserWritingStats 获取用户写作统计
|
||||
func (s *WritingService) GetUserWritingStats(userID string) (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
// 总提交数
|
||||
var totalSubmissions int64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ?", userID).Count(&totalSubmissions)
|
||||
stats["total_submissions"] = totalSubmissions
|
||||
|
||||
// 已完成提交数
|
||||
var completedSubmissions int64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND submitted_at IS NOT NULL", userID).Count(&completedSubmissions)
|
||||
stats["completed_submissions"] = completedSubmissions
|
||||
|
||||
// 已批改提交数
|
||||
var gradedSubmissions int64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND graded_at IS NOT NULL", userID).Count(&gradedSubmissions)
|
||||
stats["graded_submissions"] = gradedSubmissions
|
||||
|
||||
// 平均分数
|
||||
var avgScore sql.NullFloat64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND score IS NOT NULL", userID).Select("AVG(score)").Scan(&avgScore)
|
||||
if avgScore.Valid {
|
||||
stats["average_score"] = fmt.Sprintf("%.2f", avgScore.Float64)
|
||||
} else {
|
||||
stats["average_score"] = "0.00"
|
||||
}
|
||||
|
||||
// 平均语法分数
|
||||
var avgGrammarScore sql.NullFloat64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND grammar_score IS NOT NULL", userID).Select("AVG(grammar_score)").Scan(&avgGrammarScore)
|
||||
if avgGrammarScore.Valid {
|
||||
stats["average_grammar_score"] = fmt.Sprintf("%.2f", avgGrammarScore.Float64)
|
||||
} else {
|
||||
stats["average_grammar_score"] = "0.00"
|
||||
}
|
||||
|
||||
// 平均词汇分数
|
||||
var avgVocabScore sql.NullFloat64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND vocab_score IS NOT NULL", userID).Select("AVG(vocab_score)").Scan(&avgVocabScore)
|
||||
if avgVocabScore.Valid {
|
||||
stats["average_vocab_score"] = fmt.Sprintf("%.2f", avgVocabScore.Float64)
|
||||
} else {
|
||||
stats["average_vocab_score"] = "0.00"
|
||||
}
|
||||
|
||||
// 平均连贯性分数
|
||||
var avgCoherenceScore sql.NullFloat64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND coherence_score IS NOT NULL", userID).Select("AVG(coherence_score)").Scan(&avgCoherenceScore)
|
||||
if avgCoherenceScore.Valid {
|
||||
stats["average_coherence_score"] = fmt.Sprintf("%.2f", avgCoherenceScore.Float64)
|
||||
} else {
|
||||
stats["average_coherence_score"] = "0.00"
|
||||
}
|
||||
|
||||
// 总写作时间
|
||||
var totalTimeSpent sql.NullInt64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND time_spent IS NOT NULL", userID).Select("SUM(time_spent)").Scan(&totalTimeSpent)
|
||||
if totalTimeSpent.Valid {
|
||||
stats["total_time_spent"] = totalTimeSpent.Int64
|
||||
} else {
|
||||
stats["total_time_spent"] = 0
|
||||
}
|
||||
|
||||
// 平均写作时间
|
||||
var avgTimeSpent sql.NullFloat64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND time_spent IS NOT NULL", userID).Select("AVG(time_spent)").Scan(&avgTimeSpent)
|
||||
if avgTimeSpent.Valid {
|
||||
stats["average_time_spent"] = fmt.Sprintf("%.2f", avgTimeSpent.Float64)
|
||||
} else {
|
||||
stats["average_time_spent"] = "0.00"
|
||||
}
|
||||
|
||||
// 总字数
|
||||
var totalWordCount sql.NullInt64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND word_count IS NOT NULL", userID).Select("SUM(word_count)").Scan(&totalWordCount)
|
||||
if totalWordCount.Valid {
|
||||
stats["total_word_count"] = totalWordCount.Int64
|
||||
} else {
|
||||
stats["total_word_count"] = 0
|
||||
}
|
||||
|
||||
// 平均字数
|
||||
var avgWordCount sql.NullFloat64
|
||||
s.db.Model(&models.WritingSubmission{}).Where("user_id = ? AND word_count IS NOT NULL", userID).Select("AVG(word_count)").Scan(&avgWordCount)
|
||||
if avgWordCount.Valid {
|
||||
stats["average_word_count"] = fmt.Sprintf("%.2f", avgWordCount.Float64)
|
||||
} else {
|
||||
stats["average_word_count"] = "0.00"
|
||||
}
|
||||
|
||||
// 连续写作天数
|
||||
continuousDays := s.calculateContinuousWritingDays(userID)
|
||||
stats["continuous_writing_days"] = continuousDays
|
||||
|
||||
// 按难度统计
|
||||
difficultyStats := s.getWritingStatsByDifficulty(userID)
|
||||
stats["difficulty_stats"] = difficultyStats
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// calculateContinuousWritingDays 计算连续写作天数
|
||||
func (s *WritingService) calculateContinuousWritingDays(userID string) int {
|
||||
var dates []time.Time
|
||||
s.db.Model(&models.WritingSubmission{}).
|
||||
Where("user_id = ? AND submitted_at IS NOT NULL", userID).
|
||||
Select("DATE(submitted_at) as date").
|
||||
Group("DATE(submitted_at)").
|
||||
Order("date DESC").
|
||||
Pluck("date", &dates)
|
||||
|
||||
if len(dates) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
continuousDays := 1
|
||||
for i := 1; i < len(dates); i++ {
|
||||
diff := dates[i-1].Sub(dates[i]).Hours() / 24
|
||||
if diff == 1 {
|
||||
continuousDays++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return continuousDays
|
||||
}
|
||||
|
||||
// getWritingStatsByDifficulty 按难度获取写作统计
|
||||
func (s *WritingService) getWritingStatsByDifficulty(userID string) map[string]interface{} {
|
||||
type DifficultyStats struct {
|
||||
Difficulty string `json:"difficulty"`
|
||||
Count int64 `json:"count"`
|
||||
AvgScore float64 `json:"avg_score"`
|
||||
}
|
||||
|
||||
var stats []DifficultyStats
|
||||
s.db.Model(&models.WritingSubmission{}).
|
||||
Select("p.level as difficulty, COUNT(*) as count, COALESCE(AVG(writing_submissions.score), 0) as avg_score").
|
||||
Joins("JOIN writing_prompts p ON writing_submissions.prompt_id = p.id").
|
||||
Where("writing_submissions.user_id = ? AND writing_submissions.submitted_at IS NOT NULL", userID).
|
||||
Group("p.level").
|
||||
Scan(&stats)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, stat := range stats {
|
||||
result[stat.Difficulty] = map[string]interface{}{
|
||||
"count": stat.Count,
|
||||
"avg_score": fmt.Sprintf("%.2f", stat.AvgScore),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetWritingProgress 获取写作进度
|
||||
func (s *WritingService) GetWritingProgress(userID string, promptID string) (*models.WritingSubmission, error) {
|
||||
var submission models.WritingSubmission
|
||||
err := s.db.Where("user_id = ? AND prompt_id = ?", userID, promptID).First(&submission).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &submission, nil
|
||||
}
|
||||
Reference in New Issue
Block a user