Files
ai_english/serve/api/handlers/speaking_handler.go
2025-11-17 13:39:05 +08:00

449 lines
13 KiB
Go

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