package handlers import ( "net/http" "strconv" "time" "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/services" "github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/utils" "github.com/gin-gonic/gin" ) // SpeakingHandler 口语练习处理器 type SpeakingHandler struct { speakingService *services.SpeakingService } // NewSpeakingHandler 创建口语练习处理器实例 func NewSpeakingHandler(speakingService *services.SpeakingService) *SpeakingHandler { return &SpeakingHandler{ speakingService: speakingService, } } // ==================== 口语场景管理 ==================== // GetSpeakingScenarios 获取口语场景列表 func (h *SpeakingHandler) GetSpeakingScenarios(c *gin.Context) { level := c.Query("level") category := c.Query("category") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } scenarios, total, err := h.speakingService.GetSpeakingScenarios(level, category, page, pageSize) if err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "获取口语场景列表失败") return } response := gin.H{ "scenarios": scenarios, "pagination": gin.H{ "page": page, "page_size": pageSize, "total": total, "total_pages": (total + int64(pageSize) - 1) / int64(pageSize), }, } common.SuccessResponse(c, response) } // GetSpeakingScenario 获取单个口语场景 func (h *SpeakingHandler) GetSpeakingScenario(c *gin.Context) { id := c.Param("id") if id == "" { common.ErrorResponse(c, http.StatusBadRequest, "场景ID不能为空") return } scenario, err := h.speakingService.GetSpeakingScenario(id) if err != nil { common.ErrorResponse(c, http.StatusNotFound, "口语场景不存在") return } common.SuccessResponse(c, scenario) } // CreateSpeakingScenarioRequest 创建口语场景请求 type CreateSpeakingScenarioRequest struct { Title string `json:"title" binding:"required,max=200"` Description string `json:"description" binding:"required"` Context *string `json:"context"` Level string `json:"level" binding:"required,oneof=beginner intermediate advanced"` Category string `json:"category" binding:"max=50"` Tags *string `json:"tags"` Dialogue *string `json:"dialogue"` KeyPhrases *string `json:"key_phrases"` } // CreateSpeakingScenario 创建口语场景 func (h *SpeakingHandler) CreateSpeakingScenario(c *gin.Context) { var req CreateSpeakingScenarioRequest if err := c.ShouldBindJSON(&req); err != nil { common.ErrorResponse(c, http.StatusBadRequest, "请求参数错误") return } scenario := &models.SpeakingScenario{ ID: utils.GenerateUUID(), Title: req.Title, Description: req.Description, Context: req.Context, Level: req.Level, Category: req.Category, Tags: req.Tags, Dialogue: req.Dialogue, KeyPhrases: req.KeyPhrases, IsActive: true, } if err := h.speakingService.CreateSpeakingScenario(scenario); err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "创建口语场景失败") return } common.SuccessResponse(c, scenario) } // UpdateSpeakingScenarioRequest 更新口语场景请求 type UpdateSpeakingScenarioRequest struct { Title *string `json:"title" binding:"omitempty,max=200"` Description *string `json:"description"` Context *string `json:"context"` Level *string `json:"level" binding:"omitempty,oneof=beginner intermediate advanced"` Category *string `json:"category" binding:"omitempty,max=50"` Tags *string `json:"tags"` Dialogue *string `json:"dialogue"` KeyPhrases *string `json:"key_phrases"` } // UpdateSpeakingScenario 更新口语场景 func (h *SpeakingHandler) UpdateSpeakingScenario(c *gin.Context) { id := c.Param("id") if id == "" { common.ErrorResponse(c, http.StatusBadRequest, "场景ID不能为空") return } var req UpdateSpeakingScenarioRequest if err := c.ShouldBindJSON(&req); err != nil { common.ErrorResponse(c, http.StatusBadRequest, "请求参数错误") return } updateData := &models.SpeakingScenario{} if req.Title != nil { updateData.Title = *req.Title } if req.Description != nil { updateData.Description = *req.Description } if req.Context != nil { updateData.Context = req.Context } if req.Level != nil { updateData.Level = *req.Level } if req.Category != nil { updateData.Category = *req.Category } if req.Tags != nil { updateData.Tags = req.Tags } if req.Dialogue != nil { updateData.Dialogue = req.Dialogue } if req.KeyPhrases != nil { updateData.KeyPhrases = req.KeyPhrases } if err := h.speakingService.UpdateSpeakingScenario(id, updateData); err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "更新口语场景失败") return } common.SuccessResponse(c, gin.H{"message": "更新成功"}) } // DeleteSpeakingScenario 删除口语场景 func (h *SpeakingHandler) DeleteSpeakingScenario(c *gin.Context) { id := c.Param("id") if id == "" { common.ErrorResponse(c, http.StatusBadRequest, "场景ID不能为空") return } if err := h.speakingService.DeleteSpeakingScenario(id); err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "删除口语场景失败") return } common.SuccessResponse(c, gin.H{"message": "删除成功"}) } // SearchSpeakingScenarios 搜索口语场景 func (h *SpeakingHandler) SearchSpeakingScenarios(c *gin.Context) { keyword := c.Query("keyword") level := c.Query("level") category := c.Query("category") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } scenarios, total, err := h.speakingService.SearchSpeakingScenarios(keyword, level, category, page, pageSize) if err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "搜索口语场景失败") return } response := gin.H{ "scenarios": scenarios, "pagination": gin.H{ "page": page, "page_size": pageSize, "total": total, "total_pages": (total + int64(pageSize) - 1) / int64(pageSize), }, } common.SuccessResponse(c, response) } // GetRecommendedScenarios 获取推荐的口语场景 func (h *SpeakingHandler) GetRecommendedScenarios(c *gin.Context) { userIDInt, exists := utils.GetUserIDFromContext(c) if !exists { common.ErrorResponse(c, http.StatusUnauthorized, "用户未认证") return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) if limit < 1 || limit > 50 { limit = 10 } userID := utils.Int64ToString(userIDInt) scenarios, err := h.speakingService.GetRecommendedScenarios(userID, limit) if err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "获取推荐场景失败") return } common.SuccessResponse(c, scenarios) } // ==================== 口语练习记录管理 ==================== // CreateSpeakingRecordRequest 创建口语练习记录请求 type CreateSpeakingRecordRequest struct { ScenarioID string `json:"scenario_id" binding:"required"` } // CreateSpeakingRecord 创建口语练习记录 func (h *SpeakingHandler) CreateSpeakingRecord(c *gin.Context) { userIDInt, exists := utils.GetUserIDFromContext(c) if !exists { common.ErrorResponse(c, http.StatusUnauthorized, "用户未认证") return } var req CreateSpeakingRecordRequest if err := c.ShouldBindJSON(&req); err != nil { common.ErrorResponse(c, http.StatusBadRequest, "请求参数错误") return } now := time.Now() record := &models.SpeakingRecord{ ID: utils.GenerateUUID(), UserID: utils.Int64ToString(userIDInt), ScenarioID: req.ScenarioID, StartedAt: now, CreatedAt: now, UpdatedAt: now, } if err := h.speakingService.CreateSpeakingRecord(record); err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "创建口语练习记录失败") return } common.SuccessResponse(c, record) } // GetUserSpeakingRecords 获取用户的口语练习记录 func (h *SpeakingHandler) GetUserSpeakingRecords(c *gin.Context) { userIDInt, exists := utils.GetUserIDFromContext(c) if !exists { common.ErrorResponse(c, http.StatusUnauthorized, "用户未认证") return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } userID := utils.Int64ToString(userIDInt) records, total, err := h.speakingService.GetUserSpeakingRecords(userID, page, pageSize) if err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "获取口语练习记录失败") return } response := gin.H{ "records": records, "pagination": gin.H{ "page": page, "page_size": pageSize, "total": total, "total_pages": (total + int64(pageSize) - 1) / int64(pageSize), }, } common.SuccessResponse(c, response) } // GetSpeakingRecord 获取单个口语练习记录 func (h *SpeakingHandler) GetSpeakingRecord(c *gin.Context) { id := c.Param("id") if id == "" { common.ErrorResponse(c, http.StatusBadRequest, "记录ID不能为空") return } record, err := h.speakingService.GetSpeakingRecord(id) if err != nil { common.ErrorResponse(c, http.StatusNotFound, "口语练习记录不存在") return } common.SuccessResponse(c, record) } // SubmitSpeakingRequest 提交口语练习请求 type SubmitSpeakingRequest struct { AudioURL string `json:"audio_url" binding:"required"` Transcript string `json:"transcript"` } // SubmitSpeaking 提交口语练习 func (h *SpeakingHandler) SubmitSpeaking(c *gin.Context) { id := c.Param("id") if id == "" { common.ErrorResponse(c, http.StatusBadRequest, "记录ID不能为空") return } var req SubmitSpeakingRequest if err := c.ShouldBindJSON(&req); err != nil { common.ErrorResponse(c, http.StatusBadRequest, "请求参数错误") return } if err := h.speakingService.SubmitSpeaking(id, req.AudioURL, req.Transcript); err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "提交口语练习失败") return } common.SuccessResponse(c, gin.H{"message": "提交成功"}) } // GradeSpeakingRequest 评分口语练习请求 type GradeSpeakingRequest struct { PronunciationScore float64 `json:"pronunciation_score" binding:"required,min=0,max=100"` FluencyScore float64 `json:"fluency_score" binding:"required,min=0,max=100"` AccuracyScore float64 `json:"accuracy_score" binding:"required,min=0,max=100"` OverallScore float64 `json:"overall_score" binding:"required,min=0,max=100"` Feedback string `json:"feedback"` Suggestions string `json:"suggestions"` } // GradeSpeaking 评分口语练习 func (h *SpeakingHandler) GradeSpeaking(c *gin.Context) { id := c.Param("id") if id == "" { common.ErrorResponse(c, http.StatusBadRequest, "记录ID不能为空") return } var req GradeSpeakingRequest if err := c.ShouldBindJSON(&req); err != nil { common.ErrorResponse(c, http.StatusBadRequest, "请求参数错误") return } if err := h.speakingService.GradeSpeaking(id, req.PronunciationScore, req.FluencyScore, req.AccuracyScore, req.OverallScore, req.Feedback, req.Suggestions); err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "评分口语练习失败") return } common.SuccessResponse(c, gin.H{"message": "评分成功"}) } // ==================== 口语学习统计和进度 ==================== // GetSpeakingStats 获取口语学习统计 func (h *SpeakingHandler) GetSpeakingStats(c *gin.Context) { userIDInt, exists := utils.GetUserIDFromContext(c) if !exists { common.ErrorResponse(c, http.StatusUnauthorized, "用户未认证") return } userID := utils.Int64ToString(userIDInt) stats, err := h.speakingService.GetUserSpeakingStats(userID) if err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "获取口语学习统计失败") return } common.SuccessResponse(c, stats) } // GetSpeakingProgress 获取口语学习进度 func (h *SpeakingHandler) GetSpeakingProgress(c *gin.Context) { userIDInt, exists := utils.GetUserIDFromContext(c) if !exists { common.ErrorResponse(c, http.StatusUnauthorized, "用户未认证") return } scenarioID := c.Param("scenario_id") if scenarioID == "" { common.ErrorResponse(c, http.StatusBadRequest, "场景ID不能为空") return } userID := utils.Int64ToString(userIDInt) progress, err := h.speakingService.GetSpeakingProgress(userID, scenarioID) if err != nil { common.ErrorResponse(c, http.StatusInternalServerError, "获取口语学习进度失败") return } common.SuccessResponse(c, progress) }