This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

View 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
}

View 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
}

View 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(&notifications).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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}