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

436 lines
13 KiB
Go
Raw Normal View History

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